Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcffbc65f4 | ||
|
|
6d240b6a07 | ||
|
|
d31b88b94a | ||
|
|
0442ddc739 | ||
|
|
3b04d54ea3 | ||
|
|
71df46cc06 | ||
|
|
5660c6c369 | ||
|
|
698d566b76 | ||
|
|
5d968afb7e | ||
|
|
63ff9e9e32 | ||
|
|
d95b6b7530 | ||
|
|
4deb756e64 | ||
|
|
543b644ed1 | ||
|
|
e6c169e566 | ||
|
|
3c6f222c8b | ||
|
|
50282ea2e9 | ||
|
|
a91aefc185 | ||
|
|
1fe29d921b | ||
|
|
91e19c4faf | ||
|
|
60580ff8bd | ||
|
|
17bc6dde28 | ||
|
|
4e6f3f6d1d | ||
|
|
9bd3076abf | ||
|
|
05010fc707 | ||
|
|
118b26a8b9 | ||
|
|
e00dcef4d6 | ||
|
|
1f84fe4701 | ||
|
|
9baf04026e | ||
|
|
b5195ed8eb | ||
|
|
4c87cc5fd9 | ||
|
|
8b4849be05 | ||
|
|
7b929a7c53 | ||
|
|
af52e68b6e | ||
|
|
05aef6a91f | ||
|
|
8489d1a63e | ||
|
|
e0a00bfd04 | ||
|
|
914bc409f6 | ||
|
|
aa90253d71 | ||
|
|
c9bef96c32 | ||
|
|
9cb8dc73e5 | ||
|
|
d10ba420f4 | ||
|
|
be3a1759de | ||
|
|
dcd8b42522 | ||
|
|
3b4639de08 | ||
|
|
d5159ae4f7 | ||
|
|
629635e0dd | ||
|
|
876d229b7e | ||
|
|
ad5a18f1ee | ||
|
|
cdc62d3020 | ||
|
|
3e99c2f69d | ||
|
|
b3d86d5395 | ||
|
|
50327be6e7 | ||
|
|
e135599c96 | ||
|
|
d6a66bc4d8 | ||
|
|
32a4fbebd5 | ||
|
|
283e6b2ff0 | ||
|
|
cfbbfa004c | ||
|
|
b81d45901b | ||
|
|
df39026d0d | ||
|
|
880d84b030 | ||
|
|
f9c2b9594f | ||
|
|
1f28f1043a | ||
|
|
e78eee50ed | ||
|
|
ba2072c979 | ||
|
|
49b95c1225 | ||
|
|
14063a0b89 | ||
|
|
2cb114ffaf | ||
|
|
fb2aad0b1d | ||
|
|
861afde3a0 | ||
|
|
29b6c2d49e | ||
|
|
c223d93a53 | ||
|
|
c432f5a66c | ||
|
|
9cb0557120 | ||
|
|
f7aab5e901 | ||
|
|
75ee1e65d1 | ||
|
|
b7be95b239 | ||
|
|
376a2c8ff6 | ||
|
|
3e891972d0 | ||
|
|
cdf7b3dbef | ||
|
|
ede906e152 | ||
|
|
5cafa599fa | ||
|
|
e3c52afbbb | ||
|
|
c61fc7419c | ||
|
|
67106f0813 | ||
|
|
55506bac3a | ||
|
|
b41eecf58c | ||
|
|
0a7a92287b | ||
|
|
6237ddca89 | ||
|
|
ef453ad62e | ||
|
|
e6791246e4 | ||
|
|
50272f7401 | ||
|
|
9964e0b8ec | ||
|
|
9d11c76ea1 | ||
|
|
da9c6e24ce | ||
|
|
cd72dbe4ea | ||
|
|
e89e18f5ba | ||
|
|
cfcb708d26 | ||
|
|
9a96ebf812 | ||
|
|
9812630bb4 | ||
|
|
47bc2c2647 | ||
|
|
1cf95be97d | ||
|
|
8396249a38 | ||
|
|
e231feb522 | ||
|
|
919b65ea19 | ||
|
|
0c33a23c50 | ||
|
|
cc2f1df78e | ||
|
|
9b2fd1253b | ||
|
|
fe7f0558c1 | ||
|
|
ab600cb798 | ||
|
|
024697f3bf | ||
|
|
8d2ff6f92a | ||
|
|
69224cb53c | ||
|
|
3468177c0c | ||
|
|
89b2120c46 | ||
|
|
6c769d6024 | ||
|
|
67ab06715f | ||
|
|
82a063b1e2 | ||
|
|
3ca09e7341 | ||
|
|
b3ac82baf8 | ||
|
|
7f51e1ad1b | ||
|
|
c7dcbfec60 | ||
|
|
3f57da5579 | ||
|
|
8e71305605 | ||
|
|
ce2756f399 | ||
|
|
6cfba9b406 | ||
|
|
02f963d65c | ||
|
|
8a22f243f8 | ||
|
|
f70d1c07c1 | ||
|
|
29b98ba862 | ||
|
|
971ca17b5f | ||
|
|
9ab17e6299 | ||
|
|
d8b76575ca | ||
|
|
5b00312158 | ||
|
|
d920e93d1e | ||
|
|
50f60249ba | ||
|
|
a8080d0cf4 | ||
|
|
e19eb66226 | ||
|
|
c3f62f1506 | ||
|
|
d2a0cba933 | ||
|
|
66da4a7775 | ||
|
|
11c6ea903e | ||
|
|
5a56541aa5 | ||
|
|
ebe93fbbac | ||
|
|
93873c378e | ||
|
|
d86064e1e5 | ||
|
|
08400b67e6 | ||
|
|
ba6a0fcfe0 | ||
|
|
8daf7d22a7 | ||
|
|
b403518986 | ||
|
|
fd3808ae38 | ||
|
|
c4be7842a7 | ||
|
|
1726180b06 | ||
|
|
3b84fe3557 | ||
|
|
8dc9ec477d | ||
|
|
d76293bacc | ||
|
|
595e4ede69 | ||
|
|
179caacf58 | ||
|
|
434189ee47 | ||
|
|
b4d5c8fd51 | ||
|
|
933fa544d7 | ||
|
|
b019f0f849 | ||
|
|
7346a14d86 | ||
|
|
da436f839f | ||
|
|
ecfaee3b04 | ||
|
|
80183964bb | ||
|
|
5161a2d3b0 | ||
|
|
25e7f1927c | ||
|
|
5c7a25fdc7 | ||
|
|
290334baaa | ||
|
|
407e7a24da | ||
|
|
50172ce16a | ||
|
|
aaad36aa69 | ||
|
|
445652d7f4 | ||
|
|
3f3c9f8823 | ||
|
|
95c6173b5c | ||
|
|
f03a7d332b | ||
|
|
86bbdfe7d3 | ||
|
|
8d490783b5 | ||
|
|
c2c4dc961c | ||
|
|
7f2a78f2fe | ||
|
|
13e368ca28 | ||
|
|
366c1f9960 | ||
|
|
ab25074711 | ||
|
|
e6967a41cd | ||
|
|
feba51a49d | ||
|
|
b87ae6ce46 | ||
|
|
0442b9e317 | ||
|
|
3db0ec7efc | ||
|
|
0f53ba8a18 | ||
|
|
62cedeec13 | ||
|
|
45593c6311 | ||
|
|
2b714d3022 | ||
|
|
225fed5f72 | ||
|
|
419625a919 | ||
|
|
ed8e9416db | ||
|
|
f4515d7c2e | ||
|
|
853c1acf7e | ||
|
|
b1e447d0c5 | ||
|
|
df11e438f4 | ||
|
|
e5d3927cb6 | ||
|
|
7572ea350b | ||
|
|
ec6a38e052 | ||
|
|
66c997a843 | ||
|
|
fc5f20745b | ||
|
|
6a032390c2 | ||
|
|
ea8ec4e375 | ||
|
|
2dd2329067 | ||
|
|
57bec5192b | ||
|
|
afed7b08f8 | ||
|
|
3d8707e536 | ||
|
|
930a29f3d5 | ||
|
|
dda69a1ead | ||
|
|
8f171564b7 | ||
|
|
2c3af1b329 | ||
|
|
343bb853fd | ||
|
|
3f10bd8c21 | ||
|
|
1fdeb4f251 | ||
|
|
ceab77d911 | ||
|
|
e959850a66 | ||
|
|
e62e55d1c0 | ||
|
|
28fc81756a | ||
|
|
df56f1985b | ||
|
|
76fcb4ade2 | ||
|
|
c20b0ab3c9 | ||
|
|
8bc3de10a2 | ||
|
|
5e212f7503 | ||
|
|
012959dc3e | ||
|
|
41bf741901 | ||
|
|
d239849cc5 | ||
|
|
6fd6319b12 | ||
|
|
a84c114f54 | ||
|
|
2d5e34b49d | ||
|
|
8878bdacd3 | ||
|
|
54f50e0686 | ||
|
|
f784f62303 | ||
|
|
81ae9f41e8 | ||
|
|
b0f3009c49 | ||
|
|
b13eb8aaf8 | ||
|
|
23df7f1ddc | ||
|
|
3493d9fbc4 | ||
|
|
0e73c5472a | ||
|
|
49715d6f36 | ||
|
|
f772c26e6e | ||
|
|
1fc8b4ff7f | ||
|
|
a8f58d8f75 | ||
|
|
3a39728803 | ||
|
|
f7bee379d7 | ||
|
|
713d5eff5e | ||
|
|
a463c99e4c | ||
|
|
367a13b0e7 | ||
|
|
e9a5dafc62 | ||
|
|
822cfe9c28 | ||
|
|
275478066e | ||
|
|
b01def0872 | ||
|
|
b106305167 | ||
|
|
103631de15 |
+18
-18
@@ -1,8 +1,8 @@
|
||||
# Contributing to Umbraco CMS
|
||||
|
||||
👍🎉 First off, thanks for taking the time to contribute! 🎉👍
|
||||
👍🎉 First of all, thanks for taking the time to contribute! 🎉👍
|
||||
|
||||
These contribution guidelines are mostly just that - guidelines, not rules. This is what we've found to work best over the years, but if you choose to ignore them, we still love you! 💖 Use your best judgement, and feel free to propose changes to this document in a pull request.
|
||||
These contribution guidelines are mostly just that - guidelines, not rules. This is what we've found to work best over the years, but if you choose to ignore them, we still love you! 💖 Use your best judgment, and feel free to propose changes to this document in a pull request.
|
||||
|
||||
We have a guide on [what to consider before you start](contributing-before-you-start.md) and more detailed guides at the end of this article.
|
||||
|
||||
@@ -12,51 +12,51 @@ This guide describes each step to make your first contribution:
|
||||
|
||||
1. **Fork**
|
||||
|
||||
Create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS)
|
||||
Create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS)
|
||||
|
||||

|
||||

|
||||
|
||||
2. **Clone**
|
||||
|
||||
When GitHub has created your fork, you can clone it in your favorite Git tool or on the command line with `git clone https://github.com/[YourUsername]/Umbraco-CMS`.
|
||||
When GitHub has created your fork, you can clone it in your favorite Git tool or on the command line with `git clone https://github.com/[YourUsername]/Umbraco-CMS`.
|
||||
|
||||

|
||||

