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 | ||
|
|
919b65ea19 | ||
|
|
cc2f1df78e | ||
|
|
9b2fd1253b | ||
|
|
fe7f0558c1 | ||
|
|
024697f3bf | ||
|
|
8d2ff6f92a | ||
|
|
69224cb53c | ||
|
|
89b2120c46 | ||
|
|
6c769d6024 | ||
|
|
67ab06715f | ||
|
|
b3ac82baf8 | ||
|
|
7f51e1ad1b | ||
|
|
c7dcbfec60 | ||
|
|
3f57da5579 | ||
|
|
ce2756f399 | ||
|
|
6cfba9b406 | ||
|
|
29b98ba862 | ||
|
|
971ca17b5f | ||
|
|
d8b76575ca | ||
|
|
5b00312158 | ||
|
|
d920e93d1e | ||
|
|
d2a0cba933 | ||
|
|
5a56541aa5 | ||
|
|
93873c378e | ||
|
|
ba6a0fcfe0 | ||
|
|
b403518986 | ||
|
|
fd3808ae38 | ||
|
|
c4be7842a7 | ||
|
|
1726180b06 | ||
|
|
3b84fe3557 | ||
|
|
d76293bacc | ||
|
|
179caacf58 | ||
|
|
933fa544d7 | ||
|
|
b019f0f849 | ||
|
|
7346a14d86 | ||
|
|
ecfaee3b04 | ||
|
|
80183964bb | ||
|
|
445652d7f4 | ||
|
|
13e368ca28 | ||
|
|
e6967a41cd | ||
|
|
feba51a49d | ||
|
|
b87ae6ce46 | ||
|
|
3db0ec7efc | ||
|
|
0f53ba8a18 | ||
|
|
45593c6311 | ||
|
|
225fed5f72 | ||
|
|
419625a919 | ||
|
|
853c1acf7e | ||
|
|
b1e447d0c5 | ||
|
|
df11e438f4 | ||
|
|
7572ea350b | ||
|
|
ec6a38e052 | ||
|
|
ea8ec4e375 | ||
|
|
ceab77d911 | ||
|
|
e62e55d1c0 | ||
|
|
76fcb4ade2 | ||
|
|
c20b0ab3c9 | ||
|
|
b42888dffa | ||
|
|
2b00f15d1a | ||
|
|
5d9d94a0e8 | ||
|
|
a5d32336fc | ||
|
|
712cbe84c1 | ||
|
|
d1f0437daf | ||
|
|
a31923c1e2 | ||
|
|
f42328e46f | ||
|
|
4d7641be97 | ||
|
|
41ca028023 | ||
|
|
cdadd6c29d | ||
|
|
489322adac | ||
|
|
2d7037df4b | ||
|
|
44552ccf75 | ||
|
|
6f4a8a2041 | ||
|
|
b2e80865b8 | ||
|
|
641e549bae | ||
|
|
4dd7ce1a73 | ||
|
|
358ab7b205 | ||
|
|
7e647c6b38 | ||
|
|
6ce4fbfc0c | ||
|
|
f39094fcea | ||
|
|
12049e9c03 | ||
|
|
d83e9f736d | ||
|
|
1c1035e1ea | ||
|
|
03523ad90b | ||
|
|
abf068934d | ||
|
|
057fd05635 | ||
|
|
dc4355cbe2 | ||
|
|
8e5f3c1cdf | ||
|
|
acdaee6ad8 | ||
|
|
f7a2569506 | ||
|
|
581798f097 | ||
|
|
5469d9841b |
@@ -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',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
# **Contributing to Localization in the Backoffice**
|
||||
|
||||
Do you want to help keep our translations accurate and up to standard? 🌍✨
|
||||
|
||||
Your input makes a real difference! By reviewing, refining, or suggesting improvements, you ensure that our translations remain clear, consistent, and user-friendly for everyone.
|
||||
|
||||
|
||||
## **How Can I Contribute?**
|
||||
|
||||
To contribute to localization in the Backoffice, follow this step-by-step guide:
|
||||
|
||||
|
||||
### **1. Change the Language in Backoffice**
|
||||
|
||||
|
||||
|
||||
1. Open the Backoffice, click on your profile icon in the top-right corner, and select "Edit."
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
2. Under "UI Culture," select the language you want to review from the dropdown menu.
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### **2. Find a Translation Error**
|
||||
|
||||
|
||||
|
||||
1. Navigate through the Backoffice and check if everything is translated correctly.
|
||||
|
||||
2. When you find a translation error, right-click on it and select "Inspect."
|
||||
|
||||
3. Look for the nearest element that starts with `umb-` and has a name indicating something specific to the given location.
|
||||
|
||||
**Example:**
|
||||
|
||||
* The closest parent element should be specific, such as `umb-document-type-workspace-view-settings` instead of a generic element like `umb-property-layout.`
|
||||
|
||||
|
||||
### **3. Find the Code in VS Code**
|
||||
|
||||
|
||||
|
||||
1. Open VS Code and search for the nearest `umb-` element you identified.
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
2. Scroll down to find `render() {` and look for the element label that needs updating.
|
||||
|
||||
|
||||