|
||||
|
||||
3. **Switch to the correct branch**
|
||||
|
||||
Switch to the `contrib` branch
|
||||
Switch to the `main` branch
|
||||
|
||||
4. **Branch out**
|
||||
|
||||
Create a new branch based on `contrib` and name it after the issue you're fixing, For example: `v15/bugfix/18132-rte-tinymce-onchange-value-check`.
|
||||
Create a new branch based on `main` and name it after the issue you're fixing. For example: `v15/bugfix/18132-rte-tinymce-onchange-value-check`.
|
||||
|
||||
Please follow this format for branches: `v{major}/{feature|bugfix|task}/{issue}-{description}`.
|
||||
Please follow this format for branches: `v{major}/{feature|bugfix|task}/{issue}-{description}`.
|
||||
|
||||
This is a development branch for the particular issue you're working on, in this case a bug-fix for issue number `18132` that affects Umbraco v.15.
|
||||
This is a development branch for the particular issue you're working on, in this case, a bug-fix for issue number `18132` that affects Umbraco v.15.
|
||||
|
||||
Don't commit to `contrib`, create a new branch first.
|
||||
Don't commit to `main`, create a new branch first.
|
||||
|
||||
5. **Build or run a Development Server**
|
||||
|
||||
You can build or run a Development Server with any IDE that supports DotNet or the command line.
|
||||
You can build or run a Development Server with any IDE that supports .NET or the command line.
|
||||
|
||||
Read [Build or run a Development Server](BUILD.md) for the right approach to your needs.
|
||||
Read [Build or run a Development Server](BUILD.md) for the right approach to your needs.
|
||||
|
||||
6. **Change**
|
||||
|
||||
Make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](contributing-first-issue.md#questions).
|
||||
Make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](contributing-first-issue.md#questions).
|
||||
|
||||
7. **Commit and push**
|
||||
|
||||
Done? Yay! 🎉
|
||||
Done? Yay! 🎉
|
||||
|
||||
Remember to commit to your branch. When it's ready push the changes to your fork on GitHub.
|
||||
Remember to commit to your branch. When it's ready, push the changes to your fork on GitHub.
|
||||
|
||||
8. **Create pull request**
|
||||
|
||||
On GitHub, in your forked repository (`https://github.com/[YourUsername]/Umbraco-CMS`) you will see a banner saying that you pushed a new branch and a button to make a pull request. Tap the button and follow the instuctions.
|
||||
On GitHub, in your forked repository (`https://github.com/[YourUsername]/Umbraco-CMS`), you will see a banner saying that you pushed a new branch and a button to make a pull request. Tap the button and follow the instructions.
|
||||
|
||||
Want to read further? [Creating a pull request and what happens next](contributing-creating-a-pr.md).
|
||||
Would you like to read further? [Creating a pull request and what happens next](contributing-creating-a-pr.md).
|
||||
|
||||
## Further contribution guides
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ To declare the Published Cache Status Dashboard as a new manifest, we need to ad
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Condition.SectionAlias',
|
||||
alias: UMB_SECTION_ALIAS_CONDITION_ALIAS,
|
||||
match: 'Umb.Section.Settings',
|
||||
},
|
||||
],
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -20,7 +20,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && (contains(github.event.pull_request.labels.*.name, 'preview/backoffice')))
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/backoffice') && github.repository == github.event.pull_request.head.repo.full_name)
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and Deploy Job
|
||||
steps:
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
###### End of Repository/Build Configurations ######
|
||||
|
||||
close_pull_request_job:
|
||||
if: github.event_name == 'pull_request' && github.event.action == 'closed'
|
||||
if: github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/backoffice') && github.repository == github.event.pull_request.head.repo.full_name
|
||||
runs-on: ubuntu-latest
|
||||
name: Close Pull Request Job
|
||||
steps:
|
||||
|
||||
@@ -23,7 +23,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && (contains(github.event.pull_request.labels.*.name, 'preview/storybook')))
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/storybook') && github.repository == github.event.pull_request.head.repo.full_name)
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and Deploy Job
|
||||
steps:
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
###### End of Repository/Build Configurations ######
|
||||
|
||||
close_pull_request_job:
|
||||
if: github.event_name == 'pull_request' && github.event.action == 'closed'
|
||||
if: github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/storybook') && github.repository == github.event.pull_request.head.repo.full_name
|
||||
runs-on: ubuntu-latest
|
||||
name: Close Pull Request Job
|
||||
steps:
|
||||
|
||||
Vendored
+2
@@ -62,6 +62,7 @@
|
||||
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
|
||||
"stopAtEntry": false,
|
||||
"requireExactSource": false,
|
||||
"postDebugTask": "kill-umbraco-web-ui",
|
||||
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
@@ -96,6 +97,7 @@
|
||||
"stopAtEntry": false,
|
||||
"requireExactSource": false,
|
||||
"checkForDevCert": true,
|
||||
"postDebugTask": "kill-umbraco-web-ui",
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_URLS": "https://localhost:44339",
|
||||
|
||||
Vendored
+85
-74
@@ -1,76 +1,87 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build",
|
||||
"detail": "Builds the client and SLN",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"dependsOn": [
|
||||
"Client Build",
|
||||
"Dotnet build"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Client Install",
|
||||
"detail": "install npm for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"type": "npm",
|
||||
"script": "install",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Client Build",
|
||||
"detail": "runs npm run build for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "build:for:cms",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Client Watch",
|
||||
"detail": "runs npm run dev for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "dev",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Dotnet build",
|
||||
"detail": "Dotnet build of SLN",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/umbraco.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Dotnet watch",
|
||||
"detail": "Dotnet run and watch of Web.UI",
|
||||
"promptOnClose": true,
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build",
|
||||
"detail": "Builds the client and SLN",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"dependsOn": ["Client Build", "Dotnet build"],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Client Install",
|
||||
"detail": "install npm for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"type": "npm",
|
||||
"script": "install",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Client Build",
|
||||
"detail": "runs npm run build for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "build:for:cms",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Client Watch",
|
||||
"detail": "runs npm run dev for Umbraco.Web.UI.Client",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "dev",
|
||||
"path": "src/Umbraco.Web.UI.Client/",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Dotnet build",
|
||||
"detail": "Dotnet build of SLN",
|
||||
"promptOnClose": true,
|
||||
"group": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/umbraco.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Dotnet watch",
|
||||
"detail": "Dotnet run and watch of Web.UI",
|
||||
"promptOnClose": true,
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "kill-umbraco-web-ui",
|
||||
"type": "shell",
|
||||
"problemMatcher": [],
|
||||
"osx": {
|
||||
"command": "pkill -f Umbraco.Web.UI"
|
||||
},
|
||||
"linux": {
|
||||
"command": "pkill -f Umbraco.Web.UI"
|
||||
},
|
||||
"windows": {
|
||||
"command": "taskkill /IM Umbraco.Web.UI.exe /F"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
<!-- Package Validation -->
|
||||
<PropertyGroup>
|
||||
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
|
||||
<EnablePackageValidation>false</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>15.0.0</PackageValidationBaselineVersion>
|
||||
<EnablePackageValidation>true</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>16.0.0</PackageValidationBaselineVersion>
|
||||
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
|
||||
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -432,6 +432,33 @@ stages:
|
||||
displayName: Start SQL Server Docker image (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- powershell: |
|
||||
$maxAttempts = 12
|
||||
$attempt = 0
|
||||
$status = ""
|
||||
|
||||
while (($status -ne 'running') -and ($attempt -lt $maxAttempts)) {
|
||||
Start-Sleep -Seconds 5
|
||||
# We use the docker inspect command to check the status of the container. If the container is not running, we wait 5 seconds and try again. And if reaches 12 attempts, we fail the build.
|
||||
$status = docker inspect -f '{{.State.Status}}' mssql
|
||||
|
||||
if ($status -ne 'running') {
|
||||
Write-Host "Waiting for SQL Server to be ready... Attempt $($attempt + 1)"
|
||||
$attempt++
|
||||
}
|
||||
}
|
||||
|
||||
if ($status -eq 'running') {
|
||||
Write-Host "SQL Server container is running"
|
||||
docker ps -a
|
||||
} else {
|
||||
Write-Host "SQL Server did not become ready in time. Last known status: $status"
|
||||
docker logs mssql
|
||||
exit 1
|
||||
}
|
||||
displayName: Wait for SQL Server to be ready (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: SqlLocalDB start MSSQLLocalDB
|
||||
displayName: Start SQL Server LocalDB (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
@@ -540,7 +567,8 @@ stages:
|
||||
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
URL=$(ASPNETCORE_URLS)
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
|
||||
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
@@ -623,6 +651,14 @@ stages:
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Copy console error log
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/console-errors.json) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/console-errors.json $(Build.ArtifactStagingDirectory)
|
||||
}
|
||||
displayName: Copy console error log
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Publish test artifacts
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish test artifacts
|
||||
@@ -698,7 +734,8 @@ stages:
|
||||
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
URL=$(ASPNETCORE_URLS)
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
|
||||
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
@@ -799,6 +836,14 @@ stages:
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Copy console error log
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/console-errors.json) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/console-errors.json $(Build.ArtifactStagingDirectory)
|
||||
}
|
||||
displayName: Copy console error log
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Publish test artifacts
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish test artifacts
|
||||
|
||||
@@ -8,10 +8,17 @@ schedules:
|
||||
displayName: Daily midnight build
|
||||
branches:
|
||||
include:
|
||||
- v14/dev
|
||||
- v15/dev
|
||||
- release/16.0
|
||||
- main
|
||||
|
||||
parameters:
|
||||
# Skipped due to DB locks
|
||||
- name: sqliteAcceptanceTests
|
||||
displayName: Run SQLite Acceptance Tests
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
variables:
|
||||
nodeVersion: 20
|
||||
solution: umbraco.sln
|
||||
@@ -122,6 +129,7 @@ stages:
|
||||
# E2E Tests
|
||||
- job:
|
||||
displayName: E2E Tests (SQLite)
|
||||
condition: eq(${{parameters.sqliteAcceptanceTests}}, True)
|
||||
timeoutInMinutes: 180
|
||||
variables:
|
||||
# Connection string
|
||||
@@ -131,22 +139,22 @@ stages:
|
||||
matrix:
|
||||
LinuxPart1Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
LinuxPart2Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
LinuxPart3Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
@@ -172,7 +180,8 @@ stages:
|
||||
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
URL=$(ASPNETCORE_URLS)
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
|
||||
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
@@ -260,6 +269,14 @@ stages:
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Copy console error log
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/console-errors.json) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/console-errors.json $(Build.ArtifactStagingDirectory)
|
||||
}
|
||||
displayName: Copy console error log
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Publish
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish test artifacts
|
||||
@@ -288,29 +305,29 @@ stages:
|
||||
strategy:
|
||||
matrix:
|
||||
LinuxPart1Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
LinuxPart2Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
LinuxPart3Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
@@ -335,7 +352,8 @@ stages:
|
||||
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
URL=$(ASPNETCORE_URLS)
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
|
||||
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
@@ -441,6 +459,14 @@ stages:
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Copy console error log
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/console-errors.json) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/console-errors.json $(Build.ArtifactStagingDirectory)
|
||||
}
|
||||
displayName: Copy console error log
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Publish
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish test artifacts
|
||||
|
||||
@@ -18,7 +18,7 @@ public class ProcessRequestContextHandler
|
||||
var backOfficePathSegment = Constants.System.DefaultUmbracoPath.TrimStart(Constants.CharArrays.Tilde)
|
||||
.EnsureStartsWith('/')
|
||||
.EnsureEndsWith('/');
|
||||
_pathsToHandle = [backOfficePathSegment, "/.well-known/openid-configuration"];
|
||||
_pathsToHandle = [backOfficePathSegment, "/.well-known/openid-configuration", "/.well-known/jwks"];
|
||||
}
|
||||
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ProcessRequestContext context)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Caching;
|
||||
@@ -7,9 +8,13 @@ namespace Umbraco.Cms.Api.Delivery.Caching;
|
||||
internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
|
||||
{
|
||||
private readonly TimeSpan _duration;
|
||||
private readonly StringValues _varyByHeaderNames;
|
||||
|
||||
public DeliveryApiOutputCachePolicy(TimeSpan duration)
|
||||
=> _duration = duration;
|
||||
public DeliveryApiOutputCachePolicy(TimeSpan duration, StringValues varyByHeaderNames)
|
||||
{
|
||||
_duration = duration;
|
||||
_varyByHeaderNames = varyByHeaderNames;
|
||||
}
|
||||
|
||||
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -20,6 +25,7 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
|
||||
|
||||
context.EnableOutputCaching = requestPreviewService.IsPreview() is false;
|
||||
context.ResponseExpirationTimeSpan = _duration;
|
||||
context.CacheVaryByRules.HeaderNames = _varyByHeaderNames;
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Delivery.Accessors;
|
||||
using Umbraco.Cms.Api.Delivery.Caching;
|
||||
@@ -110,12 +111,20 @@ public static class UmbracoBuilderExtensions
|
||||
|
||||
if (outputCacheSettings.ContentDuration.TotalSeconds > 0)
|
||||
{
|
||||
options.AddPolicy(Constants.DeliveryApi.OutputCache.ContentCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.ContentDuration));
|
||||
options.AddPolicy(
|
||||
Constants.DeliveryApi.OutputCache.ContentCachePolicy,
|
||||
new DeliveryApiOutputCachePolicy(
|
||||
outputCacheSettings.ContentDuration,
|
||||
new StringValues([Constants.DeliveryApi.HeaderNames.AcceptLanguage, Constants.DeliveryApi.HeaderNames.AcceptSegment, Constants.DeliveryApi.HeaderNames.StartItem])));
|
||||
}
|
||||
|
||||
if (outputCacheSettings.MediaDuration.TotalSeconds > 0)
|
||||
{
|
||||
options.AddPolicy(Constants.DeliveryApi.OutputCache.MediaCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.MediaDuration));
|
||||
options.AddPolicy(
|
||||
Constants.DeliveryApi.OutputCache.MediaCachePolicy,
|
||||
new DeliveryApiOutputCachePolicy(
|
||||
outputCacheSettings.MediaDuration,
|
||||
Constants.DeliveryApi.HeaderNames.StartItem));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
@@ -21,7 +21,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Accept-Language",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptLanguage,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the language to return. Use this when querying language variant content items.",
|
||||
@@ -35,7 +35,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Accept-Segment",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptSegment,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the segment to return. Use this when querying segment variant content items.",
|
||||
@@ -51,7 +51,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Preview",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.Preview,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Whether to request draft content.",
|
||||
@@ -60,7 +60,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Start-Item",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.StartItem,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "URL segment or GUID of a root content item.",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
@@ -63,7 +63,7 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
protected void AddApiKey(OpenApiOperation operation) =>
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Api-Key",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.ApiKey,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
@@ -29,7 +29,7 @@ internal sealed class ApiAccessService : RequestHeaderHandler, IApiAccessService
|
||||
private bool IfEnabled(Func<bool> condition) => _deliveryApiSettings.Enabled && condition();
|
||||
|
||||
private bool HasValidApiKey() => _deliveryApiSettings.ApiKey.IsNullOrWhiteSpace() == false
|
||||
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue("Api-Key"));
|
||||
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.ApiKey));
|
||||
|
||||
private bool IfMediaEnabled(Func<bool> condition) => _deliveryApiSettings is { Enabled: true, Media.Enabled: true } && condition();
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@ internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPrev
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPreview() => string.Equals(GetHeaderValue("Preview"), "true", StringComparison.OrdinalIgnoreCase);
|
||||
public bool IsPreview() => string.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.Preview), "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
@@ -12,5 +12,5 @@ internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegm
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? GetRequestedSegment()
|
||||
=> GetHeaderValue("Accept-Segment");
|
||||
=> GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.AcceptSegment);
|
||||
}
|
||||
|
||||
@@ -60,5 +60,5 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? RequestedStartItem() => GetHeaderValue("Start-Item");
|
||||
public string? RequestedStartItem() => GetHeaderValue(Constants.DeliveryApi.HeaderNames.StartItem);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ internal abstract class RoutingServiceBase
|
||||
}
|
||||
|
||||
protected static string GetContentRoute(DomainAndUri domainAndUri, Uri contentRoute)
|
||||
=> $"{domainAndUri.ContentId}{DomainUtilities.PathRelativeToDomain(domainAndUri.Uri, contentRoute.AbsolutePath)}";
|
||||
=> $"{domainAndUri.ContentId}{DomainUtilities.PathRelativeToDomain(domainAndUri.Uri, contentRoute.LocalPath)}"; // Use LocalPath over AbsolutePath to keep the path decoded.
|
||||
|
||||
protected DomainAndUri? GetDomainAndUriForRoute(Uri contentUrl)
|
||||
{
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.Integration</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
|
||||
|
||||
public class SiblingsDataTypeTreeController : DataTypeTreeControllerBase
|
||||
{
|
||||
public SiblingsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
|
||||
: base(entityService, dataTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<DataTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
|
||||
=> GetSiblings(target, before, after);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -42,12 +42,16 @@ public class CopyDocumentController : DocumentControllerBase
|
||||
Guid id,
|
||||
CopyDocumentRequestModel copyDocumentRequestModel)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
AuthorizationResult sourceAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionCopy.ActionLetter, new[] { copyDocumentRequestModel.Target?.Id, id }),
|
||||
ContentPermissionResource.WithKeys(ActionCopy.ActionLetter, [id]),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
AuthorizationResult destinationAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionNew.ActionLetter, [copyDocumentRequestModel.Target?.Id]),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
if (sourceAuthorizationResult.Succeeded is false || destinationAuthorizationResult.Succeeded is false)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
+2
-1
@@ -28,12 +28,13 @@ public class SearchDocumentItemController : DocumentItemControllerBase
|
||||
CancellationToken cancellationToken,
|
||||
string query,
|
||||
bool? trashed = null,
|
||||
string? culture = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? parentId = null,
|
||||
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, skip, take);
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, culture, skip, take);
|
||||
var result = new PagedModel<DocumentItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModel),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -39,12 +39,16 @@ public class MoveDocumentController : DocumentControllerBase
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Move(CancellationToken cancellationToken, Guid id, MoveDocumentRequestModel moveDocumentRequestModel)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
AuthorizationResult sourceAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionMove.ActionLetter, new[] { moveDocumentRequestModel.Target?.Id, id }),
|
||||
ContentPermissionResource.WithKeys(ActionMove.ActionLetter, [id]),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
AuthorizationResult destinationAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionNew.ActionLetter, [moveDocumentRequestModel.Target?.Id]),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
if (sourceAuthorizationResult.Succeeded is false || destinationAuthorizationResult.Succeeded is false)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Services.Entities;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class SiblingsDocumentTreeController : DocumentTreeControllerBase
|
||||
{
|
||||
public SiblingsDocumentTreeController(
|
||||
IEntityService entityService,
|
||||
IUserStartNodeEntitiesService userStartNodeEntitiesService,
|
||||
IDataTypeService dataTypeService,
|
||||
IPublicAccessService publicAccessService,
|
||||
AppCaches appCaches,
|
||||
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
: base(
|
||||
entityService,
|
||||
userStartNodeEntitiesService,
|
||||
dataTypeService,
|
||||
publicAccessService,
|
||||
appCaches,
|
||||
backofficeSecurityAccessor,
|
||||
documentPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<DocumentTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
|
||||
=> GetSiblings(target, before, after);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Tree;
|
||||
|
||||
public class SiblingsDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase
|
||||
{
|
||||
public SiblingsDocumentBlueprintTreeController(IEntityService entityService, IDocumentPresentationFactory documentPresentationFactory)
|
||||
: base(entityService, documentPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DocumentBlueprintTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<DocumentBlueprintTreeItemResponseModel>>> Siblings(
|
||||
CancellationToken cancellationToken,
|
||||
Guid target,
|
||||
int before,
|
||||
int after) =>
|
||||
GetSiblings(target, before, after);
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Tree;
|
||||
|
||||
public class SiblingsDocumentTypeTreeController : DocumentTypeTreeControllerBase
|
||||
{
|
||||
public SiblingsDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService)
|
||||
: base(entityService, contentTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DocumentTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<DocumentTypeTreeItemResponseModel>>> Siblings(
|
||||
CancellationToken cancellationToken,
|
||||
Guid target,
|
||||
int before,
|
||||
int after) =>
|
||||
GetSiblings(target, before, after);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Features;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Cms.Web.Common.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers;
|
||||
|
||||
@@ -22,6 +23,7 @@ namespace Umbraco.Cms.Api.Management.Controllers;
|
||||
[MapToApi(ManagementApiConfiguration.ApiName)]
|
||||
[JsonOptionsName(Constants.JsonOptionsNames.BackOffice)]
|
||||
[AppendEventMessages]
|
||||
[DisableBrowserCache]
|
||||
[Produces("application/json")]
|
||||
public abstract class ManagementApiControllerBase : Controller, IUmbracoFeature
|
||||
{
|
||||
|
||||
@@ -21,17 +21,20 @@ public class SearchMediaItemController : MediaItemControllerBase
|
||||
_mediaPresentationFactory = mediaPresentationFactory;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
|
||||
public Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
=> SearchFromParentWithAllowedTypes(cancellationToken, query, null, skip, take, parentId);
|
||||
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<MediaItemResponseModel>), StatusCodes.Status200OK)]
|
||||
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)
|
||||
public async Task<IActionResult> SearchFromParentWithAllowedTypes(
|
||||
CancellationToken cancellationToken,
|
||||
string query,
|
||||
bool? trashed = null,
|
||||
string? culture = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? parentId = null,
|
||||
[FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, skip, take);
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, culture, skip, take);
|
||||
var result = new PagedModel<MediaItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IMediaEntitySlim>().Select(_mediaPresentationFactory.CreateItemResponseModel),
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Services.Entities;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
|
||||
|
||||
public class SiblingsMediaTreeController : MediaTreeControllerBase
|
||||
{
|
||||
public SiblingsMediaTreeController(
|
||||
IEntityService entityService,
|
||||
IUserStartNodeEntitiesService userStartNodeEntitiesService,
|
||||
IDataTypeService dataTypeService,
|
||||
AppCaches appCaches,
|
||||
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
||||
IMediaPresentationFactory mediaPresentationFactory)
|
||||
: base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor, mediaPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<MediaTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<MediaTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
|
||||
=> GetSiblings(target, before, after);
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Tree;
|
||||
|
||||
public class SiblingsMediaTypeTreeController : MediaTypeTreeControllerBase
|
||||
{
|
||||
public SiblingsMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService)
|
||||
: base(entityService, mediaTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<MediaTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<MediaTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
|
||||
=> GetSiblings(target, before, after);
|
||||
}
|
||||
+19
-7
@@ -1,8 +1,11 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Package;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Packaging;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -14,12 +17,25 @@ namespace Umbraco.Cms.Api.Management.Controllers.Package;
|
||||
public class AllMigrationStatusPackageController : PackageControllerBase
|
||||
{
|
||||
private readonly IPackagingService _packagingService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
private readonly IPackagePresentationFactory _packagePresentationFactory;
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V18.")]
|
||||
public AllMigrationStatusPackageController(IPackagingService packagingService, IUmbracoMapper umbracoMapper)
|
||||
: this(packagingService, StaticServiceProvider.Instance.GetRequiredService<IPackagePresentationFactory>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V18.")]
|
||||
public AllMigrationStatusPackageController(IPackagingService packagingService, IUmbracoMapper umbracoMapper, IPackagePresentationFactory packagePresentationFactory)
|
||||
: this(packagingService, packagePresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public AllMigrationStatusPackageController(IPackagingService packagingService, IPackagePresentationFactory packagePresentationFactory)
|
||||
{
|
||||
_packagingService = packagingService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
_packagePresentationFactory = packagePresentationFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -38,11 +54,7 @@ public class AllMigrationStatusPackageController : PackageControllerBase
|
||||
{
|
||||
PagedModel<InstalledPackage> migrationPlans = await _packagingService.GetInstalledPackagesFromMigrationPlansAsync(skip, take);
|
||||
|
||||
var viewModel = new PagedViewModel<PackageMigrationStatusResponseModel>
|
||||
{
|
||||
Total = migrationPlans.Total,
|
||||
Items = _umbracoMapper.MapEnumerable<InstalledPackage, PackageMigrationStatusResponseModel>(migrationPlans.Items)
|
||||
};
|
||||
PagedViewModel<PackageMigrationStatusResponseModel> viewModel = _packagePresentationFactory.CreatePackageMigrationStatusResponseModel(migrationPlans);
|
||||
|
||||
return Ok(viewModel);
|
||||
}
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
|
||||
[HttpPost("rebuild")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Rebuild(CancellationToken cancellationToken)
|
||||
public Task<IActionResult> Rebuild(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_databaseCacheRebuilder.IsRebuilding())
|
||||
{
|
||||
@@ -27,10 +27,10 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
|
||||
Type = "Error",
|
||||
};
|
||||
|
||||
return await Task.FromResult(Conflict(problemDetails));
|
||||
return Task.FromResult<IActionResult>(Conflict(problemDetails));
|
||||
}
|
||||
|
||||
_databaseCacheRebuilder.Rebuild(true);
|
||||
return await Task.FromResult(Ok());
|
||||
return Task.FromResult<IActionResult>(Ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
@@ -9,12 +8,10 @@ using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Segment;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)]
|
||||
public class AllSegmentController : SegmentControllerBase
|
||||
{
|
||||
private readonly ISegmentService _segmentService;
|
||||
@@ -30,9 +27,9 @@ public class AllSegmentController : SegmentControllerBase
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<SegmentResponseModel>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> GetAll(CancellationToken cancellationToken, int skip = 0, int take = 100)
|
||||
public async Task<IActionResult> GetAll(CancellationToken cancellationToken, int skip = 0, int take = 100, Guid? documentId = null, Guid? documentTypeId = null)
|
||||
{
|
||||
Attempt<PagedModel<Core.Models.Segment>?, SegmentOperationStatus> pagedAttempt = await _segmentService.GetPagedSegmentsAsync(skip, take);
|
||||
Attempt<PagedModel<Core.Models.Segment>?, SegmentOperationStatus> pagedAttempt = await _segmentService.GetPagedSegmentsAsync(skip, take, documentId, documentTypeId);
|
||||
|
||||
if (pagedAttempt.Success is false)
|
||||
{
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Template.Tree;
|
||||
|
||||
public class SiblingsTemplateTreeController : TemplateTreeControllerBase
|
||||
{
|
||||
public SiblingsTemplateTreeController(IEntityService entityService)
|
||||
: base(entityService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<NamedEntityTreeItemResponseModel>>> Siblings(
|
||||
CancellationToken cancellationToken,
|
||||
Guid target,
|
||||
int before,
|
||||
int after) =>
|
||||
GetSiblings(target, before, after);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
@@ -44,6 +44,23 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
|
||||
return Task.FromResult<ActionResult<PagedViewModel<TItem>>>(Ok(result));
|
||||
}
|
||||
|
||||
protected Task<ActionResult<IEnumerable<TItem>>> GetSiblings(Guid target, int before, int after)
|
||||
{
|
||||
IEntitySlim[] siblings = EntityService.GetSiblings(target, ItemObjectType, before, after, ItemOrdering).ToArray();
|
||||
if (siblings.Length == 0)
|
||||
{
|
||||
return Task.FromResult<ActionResult<IEnumerable<TItem>>>(NotFound());
|
||||
}
|
||||
|
||||
IEntitySlim? entity = siblings.FirstOrDefault();
|
||||
Guid? parentKey = entity?.ParentId > 0
|
||||
? EntityService.GetKey(entity.ParentId, ItemObjectType).Result
|
||||
: Constants.System.RootKey;
|
||||
|
||||
TItem[] treeItemsViewModels = MapTreeItemViewModels(parentKey, siblings);
|
||||
return Task.FromResult<ActionResult<IEnumerable<TItem>>>(Ok(treeItemsViewModels));
|
||||
}
|
||||
|
||||
protected virtual async Task<ActionResult<IEnumerable<TItem>>> GetAncestors(Guid descendantKey, bool includeSelf = true)
|
||||
{
|
||||
IEntitySlim[] ancestorEntities = await GetAncestorEntitiesAsync(descendantKey, includeSelf);
|
||||
|
||||
@@ -43,7 +43,7 @@ internal static class ApplicationBuilderExtensions
|
||||
|
||||
var response = new ProblemDetails
|
||||
{
|
||||
Title = exception.Message,
|
||||
Title = isDebug ? exception.Message : "Server Error",
|
||||
Detail = isDebug ? exception.StackTrace : null,
|
||||
Status = statusCode ?? StatusCodes.Status500InternalServerError,
|
||||
Instance = isDebug ? exception.GetType().Name : null,
|
||||
|
||||
+5
@@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.NotificationHandlers;
|
||||
using Umbraco.Cms.Api.Management.Security;
|
||||
using Umbraco.Cms.Api.Management.Services;
|
||||
using Umbraco.Cms.Api.Management.Telemetry;
|
||||
@@ -12,6 +13,7 @@ using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Net;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
@@ -74,6 +76,9 @@ public static partial class UmbracoBuilderExtensions
|
||||
|
||||
services.AddScoped<IBackOfficeExternalLoginService, BackOfficeExternalLoginService>();
|
||||
|
||||
// Register a notification handler to interrogate the registered external login providers at startup.
|
||||
builder.AddNotificationHandler<UmbracoApplicationStartingNotification, ExternalLoginProviderStartupHandler>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
@@ -19,17 +20,26 @@ internal sealed class DocumentVersionPresentationFactory : IDocumentVersionPrese
|
||||
_userIdKeyResolver = userIdKeyResolver;
|
||||
}
|
||||
|
||||
public async Task<DocumentVersionItemResponseModel> CreateAsync(ContentVersionMeta contentVersion) =>
|
||||
new(
|
||||
public async Task<DocumentVersionItemResponseModel> CreateAsync(ContentVersionMeta contentVersion)
|
||||
{
|
||||
ReferenceByIdModel userReference = contentVersion.UserId switch
|
||||
{
|
||||
Constants.Security.UnknownUserId => new ReferenceByIdModel(),
|
||||
_ => new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)),
|
||||
};
|
||||
|
||||
return new DocumentVersionItemResponseModel(
|
||||
contentVersion.VersionId.ToGuid(), // this is a magic guid since versions do not have keys in the DB
|
||||
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentId, UmbracoObjectTypes.Document).Result),
|
||||
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType)
|
||||
.Result),
|
||||
new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)),
|
||||
new ReferenceByIdModel(
|
||||
_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType)
|
||||
.Result),
|
||||
userReference,
|
||||
new DateTimeOffset(contentVersion.VersionDate),
|
||||
contentVersion.CurrentPublishedVersion,
|
||||
contentVersion.CurrentDraftVersion,
|
||||
contentVersion.PreventCleanup);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DocumentVersionItemResponseModel>> CreateMultipleAsync(IEnumerable<ContentVersionMeta> contentVersions)
|
||||
=> await CreateMultipleImplAsync(contentVersions).ToArrayAsync();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Package;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Packaging;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
@@ -8,4 +10,6 @@ public interface IPackagePresentationFactory
|
||||
PackageDefinition CreatePackageDefinition(CreatePackageRequestModel createPackageRequestModel);
|
||||
|
||||
PackageConfigurationResponseModel CreateConfigurationResponseModel();
|
||||
|
||||
PagedViewModel<PackageMigrationStatusResponseModel> CreatePackageMigrationStatusResponseModel(PagedModel<InstalledPackage> installedPackages) => new();
|
||||
}
|
||||
|
||||
@@ -6,25 +6,58 @@ using Umbraco.Cms.Core.Models.Membership;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
/// <summary>
|
||||
/// Defines factory methods for the creation of user presentation models.
|
||||
/// </summary>
|
||||
public interface IUserPresentationFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a response model for the provided user.
|
||||
/// </summary>
|
||||
UserResponseModel CreateResponseModel(IUser user);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a create model for a user based on the provided request model.
|
||||
/// </summary>
|
||||
Task<UserCreateModel> CreateCreationModelAsync(CreateUserRequestModel requestModel);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an invite model for a user based on the provided request model.
|
||||
/// </summary>
|
||||
Task<UserInviteModel> CreateInviteModelAsync(InviteUserRequestModel requestModel);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an update model for an existing user based on the provided request model.
|
||||
/// </summary>
|
||||
Task<UserUpdateModel> CreateUpdateModelAsync(Guid existingUserKey, UpdateUserRequestModel updateModel);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a response model for the current user based on the provided user.
|
||||
/// </summary>
|
||||
Task<CurrentUserResponseModel> CreateCurrentUserResponseModelAsync(IUser user);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an resend invite model for a user based on the provided request model.
|
||||
/// </summary>
|
||||
Task<UserResendInviteModel> CreateResendInviteModelAsync(ResendInviteUserRequestModel requestModel);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a user configuration model that contains the necessary data for user management operations.
|
||||
/// </summary>
|
||||
Task<UserConfigurationResponseModel> CreateUserConfigurationModelAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a current user configuration model that contains the necessary data for the current user's management operations.
|
||||
/// </summary>
|
||||
Task<CurrentUserConfigurationResponseModel> CreateCurrentUserConfigurationModelAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a user item response model for the provided user.
|
||||
/// </summary>
|
||||
UserItemResponseModel CreateItemResponseModel(IUser user);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a calculated user start nodes response model based on the provided user.
|
||||
/// </summary>
|
||||
Task<CalculatedUserStartNodesResponseModel> CreateCalculatedUserStartNodesResponseModelAsync(IUser user);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Package;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Packaging;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
@@ -42,6 +44,25 @@ internal class PackagePresentationFactory : IPackagePresentationFactory
|
||||
MarketplaceUrl = GetMarketplaceUrl(),
|
||||
};
|
||||
|
||||
public PagedViewModel<PackageMigrationStatusResponseModel> CreatePackageMigrationStatusResponseModel(PagedModel<InstalledPackage> installedPackages)
|
||||
{
|
||||
InstalledPackage[] installedPackagesAsArray = installedPackages.Items as InstalledPackage[] ?? installedPackages.Items.ToArray();
|
||||
|
||||
return new PagedViewModel<PackageMigrationStatusResponseModel>
|
||||
{
|
||||
Total = installedPackages.Total,
|
||||
Items = installedPackagesAsArray
|
||||
.GroupBy(package => package.PackageName)
|
||||
.Select(packages => packages.OrderByDescending(package => package.HasPendingMigrations).First())
|
||||
.Select(package => new PackageMigrationStatusResponseModel
|
||||
{
|
||||
PackageName = package.PackageName ?? string.Empty,
|
||||
HasPendingMigrations = package.HasPendingMigrations,
|
||||
})
|
||||
.ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
private string GetMarketplaceUrl()
|
||||
{
|
||||
var uriBuilder = new UriBuilder(Constants.Marketplace.Url);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.Mapping.Permissions;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.Security;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
@@ -22,6 +23,9 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating user presentation models, implementing <see cref="IUserPresentationFactory"/>.
|
||||
/// </summary>
|
||||
public class UserPresentationFactory : IUserPresentationFactory
|
||||
{
|
||||
private readonly IEntityService _entityService;
|
||||
@@ -34,9 +38,11 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
private readonly IPasswordConfigurationPresentationFactory _passwordConfigurationPresentationFactory;
|
||||
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
|
||||
private readonly SecuritySettings _securitySettings;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IContentService _contentService;
|
||||
private readonly Dictionary<Type, IPermissionPresentationMapper> _permissionPresentationMappersByType;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
public UserPresentationFactory(
|
||||
IEntityService entityService,
|
||||
@@ -50,7 +56,7 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
IOptionsSnapshot<SecuritySettings> securitySettings,
|
||||
IBackOfficeExternalLoginProviders externalLoginProviders)
|
||||
: this(
|
||||
entityService,
|
||||
entityService,
|
||||
appCaches,
|
||||
mediaFileManager,
|
||||
imageUrlGenerator,
|
||||
@@ -61,10 +67,15 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
securitySettings,
|
||||
externalLoginProviders,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUserService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IContentService>())
|
||||
StaticServiceProvider.Instance.GetRequiredService<IContentService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IEnumerable<IPermissionPresentationMapper>>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
public UserPresentationFactory(
|
||||
IEntityService entityService,
|
||||
AppCaches appCaches,
|
||||
@@ -78,6 +89,44 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
IBackOfficeExternalLoginProviders externalLoginProviders,
|
||||
IUserService userService,
|
||||
IContentService contentService)
|
||||
: this(
|
||||
entityService,
|
||||
appCaches,
|
||||
mediaFileManager,
|
||||
imageUrlGenerator,
|
||||
userGroupPresentationFactory,
|
||||
absoluteUrlBuilder,
|
||||
emailSender,
|
||||
passwordConfigurationPresentationFactory,
|
||||
securitySettings,
|
||||
externalLoginProviders,
|
||||
userService,
|
||||
contentService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IEnumerable<IPermissionPresentationMapper>>())
|
||||
{
|
||||
}
|
||||
|
||||
// TODO (V17): Remove the unused userService and contentService parameters from this constructor.
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
|
||||
/// </summary>
|
||||
public UserPresentationFactory(
|
||||
IEntityService entityService,
|
||||
AppCaches appCaches,
|
||||
MediaFileManager mediaFileManager,
|
||||
IImageUrlGenerator imageUrlGenerator,
|
||||
IUserGroupPresentationFactory userGroupPresentationFactory,
|
||||
IAbsoluteUrlBuilder absoluteUrlBuilder,
|
||||
IEmailSender emailSender,
|
||||
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
|
||||
IOptionsSnapshot<SecuritySettings> securitySettings,
|
||||
IBackOfficeExternalLoginProviders externalLoginProviders,
|
||||
#pragma warning disable IDE0060 // Remove unused parameter - need to keep these until the next major to avoid breaking changes and/or ambiguous constructor errors
|
||||
IUserService userService,
|
||||
IContentService contentService,
|
||||
#pragma warning restore IDE0060 // Remove unused parameter
|
||||
IEnumerable<IPermissionPresentationMapper> permissionPresentationMappers)
|
||||
{
|
||||
_entityService = entityService;
|
||||
_appCaches = appCaches;
|
||||
@@ -89,10 +138,10 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
_externalLoginProviders = externalLoginProviders;
|
||||
_securitySettings = securitySettings.Value;
|
||||
_absoluteUrlBuilder = absoluteUrlBuilder;
|
||||
_userService = userService;
|
||||
_contentService = contentService;
|
||||
_permissionPresentationMappersByType = permissionPresentationMappers.ToDictionary(x => x.PresentationModelToHandle);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UserResponseModel CreateResponseModel(IUser user)
|
||||
{
|
||||
var responseModel = new UserResponseModel
|
||||
@@ -123,6 +172,7 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
return responseModel;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UserItemResponseModel CreateItemResponseModel(IUser user) =>
|
||||
new()
|
||||
{
|
||||
@@ -130,9 +180,10 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
Name = user.Name ?? user.Username,
|
||||
AvatarUrls = user.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator)
|
||||
.Select(url => _absoluteUrlBuilder.ToAbsoluteUrl(url).ToString()),
|
||||
Kind = user.Kind
|
||||
Kind = user.Kind,
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<UserCreateModel> CreateCreationModelAsync(CreateUserRequestModel requestModel)
|
||||
{
|
||||
var createModel = new UserCreateModel
|
||||
@@ -142,12 +193,13 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
Name = requestModel.Name,
|
||||
UserName = requestModel.UserName,
|
||||
UserGroupKeys = requestModel.UserGroupIds.Select(x => x.Id).ToHashSet(),
|
||||
Kind = requestModel.Kind
|
||||
Kind = requestModel.Kind,
|
||||
};
|
||||
|
||||
return Task.FromResult(createModel);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<UserInviteModel> CreateInviteModelAsync(InviteUserRequestModel requestModel)
|
||||
{
|
||||
var inviteModel = new UserInviteModel
|
||||
@@ -162,6 +214,7 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
return Task.FromResult(inviteModel);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<UserResendInviteModel> CreateResendInviteModelAsync(ResendInviteUserRequestModel requestModel)
|
||||
{
|
||||
var inviteModel = new UserResendInviteModel
|
||||
@@ -173,6 +226,7 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
return Task.FromResult(inviteModel);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<CurrentUserConfigurationResponseModel> CreateCurrentUserConfigurationModelAsync()
|
||||
{
|
||||
var model = new CurrentUserConfigurationResponseModel
|
||||
@@ -188,6 +242,7 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
return Task.FromResult(model);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<UserConfigurationResponseModel> CreateUserConfigurationModelAsync() =>
|
||||
Task.FromResult(new UserConfigurationResponseModel
|
||||
{
|
||||
@@ -201,6 +256,7 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
AllowTwoFactor = _externalLoginProviders.HasDenyLocalLogin() is false,
|
||||
});
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<UserUpdateModel> CreateUpdateModelAsync(Guid existingUserKey, UpdateUserRequestModel updateModel)
|
||||
{
|
||||
var model = new UserUpdateModel
|
||||
@@ -214,24 +270,24 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
HasContentRootAccess = updateModel.HasDocumentRootAccess,
|
||||
MediaStartNodeKeys = updateModel.MediaStartNodeIds.Select(x => x.Id).ToHashSet(),
|
||||
HasMediaRootAccess = updateModel.HasMediaRootAccess,
|
||||
UserGroupKeys = updateModel.UserGroupIds.Select(x => x.Id).ToHashSet()
|
||||
};
|
||||
|
||||
model.UserGroupKeys = updateModel.UserGroupIds.Select(x => x.Id).ToHashSet();
|
||||
|
||||
return Task.FromResult(model);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<CurrentUserResponseModel> CreateCurrentUserResponseModelAsync(IUser user)
|
||||
{
|
||||
var presentationUser = CreateResponseModel(user);
|
||||
var presentationGroups = await _userGroupPresentationFactory.CreateMultipleAsync(user.Groups);
|
||||
UserResponseModel presentationUser = CreateResponseModel(user);
|
||||
IEnumerable<UserGroupResponseModel> presentationGroups = await _userGroupPresentationFactory.CreateMultipleAsync(user.Groups);
|
||||
var languages = presentationGroups.SelectMany(x => x.Languages).Distinct().ToArray();
|
||||
var mediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
|
||||
var mediaStartNodeKeys = GetKeysFromIds(mediaStartNodeIds, UmbracoObjectTypes.Media);
|
||||
ISet<ReferenceByIdModel> mediaStartNodeKeys = GetKeysFromIds(mediaStartNodeIds, UmbracoObjectTypes.Media);
|
||||
var contentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches);
|
||||
var documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document);
|
||||
ISet<ReferenceByIdModel> documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document);
|
||||
|
||||
var permissions = GetAggregatedGranularPermissions(user, presentationGroups);
|
||||
HashSet<IPermissionPresentationModel> permissions = GetAggregatedGranularPermissions(user, presentationGroups);
|
||||
var fallbackPermissions = presentationGroups.SelectMany(x => x.FallbackPermissions).ToHashSet();
|
||||
|
||||
var hasAccessToAllLanguages = presentationGroups.Any(x => x.HasAccessToAllLanguages);
|
||||
@@ -263,70 +319,56 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
|
||||
private HashSet<IPermissionPresentationModel> GetAggregatedGranularPermissions(IUser user, IEnumerable<UserGroupResponseModel> presentationGroups)
|
||||
{
|
||||
var aggregatedPermissions = new HashSet<IPermissionPresentationModel>();
|
||||
|
||||
var permissions = presentationGroups.SelectMany(x => x.Permissions).ToHashSet();
|
||||
return GetAggregatedGranularPermissions(user, permissions);
|
||||
}
|
||||
|
||||
AggregateAndAddDocumentPermissions(user, aggregatedPermissions, permissions);
|
||||
private HashSet<IPermissionPresentationModel> GetAggregatedGranularPermissions(IUser user, HashSet<IPermissionPresentationModel> permissions)
|
||||
{
|
||||
// The raw permission data consists of several permissions for each entity (e.g. document), as permissions are assigned to user groups
|
||||
// and a user may be part of multiple groups. We want to aggregate this server-side so we return one set of aggregate permissions per
|
||||
// entity that the client will use.
|
||||
// We need to handle here not just permissions known to core (e.g. document and document property value permissions), but also custom
|
||||
// permissions defined by packages or implemetors.
|
||||
IEnumerable<(Type, IEnumerable<IPermissionPresentationModel>)> permissionModelsByType = permissions
|
||||
.GroupBy(x => x.GetType())
|
||||
.Select(x => (x.Key, x.Select(y => y)));
|
||||
|
||||
AggregateAndAddDocumentPropertyValuePermissions(aggregatedPermissions, permissions);
|
||||
var aggregatedPermissions = new HashSet<IPermissionPresentationModel>();
|
||||
foreach ((Type Type, IEnumerable<IPermissionPresentationModel> Models) permissionModelByType in permissionModelsByType)
|
||||
{
|
||||
if (_permissionPresentationMappersByType.TryGetValue(permissionModelByType.Type, out IPermissionPresentationMapper? mapper))
|
||||
{
|
||||
|
||||
IEnumerable<IPermissionPresentationModel> aggregatedModels = mapper.AggregatePresentationModels(user, permissionModelByType.Models);
|
||||
foreach (IPermissionPresentationModel aggregatedModel in aggregatedModels)
|
||||
{
|
||||
aggregatedPermissions.Add(aggregatedModel);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
IEnumerable<(string Context, ISet<string> Verbs)> groupedModels = permissionModelByType.Models
|
||||
.Where(x => x is UnknownTypePermissionPresentationModel)
|
||||
.Cast<UnknownTypePermissionPresentationModel>()
|
||||
.GroupBy(x => x.Context)
|
||||
.Select(x => (x.Key, (ISet<string>)x.SelectMany(y => y.Verbs).Distinct().ToHashSet()));
|
||||
|
||||
foreach ((string context, ISet<string> verbs) in groupedModels)
|
||||
{
|
||||
aggregatedPermissions.Add(new UnknownTypePermissionPresentationModel
|
||||
{
|
||||
Context = context,
|
||||
Verbs = verbs
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return aggregatedPermissions;
|
||||
}
|
||||
|
||||
private void AggregateAndAddDocumentPermissions(IUser user, HashSet<IPermissionPresentationModel> aggregatedPermissions, HashSet<IPermissionPresentationModel> permissions)
|
||||
{
|
||||
// The raw permission data consists of several permissions for each document. We want to aggregate this server-side so
|
||||
// we return one set of aggregate permissions per document that the client will use.
|
||||
|
||||
// Get the unique document keys that have granular permissions.
|
||||
IEnumerable<Guid> documentKeysWithGranularPermissions = permissions
|
||||
.Where(x => x is DocumentPermissionPresentationModel)
|
||||
.Cast<DocumentPermissionPresentationModel>()
|
||||
.Select(x => x.Document.Id)
|
||||
.Distinct();
|
||||
|
||||
foreach (Guid documentKey in documentKeysWithGranularPermissions)
|
||||
{
|
||||
// Retrieve the path of the document.
|
||||
var path = _contentService.GetById(documentKey)?.Path;
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// With the path we can call the same logic as used server-side for authorizing access to resources.
|
||||
EntityPermissionSet permissionsForPath = _userService.GetPermissionsForPath(user, path);
|
||||
aggregatedPermissions.Add(new DocumentPermissionPresentationModel
|
||||
{
|
||||
Document = new ReferenceByIdModel(documentKey),
|
||||
Verbs = permissionsForPath.GetAllPermissions()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void AggregateAndAddDocumentPropertyValuePermissions(HashSet<IPermissionPresentationModel> aggregatedPermissions, HashSet<IPermissionPresentationModel> permissions)
|
||||
{
|
||||
// We also have permissions for document type/property type combinations.
|
||||
// These don't have an ancestor relationship that we need to take into account, but should be aggregated
|
||||
// and included in the set.
|
||||
IEnumerable<((Guid DocumentTypeId, Guid PropertyTypeId) Key, ISet<string> Verbs)> documentTypePropertyTypeKeysWithGranularPermissions = permissions
|
||||
.Where(x => x is DocumentPropertyValuePermissionPresentationModel)
|
||||
.Cast<DocumentPropertyValuePermissionPresentationModel>()
|
||||
.GroupBy(x => (x.DocumentType.Id, x.PropertyType.Id))
|
||||
.Select(x => (x.Key, (ISet<string>)x.SelectMany(y => y.Verbs).Distinct().ToHashSet()));
|
||||
|
||||
foreach (((Guid DocumentTypeId, Guid PropertyTypeId) Key, ISet<string> Verbs) documentTypePropertyTypeKey in documentTypePropertyTypeKeysWithGranularPermissions)
|
||||
{
|
||||
aggregatedPermissions.Add(new DocumentPropertyValuePermissionPresentationModel
|
||||
{
|
||||
DocumentType = new ReferenceByIdModel(documentTypePropertyTypeKey.Key.DocumentTypeId),
|
||||
PropertyType = new ReferenceByIdModel(documentTypePropertyTypeKey.Key.PropertyTypeId),
|
||||
Verbs = documentTypePropertyTypeKey.Verbs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<CalculatedUserStartNodesResponseModel> CreateCalculatedUserStartNodesResponseModelAsync(IUser user)
|
||||
{
|
||||
var mediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
|
||||
@@ -357,6 +399,6 @@ public class UserPresentationFactory : IUserPresentationFactory
|
||||
: new HashSet<ReferenceByIdModel>(models);
|
||||
}
|
||||
|
||||
private bool HasRootAccess(IEnumerable<int>? startNodeIds)
|
||||
private static bool HasRootAccess(IEnumerable<int>? startNodeIds)
|
||||
=> startNodeIds?.Contains(Constants.System.Root) is true;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ public class PackageViewModelMapDefinition : IMapDefinition
|
||||
}
|
||||
|
||||
// Umbraco.Code.MapAll
|
||||
[Obsolete("Please use the IPackagePresentationFactory instead. Scheduled for removal in V18.")]
|
||||
private static void Map(InstalledPackage source, PackageMigrationStatusResponseModel target, MapperContext context)
|
||||
{
|
||||
if (source.PackageName is not null)
|
||||
|
||||
@@ -1,19 +1,50 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Models.Membership.Permissions;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Mapping.Permissions;
|
||||
|
||||
/// <summary>
|
||||
/// Mapping required for mapping all the way from viewmodel to database and back.
|
||||
/// Implements <see cref="IPermissionPresentationMapper" /> for document permissions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This mapping maps all the way from management api to database in one file intentionally, so it is very clear what it takes, if we wanna add permissions to media or other types in the future.
|
||||
/// </remarks>
|
||||
public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissionMapper
|
||||
{
|
||||
private readonly Lazy<IEntityService> _entityService;
|
||||
private readonly Lazy<IUserService> _userService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DocumentPermissionMapper"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
public DocumentPermissionMapper()
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<Lazy<IEntityService>>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<Lazy<IUserService>>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DocumentPermissionMapper"/> class.
|
||||
/// </summary>
|
||||
public DocumentPermissionMapper(Lazy<IEntityService> entityService, Lazy<IUserService> userService)
|
||||
{
|
||||
_entityService = entityService;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Context => DocumentGranularPermission.ContextType;
|
||||
|
||||
public IGranularPermission MapFromDto(UserGroup2GranularPermissionDto dto) =>
|
||||
new DocumentGranularPermission()
|
||||
{
|
||||
@@ -21,8 +52,10 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
|
||||
Permission = dto.Permission,
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Type PresentationModelToHandle => typeof(DocumentPermissionPresentationModel);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<IPermissionPresentationModel> MapManyAsync(IEnumerable<IGranularPermission> granularPermissions)
|
||||
{
|
||||
IEnumerable<IGrouping<Guid?, IGranularPermission>> keyGroups = granularPermissions.GroupBy(x => x.Key);
|
||||
@@ -40,6 +73,7 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<IGranularPermission> MapToGranularPermissions(IPermissionPresentationModel permissionViewModel)
|
||||
{
|
||||
if (permissionViewModel is not DocumentPermissionPresentationModel documentPermissionPresentationModel)
|
||||
@@ -47,7 +81,7 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
|
||||
yield break;
|
||||
}
|
||||
|
||||
if(documentPermissionPresentationModel.Verbs.Any() is false || (documentPermissionPresentationModel.Verbs.Count == 1 && documentPermissionPresentationModel.Verbs.Contains(string.Empty)))
|
||||
if (documentPermissionPresentationModel.Verbs.Any() is false || (documentPermissionPresentationModel.Verbs.Count == 1 && documentPermissionPresentationModel.Verbs.Contains(string.Empty)))
|
||||
{
|
||||
yield return new DocumentGranularPermission
|
||||
{
|
||||
@@ -56,12 +90,14 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
|
||||
};
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var verb in documentPermissionPresentationModel.Verbs)
|
||||
{
|
||||
if (string.IsNullOrEmpty(verb))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new DocumentGranularPermission
|
||||
{
|
||||
Key = documentPermissionPresentationModel.Document.Id,
|
||||
@@ -69,4 +105,37 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<IPermissionPresentationModel> AggregatePresentationModels(IUser user, IEnumerable<IPermissionPresentationModel> models)
|
||||
{
|
||||
// Get the unique document keys that have granular permissions.
|
||||
Guid[] documentKeysWithGranularPermissions = models
|
||||
.Cast<DocumentPermissionPresentationModel>()
|
||||
.Select(x => x.Document.Id)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
// Batch retrieve all documents by their keys.
|
||||
var documents = _entityService.Value.GetAll<IContent>(documentKeysWithGranularPermissions)
|
||||
.ToDictionary(doc => doc.Key, doc => doc.Path);
|
||||
|
||||
// Iterate through each document key that has granular permissions.
|
||||
foreach (Guid documentKey in documentKeysWithGranularPermissions)
|
||||
{
|
||||
// Retrieve the path from the pre-fetched documents.
|
||||
if (!documents.TryGetValue(documentKey, out var path) || string.IsNullOrEmpty(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// With the path we can call the same logic as used server-side for authorizing access to resources.
|
||||
EntityPermissionSet permissionsForPath = _userService.Value.GetPermissionsForPath(user, path);
|
||||
yield return new DocumentPermissionPresentationModel
|
||||
{
|
||||
Document = new ReferenceByIdModel(documentKey),
|
||||
Verbs = permissionsForPath.GetAllPermissions(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-3
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Models.Membership.Permissions;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
|
||||
@@ -7,10 +8,18 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Mapping.Permissions;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="IPermissionPresentationMapper" /> for document property value permissions.
|
||||
/// </summary>
|
||||
public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapper, IPermissionMapper
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Context => DocumentPropertyValueGranularPermission.ContextType;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Type PresentationModelToHandle => typeof(DocumentPropertyValuePermissionPresentationModel);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IGranularPermission MapFromDto(UserGroup2GranularPermissionDto dto) =>
|
||||
new DocumentPropertyValueGranularPermission()
|
||||
{
|
||||
@@ -18,8 +27,7 @@ public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapp
|
||||
Permission = dto.Permission,
|
||||
};
|
||||
|
||||
public Type PresentationModelToHandle => typeof(DocumentPropertyValuePermissionPresentationModel);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<IPermissionPresentationModel> MapManyAsync(IEnumerable<IGranularPermission> granularPermissions)
|
||||
{
|
||||
var intermediate = granularPermissions.Where(p => p.Key.HasValue).Select(p =>
|
||||
@@ -50,6 +58,7 @@ public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapp
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<IGranularPermission> MapToGranularPermissions(IPermissionPresentationModel permissionViewModel)
|
||||
{
|
||||
if (permissionViewModel is not DocumentPropertyValuePermissionPresentationModel documentTypePermissionPresentationModel)
|
||||
@@ -66,4 +75,23 @@ public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapp
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<IPermissionPresentationModel> AggregatePresentationModels(IUser user, IEnumerable<IPermissionPresentationModel> models)
|
||||
{
|
||||
IEnumerable<((Guid DocumentTypeId, Guid PropertyTypeId) Key, ISet<string> Verbs)> groupedModels = models
|
||||
.Cast<DocumentPropertyValuePermissionPresentationModel>()
|
||||
.GroupBy(x => (x.DocumentType.Id, x.PropertyType.Id))
|
||||
.Select(x => (x.Key, (ISet<string>)x.SelectMany(y => y.Verbs).Distinct().ToHashSet()));
|
||||
|
||||
foreach (((Guid DocumentTypeId, Guid PropertyTypeId) key, ISet<string> verbs) in groupedModels)
|
||||
{
|
||||
yield return new DocumentPropertyValuePermissionPresentationModel
|
||||
{
|
||||
DocumentType = new ReferenceByIdModel(key.DocumentTypeId),
|
||||
PropertyType = new ReferenceByIdModel(key.PropertyTypeId),
|
||||
Verbs = verbs
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Models.Membership.Permissions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Mapping.Permissions;
|
||||
|
||||
/// <summary>
|
||||
/// Defines methods for mapping and aggregating granular permissions to presentation models.
|
||||
/// </summary>
|
||||
public interface IPermissionPresentationMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the context type for the permissions being handled by this mapper.
|
||||
/// </summary>
|
||||
string Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the presentation model that this mapper handles.
|
||||
/// </summary>
|
||||
Type PresentationModelToHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maps a granular permission entity to a granular permission model.
|
||||
/// </summary>
|
||||
IEnumerable<IPermissionPresentationModel> MapManyAsync(IEnumerable<IGranularPermission> granularPermissions);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a granular permission to a granular permission model.
|
||||
/// </summary>
|
||||
IEnumerable<IGranularPermission> MapToGranularPermissions(IPermissionPresentationModel permissionViewModel);
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates multiple permission presentation models into a collection containing only one item per entity with aggregated permissions.
|
||||
/// </summary>
|
||||
IEnumerable<IPermissionPresentationModel> AggregatePresentationModels(IUser user, IEnumerable<IPermissionPresentationModel> models) => [];
|
||||
}
|
||||
|
||||
+9
-1
@@ -65,13 +65,21 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware
|
||||
return;
|
||||
}
|
||||
|
||||
if (_knownHosts.Add($"{context.Request.Scheme}://{context.Request.Host}") is false)
|
||||
var host = $"{context.Request.Scheme}://{context.Request.Host}";
|
||||
if (_knownHosts.Contains(host))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _firstBackOfficeRequestLocker.WaitAsync();
|
||||
|
||||
// NOTE: _knownHosts is not thread safe; check again after entering the semaphore
|
||||
if (_knownHosts.Add(host) is false)
|
||||
{
|
||||
_firstBackOfficeRequestLocker.Release();
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure we explicitly add UmbracoApplicationUrl if configured (https://github.com/umbraco/Umbraco-CMS/issues/16179)
|
||||
if (_webRoutingSettings.UmbracoApplicationUrl.IsNullOrWhiteSpace() is false)
|
||||
{
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
using Umbraco.Cms.Api.Management.Security;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.NotificationHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates backoffice sessions and clears external logins for removed providers if the external login
|
||||
/// provider setup has changed.
|
||||
/// </summary>
|
||||
internal sealed class ExternalLoginProviderStartupHandler : INotificationHandler<UmbracoApplicationStartingNotification>
|
||||
{
|
||||
private readonly IBackOfficeExternalLoginProviders _backOfficeExternalLoginProviders;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly IServerRoleAccessor _serverRoleAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExternalLoginProviderStartupHandler"/> class.
|
||||
/// </summary>
|
||||
public ExternalLoginProviderStartupHandler(
|
||||
IBackOfficeExternalLoginProviders backOfficeExternalLoginProviders,
|
||||
IRuntimeState runtimeState,
|
||||
IServerRoleAccessor serverRoleAccessor)
|
||||
{
|
||||
_backOfficeExternalLoginProviders = backOfficeExternalLoginProviders;
|
||||
_runtimeState = runtimeState;
|
||||
_serverRoleAccessor = serverRoleAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(UmbracoApplicationStartingNotification notification)
|
||||
{
|
||||
if (_runtimeState.Level != RuntimeLevel.Run ||
|
||||
_serverRoleAccessor.CurrentServerRole == ServerRole.Subscriber)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_backOfficeExternalLoginProviders.InvalidateSessionsIfExternalLoginProvidersChanged();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,9 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security;
|
||||
|
||||
@@ -8,13 +12,37 @@ public class BackOfficeExternalLoginProviders : IBackOfficeExternalLoginProvider
|
||||
{
|
||||
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
|
||||
private readonly Dictionary<string, BackOfficeExternalLoginProvider> _externalLogins;
|
||||
private readonly IKeyValueService _keyValueService;
|
||||
private readonly IExternalLoginWithKeyService _externalLoginWithKeyService;
|
||||
private readonly ILogger<BackOfficeExternalLoginProviders> _logger;
|
||||
|
||||
private const string ExternalLoginProvidersKey = "Umbraco.Cms.Web.BackOffice.Security.BackOfficeExternalLoginProviders";
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
public BackOfficeExternalLoginProviders(
|
||||
IEnumerable<BackOfficeExternalLoginProvider> externalLogins,
|
||||
IAuthenticationSchemeProvider authenticationSchemeProvider)
|
||||
: this(
|
||||
externalLogins,
|
||||
authenticationSchemeProvider,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IKeyValueService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IExternalLoginWithKeyService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILogger<BackOfficeExternalLoginProviders>>())
|
||||
{
|
||||
}
|
||||
|
||||
public BackOfficeExternalLoginProviders(
|
||||
IEnumerable<BackOfficeExternalLoginProvider> externalLogins,
|
||||
IAuthenticationSchemeProvider authenticationSchemeProvider,
|
||||
IKeyValueService keyValueService,
|
||||
IExternalLoginWithKeyService externalLoginWithKeyService,
|
||||
ILogger<BackOfficeExternalLoginProviders> logger)
|
||||
{
|
||||
_externalLogins = externalLogins.ToDictionary(x => x.AuthenticationType);
|
||||
_authenticationSchemeProvider = authenticationSchemeProvider;
|
||||
_keyValueService = keyValueService;
|
||||
_externalLoginWithKeyService = externalLoginWithKeyService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -60,4 +88,25 @@ public class BackOfficeExternalLoginProviders : IBackOfficeExternalLoginProvider
|
||||
var found = _externalLogins.Values.Where(x => x.Options.DenyLocalLogin).ToList();
|
||||
return found.Count > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void InvalidateSessionsIfExternalLoginProvidersChanged()
|
||||
{
|
||||
var previousExternalLoginProvidersValue = _keyValueService.GetValue(ExternalLoginProvidersKey);
|
||||
var currentExternalLoginProvidersValue = string.Join("|", _externalLogins.Keys.OrderBy(key => key));
|
||||
|
||||
if ((previousExternalLoginProvidersValue ?? string.Empty) != currentExternalLoginProvidersValue)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"The configured external login providers have changed. Existing backoffice sessions using the removed providers will be invalidated and external login data removed.");
|
||||
|
||||
_externalLoginWithKeyService.PurgeLoginsForRemovedProviders(_externalLogins.Keys);
|
||||
|
||||
_keyValueService.SetValue(ExternalLoginProvidersKey, currentExternalLoginProvidersValue);
|
||||
}
|
||||
else if (previousExternalLoginProvidersValue is null)
|
||||
{
|
||||
_keyValueService.SetValue(ExternalLoginProvidersKey, string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,11 @@ public interface IBackOfficeExternalLoginProviders
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool HasDenyLocalLogin();
|
||||
|
||||
/// <summary>
|
||||
/// Used during startup to see if the configured external login providers is different from the persisted information.
|
||||
/// If they are different, this will invalidate backoffice sessions and clear external logins for removed providers
|
||||
/// if the external login provider setup has changed.
|
||||
/// </summary>
|
||||
void InvalidateSessionsIfExternalLoginProvidersChanged() { }
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public static class AppCacheExtensions
|
||||
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey)
|
||||
{
|
||||
var result = provider.Get(cacheKey);
|
||||
if (result == null)
|
||||
if (IsRetrievedItemNull(result))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public static class AppCacheExtensions
|
||||
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey, Func<T> getCacheItem)
|
||||
{
|
||||
var result = provider.Get(cacheKey, () => getCacheItem());
|
||||
if (result == null)
|
||||
if (IsRetrievedItemNull(result))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
@@ -60,6 +60,8 @@ public static class AppCacheExtensions
|
||||
return result.TryConvertTo<T>().Result;
|
||||
}
|
||||
|
||||
private static bool IsRetrievedItemNull(object? result) => result is null or (object)Cms.Core.Constants.Cache.NullRepresentationInCache;
|
||||
|
||||
public static async Task<T?> GetCacheItemAsync<T>(
|
||||
this IAppPolicyCache provider,
|
||||
string cacheKey,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
@@ -152,11 +153,49 @@ public static class DistributedCacheExtensions
|
||||
|
||||
#region MemberCacheRefresher
|
||||
|
||||
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public static void RefreshMemberCache(this DistributedCache dc, IEnumerable<IMember> members)
|
||||
=> dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false)));
|
||||
=> dc.RefreshMemberCache(members, new Dictionary<string, object?>());
|
||||
|
||||
public static void RefreshMemberCache(this DistributedCache dc, IEnumerable<IMember> members, IDictionary<string, object?> state)
|
||||
=> dc.RefreshByPayload(
|
||||
MemberCacheRefresher.UniqueId,
|
||||
GetPayloads(members, state, false));
|
||||
|
||||
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public static void RemoveMemberCache(this DistributedCache dc, IEnumerable<IMember> members)
|
||||
=> dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true)));
|
||||
=> dc.RemoveMemberCache(members, new Dictionary<string, object?>());
|
||||
|
||||
public static void RemoveMemberCache(this DistributedCache dc, IEnumerable<IMember> members, IDictionary<string, object?> state)
|
||||
=> dc.RefreshByPayload(
|
||||
MemberCacheRefresher.UniqueId,
|
||||
GetPayloads(members, state, true));
|
||||
|
||||
// Internal for unit test.
|
||||
internal static IEnumerable<MemberCacheRefresher.JsonPayload> GetPayloads(IEnumerable<IMember> members, IDictionary<string, object?> state, bool removed)
|
||||
=> members
|
||||
.DistinctBy(x => (x.Id, x.Username))
|
||||
.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, removed)
|
||||
{
|
||||
PreviousUsername = GetPreviousUsername(x, state)
|
||||
});
|
||||
|
||||
private static string? GetPreviousUsername(IMember x, IDictionary<string, object?> state)
|
||||
{
|
||||
if (state.TryGetValue(MemberSavedNotification.PreviousUsernameStateKey, out object? previousUserNames) is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (previousUserNames is not IDictionary<Guid, string> previousUserNamesDictionary)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return previousUserNamesDictionary.TryGetValue(x.Key, out string? previousUsername)
|
||||
? previousUsername
|
||||
: null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
+25
-2
@@ -10,11 +10,20 @@ public abstract class DistributedCacheNotificationHandlerBase<TEntity, TNotifica
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Handle(TNotification notification)
|
||||
=> Handle(GetEntities(notification));
|
||||
=> Handle(
|
||||
GetEntities(notification),
|
||||
notification is StatefulNotification statefulNotification
|
||||
? statefulNotification.State
|
||||
: new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Handle(IEnumerable<TNotification> notifications)
|
||||
=> Handle(notifications.SelectMany(GetEntities));
|
||||
{
|
||||
foreach (TNotification notification in notifications)
|
||||
{
|
||||
Handle(notification);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entities from the specified notification.
|
||||
@@ -25,9 +34,23 @@ public abstract class DistributedCacheNotificationHandlerBase<TEntity, TNotifica
|
||||
/// </returns>
|
||||
protected abstract IEnumerable<TEntity> GetEntities(TNotification notification);
|
||||
|
||||
// TODO (V18): When removing the obsolete method, make the remaining Handle method abstract
|
||||
// rather than virtual. It couldn't be made abstract when introduced as that would be a breaking change.
|
||||
|
||||
/// <summary>
|
||||
/// Handles the specified entities.
|
||||
/// </summary>
|
||||
/// <param name="entities">The entities.</param>
|
||||
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
protected abstract void Handle(IEnumerable<TEntity> entities);
|
||||
|
||||
/// <summary>
|
||||
/// Handles the specified entities.
|
||||
/// </summary>
|
||||
/// <param name="entities">The entities.</param>
|
||||
/// <param name="state">The notification state.</param>
|
||||
protected virtual void Handle(IEnumerable<TEntity> entities, IDictionary<string, object?> state)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
=> Handle(entities);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
+5
@@ -18,6 +18,11 @@ public sealed class ContentTreeChangeDistributedCacheNotificationHandler : TreeC
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<TreeChange<IContent>> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<TreeChange<IContent>> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshContentCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -18,6 +18,11 @@ public sealed class ContentTypeChangedDistributedCacheNotificationHandler : Cont
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<ContentTypeChange<IContentType>> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<ContentTypeChange<IContentType>> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshContentTypeCache(entities);
|
||||
}
|
||||
|
||||
+6
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Cache;
|
||||
@@ -17,7 +18,12 @@ public sealed class DataTypeDeletedDistributedCacheNotificationHandler : Deleted
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IDataType> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IDataType> entities, IDictionary<string, object?> state)
|
||||
{
|
||||
_distributedCache.RemoveDataTypeCache(entities);
|
||||
_distributedCache.RefreshValueEditorCache(entities);
|
||||
|
||||
+5
@@ -17,7 +17,12 @@ public sealed class DataTypeSavedDistributedCacheNotificationHandler : SavedDist
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IDataType> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IDataType> entities, IDictionary<string, object?> state)
|
||||
{
|
||||
_distributedCache.RefreshDataTypeCache(entities);
|
||||
_distributedCache.RefreshValueEditorCache(entities);
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class DictionaryItemDeletedDistributedCacheNotificationHandler : D
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IDictionaryItem> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IDictionaryItem> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RemoveDictionaryCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class DictionaryItemSavedDistributedCacheNotificationHandler : Sav
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IDictionaryItem> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IDictionaryItem> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshDictionaryCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class DomainDeletedDistributedCacheNotificationHandler : DeletedDi
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IDomain> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IDomain> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RemoveDomainCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class DomainSavedDistributedCacheNotificationHandler : SavedDistri
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IDomain> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IDomain> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshDomainCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class LanguageDeletedDistributedCacheNotificationHandler : Deleted
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<ILanguage> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<ILanguage> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RemoveLanguageCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class LanguageSavedDistributedCacheNotificationHandler : SavedDist
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<ILanguage> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<ILanguage> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshLanguageCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -18,6 +18,11 @@ public sealed class MediaTreeChangeDistributedCacheNotificationHandler : TreeCha
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<TreeChange<IMedia>> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<TreeChange<IMedia>> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshMediaCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -18,6 +18,11 @@ public sealed class MediaTypeChangedDistributedCacheNotificationHandler : Conten
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<ContentTypeChange<IMediaType>> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<ContentTypeChange<IMediaType>> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshContentTypeCache(entities);
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Cache;
|
||||
@@ -17,6 +18,11 @@ public sealed class MemberDeletedDistributedCacheNotificationHandler : DeletedDi
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IMember> entities)
|
||||
=> _distributedCache.RemoveMemberCache(entities);
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IMember> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RemoveMemberCache(entities, state);
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class MemberGroupDeletedDistributedCacheNotificationHandler : Dele
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IMemberGroup> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IMemberGroup> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RemoveMemberGroupCache(entities);
|
||||
}
|
||||
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -17,6 +17,11 @@ public sealed class MemberGroupSavedDistributedCacheNotificationHandler : SavedD
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IMemberGroup> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IMemberGroup> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshMemberGroupCache(entities);
|
||||
}
|
||||
|
||||
+6
-1
@@ -17,6 +17,11 @@ public sealed class MemberSavedDistributedCacheNotificationHandler : SavedDistri
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IMember> entities)
|
||||
=> _distributedCache.RefreshMemberCache(entities);
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IMember> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshMemberCache(entities, state);
|
||||
}
|
||||
|
||||
+5
@@ -19,6 +19,11 @@ public sealed class MemberTypeChangedDistributedCacheNotificationHandler : Conte
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<ContentTypeChange<IMemberType>> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<ContentTypeChange<IMemberType>> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshContentTypeCache(entities);
|
||||
}
|
||||
|
||||
+6
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Cache;
|
||||
@@ -17,6 +18,11 @@ public sealed class PublicAccessEntryDeletedDistributedCacheNotificationHandler
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<PublicAccessEntry> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<PublicAccessEntry> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshPublicAccess();
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class PublicAccessEntrySavedDistributedCacheNotificationHandler :
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<PublicAccessEntry> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<PublicAccessEntry> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshPublicAccess();
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class RelationTypeDeletedDistributedCacheNotificationHandler : Del
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IRelationType> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IRelationType> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RemoveRelationTypeCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class RelationTypeSavedDistributedCacheNotificationHandler : Saved
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IRelationType> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IRelationType> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshRelationTypeCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class TemplateDeletedDistributedCacheNotificationHandler : Deleted
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<ITemplate> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<ITemplate> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RemoveTemplateCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class TemplateSavedDistributedCacheNotificationHandler : SavedDist
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<ITemplate> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<ITemplate> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshTemplateCache(entities);
|
||||
}
|
||||
|
||||
+6
@@ -1,3 +1,4 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Extensions;
|
||||
@@ -17,6 +18,11 @@ public sealed class UserDeletedDistributedCacheNotificationHandler : DeletedDist
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IUser> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IUser> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RemoveUserCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class UserGroupDeletedDistributedCacheNotificationHandler : Delete
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IUserGroup> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IUserGroup> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RemoveUserGroupCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -22,6 +22,11 @@ public sealed class UserGroupWithUsersSavedDistributedCacheNotificationHandler :
|
||||
=> notification.SavedEntities.Select(x => x.UserGroup);
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IUserGroup> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IUserGroup> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshUserGroupCache(entities);
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,11 @@ public sealed class UserSavedDistributedCacheNotificationHandler : SavedDistribu
|
||||
=> _distributedCache = distributedCache;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
protected override void Handle(IEnumerable<IUser> entities)
|
||||
=> Handle(entities, new Dictionary<string, object?>());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Handle(IEnumerable<IUser> entities, IDictionary<string, object?> state)
|
||||
=> _distributedCache.RefreshUserCache(entities);
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
|
||||
|
||||
/// <summary>
|
||||
/// Defines behaviours for clearing of cached partials views that are configured to be cached individually by member.
|
||||
/// </summary>
|
||||
public interface IMemberPartialViewCacheInvalidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears the partial view cache items for the specified member ids.
|
||||
/// </summary>
|
||||
/// <param name="memberIds">The member Ids to clear the cache for.</param>
|
||||
/// <remarks>
|
||||
/// Called from the <see cref="MemberCacheRefresher"/> when a member is saved or deleted.
|
||||
/// </remarks>
|
||||
void ClearPartialViewCacheItems(IEnumerable<int> memberIds);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a no-operation implementation of <see cref="IMemberPartialViewCacheInvalidator"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default implementation is added in Umbraco.Web.Website, but we need this to ensure we have a service
|
||||
/// registered for this interface even in headless setups).
|
||||
/// </remarks>
|
||||
internal class NoopMemberPartialViewCacheInvalidator : IMemberPartialViewCacheInvalidator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void ClearPartialViewCacheItems(IEnumerable<int> memberIds)
|
||||
{
|
||||
// No operation performed, this is a no-op implementation.
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Cache;
|
||||
|
||||
@@ -13,10 +15,32 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
|
||||
public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA");
|
||||
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
private readonly IMemberPartialViewCacheInvalidator _memberPartialViewCacheInvalidator;
|
||||
|
||||
[Obsolete("Use the non obsoleted contructor instead. Planned for removal in V18")]
|
||||
public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
|
||||
: base(appCaches, serializer, eventAggregator, factory) =>
|
||||
: this(
|
||||
appCaches,
|
||||
serializer,
|
||||
idKeyMap,
|
||||
eventAggregator,
|
||||
factory,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMemberPartialViewCacheInvalidator>())
|
||||
{
|
||||
}
|
||||
|
||||
public MemberCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IIdKeyMap idKeyMap,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory,
|
||||
IMemberPartialViewCacheInvalidator memberPartialViewCacheInvalidator)
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_idKeyMap = idKeyMap;
|
||||
_memberPartialViewCacheInvalidator = memberPartialViewCacheInvalidator;
|
||||
}
|
||||
|
||||
#region Indirect
|
||||
|
||||
@@ -38,6 +62,8 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
|
||||
|
||||
public string? Username { get; }
|
||||
|
||||
public string? PreviousUsername { get; set; }
|
||||
|
||||
public bool Removed { get; }
|
||||
}
|
||||
|
||||
@@ -65,7 +91,8 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
|
||||
|
||||
private void ClearCache(params JsonPayload[] payloads)
|
||||
{
|
||||
AppCaches.ClearPartialViewCache();
|
||||
_memberPartialViewCacheInvalidator.ClearPartialViewCacheItems(payloads.Select(p => p.Id));
|
||||
|
||||
Attempt<IAppPolicyCache?> memberCache = AppCaches.IsolatedCaches.Get<IMember>();
|
||||
|
||||
foreach (JsonPayload p in payloads)
|
||||
@@ -87,6 +114,13 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
|
||||
// https://github.com/umbraco/Umbraco-CMS/pull/17350
|
||||
// https://github.com/umbraco/Umbraco-CMS/pull/17815
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.Username));
|
||||
|
||||
// If provided, clear the cache by the previous user name too.
|
||||
if (string.IsNullOrEmpty(p.PreviousUsername) is false)
|
||||
{
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.PreviousUsername));
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.PreviousUsername));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
|
||||
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Core.Configuration.Models.ContentSettings.get_Error404Collection</Target>
|
||||
<Left>lib/net9.0/Umbraco.Core.dll</Left>
|
||||
<Right>lib/net9.0/Umbraco.Core.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
@@ -25,8 +25,8 @@ public class TypeFinderConfig : ITypeFinderConfig
|
||||
|
||||
var s = _settings.AssembliesAcceptingLoadExceptions;
|
||||
return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s)
|
||||
? Array.Empty<string>()
|
||||
: s.Split(',').Select(x => x.Trim()).ToArray();
|
||||
? []
|
||||
: s.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ public static class ContentSettingsExtensions
|
||||
/// <c>true</c> if the file extension is allowed for upload; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension)
|
||||
=> contentSettings.AllowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension)) ||
|
||||
(contentSettings.AllowedUploadedFileExtensions.Any() == false && contentSettings.DisallowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension)) == false);
|
||||
=> contentSettings.AllowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension.Trim())) ||
|
||||
(contentSettings.AllowedUploadedFileExtensions.Any() == false && contentSettings.DisallowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension.Trim())) == false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the auto-fill configuration for a specified property alias.
|
||||
|
||||
@@ -50,7 +50,7 @@ public class ContentSettings
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the collection of error pages.
|
||||
/// </summary>
|
||||
public ISet<ContentErrorPage> Error404Collection { get; set; } = new HashSet<ContentErrorPage>();
|
||||
public IEnumerable<ContentErrorPage> Error404Collection { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the preview badge mark-up.
|
||||
|
||||
@@ -4,10 +4,23 @@
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The serializer type that nucache uses to persist documents in the database.
|
||||
/// The serializer type that the published content cache uses to persist documents in the database.
|
||||
/// </summary>
|
||||
public enum NuCacheSerializerType
|
||||
{
|
||||
MessagePack = 1, // Default
|
||||
/// <summary>
|
||||
/// The default serializer type, which uses MessagePack for serialization.
|
||||
/// </summary>
|
||||
MessagePack = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The legacy JSON serializer type, which uses JSON for serialization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This option was provided for backward compatibility for the Umbraco cache implementation used from Umbraco 8 to 14 (NuCache).
|
||||
/// It is no longer supported with the cache implementation from Umbraco 15 based on .NET's Hybrid cache.
|
||||
/// Use the faster and more compact <see cref="MessagePack"/> instead.
|
||||
/// The option is kept available only for a more readable format suitable for testing purposes.
|
||||
/// </remarks>
|
||||
JSON = 2,
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ public class SecuritySettings
|
||||
|
||||
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
private const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
|
||||
private const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
|
||||
internal const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
|
||||
internal const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
|
||||
internal const string StaticAuthorizeCallbackPathName = "/umbraco/oauth_complete";
|
||||
internal const string StaticAuthorizeCallbackLogoutPathName = "/umbraco/logout";
|
||||
internal const string StaticAuthorizeCallbackErrorPathName = "/umbraco/error";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
namespace Umbraco.Cms.Core;
|
||||
public static partial class Constants
|
||||
{
|
||||
public static class Cache
|
||||
@@ -9,5 +9,13 @@ public static partial class Constants
|
||||
|
||||
public const string Media = "media";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the string used to represent a null value in the cache.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used in conjunction with the option to cache null values on the repository caches, so we
|
||||
/// can distinguish a true null "not found" value and a cached null value.</remarks>
|
||||
public const string NullRepresentationInCache = "*NULL*";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ public static partial class Constants
|
||||
/// <summary>
|
||||
/// Guid for List View - Members as string
|
||||
/// </summary>
|
||||
[Obsolete("No longer used in Umbraco. Scheduled for removal in Umbraco 17.")]
|
||||
public const string ListViewMembers = "AA2C52A0-CE87-4E65-A47C-7DF09358585D";
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
public static partial class Constants
|
||||
{
|
||||
@@ -24,14 +24,45 @@ public static partial class Constants
|
||||
public static class OutputCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Output cache policy name for content
|
||||
/// Output cache policy name for content.
|
||||
/// </summary>
|
||||
public const string ContentCachePolicy = "DeliveryApiContent";
|
||||
|
||||
/// <summary>
|
||||
/// Output cache policy name for media
|
||||
/// Output cache policy name for media.
|
||||
/// </summary>
|
||||
public const string MediaCachePolicy = "DeliveryApiMedia";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for Delivery API header names.
|
||||
/// </summary>
|
||||
public static class HeaderNames
|
||||
{
|
||||
/// <summary>
|
||||
/// Header name for accept language.
|
||||
/// </summary>
|
||||
public const string AcceptLanguage = "Accept-Language";
|
||||
|
||||
/// <summary>
|
||||
/// Header name for accept segment.
|
||||
/// </summary>
|
||||
public const string AcceptSegment = "Accept-Segment";
|
||||
|
||||
/// <summary>
|
||||
/// Header name for API key.
|
||||
/// </summary>
|
||||
public const string ApiKey = "Api-Key";
|
||||
|
||||
/// <summary>
|
||||
/// Header name for preview.
|
||||
/// </summary>
|
||||
public const string Preview = "Preview";
|
||||
|
||||
/// <summary>
|
||||
/// Header name for start item.
|
||||
/// </summary>
|
||||
public const string StartItem = "Start-Item";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Diagnostics;
|
||||
using Umbraco.Cms.Core.Dictionary;
|
||||
using Umbraco.Cms.Core.DynamicRoot;
|
||||
using Umbraco.Cms.Core.Editors;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Features;
|
||||
@@ -32,7 +34,6 @@ using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.DynamicRoot;
|
||||
using Umbraco.Cms.Core.Preview;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.PublishedCache.Internal;
|
||||
@@ -389,6 +390,11 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddUnique<IDataTypeConfigurationCache, DataTypeConfigurationCache>();
|
||||
Services.AddNotificationHandler<DataTypeCacheRefresherNotification, DataTypeConfigurationCacheRefresher>();
|
||||
|
||||
// Partial view cache invalidators (no-op, shipped implementation is added in Umbraco.Web.Website, but we
|
||||
// need this to ensure we have a service registered for this interface even in headless setups).
|
||||
// See: https://github.com/umbraco/Umbraco-CMS/issues/19661
|
||||
Services.AddUnique<IMemberPartialViewCacheInvalidator, NoopMemberPartialViewCacheInvalidator>();
|
||||
|
||||
// Two factor providers
|
||||
Services.AddUnique<ITwoFactorLoginService, TwoFactorLoginService>();
|
||||
Services.AddUnique<IUserTwoFactorLoginService, UserTwoFactorLoginService>();
|
||||
|
||||
@@ -5,6 +5,9 @@ using System.Globalization;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides Extensions for <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
public static class DateTimeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
@@ -18,6 +21,7 @@ public static class DateTimeExtensions
|
||||
Hour,
|
||||
Minute,
|
||||
Second,
|
||||
Millisecond,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -35,32 +39,15 @@ public static class DateTimeExtensions
|
||||
/// <param name="truncateTo">The level to truncate the date to.</param>
|
||||
/// <returns>The truncated date.</returns>
|
||||
public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo)
|
||||
{
|
||||
if (truncateTo == DateTruncate.Year)
|
||||
=> truncateTo switch
|
||||
{
|
||||
return new DateTime(dt.Year, 1, 1, 0, 0, 0, dt.Kind);
|
||||
}
|
||||
|
||||
if (truncateTo == DateTruncate.Month)
|
||||
{
|
||||
return new DateTime(dt.Year, dt.Month, 1, 0, 0, 0, dt.Kind);
|
||||
}
|
||||
|
||||
if (truncateTo == DateTruncate.Day)
|
||||
{
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0, dt.Kind);
|
||||
}
|
||||
|
||||
if (truncateTo == DateTruncate.Hour)
|
||||
{
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0, dt.Kind);
|
||||
}
|
||||
|
||||
if (truncateTo == DateTruncate.Minute)
|
||||
{
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind);
|
||||
}
|
||||
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Kind);
|
||||
}
|
||||
DateTruncate.Year => new DateTime(dt.Year, 1, 1, 0, 0, 0, dt.Kind),
|
||||
DateTruncate.Month => new DateTime(dt.Year, dt.Month, 1, 0, 0, 0, dt.Kind),
|
||||
DateTruncate.Day => new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0, dt.Kind),
|
||||
DateTruncate.Hour => new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0, dt.Kind),
|
||||
DateTruncate.Minute => new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind),
|
||||
DateTruncate.Second => new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Kind),
|
||||
DateTruncate.Millisecond => new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Millisecond, dt.Kind),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(truncateTo), truncateTo, "Invalid truncation level"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
public static class IntExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Does something 'x' amount of times
|
||||
/// Does something 'x' amount of times.
|
||||
/// </summary>
|
||||
/// <param name="n"></param>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="n">Number of times to execute the action.</param>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
public static void Times(this int n, Action<int> action)
|
||||
{
|
||||
for (var i = 0; i < n; i++)
|
||||
@@ -19,11 +21,11 @@ public static class IntExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Guid based on an integer value
|
||||
/// Creates a Guid based on an integer value.
|
||||
/// </summary>
|
||||
/// <param name="value"><see cref="int" /> value to convert</param>
|
||||
/// <param name="value">The <see cref="int" /> value to convert.</param>
|
||||
/// <returns>
|
||||
/// <see cref="Guid" />
|
||||
/// The converted <see cref="Guid" />.
|
||||
/// </returns>
|
||||
public static Guid ToGuid(this int value)
|
||||
{
|
||||
@@ -31,4 +33,25 @@ public static class IntExtensions
|
||||
BitConverter.GetBytes(value).CopyTo(bytes);
|
||||
return new Guid(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a GUID previously created from an integer value using <see cref="ToGuid" />.
|
||||
/// </summary>
|
||||
/// <param name="value">The <see cref="Guid" /> value to convert.</param>
|
||||
/// <param name="result">The converted <see cref="int" />.</param>
|
||||
/// <returns>
|
||||
/// True if the <see cref="int" /> value could be created, otherwise false.
|
||||
/// </returns>
|
||||
public static bool TryParseFromGuid(Guid value, [NotNullWhen(true)] out int? result)
|
||||
{
|
||||
if (value.ToString().EndsWith("-0000-0000-0000-000000000000") is false)
|
||||
{
|
||||
// We have a proper GUID, not one converted from an integer.
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BitConverter.ToInt32(value.ToByteArray());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,8 +509,7 @@ public static class StringExtensions
|
||||
var convertToHex = input.ConvertToHex();
|
||||
var hexLength = convertToHex.Length < 32 ? convertToHex.Length : 32;
|
||||
var hex = convertToHex[..hexLength].PadLeft(32, '0');
|
||||
Guid output = Guid.Empty;
|
||||
return Guid.TryParse(hex, out output) ? output : Guid.Empty;
|
||||
return Guid.TryParse(hex, out Guid output) ? output : Guid.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1575,4 +1574,22 @@ public static class StringExtensions
|
||||
// this is by far the fastest way to find string needles in a string haystack
|
||||
public static int CountOccurrences(this string haystack, string needle)
|
||||
=> haystack.Length - haystack.Replace(needle, string.Empty).Length;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the provided string is a valid culture code and returns it in a consistent casing.
|
||||
/// </summary>
|
||||
/// <param name="culture">Culture code.</param>
|
||||
/// <returns>Culture code in standard casing.</returns>
|
||||
public static string? EnsureCultureCode(this string? culture)
|
||||
{
|
||||
if (string.IsNullOrEmpty(culture) || culture == "*")
|
||||
{
|
||||
return culture;
|
||||
}
|
||||
|
||||
// Create as CultureInfo instance from provided name so we can ensure consistent casing of culture code when persisting.
|
||||
// This will accept mixed case but once created have a `Name` property that is consistently and correctly cased.
|
||||
// Will throw in an invalid culture code is provided.
|
||||
return new CultureInfo(culture).Name;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user