|
||||
|
||||
3. If the label is hardcoded, it must be updated.
|
||||
|
||||
**Example:**
|
||||
`label="Vary by culture"`
|
||||
|
||||
|
||||
### **4. Find the Correct Translation**
|
||||
|
||||
|
||||
|
||||
1. Open the `en.ts` or `en-us.ts` file and search for relevant keywords. \
|
||||
\
|
||||
**Example:**
|
||||
|
||||
* If the text is "Vary by culture," search for `vary`, `culture`, or `Vary by culture`.
|
||||
|
||||
|
||||
2. Once you find the translation, take the element name and search for it in the target language file (e.g., `da-dk.ts` for Danish).
|
||||
|
||||
|
||||

|
||||
|
||||
3. If a translation exists, insert it into the label element found earlier.
|
||||
|
||||
|
||||
|
||||
### **5. Insert the Translation**
|
||||
|
||||
To display the new translation correctly, insert the following code inside the label element:
|
||||
|
||||
`${this.localize.term('action_key')}`
|
||||
|
||||
Replace `action_key` with the correct translation key.
|
||||
|
||||
**Example:**
|
||||
|
||||
`${this.localize.term('contentTypeEditor_allowVaryByCulture')}`
|
||||
|
||||

|
||||
|
||||
|
||||
Save the changes and return to the Backoffice to see the update.
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### **6. Commit and Push**
|
||||
|
||||
|
||||
|
||||
1. Commit your changes to a new temporary branch (avoid committing directly to `contrib`).
|
||||
|
||||
2. Push the changes to your fork on GitHub.
|
||||
|
||||
|
||||
### **7. Create a Pull Request**
|
||||
|
||||
|
||||
|
||||
1. In your forked repository on GitHub (`https://github.com/[YourUsername]/Umbraco-CMS`), a banner will appear stating that you pushed a new branch.
|
||||
|
||||
2. Click the button to create a pull request and follow the instructions.
|
||||
|
||||
|
||||
## **I Can’t Find the Correct Translation**
|
||||
|
||||
If you can’t find the translation you need, it may not exist yet. In this case, you can create a new action with related keys.
|
||||
|
||||
|
||||
### **1. Ensure It Doesn’t Already Exist**
|
||||
|
||||
Search thoroughly in `en.ts` or `en-us.ts` for all relevant keywords.
|
||||
|
||||
|
||||
### **2. Create an Action**
|
||||
|
||||
|
||||
|
||||
1. Choose a meaningful name for the action to avoid confusion. \
|
||||
\
|
||||
**Example:** Translation for the Data Type "Color Picker."
|
||||
|
||||
* **Good name:** `colorPickerConfigurations`
|
||||
* **Bad name:** `colorpicker`
|
||||
2. A specific action name prevents unnecessarily long key names.
|
||||
|
||||
3. Define the action:
|
||||
|
||||
|
||||
|
||||
### **3. Create Keys**
|
||||
|
||||
|
||||
|
||||
1. Use clear and descriptive key names. \
|
||||
\
|
||||
**Example:**
|
||||
* **Good name:** `colorsTitle`
|
||||
* **Bad name:** `colors`
|
||||
2. Add the necessary keys inside the action with proper translations.
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## **I Can’t Find a <code>render()</code> Code in VS Code**
|
||||
|
||||
In some cases, such as Data Types, the label might not be inside `render()`. Instead, it may be in a manifest file.
|
||||
|
||||
|
||||
### 1. Search for the Text
|
||||
|
||||
Copy the text from the Backoffice and search for it in the code.
|
||||
|
||||
|
||||
### 2. Open the Manifest File
|
||||
|
||||
Once you find the relevant manifest file, open it to confirm you’re in the right place.
|
||||
|
||||
|
||||
### 3. Change the Label
|
||||
|
||||
In Markdown files, localization is slightly different. Instead of:
|
||||
`${this.localize.term('action_key')}`
|
||||
|
||||
Use: `#action_key`
|
||||
|
||||
**Example:**
|
||||
`#colorPickerConfigurations_showLabelTitle`
|
||||
|
||||
### 4. Change the Description
|
||||
|
||||
For descriptions in Markdown files, use:
|
||||
`{umbLocalize: action_key}`
|
||||
|
||||
**Example:**
|
||||
`{umbLocalize: colorPickerConfigurations_showLabelDescription}`
|
||||
|
||||
|
||||
### 5. Save and Verify
|
||||
|
||||
Once all changes are made, your manifest should look something like this:
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Thank you
|
||||
|
||||
Following these steps ensures that the Umbraco Backoffice remains accessible and user-friendly in all supported languages. Thanks for contributing! 🎉
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -3,8 +3,9 @@ name: Backoffice Static Web Apps CI/CD
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- contrib
|
||||
- main
|
||||
- v*/dev
|
||||
- v*/main
|
||||
paths:
|
||||
- src/Umbraco.Web.UI.Client/package.json
|
||||
- src/Umbraco.Web.UI.Client/package-lock.json
|
||||
@@ -13,12 +14,13 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- contrib
|
||||
- main
|
||||
- v*/dev
|
||||
- v*/main
|
||||
|
||||
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:
|
||||
@@ -29,7 +31,7 @@ jobs:
|
||||
id: builddeploy
|
||||
uses: Azure/static-web-apps-deploy@v1
|
||||
with:
|
||||
production_branch: contrib
|
||||
production_branch: main
|
||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_VICTORIOUS_GROUND_017B08103 }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
|
||||
action: "upload"
|
||||
@@ -42,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:
|
||||
|
||||
@@ -3,8 +3,9 @@ name: Storybook CI/CD
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- contrib
|
||||
- main
|
||||
- v*/dev
|
||||
- v*/main
|
||||
paths:
|
||||
- src/Umbraco.Web.UI.Client/package.json
|
||||
- src/Umbraco.Web.UI.Client/package-lock.json
|
||||
@@ -13,15 +14,16 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- contrib
|
||||
- main
|
||||
- v*/dev
|
||||
- v*/main
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
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:
|
||||
@@ -30,7 +32,7 @@ jobs:
|
||||
id: builddeploy
|
||||
uses: Azure/static-web-apps-deploy@v1
|
||||
with:
|
||||
production_branch: contrib
|
||||
production_branch: main
|
||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ORANGE_SEA_0C7411A03 }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
|
||||
action: "upload"
|
||||
@@ -43,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:
|
||||
|
||||
@@ -4,15 +4,15 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "*/dev"
|
||||
- "*/contrib"
|
||||
- "contrib"
|
||||
- "*/main"
|
||||
- "main"
|
||||
- "release/*"
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches:
|
||||
- "*/dev"
|
||||
- "*/contrib"
|
||||
- "contrib"
|
||||
- "*/main"
|
||||
- "main"
|
||||
- "release/*"
|
||||
schedule:
|
||||
- cron: "33 2 * * 1"
|
||||
|
||||
@@ -3,16 +3,18 @@ name: Test Backoffice
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- contrib
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
- v*/main
|
||||
paths:
|
||||
- src/Umbraco.Web.UI.Client/**
|
||||
pull_request:
|
||||
branches:
|
||||
- contrib
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
- v*/main
|
||||
paths:
|
||||
- src/Umbraco.Web.UI.Client/**
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,7 +40,7 @@
|
||||
<!-- Package Validation -->
|
||||
<PropertyGroup>
|
||||
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
|
||||
<EnablePackageValidation>false</EnablePackageValidation> <!-- TODO: Update to true once 16.0.0 is released to NuGet. -->
|
||||
<EnablePackageValidation>true</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>16.0.0</PackageValidationBaselineVersion>
|
||||
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
|
||||
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
|
||||
|
||||
@@ -567,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
|
||||
|
||||
@@ -650,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
|
||||
@@ -725,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
|
||||
|
||||
@@ -826,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);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);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);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
|
||||
|
||||
@@ -10,9 +10,7 @@ schedules:
|
||||
include:
|
||||
- v10/dev
|
||||
- v13/dev
|
||||
- v14/dev
|
||||
- v15/dev
|
||||
- v16/dev
|
||||
- main
|
||||
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -53,16 +53,6 @@ public class MoveDocumentController : DocumentControllerBase
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionMove.ActionLetter, new[] { moveDocumentRequestModel.Target?.Id, id }),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
Attempt<IContent?, ContentEditingOperationStatus> result = await _contentEditingService.MoveAsync(
|
||||
id,
|
||||
moveDocumentRequestModel.Target?.Id,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,7 +8,6 @@ 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;
|
||||
|
||||
@@ -29,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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.AuditLog;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -19,19 +20,17 @@ public class AuditLogPresentationFactory : IAuditLogPresentationFactory
|
||||
|
||||
public IEnumerable<AuditLogResponseModel> CreateAuditLogViewModel(IEnumerable<IAuditItem> auditItems) => auditItems.Select(CreateAuditLogViewModel);
|
||||
|
||||
private AuditLogResponseModel CreateAuditLogViewModel(IAuditItem auditItem)
|
||||
{
|
||||
Guid userKey = _userIdKeyResolver.GetAsync(auditItem.UserId).GetAwaiter().GetResult();
|
||||
IUser user = _userService.GetAsync(userKey).GetAwaiter().GetResult()
|
||||
?? throw new ArgumentException($"Could not find user with id {auditItem.UserId}");
|
||||
|
||||
return new AuditLogResponseModel
|
||||
private AuditLogResponseModel CreateAuditLogViewModel(IAuditItem auditItem) =>
|
||||
new()
|
||||
{
|
||||
Comment = auditItem.Comment,
|
||||
LogType = auditItem.AuditType,
|
||||
Parameters = auditItem.Parameters,
|
||||
Timestamp = auditItem.CreateDate,
|
||||
User = new ReferenceByIdModel(user.Key)
|
||||
User = auditItem.UserId switch
|
||||
{
|
||||
Constants.Security.UnknownUserId => new ReferenceByIdModel(),
|
||||
_ => new ReferenceByIdModel(_userIdKeyResolver.GetAsync(auditItem.UserId).GetAwaiter().GetResult()),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => [];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||