Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5fe779db0 | ||
|
|
bf82d29e53 |
+2
-2
@@ -1,3 +1,3 @@
|
||||
**/*
|
||||
!tests/Umbraco.Tests.Integration/bin/**
|
||||
!tests/Umbraco.Tests.UnitTests/bin/**
|
||||
!**/bin/**
|
||||
!**/obj/**
|
||||
|
||||
@@ -22,3 +22,7 @@ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/
|
||||
# https://docs.npmjs.com/cli/v6/using-npm/config#unsafe-perm
|
||||
# Default: false if running as root, true otherwise (we are ROOT)
|
||||
#RUN npm -g config set user vscode && npm -g config set unsafe-perm
|
||||
|
||||
# Generate and trust a local developer certificate for Kestrel
|
||||
# This is needed for Kestrel to bind on https
|
||||
RUN dotnet dev-certs https --trust
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspace",
|
||||
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"omnisharp.defaultLaunchSolution": "umbraco.sln",
|
||||
@@ -14,62 +13,13 @@
|
||||
"omnisharp.enableRoslynAnalyzers": true
|
||||
},
|
||||
|
||||
"features": {
|
||||
// Workaround until the image is updated to include the latest version of .NET Core - .NET7
|
||||
// https://github.com/devcontainers/templates/issues/38#issuecomment-1310803259
|
||||
"ghcr.io/devcontainers/features/dotnet:1": {
|
||||
"version": "7"
|
||||
},
|
||||
|
||||
// Adds SSH support to the container
|
||||
// Allowing the Github CLI `gh codespace ssh` to work
|
||||
"ghcr.io/devcontainers/features/sshd:1": {
|
||||
"version": "latest"
|
||||
},
|
||||
|
||||
// Adds SQLite feature from apt install
|
||||
// Also adds the SQLite VSCode extension
|
||||
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"ms-dotnettools.csharp"
|
||||
],
|
||||
|
||||
// This is used in the prebuilds - so dotnet build (nuget restore and node stuff) is done
|
||||
"updateContentCommand": "dotnet build umbraco.sln && dotnet dev-certs https --trust",
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locallAdd "forwardPorts": [9000, 5000, 25],
|
||||
// Port needed to help discover SMTP4Dev
|
||||
"forwardPorts": [
|
||||
5000
|
||||
],
|
||||
|
||||
"portsAttributes": {
|
||||
"5000": {
|
||||
"label": "SMTP4Dev",
|
||||
"protocol": "http",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"9000": {
|
||||
"label": "Umbraco HTTP",
|
||||
"protocol": "http",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"44331": {
|
||||
"label": "Umbraco HTTPS",
|
||||
"protocol": "https",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
"codespaces": {
|
||||
"openFiles": [
|
||||
".github/codespaces-readme.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [9000, 5000, 25]
|
||||
|
||||
// [Optional] To reuse of your local HTTPS dev cert:
|
||||
//
|
||||
|
||||
@@ -248,7 +248,6 @@ csharp_preserve_single_line_blocks = true
|
||||
##########################################
|
||||
|
||||
[*.{cs,csx,cake,vb,vbx}]
|
||||
dotnet_diagnostic.CS1591.severity = suggestion
|
||||
|
||||
##########################################
|
||||
# Styles
|
||||
|
||||
+154
-27
@@ -1,4 +1,4 @@
|
||||
# Umbraco CMS Build
|
||||
# Umbraco CMS Build
|
||||
|
||||
## Are you sure?
|
||||
|
||||
@@ -27,7 +27,7 @@ If you want to run a build without debugging, see [Building from source](#buildi
|
||||
In order to build the Umbraco source code locally with Visual Studio Code, first make sure you have the following installed.
|
||||
|
||||
* [Visual Studio Code](https://code.visualstudio.com/)
|
||||
* [dotnet SDK v7+](https://dotnet.microsoft.com/en-us/download)
|
||||
* [dotnet SDK v6.0.2+](https://dotnet.microsoft.com/en-us/download)
|
||||
* [Node.js v14+](https://nodejs.org/en/download/)
|
||||
* npm v7+ (installed with Node.js)
|
||||
* [Git command line](https://git-scm.com/download/)
|
||||
@@ -40,22 +40,18 @@ You can also run the tasks manually on the command line:
|
||||
|
||||
```
|
||||
cd src\Umbraco.Web.UI.Client
|
||||
npm i
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
If you just want to build the UI Client to `Umbraco.Web.UI` then instead of running `dev`, you can do: `npm run build`.
|
||||
|
||||
The login screen is a different frontend build, for that one you can run it as follows:
|
||||
or
|
||||
|
||||
```
|
||||
cd src\Umbraco.Web.UI.Login
|
||||
npm i
|
||||
npm run dev
|
||||
cd src\Umbraco.Web.UI.Client
|
||||
npm install
|
||||
gulp dev
|
||||
```
|
||||
|
||||
If you just want to build the Login screen to `Umbraco.Web.UI` then instead of running `dev`, you can do: `npm run build`.
|
||||
|
||||
**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.**
|
||||
|
||||
You might run into [Gulp quirks](#gulp-quirks).
|
||||
@@ -76,7 +72,7 @@ When the page eventually loads in your web browser, you can follow the installer
|
||||
|
||||
In order to build the Umbraco source code locally with Visual Studio, first make sure you have the following installed.
|
||||
|
||||
* [Visual Studio 2022 v17+ with .NET 7+](https://visualstudio.microsoft.com/vs/) ([the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects)
|
||||
* [Visual Studio 2019 v16.8+ with .NET 6.0.2+](https://visualstudio.microsoft.com/vs/) ([the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects)
|
||||
* [Node.js v14+](https://nodejs.org/en/download/)
|
||||
* npm v7+ (installed with Node.js)
|
||||
* [Git command line](https://git-scm.com/download/)
|
||||
@@ -103,33 +99,111 @@ When the page eventually loads in your web browser, you can follow the installer
|
||||
|
||||
Did you read ["Are you sure"](#are-you-sure)?
|
||||
|
||||
Do note that this is only required if you want to test out your custom changes in a separate site (not the one in the Umbraco.Web.UI), if you just want to test your changes you can run the included test site using: `dotnet run` from `src/Umbraco.Web.UI/`
|
||||
### Quick!
|
||||
|
||||
You may want to build a set of NuGet packages with your changes, this can be done using the dotnet pack command.
|
||||
To build Umbraco, fire up PowerShell and move to Umbraco's repository root (the directory that contains `src`, `build`, `LICENSE.md`...). There, trigger the build with the following command:
|
||||
|
||||
First enter the root of the project in a command line environment, and then use the following command to build the NuGet packages:
|
||||
build/build.ps1
|
||||
|
||||
`dotnet pack -c Release -o Build.Out`
|
||||
If you only see a build.bat-file, you're probably on the wrong branch. If you switch to the correct branch (v8/contrib) the file will appear and you can build it.
|
||||
|
||||
This will restore and build the project using the release configuration, and put all the outputted files in a folder called `Build.Out`
|
||||
You might run into [Powershell quirks](#powershell-quirks).
|
||||
|
||||
You can then add these as a local NuGet feed using the following command:
|
||||
If it runs without errors; Hooray! Now you can continue with [the next step](CONTRIBUTING.md#how-do-i-begin) and open the solution and build it.
|
||||
|
||||
`dotnet nuget add source <Path to Build.Out folder> -n MyLocalFeed`
|
||||
|
||||
This will add a local nuget feed with the name "MyLocalFeed" and you'll now be able to use your custom built NuGet packages.
|
||||
### Build Infrastructure
|
||||
|
||||
The Umbraco Build infrastructure relies on a PowerShell object. The object can be retrieved with:
|
||||
|
||||
$ubuild = build/build.ps1 -get
|
||||
|
||||
The object exposes various properties and methods that can be used to fine-grain build Umbraco. Some, but not all, of them are detailed below.
|
||||
|
||||
#### Properties
|
||||
|
||||
The object exposes the following properties:
|
||||
|
||||
* `SolutionRoot`: the absolute path to the solution root
|
||||
* `VisualStudio`: a Visual Studio object (see below)
|
||||
* `NuGet`: the absolute path to the NuGet executable
|
||||
* `Zip`: the absolute path to the 7Zip executable
|
||||
* `VsWhere`: the absolute path to the VsWhere executable
|
||||
* `NodePath`: the absolute path to the Node install
|
||||
* `NpmPath`: the absolute path to the Npm install
|
||||
|
||||
The Visual Studio object is `null` when Visual Studio has not been detected (eg on VSTS). When not null, the object exposes the following properties:
|
||||
|
||||
* `Path`: Visual Studio installation path (eg some place under `Program Files`)
|
||||
* `Major`: Visual Studio major version (eg `15` for VS 2017)
|
||||
* `Minor`: Visual Studio minor version
|
||||
* `MsBuild`: the absolute path to the MsBuild executable
|
||||
|
||||
#### GetUmbracoVersion
|
||||
|
||||
Gets an object representing the current Umbraco version. Example:
|
||||
|
||||
$v = $ubuild.GetUmbracoVersion()
|
||||
Write-Host $v.Semver
|
||||
|
||||
The object exposes the following properties:
|
||||
|
||||
* `Semver`: the semver object representing the version
|
||||
* `Release`: the main part of the version (eg `7.6.33`)
|
||||
* `Comment`: the pre release part of the version (eg `alpha02`)
|
||||
* `Build`: the build number part of the version (eg `1234`)
|
||||
|
||||
#### SetUmbracoVersion
|
||||
|
||||
Modifies Umbraco files with the new version.
|
||||
|
||||
>This entirely replaces the legacy `UmbracoVersion.txt` file. Do *not* edit version infos in files.
|
||||
|
||||
The version must be a valid semver version. It can include a *pre release* part (eg `alpha02`) and/or a *build number* (eg `1234`). Examples:
|
||||
|
||||
$ubuild.SetUmbracoVersion("7.6.33")
|
||||
$ubuild.SetUmbracoVersion("7.6.33-alpha.2")
|
||||
$ubuild.SetUmbracoVersion("7.6.33+1234")
|
||||
$ubuild.SetUmbracoVersion("7.6.33-beta.5+5678")
|
||||
|
||||
#### Build
|
||||
|
||||
Builds Umbraco. Temporary files are generated in `build.tmp` while the actual artifacts (zip files, NuGet packages...) are produced in `build.out`. Example:
|
||||
|
||||
$ubuild.Build()
|
||||
|
||||
Some log files, such as MsBuild logs, are produced in `build.tmp` too. The `build` directory should remain clean during a build.
|
||||
|
||||
**Note: web.config**
|
||||
|
||||
Building Umbraco requires a clean `web.config` file in the `Umbraco.Web.UI` project. If a `web.config` file already exists, the `pre-build` task (see below) will save it as `web.config.temp-build` and replace it with a clean copy of `web.Template.config`. The original file is replaced once it is safe to do so, by the `pre-packages` task.
|
||||
|
||||
#### Build-UmbracoDocs
|
||||
|
||||
Builds umbraco documentation. Temporary files are generated in `build.tmp` while the actual artifacts (docs...) are produced in `build.out`. Example:
|
||||
|
||||
Build-UmbracoDocs
|
||||
|
||||
Some log files, such as MsBuild logs, are produced in `build.tmp` too. The `build` directory should remain clean during a build.
|
||||
|
||||
#### Verify-NuGet
|
||||
|
||||
Verifies that projects all require the same version of their dependencies, and that NuSpec files require versions that are consistent with projects. Example:
|
||||
|
||||
Verify-NuGet
|
||||
|
||||
### Cleaning up
|
||||
|
||||
Once the solution has been used to run a site, one may want to "reset" the solution in order to run a fresh new site again.
|
||||
|
||||
The easiest way to do this by deleting the following files and folders:
|
||||
* src/Umbraco.Web.UI/appsettings.json
|
||||
* src/Umbraco.Web.UI/umbraco/Data
|
||||
At the very minimum, you want
|
||||
|
||||
You only have to remove the connection strings from the appsettings, but removing the data folder ensures that the sqlite database gets deleted too.
|
||||
git clean -Xdf src/Umbraco.Web.UI/App_Data
|
||||
rm src/Umbraco.Web.UI/web.config
|
||||
|
||||
Next time you run a build the `appsettings.json` file will be re-created in its default state.
|
||||
Then, a simple 'Rebuild All' in Visual Studio will recreate a fresh `web.config` but should be quite fast (since it does not really need to rebuild anything).
|
||||
|
||||
The `clean` Git command force (`-f`) removes (`-X`, note the capital X) all files and directories (`-d`) that are ignored by Git.
|
||||
|
||||
This will leave media files and views around, but in most cases, it will be enough.
|
||||
|
||||
@@ -145,12 +219,65 @@ For git documentation see:
|
||||
|
||||
## Azure DevOps
|
||||
|
||||
Umbraco uses Azure DevOps for continuous integration, nightly builds and release builds. The Umbraco CMS project on DevOps [is available for anonymous users](https://umbraco.visualstudio.com/Umbraco%20Cms)..
|
||||
Umbraco uses Azure DevOps for continuous integration, nightly builds and release builds. The Umbraco CMS project on DevOps [is available for anonymous users](https://umbraco.visualstudio.com/Umbraco%20Cms).
|
||||
|
||||
The produced artifacts are published in a container that can be downloaded from DevOps called "nupkg" which contains all the NuGet packages that got built.
|
||||
DevOps uses the `Build-Umbraco` command several times, each time passing a different *target* parameter. The supported targets are:
|
||||
|
||||
* `pre-build`: prepares the build
|
||||
* `compile-belle`: compiles Belle
|
||||
* `compile-umbraco`: compiles Umbraco
|
||||
* `pre-tests`: prepares the tests
|
||||
* `compile-tests`: compiles the tests
|
||||
* `pre-packages`: prepares the packages
|
||||
* `pkg-zip`: creates the zip files
|
||||
* `pre-nuget`: prepares NuGet packages
|
||||
* `pkg-nuget`: creates NuGet packages
|
||||
|
||||
All these targets are executed when `Build-Umbraco` is invoked without a parameter (or with the `all` parameter). On VSTS, compilations (of Umbraco and tests) are performed by dedicated DevOps tasks. Similarly, creating the NuGet packages is also performed by dedicated DevOps tasks.
|
||||
|
||||
Finally, the produced artifacts are published in two containers that can be downloaded from DevOps: `zips` contains the zip files while `nuget` contains the NuGet packages.
|
||||
|
||||
>During a DevOps build, some environment `UMBRACO_*` variables are exported by the `pre-build` target and can be reused in other targets *and* in DevOps tasks. The `UMBRACO_TMP` environment variable is used in `Umbraco.Tests` to disable some tests that have issues with DevOps at the moment.
|
||||
|
||||
## Quirks
|
||||
|
||||
### PowerShell Quirks
|
||||
|
||||
There is a good chance that running `build.ps1` ends up in error, with messages such as
|
||||
|
||||
>The file ...\build.ps1 is not digitally signed. You cannot run this script on the current system. For more information about running scripts and setting execution policy, see about_Execution_Policies.
|
||||
|
||||
PowerShell has *Execution Policies* that may prevent the script from running. You can check the current policies with:
|
||||
|
||||
PS> Get-ExecutionPolicy -List
|
||||
|
||||
Scope ExecutionPolicy
|
||||
----- ---------------
|
||||
MachinePolicy Undefined
|
||||
UserPolicy Undefined
|
||||
Process Undefined
|
||||
CurrentUser Undefined
|
||||
LocalMachine RemoteSigned
|
||||
|
||||
Policies can be `Restricted`, `AllSigned`, `RemoteSigned`, `Unrestricted` and `Bypass`. Scopes can be `MachinePolicy`, `UserPolicy`, `Process`, `CurrentUser`, `LocalMachine`. You need the current policy to be `RemoteSigned`—as long as it is `Undefined`, the script cannot run. You can change the current user policy with:
|
||||
|
||||
PS> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
|
||||
|
||||
Alternatively, you can do it at machine level, from within an elevated PowerShell session:
|
||||
|
||||
PS> Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy RemoteSigned
|
||||
|
||||
And *then* the script should run. It *might* however still complain about executing scripts, with messages such as:
|
||||
|
||||
>Security warning - Run only scripts that you trust. While scripts from the internet can be useful, this script can potentially harm your computer. If you trust this script, use the Unblock-File cmdlet to allow the script to run without this warning message. Do you want to run ...\build.ps1?
|
||||
[D] Do not run [R] Run once [S] Suspend [?] Help (default is "D"):
|
||||
|
||||
This is usually caused by the scripts being *blocked*. And that usually happens when the source code has been downloaded as a Zip file. When Windows downloads Zip files, they are marked as *blocked* (technically, they have a Zone.Identifier alternate data stream, with a value of "3" to indicate that they were downloaded from the Internet). And when such a Zip file is un-zipped, each and every single file is also marked as blocked.
|
||||
|
||||
The best solution is to unblock the Zip file before un-zipping: right-click the files, open *Properties*, and there should be a *Unblock* checkbox at the bottom of the dialog. If, however, the Zip file has already been un-zipped, it is possible to recursively unblock all files from PowerShell with:
|
||||
|
||||
PS> Get-ChildItem -Recurse *.* | Unblock-File
|
||||
|
||||
### Git Quirks
|
||||
|
||||
Git might have issues dealing with long file paths during build. You may want/need to enable `core.longpaths` support (see [this page](https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path) for details).
|
||||
|
||||
+213
-25
@@ -4,56 +4,244 @@
|
||||
|
||||
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.
|
||||
|
||||
## Getting Started
|
||||
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.
|
||||
## Coding not your thing? Or want more ways to contribute?
|
||||
|
||||
The following steps are a quick-start guide:
|
||||
This document covers contributing to the codebase of the CMS but [the community site has plenty of inspiration for other ways to get involved.][get involved]
|
||||
|
||||
If you don't feel you'd like to make code changes here, you can visit our [documentation repository][docs repo] and use your experience to contribute to making the docs we have, even better.
|
||||
|
||||
We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the Core Collaborators and HQ. So, if you see something on the issue tracker or pull requests you feel you can add to, please don't be shy.
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Before you start](#before-you-start)
|
||||
* [Code of Conduct](#code-of-conduct)
|
||||
* [What can I contribute?](#what-can-i-contribute)
|
||||
+ [Making larger changes](#making-larger-changes)
|
||||
+ [Pull request or package?](#pull-request-or-package)
|
||||
+ [Ownership and copyright](#ownership-and-copyright)
|
||||
- [Finding your first issue: Up for grabs](#finding-your-first-issue-up-for-grabs)
|
||||
- [Making your changes](#making-your-changes)
|
||||
+ [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository)
|
||||
+ [Style guide](#style-guide)
|
||||
+ [Questions?](#questions)
|
||||
- [Creating a pull request](#creating-a-pull-request)
|
||||
- [The review process](#the-review-process)
|
||||
* [Dealing with requested changes](#dealing-with-requested-changes)
|
||||
+ [No longer available?](#no-longer-available)
|
||||
* [The Core Collaborators team](#the-core-collaborators-team)
|
||||
|
||||
## Before you start
|
||||
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
This project and everyone participating in it, is governed by the [our Code of Conduct][code of conduct].
|
||||
|
||||
### What can I contribute?
|
||||
|
||||
We categorise pull requests (PRs) into two categories:
|
||||
|
||||
| PR type | Definition |
|
||||
| --------- | ------------------------------------------------------------ |
|
||||
| Small PRs | Bug fixes and small improvements - can be recognized by seeing a small number of changes and possibly a small number of new files. |
|
||||
| Large PRs | New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGet’s packages.config, NPM’s packages.json, etc.). |
|
||||
|
||||
We’re usually able to handle small PRs pretty quickly. A community volunteer will do the initial review and flag it for Umbraco HQ as “community tested”. If everything looks good, it will be merged pretty quickly [as per the described process][review process].
|
||||
|
||||
We would love to follow the same process for larger PRs but this is not always possible due to time limitations and priorities that need to be aligned. We don’t want to put up any barriers, but this document should set the correct expectations.
|
||||
|
||||
Not all changes are wanted, so on occasion we might close a PR without merging it but if we do, we will give you feedback why we can't accept your changes. **So make sure to [talk to us before making large changes][making larger changes]**, so we can ensure that you don't put all your hard work into something we would not be able to merge.
|
||||
|
||||
#### Making larger changes
|
||||
|
||||
[making larger changes]: #making-larger-changes
|
||||
|
||||
Please make sure to describe your larger ideas in an [issue (bugs)][issues] or [discussion (new features)][discussions], it helps to put in mock up screenshots or videos. If the change makes sense for HQ to include in Umbraco CMS we will leave you some feedback on how we’d like to see it being implemented.
|
||||
|
||||
If a larger pull request is encouraged by Umbraco HQ, the process will be similar to what is described in the small PRs process above, we strive to feedback within 14 days. Finalizing and merging the PR might take longer though as it will likely need to be picked up by the development team to make sure everything is in order. We’ll keep you posted on the progress.
|
||||
|
||||
#### Pull request or package?
|
||||
|
||||
[pr or package]: #pull-request-or-package
|
||||
|
||||
If you're unsure about whether your changes belong in the core Umbraco CMS or if you should turn your idea into a package instead, make sure to [talk to us][making larger changes].
|
||||
|
||||
If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and fix bugs. Eventually, a package could "graduate" to be included in the CMS.
|
||||
|
||||
#### Ownership and copyright
|
||||
|
||||
It is your responsibility to make sure that you're allowed to share the code you're providing us. For example, you should have permission from your employer or customer to share code.
|
||||
|
||||
Similarly, if your contribution is copied or adapted from somewhere else, make sure that the license allows you to reuse that for a contribution to Umbraco-CMS.
|
||||
|
||||
If you're not sure, leave a note on your contribution and we will be happy to guide you.
|
||||
|
||||
When your contribution has been accepted, it will be [MIT licensed][MIT license] from that time onwards.
|
||||
|
||||
## Finding your first issue: Up for grabs
|
||||
|
||||
Umbraco HQ will regularly mark newly created issues on the issue tracker with [the `community/up-for-grabs` tag][up for grabs issues]. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out.
|
||||
|
||||
If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has.
|
||||
|
||||
## Making your changes
|
||||
|
||||
Great question! The short version goes like this:
|
||||
|
||||
1. **Fork**
|
||||
|
||||
Create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS)
|
||||
Create a fork of [`Umbraco-CMS` on GitHub][Umbraco CMS repo]
|
||||
|
||||

|
||||
|
||||
2. **Clone**
|
||||
1. **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
|
||||
|
||||

|
||||
|
||||
3. **Switch to the correct branch**
|
||||
1. **Switch to the correct branch**
|
||||
|
||||
Switch to the `contrib` branch
|
||||
Switch to the `v10/contrib` branch
|
||||
|
||||
4. **Build**
|
||||
1. **Build**
|
||||
|
||||
Build your fork of Umbraco locally [as described in the build documentation](BUILD.md), you can build with any IDE that supports dotnet or the command line.
|
||||
Build your fork of Umbraco locally as described in the build documentation: you can [debug with Visual Studio Code][build - debugging with code] or [with Visual Studio][build - debugging with vs].
|
||||
|
||||
5. **Branch**
|
||||
1. **Branch**
|
||||
|
||||
Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp/12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `contrib`, create a new branch first.
|
||||
Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `v10/contrib`, create a new branch first.
|
||||
|
||||
6. **Change**
|
||||
1. **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][questions].
|
||||
|
||||
7. **Commit and push**
|
||||
1. **Commit and push**
|
||||
|
||||
Done? Yay! 🎉
|
||||
|
||||
Remember to commit to your new `temp` branch, and don't commit to `contrib`. Then you can push the changes up to your fork on GitHub.
|
||||
Remember to commit to your new `temp` branch, and don't commit to `v10/contrib`. Then you can push the changes up to your fork on GitHub.
|
||||
|
||||
8. **Create pull request**
|
||||
#### Keeping your Umbraco fork in sync with the main repository
|
||||
[sync fork]: #keeping-your-umbraco-fork-in-sync-with-the-main-repository
|
||||
|
||||
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.
|
||||
Once you've already got a fork and cloned your fork locally, you can skip steps 1 and 2 going forward. Just remember to keep your fork up to date before making further changes.
|
||||
|
||||
Want to read further? [Creating a pull request and what happens next](contributing-creating-a-pr.md).
|
||||
To sync your fork with this original one, you'll have to add the upstream url. You only have to do this once:
|
||||
|
||||
## Further contribution guides
|
||||
```
|
||||
git remote add upstream https://github.com/umbraco/Umbraco-CMS.git
|
||||
```
|
||||
|
||||
- [Before you start](contributing-before-you-start.md)
|
||||
- [Finding your first issue: Up for grabs](contributing-before-you-start.md)
|
||||
- [Contributing to the new backoffice](https://docs.umbraco.com/umbraco-backoffice/)
|
||||
- [Unwanted changes](contributing-unwanted-changes.md)
|
||||
- [Other ways to contribute](contributing-other-ways-to-contribute.md)
|
||||
Then when you want to get the changes from the main repository:
|
||||
|
||||
```
|
||||
git fetch upstream
|
||||
git rebase upstream/v10/contrib
|
||||
```
|
||||
|
||||
In this command we're syncing with the `v10/contrib` branch, but you can of course choose another one if needed.
|
||||
|
||||
[More information on how this works can be found on the thoughtbot blog.][sync fork ext]
|
||||
|
||||
#### Style guide
|
||||
|
||||
To be honest, we don't like rules very much. We trust you have the best of intentions and we encourage you to create working code. If it doesn't look perfect then we'll happily help clean it up.
|
||||
|
||||
That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc.
|
||||
|
||||
#### Questions?
|
||||
[questions]: #questions
|
||||
|
||||
You can get in touch with [the core contributors team][core collabs] in multiple ways; we love open conversations and we are a friendly bunch. No question you have is stupid. Any question you have usually helps out multiple people with the same question. Ask away:
|
||||
|
||||
- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward.
|
||||
- If you want to ask questions on some code you've already written you can create a draft pull request, [detailed in a GitHub blog post][draft prs].
|
||||
- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"][contrib forum] forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction.
|
||||
|
||||
## Creating a pull request
|
||||
|
||||
Exciting! You're ready to show us your changes.
|
||||
|
||||
We recommend you to [sync with our repository][sync fork] before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier.
|
||||
|
||||
GitHub will have picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go.
|
||||

|
||||
|
||||
We like to use [git flow][git flow] as much as possible, but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v10/contrib`. If you are working on v9, this is the branch you should be targeting.
|
||||
|
||||
Please note: we are no longer accepting features for v8 and below but will continue to merge security fixes as and when they arise.
|
||||
|
||||
## The review process
|
||||
[review process]: #the-review-process
|
||||
|
||||
You've sent us your first contribution - congratulations! Now what?
|
||||
|
||||
The [Core Collaborators team][Core collabs] can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request that you make some additional changes.
|
||||
|
||||
You will get an initial automated reply from our [Friendly Umbraco Robot, Umbrabot][Umbrabot], to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. You can take this opportunity to double check everything is in order based off the handy checklist Umbrabot provides.
|
||||
|
||||
You will get feedback as soon as the [Core Collaborators team][Core collabs] can after opening the PR. You’ll most likely get feedback within a couple of weeks. Then there are a few possible outcomes:
|
||||
|
||||
- Your proposed change is awesome! We merge it in and it will be included in the next minor release of Umbraco
|
||||
- If the change is a high priority bug fix, we will cherry-pick it into the next patch release as well so that we can release it as soon as possible
|
||||
- Your proposed change is awesome but needs a bit more work, we’ll give you feedback on the changes we’d like to see
|
||||
- Your proposed change is awesome but... not something we’re looking to include at this point. We’ll close your PR and the related issue (we’ll be nice about it!). See [making larger changes][making larger changes] and [pull request or package?][pr or package]
|
||||
|
||||
### Dealing with requested changes
|
||||
|
||||
If you make the corrections we ask for in the same branch and push them to your fork again, the pull request automatically updates with the additional commit(s) so we can review it again. If all is well, we'll merge the code and your commits are forever part of Umbraco!
|
||||
|
||||
#### No longer available?
|
||||
|
||||
We understand you have other things to do and can't just drop everything to help us out.
|
||||
|
||||
So if we’re asking for your help to improve the PR we’ll wait for two weeks to give you a fair chance to make changes. We’ll ask for an update if we don’t hear back from you after that time.
|
||||
|
||||
If we don’t hear back from you for 4 weeks, we’ll close the PR so that it doesn’t just hang around forever. You’re very welcome to re-open it once you have some more time to spend on it.
|
||||
|
||||
There will be times that we really like your proposed changes and we’ll finish the final improvements we’d like to see ourselves. You still get the credits and your commits will live on in the git repository.
|
||||
|
||||
### The Core Collaborators team
|
||||
[Core collabs]: #the-core-collaborators-team
|
||||
|
||||
The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan][Sebastiaan], who gets assistance from the following community members who have committed to volunteering their free time:
|
||||
|
||||
- [Nathan Woulfe][Nathan Woulfe]
|
||||
- [Joe Glombek][Joe Glombek]
|
||||
- [Laura Weatherhead][Laura Weatherhead]
|
||||
- [Michael Latouche][Michael Latouche]
|
||||
- [Owain Williams][Owain Williams]
|
||||
|
||||
|
||||
These wonderful people aim to provide you with a reply to your PR, review and test out your changes and on occasions, they might ask more questions. If they are happy with your work, they'll let Umbraco HQ know by approving the PR. HQ will have final sign-off and will check the work again before it is merged.
|
||||
|
||||
<!-- Reference links for easy updating -->
|
||||
|
||||
<!-- Local -->
|
||||
|
||||
[MIT license]: ../LICENSE.md "Umbraco's license declaration"
|
||||
[build - debugging with vs]: BUILD.md#debugging-with-visual-studio "Details on building and debugging Umbraco with Visual Studio"
|
||||
[build - debugging with code]: BUILD.md#debugging-with-vs-code "Details on building and debugging Umbraco with Visual Studio Code"
|
||||
|
||||
<!-- External -->
|
||||
|
||||
[Nathan Woulfe]: https://github.com/nathanwoulfe "Nathan's GitHub profile"
|
||||
[Joe Glombek]: https://github.com/glombek "Joe's GitHub profile"
|
||||
[Laura Weatherhead]: https://github.com/lssweatherhead "Laura's GitHub profile"
|
||||
[Michael Latouche]: https://github.com/mikecp "Michael's GitHub profile"
|
||||
[Owain Williams]: https://github.com/OwainWilliams "Owain's GitHub profile"
|
||||
[Sebastiaan]: https://github.com/nul800sebastiaan "Senastiaan's GitHub profile"
|
||||
[ Umbrabot ]: https://github.com/umbrabot
|
||||
[git flow]: https://jeffkreeftmeijer.com/git-flow/ "An explanation of git flow"
|
||||
[sync fork ext]: http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated "Details on keeping a git fork updated"
|
||||
[draft prs]: https://github.blog/2019-02-14-introducing-draft-pull-requests/ "Github's blog post providing details on draft pull requests"
|
||||
[contrib forum]: https://our.umbraco.com/forum/contributing-to-umbraco-cms/
|
||||
[get involved]: https://community.umbraco.com/get-involved/
|
||||
[docs repo]: https://github.com/umbraco/UmbracoDocs
|
||||
[code of conduct]: https://github.com/umbraco/.github/blob/main/.github/CODE_OF_CONDUCT.md
|
||||
[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs
|
||||
[Umbraco CMS repo]: https://github.com/umbraco/Umbraco-CMS
|
||||
[issues]: https://github.com/umbraco/Umbraco-CMS/issues
|
||||
[discussions]: https://github.com/umbraco/Umbraco-CMS/discussions
|
||||
|
||||
+2
-11
@@ -1,12 +1,4 @@
|
||||
# [Umbraco CMS](https://umbraco.com)
|
||||
|
||||
[](../LICENSE.md)
|
||||
[](CONTRIBUTING.md)
|
||||
[](https://twitter.com/intent/follow?screen_name=umbraco)
|
||||
[](https://discord.gg/umbraco)
|
||||
[](https://discord-chats.umbraco.com)
|
||||
[](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301)
|
||||
[](https://github.com/codespaces/new?hide_repo_select=true&ref=contrib&repo=10601208&machine=basicLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestEurope)
|
||||
# [Umbraco CMS](https://umbraco.com) · [](../LICENSE.md) [](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [](CONTRIBUTING.md) [](https://twitter.com/intent/follow?screen_name=umbraco) [](https://discord.gg/umbraco)
|
||||
|
||||
Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
|
||||
|
||||
@@ -39,8 +31,7 @@ The documentation for Umbraco CMS can be found [on Our Umbraco](https://docs.umb
|
||||
|
||||
Our friendly community is available 24/7 at the community hub, we call ["Our Umbraco"](https://our.umbraco.com/). Our Umbraco features forums for questions and answers, documentation, downloadable plugins for Umbraco, and a rich collection of community resources.
|
||||
|
||||
Besides "Our", we all support each other in our [Community Discord Server](https://discord.gg/umbraco) and on Twitter: [Umbraco HQ](https://twitter.com/umbraco), [Release Updates](https://twitter.com/umbracoproject), [#umbraco](https://twitter.com/hashtag/umbraco)
|
||||
|
||||
Besides "Our", we all support each other also via Twitter: [Umbraco HQ](https://twitter.com/umbraco), [Release Updates](https://twitter.com/umbracoproject), [#umbraco](https://twitter.com/hashtag/umbraco)
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# GitHub CodeSpaces
|
||||
Umbraco source code can be edited inside the browser with VSCode and CodeSpaces.
|
||||
|
||||
This development environment comes with all the tools you need to build Umbraco source code.
|
||||
|
||||
|
||||
## Debugging and Running
|
||||
From VSCode browse to the Run and Debug section and then click the green button. This will build the Umbraco source code and attach a debugger and launch the site.
|
||||
|
||||
## Default Umbraco credentials
|
||||
Username: test@umbraco.com
|
||||
Password: password1234
|
||||
|
||||
## Test Email Server
|
||||
A SMTP4Dev instance for testing email is available on port 5000.
|
||||
|
||||
## SQLite Database
|
||||
The SQLite extension is preinstalled and allows you to open, query, edit the data inside the Umbraco SQLite database for ease of use.
|
||||
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
## Before you start
|
||||
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
This project and everyone participating in it, is governed by the [our Code of Conduct][code of conduct].
|
||||
|
||||
### What can I contribute?
|
||||
|
||||
We categorise pull requests (PRs) into two categories:
|
||||
|
||||
| PR type | Definition |
|
||||
| --------- | ------------------------------------------------------------ |
|
||||
| Small PRs | Bug fixes and small improvements - can be recognized by seeing a small number of changes and possibly a small number of new files. |
|
||||
| Large PRs | New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGet’s packages.config, NPM’s packages.json, etc.). |
|
||||
|
||||
We’re usually able to handle small PRs pretty quickly. A community volunteer will do the initial review and flag it for Umbraco HQ as “community tested”. If everything looks good, it will be merged pretty quickly [as per the described process][review process].
|
||||
|
||||
We would love to follow the same process for larger PRs but this is not always possible due to time limitations and priorities that need to be aligned. We don’t want to put up any barriers, but this document should set the correct expectations.
|
||||
|
||||
Not all changes are wanted, so on occasion we might close a PR without merging it but if we do, we will give you feedback why we can't accept your changes. **So make sure to [talk to us before making large changes][making larger changes]**, so we can ensure that you don't put all your hard work into something we would not be able to merge.
|
||||
|
||||
#### Making larger changes
|
||||
|
||||
[making larger changes]: #making-larger-changes
|
||||
|
||||
Please make sure to describe your larger ideas in an [issue (bugs)][issues] or [discussion (new features)][discussions], it helps to put in mock up screenshots or videos. If the change makes sense for HQ to include in Umbraco CMS we will leave you some feedback on how we’d like to see it being implemented.
|
||||
|
||||
If a larger pull request is encouraged by Umbraco HQ, the process will be similar to what is described in the small PRs process above, we strive to feedback within 14 days. Finalizing and merging the PR might take longer though as it will likely need to be picked up by the development team to make sure everything is in order. We’ll keep you posted on the progress.
|
||||
|
||||
#### Pull request or package?
|
||||
|
||||
[pr or package]: #pull-request-or-package
|
||||
|
||||
If you're unsure about whether your changes belong in the core Umbraco CMS or if you should turn your idea into a package instead, make sure to [talk to us][making larger changes].
|
||||
|
||||
If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and fix bugs. Eventually, a package could "graduate" to be included in the CMS.
|
||||
|
||||
#### Ownership and copyright
|
||||
|
||||
It is your responsibility to make sure that you're allowed to share the code you're providing us. For example, you should have permission from your employer or customer to share code.
|
||||
|
||||
Similarly, if your contribution is copied or adapted from somewhere else, make sure that the license allows you to reuse that for a contribution to Umbraco-CMS.
|
||||
|
||||
If you're not sure, leave a note on your contribution and we will be happy to guide you.
|
||||
|
||||
When your contribution has been accepted, it will be [MIT licensed][MIT license] from that time onwards.
|
||||
|
||||
|
||||
[MIT license]: ../LICENSE.md "Umbraco's license declaration"
|
||||
|
||||
|
||||
[issues]: https://github.com/umbraco/Umbraco-CMS/issues
|
||||
[discussions]: https://github.com/umbraco/Umbraco-CMS/discussions
|
||||
@@ -1,28 +0,0 @@
|
||||
### The Core Collaborators team
|
||||
[Core collabs]: #the-core-collaborators-team
|
||||
|
||||
The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan][Sebastiaan], who gets assistance from the following community members who have committed to volunteering their free time:
|
||||
|
||||
- [Busra Sengul][Busra Sengul]
|
||||
- [Emma Garland][Emma Garland]
|
||||
- [George Bidder][George Bidder]
|
||||
- [Jason Elkin][Jason Elkin]
|
||||
- [Laura Neto][Laura Neto]
|
||||
- [Kyle Eck][Kyle Eck]
|
||||
- [Michael Latouche][Michael Latouche]
|
||||
- [Sebastiaan][Sebastiaan]
|
||||
|
||||
|
||||
These wonderful people aim to provide you with a reply to your PR, review and test out your changes and on occasions, they might ask more questions. If they are happy with your work, they'll let Umbraco HQ know by approving the PR. HQ will have final sign-off and will check the work again before it is merged.
|
||||
|
||||
|
||||
<!-- External -->
|
||||
|
||||
[Busra Sengul]: https://github.com/busrasengul "Busra's GitHub profile"
|
||||
[Emma Garland]: https://github.com/emmagarland "Emma's GitHub profile"
|
||||
[George Bidder]: https://github.com/georgebid "George's GitHub profile"
|
||||
[Jason Elkin]: https://github.com/jasonelkin "Jason's GitHub profile"
|
||||
[Kyle Eck]: https://github.com/teckspeed "Kyle's GitHub profile"
|
||||
[Laura Neto]: https://github.com/lauraneto "Laura's GitHub profile"
|
||||
[Michael Latouche]: https://github.com/mikecp "Michael's GitHub profile"
|
||||
[Sebastiaan]: https://github.com/nul800sebastiaan "Sebastiaan's GitHub profile"
|
||||
@@ -1,51 +0,0 @@
|
||||
## Creating a pull request
|
||||
|
||||
Exciting! You're ready to show us your changes.
|
||||
|
||||
We recommend you to [sync with our repository][sync fork] before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier.
|
||||
|
||||
GitHub will have picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go.
|
||||

|
||||
|
||||
We like to use [git flow][git flow] as much as possible, but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to `contrib`. This is the branch you should be targeting.
|
||||
|
||||
Please note: we are no longer accepting features for v8 and below but will continue to merge security fixes as and when they arise.
|
||||
|
||||
## The review process
|
||||
[review process]: #the-review-process
|
||||
|
||||
You've sent us your contribution - congratulations! Now what?
|
||||
|
||||
The [Core Collaborators team][Core collabs] can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request that you make some additional changes.
|
||||
|
||||
You will get an initial automated reply from our [Friendly Umbraco Robot, Umbrabot][Umbrabot], to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. You can take this opportunity to double check everything is in order based off the handy checklist Umbrabot provides.
|
||||
|
||||
You will get feedback as soon as the [Core Collaborators team][Core collabs] can after opening the PR. You’ll most likely get feedback within a couple of weeks. Then there are a few possible outcomes:
|
||||
|
||||
- Your proposed change is awesome! We merge it in and it will be included in the next minor release of Umbraco
|
||||
- If the change is a high priority bug fix, we will cherry-pick it into the next patch release as well so that we can release it as soon as possible
|
||||
- Your proposed change is awesome but needs a bit more work, we’ll give you feedback on the changes we’d like to see
|
||||
- Your proposed change is awesome but... not something we’re looking to include at this point. We’ll close your PR and the related issue (we’ll be nice about it!). See [making larger changes][making larger changes] and [pull request or package?][pr or package]
|
||||
|
||||
### Dealing with requested changes
|
||||
|
||||
If you make the corrections we ask for in the same branch and push them to your fork again, the pull request automatically updates with the additional commit(s) so we can review it again. If all is well, we'll merge the code and your commits are forever part of Umbraco!
|
||||
|
||||
#### No longer available?
|
||||
|
||||
We understand you have other things to do and can't just drop everything to help us out.
|
||||
|
||||
So if we’re asking for your help to improve the PR we’ll wait for two weeks to give you a fair chance to make changes. We’ll ask for an update if we don’t hear back from you after that time.
|
||||
|
||||
If we don’t hear back from you for 4 weeks, we’ll close the PR so that it doesn’t just hang around forever. You’re very welcome to re-open it once you have some more time to spend on it.
|
||||
|
||||
There will be times that we really like your proposed changes and we’ll finish the final improvements we’d like to see ourselves. You still get the credits and your commits will live on in the git repository.
|
||||
|
||||
|
||||
[ Umbrabot ]: https://github.com/umbrabot
|
||||
[git flow]: https://jeffkreeftmeijer.com/git-flow/ "An explanation of git flow"
|
||||
|
||||
|
||||
[making larger changes]: contributing-before-you-start.md#making-large-changes
|
||||
[pr or package]: contributing-before-you-start.md#pull-request-or-package
|
||||
[Core collabs]: contributing-core-collabs-team.md
|
||||
@@ -1,93 +0,0 @@
|
||||
## Finding your first issue: Up for grabs
|
||||
|
||||
Umbraco HQ will regularly mark newly created issues on the issue tracker with [the `community/up-for-grabs` tag][up for grabs issues]. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out.
|
||||
|
||||
If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has.
|
||||
|
||||
## Making your changes
|
||||
|
||||
Great question! The short version goes like this:
|
||||
|
||||
1. **Fork**
|
||||
|
||||
Create a fork of [`Umbraco-CMS` on GitHub][Umbraco CMS repo]
|
||||
|
||||

|
||||
|
||||
1. **Clone**
|
||||
|
||||
When GitHub has created your fork, you can clone it in your favorite Git tool
|
||||
|
||||

|
||||
|
||||
1. **Switch to the correct branch**
|
||||
|
||||
Switch to the `contrib` branch
|
||||
|
||||
1. **Build**
|
||||
|
||||
Build your fork of Umbraco locally as described in the build documentation: you can [debug with Visual Studio Code][build - debugging with code] or [with Visual Studio][build - debugging with vs].
|
||||
|
||||
1. **Branch**
|
||||
|
||||
Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `contrib`, create a new branch first.
|
||||
|
||||
1. **Change**
|
||||
|
||||
Make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback][questions].
|
||||
|
||||
1. **Commit and push**
|
||||
|
||||
Done? Yay! 🎉
|
||||
|
||||
Remember to commit to your new `temp` branch, and don't commit to `contrib`. Then you can push the changes up to your fork on GitHub.
|
||||
|
||||
#### Keeping your Umbraco fork in sync with the main repository
|
||||
[sync fork]: #keeping-your-umbraco-fork-in-sync-with-the-main-repository
|
||||
|
||||
Once you've already got a fork and cloned your fork locally, you can skip steps 1 and 2 going forward. Just remember to keep your fork up to date before making further changes.
|
||||
|
||||
To sync your fork with this original one, you'll have to add the upstream url. You only have to do this once:
|
||||
|
||||
```
|
||||
git remote add upstream https://github.com/umbraco/Umbraco-CMS.git
|
||||
```
|
||||
|
||||
Then when you want to get the changes from the main repository:
|
||||
|
||||
```
|
||||
git fetch upstream
|
||||
git rebase upstream/contrib
|
||||
```
|
||||
|
||||
In this command we're syncing with the `contrib` branch, but you can of course choose another one if needed.
|
||||
|
||||
[More information on how this works can be found on the thoughtbot blog.][sync fork ext]
|
||||
|
||||
#### Style guide
|
||||
|
||||
To be honest, we don't like rules very much. We trust you have the best of intentions and we encourage you to create working code. If it doesn't look perfect then we'll happily help clean it up.
|
||||
|
||||
That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc.
|
||||
|
||||
#### Questions?
|
||||
[questions]: #questions
|
||||
|
||||
You can get in touch with [the core contributors team][core collabs] in multiple ways; we love open conversations and we are a friendly bunch. No question you have is stupid. Any question you have usually helps out multiple people with the same question. Ask away:
|
||||
|
||||
- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward.
|
||||
- If you want to ask questions on some code you've already written you can create a draft pull request, [detailed in a GitHub blog post][draft prs].
|
||||
- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"][contrib forum] forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction.
|
||||
|
||||
|
||||
<!-- Local -->
|
||||
|
||||
[build - debugging with vs]: BUILD.md#debugging-with-visual-studio "Details on building and debugging Umbraco with Visual Studio"
|
||||
[build - debugging with code]: BUILD.md#debugging-with-vs-code "Details on building and debugging Umbraco with Visual Studio Code"
|
||||
|
||||
|
||||
[sync fork ext]: http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated "Details on keeping a git fork updated"
|
||||
[draft prs]: https://github.blog/2019-02-14-introducing-draft-pull-requests/ "Github's blog post providing details on draft pull requests"
|
||||
[contrib forum]: https://our.umbraco.com/forum/contributing-to-umbraco-cms/
|
||||
[Umbraco CMS repo]: https://github.com/umbraco/Umbraco-CMS
|
||||
[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs
|
||||
@@ -1,10 +0,0 @@
|
||||
## Coding not your thing? Or want more ways to contribute?
|
||||
|
||||
This document covers contributing to the codebase of the CMS but [the community site has plenty of inspiration for other ways to get involved.][get involved]
|
||||
|
||||
If you don't feel you'd like to make code changes here, you can visit our [documentation repository][docs repo] and use your experience to contribute to making the docs we have, even better.
|
||||
|
||||
We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the Core Collaborators and HQ. So, if you see something on the issue tracker or pull requests you feel you can add to, please don't be shy.
|
||||
|
||||
[get involved]: https://community.umbraco.com/get-involved/
|
||||
[docs repo]: https://github.com/umbraco/UmbracoDocs
|
||||
@@ -1,18 +0,0 @@
|
||||
## Unwanted changes
|
||||
While most changes are welcome, there are certain types of changes that are discouraged and might get your pull request refused.
|
||||
Of course this will depend heavily on the specific change, but please take the following examples in mind.
|
||||
|
||||
- **Breaking changes (code and/or behavioral) 💥** - sometimes it can be a bit hard to know if a change is breaking or not. Fortunately, if it relates to code, the build will fail and warn you.
|
||||
- **Large refactors 🤯** - the larger the refactor, the larger the probability of introducing new bugs/issues.
|
||||
- **Changes to obsolete code and/or property editors ✍️**
|
||||
- **Adding new config options 🦾** - while having more flexibility is (most of the times) better, having too many options can also become overwhelming/confusing, especially if there are other (good/simple) ways to achieve it.
|
||||
- **Whitespace changes 🫥** - while some of our files might not follow the formatting/whitespace rules (mostly old ones), changing several of them in one go would cause major merge conflicts with open pull requests or other work in progress. Do feel free to fix these when you are working on another issue/feature and end up "touching" those files!
|
||||
- **Adding new extension/helper methods ✋** - keep in mind that more code also means more to maintain, so if a helper is only meaningful for a few, it might not be worth adding it to the core.
|
||||
|
||||
While these are only a few examples, it is important to ask yourself these questions before making a pull request:
|
||||
|
||||
- How many will benefit from this change?
|
||||
- Are there other ways to achieve this? And if so, how do they compare?
|
||||
- How maintainable is the change?
|
||||
- What would be the effort to test it properly?
|
||||
- Do the benefits outweigh the risks?
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Add issues to review project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
get-user-type:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ignored: ${{ steps.set-output.outputs.ignored }}
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm install node-fetch@2
|
||||
- uses: actions/github-script@v5
|
||||
name: "Determing HQ user or not"
|
||||
id: set-output
|
||||
with:
|
||||
script: |
|
||||
const fetch = require('node-fetch');
|
||||
const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/users/IsIgnoredUser', {
|
||||
method: 'post',
|
||||
body: JSON.stringify('${{ github.event.issue.user.login }}'),
|
||||
headers: {
|
||||
'Authorization': 'Bearer ${{ secrets.OUR_BOT_API_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
var isIgnoredUser = true;
|
||||
try {
|
||||
if(response.status === 200) {
|
||||
const data = await response.text();
|
||||
isIgnoredUser = data === "true";
|
||||
} else {
|
||||
console.log("Returned data not indicate success:", response.status);
|
||||
}
|
||||
} catch(error) {
|
||||
console.log(error);
|
||||
};
|
||||
core.setOutput("ignored", isIgnoredUser);
|
||||
console.log("Ignored is", isIgnoredUser);
|
||||
add-to-project:
|
||||
permissions:
|
||||
repository-projects: write # for actions/add-to-project
|
||||
if: needs.get-user-type.outputs.ignored == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-user-type]
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
project-url: https://github.com/orgs/${{ github.repository_owner }}/projects/21
|
||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
||||
@@ -15,8 +15,8 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
dotnetVersion: 8.x
|
||||
dotnetIncludePreviewVersions: true
|
||||
dotnetVersion: 6.x
|
||||
dotnetIncludePreviewVersions: false
|
||||
solution: umbraco.sln
|
||||
buildConfiguration: SkipTests
|
||||
DOTNET_NOLOGO: true
|
||||
|
||||
@@ -18,6 +18,8 @@ jobs:
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment', {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -11,10 +11,15 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm install node-fetch@2
|
||||
- name: Fetch random comment 🗣️ and add it to the PR
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment', {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
@@ -41,13 +46,13 @@ jobs:
|
||||
});
|
||||
} else {
|
||||
console.log("Returned data not indicate success.");
|
||||
|
||||
|
||||
if(response.status !== 200) {
|
||||
console.log("Status code:", response.status)
|
||||
}
|
||||
|
||||
console.log("Returned data:", data);
|
||||
|
||||
|
||||
if(data === '') {
|
||||
console.log("An empty response usually indicates that either no comment was found or the actor user was not eligible for getting an automated response (HQ users are not getting auto-responses).")
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ jobs:
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const fetch = require('node-fetch');
|
||||
const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment', {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
|
||||
+2
-8
@@ -101,12 +101,6 @@ preserve.belle
|
||||
/tests/Umbraco.Tests.UnitTests/[Uu]mbraco/[Dd]ata/TEMP/
|
||||
|
||||
# Ignore auto-generated schema
|
||||
/src/Umbraco.Cms.Targets/tasks/
|
||||
/src/Umbraco.Cms.Targets/appsettings-schema.*.json
|
||||
/src/Umbraco.Web.UI/appsettings-schema.json
|
||||
/src/Umbraco.Web.UI/appsettings-schema.*.json
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.json
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
|
||||
/src/Umbraco.Cms/appsettings-schema.json
|
||||
playwright-report
|
||||
trace.zip
|
||||
/src/Umbraco.Web.UI/appsettings-schema.json
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.json
|
||||
|
||||
Vendored
+1
@@ -8,6 +8,7 @@
|
||||
"name": ".NET Core Launch (web)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "Dotnet build",
|
||||
"program": "dotnet",
|
||||
"args": ["run"],
|
||||
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
|
||||
|
||||
+13
-17
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Company>Umbraco HQ</Company>
|
||||
<Authors>Umbraco</Authors>
|
||||
<Copyright>Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy'))</Copyright>
|
||||
@@ -16,41 +16,37 @@
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- SourceLink -->
|
||||
<PropertyGroup>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Package Validation -->
|
||||
<PropertyGroup>
|
||||
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
|
||||
<EnablePackageValidation>true</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>13.0.0</PackageValidationBaselineVersion>
|
||||
<PackageValidationBaselineVersion>10.0.0</PackageValidationBaselineVersion>
|
||||
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
|
||||
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Calculate version only once for the whole repository -->
|
||||
<PropertyGroup>
|
||||
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.5.113" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
<PackageReference Include="Umbraco.Code" Version="2.0.0" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
<PackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.1.1" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Include icon in generated NuGet packages -->
|
||||
<ItemGroup>
|
||||
<Content Include="$(MSBuildThisFileDirectory)icon.png" Pack="true" PackagePath="" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Use version range on project references (to limit on major version in generated packages) -->
|
||||
<Target Name="_GetProjectReferenceVersionRanges" AfterTargets="_GetProjectReferenceVersions">
|
||||
<ItemGroup>
|
||||
<_ProjectReferencesWithVersions Condition="'%(ProjectVersion)' != ''">
|
||||
<ProjectVersion>[%(ProjectVersion), $([MSBuild]::Add($([System.Text.RegularExpressions.Regex]::Match('%(ProjectVersion)', '^\d+').Value), 1)))</ProjectVersion>
|
||||
</_ProjectReferencesWithVersions>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
<PropertyGroup>
|
||||
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<!-- Global packages (private, build-time packages for all projects) -->
|
||||
<ItemGroup>
|
||||
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.139" />
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
|
||||
<GlobalPackageReference Include="Umbraco.Code" Version="2.2.0" />
|
||||
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.22" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Umbraco.JsonSchema.Extensions" Version="0.3.0" />
|
||||
<PackageVersion Include="Umbraco.CSharpTest.Net.Collections" Version="15.0.0" />
|
||||
</ItemGroup>
|
||||
<!-- Third-party packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="7.1.1" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="7.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.8.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.8.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.8.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.192" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.3.8" />
|
||||
<PackageVersion Include="ncrontab" Version="3.3.3" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageVersion Include="NPoco" Version="5.7.1" />
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="5.7.1" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="4.10.1" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="4.10.1" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="4.10.1" />
|
||||
<PackageVersion Include="Serilog" Version="3.1.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
|
||||
<PackageVersion Include="Smidge.InMemory" Version="4.6.0" />
|
||||
<PackageVersion Include="Smidge.Nuglify" Version="4.6.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||
</ItemGroup>
|
||||
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
|
||||
<ItemGroup>
|
||||
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Azure.Identity -->
|
||||
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
|
||||
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.3" />
|
||||
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.7.1" />
|
||||
<!-- Examine.Lucene bring in a vulnerable version of Lucene.Net.Replicator -->
|
||||
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
|
||||
<!-- Both OpenIddict.AspNetCore, Microsoft.EntityFrameworkCore.* bring in a vulnerable version of Microsoft.Extensions.Caching.Memory -->
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.6" />
|
||||
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+187
-524
@@ -5,10 +5,6 @@ parameters:
|
||||
displayName: Run SQL Server Integration Tests
|
||||
type: boolean
|
||||
default: false
|
||||
- name: sqlServerAcceptanceTests
|
||||
displayName: Run SQL Server Acceptance Tests
|
||||
type: boolean
|
||||
default: false
|
||||
- name: myGetDeploy
|
||||
displayName: Deploy to MyGet
|
||||
type: boolean
|
||||
@@ -25,37 +21,11 @@ parameters:
|
||||
displayName: Upload API docs
|
||||
type: boolean
|
||||
default: false
|
||||
- name: uploadDependencyTrack
|
||||
displayName: Upload BOMs to Dependency Track
|
||||
type: boolean
|
||||
default: false
|
||||
- name: forceReleaseTestFilter
|
||||
displayName: Force to use the release test filters
|
||||
type: boolean
|
||||
default: false
|
||||
- name: integrationNonReleaseTestFilter
|
||||
displayName: TestFilter used for non-release type builds
|
||||
type: string
|
||||
default: '--filter TestCategory!=LongRunning&TestCategory!=NonCritical'
|
||||
- name: integrationReleaseTestFilter
|
||||
displayName: TestFilter used for release type builds
|
||||
type: string
|
||||
default: ' '
|
||||
- name: nonWindowsIntegrationNonReleaseTestFilter
|
||||
displayName: TestFilter used for non-release type builds on non Windows agents
|
||||
type: string
|
||||
default: '--filter TestCategory!=LongRunning&TestCategory!=NonCritical'
|
||||
- name: nonWindowsIntegrationReleaseTestFilter
|
||||
displayName: TestFilter used for release type builds on non Windows agents
|
||||
type: string
|
||||
default: ' '
|
||||
- name: isNightly
|
||||
displayName: 'Is nightly build (used for MyGet feed)'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
variables:
|
||||
nodeVersion: 20
|
||||
nodeVersion: 14.18.1
|
||||
dotnetVersion: 6.x
|
||||
dotnetIncludePreviewVersions: false
|
||||
solution: umbraco.sln
|
||||
buildConfiguration: Release
|
||||
UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042
|
||||
@@ -75,15 +45,10 @@ stages:
|
||||
- job: A
|
||||
displayName: Build Umbraco CMS
|
||||
pool:
|
||||
vmImage: 'windows-latest'
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 500
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
retryCountOnTaskFailure: 3
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
- task: Cache@2
|
||||
@@ -96,56 +61,63 @@ stages:
|
||||
path: $(npm_config_cache)
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
displayName: Run npm ci (Backoffice)
|
||||
- powershell: |
|
||||
npm install --global @cyclonedx/cyclonedx-npm
|
||||
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)\bom\bom-backoffice.xml --ignore-npm-errors --verbose
|
||||
displayName: Generate Backoffice UI BOM
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
displayName: Run npm ci
|
||||
- task: gulp@0
|
||||
displayName: Run gulp build (Backoffice)
|
||||
displayName: Run gulp build
|
||||
inputs:
|
||||
gulpFile: src/Umbraco.Web.UI.Client/gulpfile.js
|
||||
targets: coreBuild
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- task: npm@1
|
||||
displayName: Run npm ci (Login)
|
||||
inputs:
|
||||
command: custom
|
||||
workingDir: src/Umbraco.Web.UI.Login
|
||||
verbose: false
|
||||
customCommand: ci
|
||||
- powershell: |
|
||||
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)\bom\bom-login.xml --ignore-npm-errors --verbose
|
||||
displayName: Generate Login UI BOM
|
||||
workingDirectory: src/Umbraco.Web.UI.Login
|
||||
- task: npm@1
|
||||
displayName: Run npm build (Login)
|
||||
inputs:
|
||||
command: custom
|
||||
workingDir: src/Umbraco.Web.UI.Login
|
||||
verbose: false
|
||||
customCommand: run build
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
displayName: Use .NET $(dotnetVersion)
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
version: $(dotnetVersion)
|
||||
performMultiLevelLookup: true
|
||||
includePreviewVersions: $(dotnetIncludePreviewVersions)
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Run dotnet restore
|
||||
inputs:
|
||||
command: restore
|
||||
projects: $(solution)
|
||||
- task: DotNetCoreCLI@2
|
||||
name: build
|
||||
displayName: Run dotnet build and generate NuGet packages
|
||||
displayName: Run dotnet build
|
||||
inputs:
|
||||
command: build
|
||||
projects: $(solution)
|
||||
arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg'
|
||||
- powershell: |
|
||||
dotnet tool install --global CycloneDX
|
||||
dotnet-CycloneDX $(solution) --output $(Build.ArtifactStagingDirectory)/bom --filename bom-dotnet.xml
|
||||
displayName: 'Generate Backend BOM'
|
||||
arguments: '--configuration $(buildConfiguration) --no-restore -p:ContinuousIntegrationBuild=true'
|
||||
- script: |
|
||||
version="$(Build.BuildNumber)"
|
||||
echo "varsion: $version"
|
||||
|
||||
major="$(echo $version | cut -d '.' -f 1)"
|
||||
echo "major version: $major"
|
||||
|
||||
echo "##vso[task.setvariable variable=majorVersion;isOutput=true]$major"
|
||||
displayName: Set major version
|
||||
name: determineMajorVersion
|
||||
- task: PowerShell@2
|
||||
displayName: Prepare nupkg
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
$umbracoVersion = "$(Build.BuildNumber)" -replace "\+",".g"
|
||||
$templatePaths = Get-ChildItem 'templates/**/.template.config/template.json'
|
||||
|
||||
foreach ($templatePath in $templatePaths) {
|
||||
$a = Get-Content $templatePath -Raw | ConvertFrom-Json
|
||||
if ($a.symbols -and $a.symbols.UmbracoVersion) {
|
||||
$a.symbols.UmbracoVersion.defaultValue = $umbracoVersion
|
||||
$a | ConvertTo-Json -Depth 32 | Set-Content $templatePath
|
||||
}
|
||||
}
|
||||
|
||||
dotnet pack $(solution) --configuration $(buildConfiguration) -p:BuildProjectReferences=false --output $(Build.ArtifactStagingDirectory)/nupkg
|
||||
- script: |
|
||||
sha="$(Build.SourceVersion)"
|
||||
sha=${sha:0:7}
|
||||
buildnumber="$(Build.BuildNumber)_$(Build.BuildId)_$sha"
|
||||
echo "##vso[build.updatebuildnumber]$buildnumber"
|
||||
displayName: Update build number
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish nupkg
|
||||
inputs:
|
||||
@@ -156,42 +128,13 @@ stages:
|
||||
inputs:
|
||||
targetPath: $(Build.SourcesDirectory)
|
||||
artifactName: build_output
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish Backend BOM
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/bom
|
||||
artifactName: bom-build
|
||||
|
||||
- stage: E2E_BOM
|
||||
displayName: E2E Tests BOM Generation
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- job:
|
||||
displayName: E2E Generate BOM
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 500
|
||||
- template: templates/e2e-install.yml
|
||||
parameters:
|
||||
nodeVersion: ${{ variables.nodeVersion }}
|
||||
npm_config_cache: ${{ variables.npm_config_cache }}
|
||||
- powershell: |
|
||||
npm install --global @cyclonedx/cyclonedx-npm
|
||||
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)/bom/bom-e2e.xml --ignore-npm-errors --verbose
|
||||
displayName: Generate E2E Tests BOM
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
- publish: $(Build.ArtifactStagingDirectory)/bom
|
||||
artifact: bom-e2e
|
||||
displayName: 'Publish E2E BOM'
|
||||
|
||||
- stage: Build_Docs
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.buildApiDocs}}))
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.buildApiDocs}}))
|
||||
displayName: Prepare API Documentation
|
||||
dependsOn: Build
|
||||
variables:
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ]
|
||||
jobs:
|
||||
# C# API Reference
|
||||
- job:
|
||||
@@ -199,16 +142,12 @@ stages:
|
||||
pool:
|
||||
vmImage: 'windows-latest'
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
- task: PowerShell@2
|
||||
displayName: Install DocFX
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
choco install docfx --version=2.59.4 -y
|
||||
choco install docfx --version=2.59.2 -y
|
||||
if ($lastexitcode -ne 0){
|
||||
throw ("Error installing DocFX")
|
||||
}
|
||||
@@ -248,16 +187,10 @@ stages:
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 1
|
||||
fetchFilter: tree:0
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js 10.15.x
|
||||
retryCountOnTaskFailure: 3
|
||||
displayName: Use Node.js 10.15.0
|
||||
inputs:
|
||||
versionSpec: 10.15.x # Won't work with higher versions
|
||||
versionSpec: 10.15.0 # Won't work with higher versions
|
||||
- script: |
|
||||
npm ci --no-fund --no-audit --prefer-offline
|
||||
npx gulp docs
|
||||
@@ -304,33 +237,28 @@ stages:
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 1
|
||||
fetchFilter: tree:0
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download build artifacts
|
||||
inputs:
|
||||
artifact: build_output
|
||||
path: $(Build.SourcesDirectory)
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
displayName: Use .NET $(dotnetVersion)
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
version: $(dotnetVersion)
|
||||
performMultiLevelLookup: true
|
||||
includePreviewVersions: $(dotnetIncludePreviewVersions)
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Run dotnet test
|
||||
inputs:
|
||||
command: test
|
||||
projects: 'tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj'
|
||||
projects: '**/*.Tests.UnitTests.csproj'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build'
|
||||
testRunTitle: Unit Tests - $(Agent.OS)
|
||||
|
||||
- stage: Integration
|
||||
displayName: Integration Tests
|
||||
dependsOn: Build
|
||||
variables:
|
||||
releaseTestFilter: eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True')
|
||||
jobs:
|
||||
# Integration Tests (SQLite)
|
||||
- job:
|
||||
@@ -345,470 +273,211 @@ stages:
|
||||
vmImage: 'macOS-latest'
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
variables:
|
||||
Tests__Database__DatabaseType: 'Sqlite'
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 1
|
||||
fetchFilter: tree:0
|
||||
# Setup test environment
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download build artifacts
|
||||
inputs:
|
||||
artifact: build_output
|
||||
path: $(Build.SourcesDirectory)
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
displayName: Use .NET $(dotnetVersion)
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
|
||||
# Test
|
||||
version: $(dotnetVersion)
|
||||
performMultiLevelLookup: true
|
||||
includePreviewVersions: $(dotnetIncludePreviewVersions)
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Run dotnet test
|
||||
inputs:
|
||||
command: test
|
||||
projects: 'tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj'
|
||||
projects: '**/*.Tests.Integration.csproj'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build'
|
||||
testRunTitle: Integration Tests SQLite - $(Agent.OS)
|
||||
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
env:
|
||||
Tests__Database__DatabaseType: 'Sqlite'
|
||||
Umbraco__CMS__Global__MainDomLock: 'FileSystemMainDomLock'
|
||||
|
||||
# Integration Tests (SQL Server)
|
||||
- job:
|
||||
timeoutInMinutes: 120
|
||||
# We are currently encountering issues when running SQL Server Linux tests Microsoft.Data.SqlClient.SqlException (0x80131904)
|
||||
# condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}})
|
||||
condition: eq(${{parameters.sqlServerIntegrationTests}}, True)
|
||||
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.sqlServerIntegrationTests}})
|
||||
displayName: Integration Tests (SQL Server)
|
||||
strategy:
|
||||
matrix:
|
||||
Windows:
|
||||
vmImage: 'windows-latest'
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
testDb: LocalDb
|
||||
connectionString: N/A
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
SA_PASSWORD: UmbracoIntegration123!
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: 'Server=(local);User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
|
||||
testDb: SqlServer
|
||||
connectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);'
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
variables:
|
||||
SA_PASSWORD: UmbracoIntegration123!
|
||||
steps:
|
||||
# Setup test environment
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download build artifacts
|
||||
inputs:
|
||||
artifact: build_output
|
||||
path: $(Build.SourcesDirectory)
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
|
||||
# Start SQL Server
|
||||
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
|
||||
displayName: Start SQL Server Docker image (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: SqlLocalDB start MSSQLLocalDB
|
||||
displayName: Start SQL Server LocalDB (Windows)
|
||||
- powershell: sqllocaldb start mssqllocaldb
|
||||
displayName: Start localdb (Windows only)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
# Test
|
||||
- powershell: docker run --name mssql -d -p 1433:1433 -e ACCEPT_EULA=Y -e SA_PASSWORD=$(SA_PASSWORD) -e MSSQL_PID=Developer mcr.microsoft.com/mssql/server:2019-latest
|
||||
displayName: Start SQL Server (Linux only)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Run dotnet test
|
||||
inputs:
|
||||
command: test
|
||||
projects: 'tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj'
|
||||
projects: '**/*.Tests.Integration.csproj'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build'
|
||||
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
|
||||
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
|
||||
# Stop SQL Server
|
||||
- pwsh: docker stop mssql
|
||||
displayName: Stop SQL Server Docker image (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: SqlLocalDB stop MSSQLLocalDB
|
||||
displayName: Stop SQL Server LocalDB (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
env:
|
||||
Tests__Database__DatabaseType: $(testDb)
|
||||
Tests__Database__SQLServerMasterConnectionString: $(connectionString)
|
||||
Umbraco__CMS__Global__MainDomLock: 'SqlMainDomLock'
|
||||
|
||||
- stage: E2E
|
||||
displayName: E2E Tests
|
||||
dependsOn: Build
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
|
||||
# Enable console logging in Release mode
|
||||
SERILOG__WRITETO__0__NAME: Async
|
||||
SERILOG__WRITETO__0__ARGS__CONFIGURE__0__NAME: Console
|
||||
# Set unattended install settings
|
||||
UMBRACO__CMS__UNATTENDED__INSTALLUNATTENDED: true
|
||||
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERNAME: Playwright Test
|
||||
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD: UmbracoAcceptance123!
|
||||
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL: playwright@umbraco.com
|
||||
# Custom Umbraco settings
|
||||
UMBRACO__CMS__CONTENT__CONTENTVERSIONCLEANUPPOLICY__ENABLECLEANUP: false
|
||||
UMBRACO__CMS__GLOBAL__DISABLEELECTIONFORSINGLESERVER: true
|
||||
UMBRACO__CMS__GLOBAL__INSTALLMISSINGDATABASE: true
|
||||
UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042
|
||||
UMBRACO__CMS__GLOBAL__VERSIONCHECKPERIOD: 0
|
||||
UMBRACO__CMS__GLOBAL__USEHTTPS: true
|
||||
UMBRACO__CMS__HEALTHCHECKS__NOTIFICATION__ENABLED: false
|
||||
UMBRACO__CMS__KEEPALIVE__DISABLEKEEPALIVETASK: true
|
||||
UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL: https://localhost:44331/
|
||||
ASPNETCORE_URLS: https://localhost:44331
|
||||
displayName: E2E Tests
|
||||
dependsOn: Build
|
||||
jobs:
|
||||
# E2E Tests
|
||||
- job:
|
||||
displayName: E2E Tests (SQLite)
|
||||
displayName: E2E Tests
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
|
||||
Umbraco__CMS__Unattended__UnattendedUserName: Playwright Test
|
||||
Umbraco__CMS__Unattended__UnattendedUserPassword: UmbracoAcceptance123!
|
||||
Umbraco__CMS__Unattended__UnattendedUserEmail: playwright@umbraco.com
|
||||
ASPNETCORE_URLS: https://localhost:8443
|
||||
strategy:
|
||||
matrix:
|
||||
LinuxPart1Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
LinuxPart2Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
LinuxPart3Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
dockerfile: umbraco-linux.docker
|
||||
dockerImageName: umbraco-linux
|
||||
Windows:
|
||||
vmImage: 'windows-latest'
|
||||
# Enable console logging in Release mode
|
||||
Serilog__WriteTo__0__Name: Async
|
||||
Serilog__WriteTo__0__Args__configure__0__Name: Console
|
||||
# Set unattended install settings
|
||||
Umbraco__CMS__Unattended__InstallUnattended: true
|
||||
Umbraco__CMS__Global__InstallMissingDatabase: true
|
||||
UmbracoDatabaseServer: (LocalDB)\MSSQLLocalDB
|
||||
UmbracoDatabaseName: Playwright
|
||||
ConnectionStrings__umbracoDbDSN: Server=$(UmbracoDatabaseServer);Database=$(UmbracoDatabaseName);Integrated Security=true;
|
||||
# Custom Umbraco settings
|
||||
Umbraco__CMS__Global__VersionCheckPeriod: 0
|
||||
Umbraco__CMS__Global__UseHttps: true
|
||||
Umbraco__CMS__HealthChecks__Notification__Enabled: false
|
||||
Umbraco__CMS__KeepAlive__DisableKeepAliveTask: true
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
# Setup test environment
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download NuGet artifacts
|
||||
displayName: Download nupkg
|
||||
inputs:
|
||||
artifact: nupkg
|
||||
path: $(Agent.BuildDirectory)/app/nupkg
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
|
||||
- template: templates/e2e-install.yml
|
||||
parameters:
|
||||
nodeVersion: $(nodeVersion)
|
||||
npm_config_cache: $(npm_config_cache)
|
||||
PlaywrightUserEmail: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
PlaywrightPassword: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
ASPNETCORE_URLS: $(ASPNETCORE_URLS)
|
||||
|
||||
# Build application
|
||||
- pwsh: |
|
||||
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
|
||||
dotnet new nugetconfig
|
||||
dotnet nuget add source ./nupkg --name Local
|
||||
dotnet new install Umbraco.Templates::$cmsVersion
|
||||
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
|
||||
dotnet restore UmbracoProject
|
||||
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
|
||||
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
|
||||
dotnet dev-certs https
|
||||
displayName: Build application
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
# Run application
|
||||
- bash: |
|
||||
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
|
||||
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
|
||||
displayName: Run application (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
- pwsh: |
|
||||
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
|
||||
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
|
||||
displayName: Run application (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
# Wait for application to start responding to requests
|
||||
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
|
||||
displayName: Wait for application
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Install Playwright and dependencies
|
||||
- pwsh: npx playwright install chromium
|
||||
displayName: Install Playwright only with Chromium browser
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: $(testCommand)
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
env:
|
||||
CI: true
|
||||
CommitId: $(Build.SourceVersion)
|
||||
AgentOs: $(Agent.OS)
|
||||
|
||||
# Stop application
|
||||
- bash: kill -15 $(AcceptanceTestProcessId)
|
||||
displayName: Stop application (Linux)
|
||||
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
|
||||
displayName: Stop application (Windows)
|
||||
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
# Copy artifacts
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
|
||||
}
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Publish test artifacts
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish test artifacts
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
|
||||
|
||||
# Publish test results
|
||||
- task: PublishTestResults@2
|
||||
displayName: "Publish test results"
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFormat: 'JUnit'
|
||||
testResultsFiles: '*.xml'
|
||||
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
|
||||
testRunTitle: "$(Agent.JobName)"
|
||||
|
||||
- job:
|
||||
displayName: E2E Tests (SQL Server)
|
||||
condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerAcceptanceTests}})
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
strategy:
|
||||
matrix:
|
||||
LinuxPart1Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
LinuxPart2Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
LinuxPart3Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
# Setup test environment
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download NuGet artifacts
|
||||
inputs:
|
||||
artifact: nupkg
|
||||
path: $(Agent.BuildDirectory)/app/nupkg
|
||||
|
||||
path: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/misc/nupkg
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
URL=$(ASPNETCORE_URLS)" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Cache and restore NPM packages
|
||||
- task: Cache@2
|
||||
displayName: Cache NPM packages
|
||||
displayName: Cache node_modules
|
||||
inputs:
|
||||
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
|
||||
key: '"npm_e2e" | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
|
||||
restoreKeys: |
|
||||
npm_e2e | "$(Agent.OS)"
|
||||
npm_e2e
|
||||
"npm_e2e" | "$(Agent.OS)"
|
||||
"npm_e2e"
|
||||
path: $(npm_config_cache)
|
||||
|
||||
- pwsh: |
|
||||
New-Item -Path "." -Name ".env" -ItemType "file" -Value "UMBRACO_USER_LOGIN=$(Umbraco__CMS__Unattended__UnattendedUserEmail)
|
||||
UMBRACO_USER_PASSWORD=$(Umbraco__CMS__Unattended__UnattendedUserPassword)
|
||||
URL=$(ASPNETCORE_URLS)"
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
displayName: Restore NPM packages
|
||||
|
||||
# Build application
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/
|
||||
displayName: Run npm ci
|
||||
- pwsh: sqllocaldb start mssqllocaldb
|
||||
displayName: Start localdb (Windows only)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
- pwsh: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer
|
||||
displayName: Create database (Windows only)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET $(dotnetVersion)
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
performMultiLevelLookup: true
|
||||
includePreviewVersions: $(dotnetIncludePreviewVersions)
|
||||
- pwsh: |
|
||||
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
|
||||
dotnet new nugetconfig
|
||||
dotnet nuget add source ./nupkg --name Local
|
||||
dotnet new install Umbraco.Templates::$cmsVersion
|
||||
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
|
||||
dotnet restore UmbracoProject
|
||||
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
|
||||
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
|
||||
$sha = 'g$(Build.SourceVersion)'.substring(0, 8)
|
||||
docker build -t $(dockerImageName):$sha -f $(dockerfile) .
|
||||
mkdir -p $(Build.ArtifactStagingDirectory)/docker-images
|
||||
docker save -o $(Build.ArtifactStagingDirectory)/docker-images/$(dockerImageName).$sha.tar $(dockerImageName):$sha
|
||||
dotnet dev-certs https -ep ${HOME}/.aspnet/https/aspnetapp.pfx -p $(Umbraco__CMS__Unattended__UnattendedUserPassword)
|
||||
docker run --name $(dockerImageName) -dp 8080:5000 -dp 8443:5001 -e UMBRACO__CMS__GLOBAL__ID=$(UMBRACO__CMS__GLOBAL__ID) -e ASPNETCORE_Kestrel__Certificates__Default__Password="$(Umbraco__CMS__Unattended__UnattendedUserPassword)" -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx -v ${HOME}/.aspnet/https:/https/ $(dockerImageName):$sha
|
||||
docker ps
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
displayName: Build and run container (Linux only)
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest/misc
|
||||
- pwsh: |
|
||||
dotnet new --install ./nupkg/Umbraco.Templates.*.nupkg
|
||||
dotnet new umbraco --name Playwright --no-restore --output .
|
||||
dotnet restore --configfile ./nuget.config
|
||||
dotnet build --configuration $(buildConfiguration) --no-restore
|
||||
dotnet dev-certs https
|
||||
displayName: Build application
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
# Start SQL Server
|
||||
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
|
||||
displayName: Start SQL Server Docker image (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: SqlLocalDB start MSSQLLocalDB
|
||||
displayName: Start SQL Server LocalDB (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
# Run application
|
||||
- bash: |
|
||||
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
|
||||
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
|
||||
displayName: Run application (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
- pwsh: |
|
||||
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
|
||||
$process = Start-Process -FilePath "dotnet" -ArgumentList "run --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
|
||||
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
|
||||
displayName: Run application (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
# Wait for application to start responding to requests
|
||||
displayName: Build and run app (Windows only)
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest/misc
|
||||
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
|
||||
displayName: Wait for application
|
||||
displayName: Wait for app
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Install Playwright and dependencies
|
||||
- pwsh: npx playwright install chromium
|
||||
displayName: Install Playwright only with Chromium browser
|
||||
- pwsh: npx playwright install --with-deps
|
||||
displayName: Install Playwright
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: $(testCommand)
|
||||
displayName: Run Playwright tests
|
||||
- pwsh: npm run test --ignore-certificate-errors
|
||||
displayName: Run Playwright (Desktop)
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
env:
|
||||
CI: true
|
||||
CommitId: $(Build.SourceVersion)
|
||||
AgentOs: $(Agent.OS)
|
||||
|
||||
# Stop application
|
||||
- bash: kill -15 $(AcceptanceTestProcessId)
|
||||
displayName: Stop application (Linux)
|
||||
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
|
||||
displayName: Stop application (Windows)
|
||||
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
# Stop SQL Server
|
||||
- pwsh: docker stop mssql
|
||||
displayName: Stop SQL Server Docker image (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: SqlLocalDB stop MSSQLLocalDB
|
||||
displayName: Stop SQL Server LocalDB (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
# Copy artifacts
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
|
||||
}
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Publish test artifacts
|
||||
docker logs $(dockerImageName) > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1
|
||||
docker stop $(dockerImageName)
|
||||
condition: eq(variables['Agent.OS'], 'Linux')
|
||||
displayName: Stop app (Linux only)
|
||||
- pwsh: Stop-Process $env:AcceptanceTestProcessId
|
||||
condition: eq(variables['Agent.OS'], 'Windows_NT')
|
||||
displayName: Stop app (Windows only)
|
||||
- task: PowerShell@2
|
||||
displayName: Check if artifacts folder exists
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
$MyVariable = Test-Path -Path $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/results
|
||||
Write-Host "##vso[task.setvariable variable=resultFolderExists;]$MyVariable"
|
||||
- task: CopyFiles@2
|
||||
displayName: Prepare artifacts
|
||||
condition: eq(variables.resultFolderExists, 'True')
|
||||
inputs:
|
||||
sourceFolder: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/results/
|
||||
targetFolder: $(Build.ArtifactStagingDirectory)/playwright
|
||||
- task: PublishPipelineArtifact@1
|
||||
condition: always()
|
||||
displayName: Publish test artifacts
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
|
||||
artifact: 'E2E artifacts - $(Agent.OS) - Attempt #$(System.JobAttempt)'
|
||||
|
||||
# Publish test results
|
||||
- task: PublishTestResults@2
|
||||
displayName: "Publish test results"
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFormat: 'JUnit'
|
||||
testResultsFiles: '*.xml'
|
||||
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
|
||||
testRunTitle: "$(Agent.JobName)"
|
||||
|
||||
- stage: Dependency_Track
|
||||
displayName: Dependency Track
|
||||
dependsOn:
|
||||
- Build
|
||||
- E2E_BOM
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadDependencyTrack}}))
|
||||
variables:
|
||||
# Determine Umbraco version based on whether it's a public release or not. If public release, use major version, else use full NuGet package version.
|
||||
umbracoVersion: $[ iif(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'], stageDependencies.Build.A.outputs['build.NBGV_NuGetPackageVersion']) ]
|
||||
jobs:
|
||||
- template: templates/dependency-track.yml
|
||||
parameters:
|
||||
projectName: "Umbraco-CMS"
|
||||
umbracoVersion: $(umbracoVersion)
|
||||
projects:
|
||||
- name: "Backend"
|
||||
artifact: "bom-build"
|
||||
bomFilePath: "bom-dotnet.xml"
|
||||
- name: "Login"
|
||||
artifact: "bom-build"
|
||||
bomFilePath: "bom-login.xml"
|
||||
- name: "Backoffice"
|
||||
artifact: "bom-build"
|
||||
bomFilePath: "bom-backoffice.xml"
|
||||
- name: "E2E"
|
||||
artifact: "bom-e2e"
|
||||
bomFilePath: "bom-e2e.xml"
|
||||
|
||||
###############################################
|
||||
## Release
|
||||
@@ -819,11 +488,9 @@ stages:
|
||||
- Unit
|
||||
- Integration
|
||||
# - E2E # TODO: Enable when stable.
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.myGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
pool:
|
||||
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
|
||||
displayName: Push to pre-release feed
|
||||
steps:
|
||||
- checkout: none
|
||||
@@ -838,20 +505,16 @@ stages:
|
||||
command: 'push'
|
||||
packagesToPush: $(Build.ArtifactStagingDirectory)/**/*.nupkg
|
||||
nuGetFeedType: 'external'
|
||||
${{ if eq(parameters.isNightly, true) }}:
|
||||
publishFeedCredentials: 'MyGet - Umbraco Nightly'
|
||||
${{ else }}:
|
||||
publishFeedCredentials: 'MyGet - Pre-releases'
|
||||
publishFeedCredentials: 'MyGet - Pre-releases'
|
||||
|
||||
- stage: Deploy_NuGet
|
||||
displayName: NuGet release
|
||||
dependsOn:
|
||||
- Deploy_MyGet
|
||||
- Build_Docs
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.nuGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
pool:
|
||||
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
|
||||
displayName: Push to NuGet
|
||||
steps:
|
||||
- checkout: none
|
||||
@@ -872,12 +535,12 @@ stages:
|
||||
pool:
|
||||
vmImage: 'windows-latest' # Apparently AzureFileCopy is windows only :(
|
||||
variables:
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ]
|
||||
displayName: Upload API Documention
|
||||
dependsOn:
|
||||
- Build
|
||||
- Deploy_NuGet
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadApiDocs}}))
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.uploadApiDocs}}))
|
||||
jobs:
|
||||
- job:
|
||||
displayName: Upload C# Docs
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Nightly_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
|
||||
|
||||
pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 0 * * *'
|
||||
displayName: Daily midnight build
|
||||
branches:
|
||||
include:
|
||||
- v10/dev
|
||||
- v12/dev
|
||||
- v13/dev
|
||||
- v14/dev
|
||||
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: TriggerBuild@4
|
||||
inputs:
|
||||
definitionIsInCurrentTeamProject: true
|
||||
buildDefinition: '301'
|
||||
queueBuildForUserThatTriggeredBuild: true
|
||||
ignoreSslCertificateErrors: false
|
||||
useSameSourceVersion: false
|
||||
useCustomSourceVersion: false
|
||||
useSameBranch: true
|
||||
waitForQueuedBuildsToFinish: false
|
||||
storeInEnvironmentVariable: false
|
||||
templateParameters: 'sqlServerIntegrationTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true'
|
||||
authenticationMethod: 'OAuth Token'
|
||||
enableBuildInQueueCondition: false
|
||||
dependentOnSuccessfulBuildCondition: false
|
||||
dependentOnFailedBuildCondition: false
|
||||
checkbuildsoncurrentbranch: false
|
||||
failTaskIfConditionsAreNotFulfilled: false
|
||||
@@ -1,56 +0,0 @@
|
||||
parameters:
|
||||
- name: projectName
|
||||
type: string
|
||||
- name: umbracoVersion
|
||||
type: string
|
||||
- name: projects
|
||||
type: object
|
||||
|
||||
jobs:
|
||||
- job: Create_DT_Project
|
||||
displayName: Create Dependency Track Project
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
- bash: |
|
||||
project_id=$(curl --no-progress-meter -H "X-Api-Key: $(DT_API_KEY)" "$(DT_API_URL)/v1/project/lookup?name=${{ parameters.projectName }}&version=${{ parameters.umbracoVersion }}" | jq -r '.uuid')
|
||||
if [ "$project_id" != "null" ] && [ -n "$project_id" ]; then
|
||||
echo "Project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' already exists (ID: $project_id)."
|
||||
else
|
||||
project_id=$(curl --no-progress-meter \
|
||||
-X PUT "$(DT_API_URL)/v1/project" \
|
||||
-H "X-Api-Key: $(DT_API_KEY)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "${{ parameters.projectName }}", "version": "${{ parameters.umbracoVersion }}", "collectionLogic": "AGGREGATE_DIRECT_CHILDREN"}' \
|
||||
| jq -r '.uuid')
|
||||
if [ -z "$project_id" ] || [ "$project_id" == "null" ]; then
|
||||
echo "Failed to create project '${{ parameters.projectName }}' version '${{ parameters.umbracoVersion }}'."
|
||||
exit 1
|
||||
fi
|
||||
echo "Created project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' (ID: $project_id)."
|
||||
fi
|
||||
displayName: Ensure main project exists in Dependency Track
|
||||
|
||||
- ${{ each project in parameters.projects }}:
|
||||
- job:
|
||||
displayName: Upload ${{ project.name }} BOM
|
||||
dependsOn: Create_DT_Project
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
- download: current
|
||||
artifact: ${{ project.artifact }}
|
||||
displayName: Download ${{ project.artifact }} artifact
|
||||
|
||||
- script: |
|
||||
curl --no-progress-meter --fail-with-body \
|
||||
-X POST "$(DT_API_URL)/v1/bom" \
|
||||
-H "X-Api-Key: $(DT_API_KEY)" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "autoCreate=true" \
|
||||
-F "projectName=${{ parameters.projectName }}-${{ project.name }}" \
|
||||
-F "projectVersion=${{ parameters.umbracoVersion }}" \
|
||||
-F "parentName=${{ parameters.projectName }}" \
|
||||
-F "parentVersion=${{ parameters.umbracoVersion }}" \
|
||||
-F "bom=@$(Pipeline.Workspace)/${{ project.artifact }}/${{ project.bomFilePath }}"
|
||||
displayName: Upload ${{ project.name }} BOM to Dependency Track
|
||||
@@ -1,47 +0,0 @@
|
||||
parameters:
|
||||
- name: nodeVersion
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: npm_config_cache
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: PlaywrightUserEmail
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: PlaywrightPassword
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: ASPNETCORE_URLS
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=${{ parameters.PlaywrightUserEmail }}
|
||||
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
|
||||
URL=${{ parameters.ASPNETCORE_URLS }}" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Cache and restore NPM packages
|
||||
- task: Cache@2
|
||||
displayName: Cache NPM packages
|
||||
inputs:
|
||||
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
|
||||
restoreKeys: |
|
||||
npm_e2e | "$(Agent.OS)"
|
||||
npm_e2e
|
||||
path: ${{ parameters.npm_config_cache }}
|
||||
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
displayName: Restore NPM packages
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.415",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
</configSections>
|
||||
|
||||
<appSettings>
|
||||
<add key="Umbraco.Core.ConfigurationStatus" value="6.0.0"/>
|
||||
<add key="Umbraco.Core.ReservedUrls" value="~/config/splashes/booting.aspx,~/install/default.aspx,~/config/splashes/noNodes.aspx,~/VSEnterpriseHelper.axd,~/.well-known" />
|
||||
<add key="Umbraco.Core.ReservedPaths" value="~/install/"/>
|
||||
<add key="Umbraco.Core.Path" value="~/umbraco"/>
|
||||
<add key="Umbraco.Core.HideTopLevelNodeFromPath" value="true"/>
|
||||
<add key="Umbraco.Core.TimeOutInMinutes" value="20"/>
|
||||
<add key="Umbraco.Core.DefaultUILanguage" value="en"/>
|
||||
<add key="Umbraco.Core.UseHttps" value="false"/>
|
||||
<add key="dataAnnotations:dataTypeAttribute:disableRegEx" value="false"/>
|
||||
</appSettings>
|
||||
|
||||
<connectionStrings>
|
||||
<add name="umbracoDbDSN" connectionString="Datasource=|DataDirectory|UmbracoNPocoTests.sdf;Flush Interval=1;" providerName="System.Data.SqlServerCe.4.0"/>
|
||||
</connectionStrings>
|
||||
|
||||
<system.data>
|
||||
<DbProviderFactories>
|
||||
<remove invariant="System.Data.SqlServerCe.4.0"/>
|
||||
<add name="Microsoft SQL Server Compact Data Provider 4.0" invariant="System.Data.SqlServerCe.4.0" description=".NET Framework Data Provider for Microsoft SQL Server Compact" type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=4.0.0.1, Culture=neutral, PublicKeyToken=89845dcd8080cc91"/>
|
||||
</DbProviderFactories>
|
||||
</system.data>
|
||||
|
||||
<system.web>
|
||||
<httpRuntime targetFramework="4.5"/>
|
||||
<compilation defaultLanguage="c#" debug="true" batch="false" targetFramework="4.0"></compilation>
|
||||
<machineKey validationKey="5E7B955FCE36F5F2A867C2A0D85DC61E7FEA9E15F1561E8386F78BFE9EE23FF18B21E6A44AA17300B3B9D5DBEB37AA61A2C73884A5BBEDA6D3B14BA408A7A8CD" decryptionKey="116B853D031219E404E088FCA0986D6CF2DFA77E1957B59FCC9404B8CA3909A1" validation="SHA1" decryption="AES"/>
|
||||
<!--<trust level="Medium" originUrl=".*"/>-->
|
||||
<!-- Sitemap provider-->
|
||||
<siteMap defaultProvider="UmbracoSiteMapProvider" enabled="true">
|
||||
<providers>
|
||||
<clear/>
|
||||
<add name="UmbracoSiteMapProvider" type="umbraco.presentation.nodeFactory.UmbracoSiteMapProvider" defaultDescriptionAlias="description" securityTrimmingEnabled="true"/>
|
||||
</providers>
|
||||
</siteMap>
|
||||
<!-- Membership Provider -->
|
||||
<membership defaultProvider="UmbracoMembershipProvider" userIsOnlineTimeWindow="15">
|
||||
<providers>
|
||||
<clear/>
|
||||
<add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco.Web" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="4" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Member" passwordFormat="Hashed"/>
|
||||
</providers>
|
||||
</membership>
|
||||
</system.web>
|
||||
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
|
||||
</startup>
|
||||
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="12.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-1.2.5.0" newVersion="1.2.5.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.ComponentModel.Annotations" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.2.1.0" newVersion="4.2.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.2.0.1" newVersion="4.2.0.1" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="4.0.0.0-4.0.3.0" newVersion="4.0.3.0"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="4.0.0.0-4.0.1.1" newVersion="4.0.1.1"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="4.0.0.0-4.1.4.0" newVersion="4.1.4.0"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
|
||||
</configuration>
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Umbraco.Tests")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("Umbraco.Tests")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2012")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("33ddf9b7-505c-4a12-8370-7fee9de5df6d")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
|
||||
// Internals must be visible to DynamicProxyGenAssembly2
|
||||
// in order to mock loggers loggers with types from the assembly
|
||||
// I.E. Mock.Of<ILogger<TestControllerFactory>>()
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Tests.Published
|
||||
{
|
||||
[TestFixture]
|
||||
public class ModelTypeTests
|
||||
{
|
||||
|
||||
//TODO these is not easy to move to the Unittest project due to underlysing NotImplementedException of Type.IsSZArray
|
||||
[Test]
|
||||
public void ModelTypeToStringTests()
|
||||
{
|
||||
var modelType = ModelType.For("alias1");
|
||||
var modelTypeArray = modelType.MakeArrayType();
|
||||
|
||||
Assert.AreEqual("{alias1}", modelType.ToString());
|
||||
|
||||
// there's an "*" there because the arrays are not true SZArray - but that changes when we map
|
||||
|
||||
Assert.AreEqual("{alias1}[*]", modelTypeArray.ToString());
|
||||
var enumArray = typeof(IEnumerable<>).MakeGenericType(modelTypeArray);
|
||||
Assert.AreEqual("System.Collections.Generic.IEnumerable`1[{alias1}[*]]", enumArray.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ModelTypeFullNameTests()
|
||||
{
|
||||
Assert.AreEqual("{alias1}", ModelType.For("alias1").FullName);
|
||||
|
||||
Type type = ModelType.For("alias1");
|
||||
Assert.AreEqual("{alias1}", type.FullName);
|
||||
|
||||
// there's an "*" there because the arrays are not true SZArray - but that changes when we map
|
||||
Assert.AreEqual("{alias1}[*]", ModelType.For("alias1").MakeArrayType().FullName);
|
||||
// note the inner assembly qualified name
|
||||
Assert.AreEqual("System.Collections.Generic.IEnumerable`1[[{alias1}[*], Umbraco.Core, Version=0.5.0.0, Culture=neutral, PublicKeyToken=null]]", typeof(IEnumerable<>).MakeGenericType(ModelType.For("alias1").MakeArrayType()).FullName);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Tests.Common;
|
||||
using Umbraco.Tests.TestHelpers;
|
||||
|
||||
namespace Umbraco.Tests.Routing
|
||||
{
|
||||
public abstract class BaseUrlProviderTest : BaseWebTest
|
||||
{
|
||||
protected IUmbracoContextAccessor UmbracoContextAccessor { get; } = new TestUmbracoContextAccessor();
|
||||
|
||||
protected abstract bool HideTopLevelNodeFromPath { get; }
|
||||
|
||||
protected override void Compose()
|
||||
{
|
||||
base.Compose();
|
||||
Builder.Services.AddTransient<ISiteDomainMapper, SiteDomainMapper>();
|
||||
}
|
||||
|
||||
protected override void ComposeSettings()
|
||||
{
|
||||
var contentSettings = new ContentSettings();
|
||||
var userPasswordConfigurationSettings = new UserPasswordConfigurationSettings();
|
||||
|
||||
Builder.Services.AddTransient(x => Microsoft.Extensions.Options.Options.Create(contentSettings));
|
||||
Builder.Services.AddTransient(x => Microsoft.Extensions.Options.Options.Create(userPasswordConfigurationSettings));
|
||||
}
|
||||
|
||||
protected IPublishedUrlProvider GetPublishedUrlProvider(IUmbracoContext umbracoContext, DefaultUrlProvider urlProvider)
|
||||
{
|
||||
var webRoutingSettings = new WebRoutingSettings();
|
||||
return new UrlProvider(
|
||||
new TestUmbracoContextAccessor(umbracoContext),
|
||||
Microsoft.Extensions.Options.Options.Create(webRoutingSettings),
|
||||
new UrlProviderCollection(new[] { urlProvider }),
|
||||
new MediaUrlProviderCollection(Enumerable.Empty<IMediaUrlProvider>()),
|
||||
Mock.Of<IVariationContextAccessor>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Tests.Common;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Tests.PublishedContent;
|
||||
using Umbraco.Tests.TestHelpers;
|
||||
using Constants = Umbraco.Cms.Core.Constants;
|
||||
|
||||
namespace Umbraco.Tests.Routing
|
||||
{
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
|
||||
public class MediaUrlProviderTests : BaseWebTest
|
||||
{
|
||||
private DefaultMediaUrlProvider _mediaUrlProvider;
|
||||
|
||||
public override void SetUp()
|
||||
{
|
||||
base.SetUp();
|
||||
|
||||
var loggerFactory = NullLoggerFactory.Instance;
|
||||
var mediaFileManager = new MediaFileManager(Mock.Of<IFileSystem>(), Mock.Of<IMediaPathScheme>(),
|
||||
loggerFactory.CreateLogger<MediaFileManager>(), Mock.Of<IShortStringHelper>());
|
||||
var contentSettings = new ContentSettings();
|
||||
var dataTypeService = Mock.Of<IDataTypeService>();
|
||||
var propertyEditors = new MediaUrlGeneratorCollection(new IMediaUrlGenerator[]
|
||||
{
|
||||
new FileUploadPropertyEditor(DataValueEditorFactory, mediaFileManager, Microsoft.Extensions.Options.Options.Create(contentSettings), dataTypeService, LocalizationService, LocalizedTextService, UploadAutoFillProperties, ContentService),
|
||||
new ImageCropperPropertyEditor(DataValueEditorFactory, loggerFactory, mediaFileManager, Microsoft.Extensions.Options.Options.Create(contentSettings), dataTypeService, IOHelper, UploadAutoFillProperties, ContentService),
|
||||
});
|
||||
_mediaUrlProvider = new DefaultMediaUrlProvider(propertyEditors, UriUtility);
|
||||
}
|
||||
|
||||
public override void TearDown()
|
||||
{
|
||||
base.TearDown();
|
||||
|
||||
_mediaUrlProvider = null;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Media_Url_Resolves_Url_From_Upload_Property_Editor()
|
||||
{
|
||||
const string expected = "/media/rfeiw584/test.jpg";
|
||||
|
||||
var umbracoContext = GetUmbracoContext("/");
|
||||
var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, expected, null);
|
||||
|
||||
var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Auto);
|
||||
|
||||
Assert.AreEqual(expected, resolvedUrl);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Media_Url_Resolves_Url_From_Image_Cropper_Property_Editor()
|
||||
{
|
||||
const string expected = "/media/rfeiw584/test.jpg";
|
||||
|
||||
var configuration = new ImageCropperConfiguration();
|
||||
var imageCropperValue = JsonConvert.SerializeObject(new ImageCropperValue
|
||||
{
|
||||
Src = expected
|
||||
});
|
||||
|
||||
var umbracoContext = GetUmbracoContext("/");
|
||||
var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.ImageCropper, imageCropperValue, configuration);
|
||||
|
||||
var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Auto);
|
||||
|
||||
Assert.AreEqual(expected, resolvedUrl);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Media_Url_Can_Resolve_Absolute_Url()
|
||||
{
|
||||
const string mediaUrl = "/media/rfeiw584/test.jpg";
|
||||
var expected = $"http://localhost{mediaUrl}";
|
||||
|
||||
var umbracoContext = GetUmbracoContext("http://localhost");
|
||||
var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, mediaUrl, null);
|
||||
|
||||
var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Absolute);
|
||||
|
||||
Assert.AreEqual(expected, resolvedUrl);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Media_Url_Returns_Absolute_Url_If_Stored_Url_Is_Absolute()
|
||||
{
|
||||
const string expected = "http://localhost/media/rfeiw584/test.jpg";
|
||||
|
||||
var umbracoContext = GetUmbracoContext("http://localhost");
|
||||
var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, expected, null);
|
||||
|
||||
var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Relative);
|
||||
|
||||
Assert.AreEqual(expected, resolvedUrl);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Media_Url_Returns_Empty_String_When_PropertyType_Is_Not_Supported()
|
||||
{
|
||||
var umbracoContext = GetUmbracoContext("/");
|
||||
var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.Boolean, "0", null);
|
||||
|
||||
var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Absolute, propertyAlias: "test");
|
||||
|
||||
Assert.AreEqual(string.Empty, resolvedUrl);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Media_Url_Can_Resolve_Variant_Property_Url()
|
||||
{
|
||||
var umbracoContext = GetUmbracoContext("http://localhost");
|
||||
|
||||
var umbracoFilePropertyType = CreatePropertyType(Constants.PropertyEditors.Aliases.UploadField, null, ContentVariation.Culture);
|
||||
|
||||
const string enMediaUrl = "/media/rfeiw584/en.jpg";
|
||||
const string daMediaUrl = "/media/uf8ewud2/da.jpg";
|
||||
|
||||
var property = new SolidPublishedPropertyWithLanguageVariants
|
||||
{
|
||||
Alias = "umbracoFile",
|
||||
PropertyType = umbracoFilePropertyType,
|
||||
};
|
||||
|
||||
property.SetSourceValue("en", enMediaUrl, true);
|
||||
property.SetSourceValue("da", daMediaUrl);
|
||||
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), new [] { umbracoFilePropertyType }, ContentVariation.Culture);
|
||||
var publishedContent = new SolidPublishedContent(contentType) {Properties = new[] {property}};
|
||||
|
||||
var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Auto, "da");
|
||||
Assert.AreEqual(daMediaUrl, resolvedUrl);
|
||||
}
|
||||
|
||||
private IPublishedUrlProvider GetPublishedUrlProvider(IUmbracoContext umbracoContext)
|
||||
{
|
||||
var webRoutingSettings = new WebRoutingSettings();
|
||||
return new UrlProvider(
|
||||
new TestUmbracoContextAccessor(umbracoContext),
|
||||
Microsoft.Extensions.Options.Options.Create(webRoutingSettings),
|
||||
new UrlProviderCollection(Enumerable.Empty<IUrlProvider>()),
|
||||
new MediaUrlProviderCollection(new []{_mediaUrlProvider}),
|
||||
Mock.Of<IVariationContextAccessor>()
|
||||
);
|
||||
}
|
||||
|
||||
private static IPublishedContent CreatePublishedContent(string propertyEditorAlias, string propertyValue, object dataTypeConfiguration)
|
||||
{
|
||||
var umbracoFilePropertyType = CreatePropertyType(propertyEditorAlias, dataTypeConfiguration, ContentVariation.Nothing);
|
||||
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(),
|
||||
new[] {umbracoFilePropertyType}, ContentVariation.Nothing);
|
||||
|
||||
return new SolidPublishedContent(contentType)
|
||||
{
|
||||
Id = 1234,
|
||||
Key = Guid.NewGuid(),
|
||||
Properties = new[]
|
||||
{
|
||||
new SolidPublishedProperty
|
||||
{
|
||||
Alias = "umbracoFile",
|
||||
SolidSourceValue = propertyValue,
|
||||
SolidHasValue = true,
|
||||
PropertyType = umbracoFilePropertyType
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static PublishedPropertyType CreatePropertyType(string propertyEditorAlias, object dataTypeConfiguration, ContentVariation variation)
|
||||
{
|
||||
var uploadDataType = new PublishedDataType(1234, propertyEditorAlias, new Lazy<object>(() => dataTypeConfiguration));
|
||||
|
||||
var propertyValueConverters = new PropertyValueConverterCollection(new IPropertyValueConverter[]
|
||||
{
|
||||
new UploadPropertyConverter(),
|
||||
new ImageCropperValueConverter(Mock.Of<ILogger<ImageCropperValueConverter>>()),
|
||||
});
|
||||
|
||||
var publishedModelFactory = Mock.Of<IPublishedModelFactory>();
|
||||
var publishedContentTypeFactory = new Mock<IPublishedContentTypeFactory>();
|
||||
publishedContentTypeFactory.Setup(x => x.GetDataType(It.IsAny<int>()))
|
||||
.Returns(uploadDataType);
|
||||
|
||||
return new PublishedPropertyType("umbracoFile", 42, true, variation, propertyValueConverters, publishedModelFactory, publishedContentTypeFactory.Object);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProductVersion>8.0.30703</ProductVersion>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<ProjectGuid>{5D3B8245-ADA6-453F-A008-50ED04BFE770}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Umbraco.Tests</RootNamespace>
|
||||
<AssemblyName>Umbraco.Tests</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
|
||||
<PublishUrl>publish\</PublishUrl>
|
||||
<Install>true</Install>
|
||||
<InstallFrom>Disk</InstallFrom>
|
||||
<UpdateEnabled>false</UpdateEnabled>
|
||||
<UpdateMode>Foreground</UpdateMode>
|
||||
<UpdateInterval>7</UpdateInterval>
|
||||
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
|
||||
<UpdatePeriodically>false</UpdatePeriodically>
|
||||
<UpdateRequired>false</UpdateRequired>
|
||||
<MapFileExtensions>true</MapFileExtensions>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
|
||||
<IsWebBootstrapper>false</IsWebBootstrapper>
|
||||
<UseApplicationTrust>false</UseApplicationTrust>
|
||||
<BootstrapperEnabled>true</BootstrapperEnabled>
|
||||
<TargetFrameworkProfile />
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
<LangVersion>8</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Data.Entity.Design" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.IO" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
<Reference Include="System.Runtime.Caching" />
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
<Reference Include="System.Text.Encoding" />
|
||||
<Reference Include="System.Web" />
|
||||
<Reference Include="System.Web.ApplicationServices" />
|
||||
<Reference Include="System.Web.Extensions" />
|
||||
<Reference Include="System.Web.Services" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Castle.Core" Version="4.4.1" />
|
||||
<PackageReference Include="Examine.Core">
|
||||
<Version>2.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Castle.Core" Version="4.3.1" />
|
||||
<PackageReference Include="Examine" Version="2.0.0" />
|
||||
<PackageReference Include="HtmlAgilityPack">
|
||||
<Version>1.11.31</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Lucene.Net.Contrib" Version="3.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNet.Identity.Core" Version="2.2.3" />
|
||||
<PackageReference Include="Microsoft.AspNet.Mvc" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Owin" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.SelfHost" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Tracing" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.WebHost" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions">
|
||||
<Version>5.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console">
|
||||
<Version>5.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Owin" Version="4.1.1" />
|
||||
<PackageReference Include="Microsoft.Owin.Hosting" Version="4.1.1" />
|
||||
<PackageReference Include="Microsoft.Owin.Security" Version="4.1.1" />
|
||||
<PackageReference Include="Microsoft.Owin.Testing" Version="4.1.1" />
|
||||
<PackageReference Include="Microsoft.Web.Infrastructure" Version="1.0.0.0" />
|
||||
<PackageReference Include="MiniProfiler" Version="4.2.22" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="MiniProfiler" Version="4.0.138" />
|
||||
<PackageReference Include="Moq" Version="4.10.1" />
|
||||
<PackageReference Include="Moq" Version="4.14.5" />
|
||||
<PackageReference Include="NPoco" Version="3.9.4" />
|
||||
<PackageReference Include="NUnit" Version="3.11.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.12.0" />
|
||||
<PackageReference Include="NPoco" Version="4.0.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
|
||||
<PackageReference Include="Owin" Version="1.0" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="5.0.0" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.8.2" />
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
|
||||
<PackageReference Include="Umbraco.SqlServerCE" Version="4.0.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Published\ModelTypeTests.cs" />
|
||||
<Compile Include="Routing\BaseUrlProviderTest.cs" />
|
||||
<Compile Include="Routing\MediaUrlProviderTests.cs" />
|
||||
<Compile Include="Scoping\ScopedNuCacheTests.cs" />
|
||||
<Compile Include="Web\Controllers\AuthenticationControllerTests.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config">
|
||||
<SubType>Designer</SubType>
|
||||
</None>
|
||||
<None Include="unit-test-logger.config">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Umbraco.Core\Umbraco.Core.csproj">
|
||||
<Project>{29aa69d9-b597-4395-8d42-43b1263c240a}</Project>
|
||||
<Name>Umbraco.Core</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\src\Umbraco.Examine.Lucene\Umbraco.Examine.Lucene.csproj">
|
||||
<Project>{0fad7d2a-d7dd-45b1-91fd-488bb6cdacea}</Project>
|
||||
<Name>Umbraco.Examine.Lucene</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="...\..\srcUmbraco.Infrastructure\Umbraco.Infrastructure.csproj">
|
||||
<Project>{3ae7bf57-966b-45a5-910a-954d7c554441}</Project>
|
||||
<Name>Umbraco.Infrastructure</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\src\Umbraco.Persistence.SqlCe\Umbraco.Persistence.SqlCe.csproj">
|
||||
<Project>{33085570-9bf2-4065-a9b0-a29d920d13ba}</Project>
|
||||
<Name>Umbraco.Persistence.SqlCe</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\src\Umbraco.PublishedCache.NuCache\Umbraco.PublishedCache.NuCache.csproj">
|
||||
<Project>{f6de8da0-07cc-4ef2-8a59-2bc81dbb3830}</Project>
|
||||
<Name>Umbraco.PublishedCache.NuCache</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Umbraco.Tests.Common\Umbraco.Tests.Common.csproj">
|
||||
<Project>{a499779c-1b3b-48a8-b551-458e582e6e96}</Project>
|
||||
<Name>Umbraco.Tests.Common</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\src\Umbraco.Web\Umbraco.Web.csproj">
|
||||
<Project>{651E1350-91B6-44B7-BD60-7207006D7003}</Project>
|
||||
<Name>Umbraco.Web</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<!-- get NuGet packages directory -->
|
||||
<PropertyGroup>
|
||||
<NuGetPackages>$(NuGetPackageFolders.Split(';')[0])</NuGetPackages>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Target Name="BeforeBuild">
|
||||
<Message Text="-BeforeBuild-" Importance="high" />
|
||||
<Message Text="MSBuildExtensionsPath: $(MSBuildExtensionsPath)" Importance="high" />
|
||||
<Message Text="WebPublishingTasks: $(WebPublishingTasks)" Importance="high" />
|
||||
<Message Text="NuGetPackageFolders: $(NuGetPackageFolders)" Importance="high" />
|
||||
<Message Text="NuGetPackages: $(NuGetPackages)" Importance="high" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,122 @@
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Features;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Tests.TestHelpers;
|
||||
using Umbraco.Tests.Testing;
|
||||
|
||||
namespace Umbraco.Tests.Web.Controllers
|
||||
{
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.None)]
|
||||
public class AuthenticationControllerTests : TestWithDatabaseBase
|
||||
{
|
||||
protected override void ComposeApplication(bool withApplication)
|
||||
{
|
||||
base.ComposeApplication(withApplication);
|
||||
//if (!withApplication) return;
|
||||
|
||||
// replace the true IUserService implementation with a mock
|
||||
// so that each test can configure the service to their liking
|
||||
Builder.Services.AddUnique(f => Mock.Of<IUserService>());
|
||||
|
||||
// kill the true IEntityService too
|
||||
Builder.Services.AddUnique(f => Mock.Of<IEntityService>());
|
||||
|
||||
Builder.Services.AddUnique<UmbracoFeatures>();
|
||||
}
|
||||
|
||||
|
||||
// TODO Reintroduce when moved to .NET Core
|
||||
// [Test]
|
||||
// public async System.Threading.Tasks.Task GetCurrentUser_Fips()
|
||||
// {
|
||||
// ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor)
|
||||
// {
|
||||
// //setup some mocks
|
||||
// var userServiceMock = Mock.Get(ServiceContext.UserService);
|
||||
// userServiceMock.Setup(service => service.GetUserById(It.IsAny<int>()))
|
||||
// .Returns(() => null);
|
||||
//
|
||||
// if (Thread.GetDomain().GetData(".appPath") != null)
|
||||
// {
|
||||
// HttpContext.Current = new HttpContext(new SimpleWorkerRequest("", "", new StringWriter()));
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// var baseDir = IOHelper.MapPath("").TrimEnd(Path.DirectorySeparatorChar);
|
||||
// HttpContext.Current = new HttpContext(new SimpleWorkerRequest("/", baseDir, "", "", new StringWriter()));
|
||||
// }
|
||||
//
|
||||
// var usersController = new AuthenticationController(
|
||||
// new TestUserPasswordConfig(),
|
||||
// Factory.GetInstance<IGlobalSettings>(),
|
||||
// Factory.GetInstance<IHostingEnvironment>(),
|
||||
// umbracoContextAccessor,
|
||||
// Factory.GetInstance<ISqlContext>(),
|
||||
// Factory.GetInstance<ServiceContext>(),
|
||||
// Factory.GetInstance<AppCaches>(),
|
||||
// Factory.GetInstance<IProfilingLogger>(),
|
||||
// Factory.GetInstance<IRuntimeState>(),
|
||||
// Factory.GetInstance<UmbracoMapper>(),
|
||||
// Factory.GetInstance<ISecuritySettings>(),
|
||||
// Factory.GetInstance<IPublishedUrlProvider>(),
|
||||
// Factory.GetInstance<IRequestAccessor>(),
|
||||
// Factory.GetInstance<IEmailSender>()
|
||||
// );
|
||||
// return usersController;
|
||||
// }
|
||||
//
|
||||
// Mock.Get(Current.SqlContext)
|
||||
// .Setup(x => x.Query<IUser>())
|
||||
// .Returns(new Query<IUser>(Current.SqlContext));
|
||||
//
|
||||
// var syntax = new SqlCeSyntaxProvider();
|
||||
//
|
||||
// Mock.Get(Current.SqlContext)
|
||||
// .Setup(x => x.SqlSyntax)
|
||||
// .Returns(syntax);
|
||||
//
|
||||
// var mappers = new MapperCollection(new[]
|
||||
// {
|
||||
// new UserMapper(new Lazy<ISqlContext>(() => Current.SqlContext), new ConcurrentDictionary<Type, ConcurrentDictionary<string, string>>())
|
||||
// });
|
||||
//
|
||||
// Mock.Get(Current.SqlContext)
|
||||
// .Setup(x => x.Mappers)
|
||||
// .Returns(mappers);
|
||||
//
|
||||
// // Testing what happens if the system were configured to only use FIPS-compliant algorithms
|
||||
// var typ = typeof(CryptoConfig);
|
||||
// var flds = typ.GetFields(BindingFlags.Static | BindingFlags.NonPublic);
|
||||
// var haveFld = flds.FirstOrDefault(f => f.Name == "s_haveFipsAlgorithmPolicy");
|
||||
// var isFld = flds.FirstOrDefault(f => f.Name == "s_fipsAlgorithmPolicy");
|
||||
// var originalFipsValue = CryptoConfig.AllowOnlyFipsAlgorithms;
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// if (!originalFipsValue)
|
||||
// {
|
||||
// haveFld.SetValue(null, true);
|
||||
// isFld.SetValue(null, true);
|
||||
// }
|
||||
//
|
||||
// var runner = new TestRunner(CtrlFactory);
|
||||
// var response = await runner.Execute("Authentication", "GetCurrentUser", HttpMethod.Get);
|
||||
//
|
||||
// var obj = JsonConvert.DeserializeObject<UserDetail>(response.Item2);
|
||||
// Assert.AreEqual(-1, obj.UserId);
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// if (!originalFipsValue)
|
||||
// {
|
||||
// haveFld.SetValue(null, false);
|
||||
// isFld.SetValue(null, false);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<log4net>
|
||||
<root>
|
||||
<priority value="OFF"/>
|
||||
</root>
|
||||
</log4net>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<appSettings>
|
||||
<!-- Global Log Level event -->
|
||||
<add key="serilog:minimum-level" value="Warning" />
|
||||
|
||||
<!-- Write to console -->
|
||||
<add key="serilog:using:Console" value="Serilog.Sinks.Console" />
|
||||
<add key="serilog:write-to:Console.theme" value="Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console" />
|
||||
<add key="serilog:write-to:Console.outputTemplate" value="[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}" />
|
||||
|
||||
<!-- Namespace log levels -->
|
||||
<add key="serilog:minimum-level:override:Umbraco.Core.Publishing.PublishingStrategy" value="Warning" />
|
||||
<add key="serilog:minimum-level:override:Umbraco.Core.TypeLoader" value="Warning" />
|
||||
<add key="serilog:minimum-level:override:Umbraco.Core.Persistence.UmbracoDatabase" value="Debug" />
|
||||
<add key="serilog:minimum-level:override:Umbraco.Core.Persistence.Migrations.Initial.DatabaseSchemaCreation" value="Warning" />
|
||||
<add key="serilog:minimum-level:override:Umbraco.Core.Persistence.Migrations.Initial.BaseDataCreation" value="Warning" />
|
||||
|
||||
</appSettings>
|
||||
</configuration>
|
||||
@@ -0,0 +1,53 @@
|
||||
name: issue-first-response
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
send-response:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm install node-fetch@2
|
||||
- name: Fetch random comment 🗣️ and add it to the issue
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment', {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
repo: '${{ github.repository }}',
|
||||
number: '${{ github.event.number }}',
|
||||
actor: '${{ github.actor }}',
|
||||
commentType: 'opened-issue-first-comment'
|
||||
}),
|
||||
headers: {
|
||||
'Authorization': 'Bearer ${{ secrets.OUR_BOT_API_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await response.text();
|
||||
|
||||
if(response.status === 200 && data !== '') {
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: data
|
||||
});
|
||||
} else {
|
||||
console.log("Status code did not indicate success:", response.status);
|
||||
console.log("Returned data:", data);
|
||||
}
|
||||
} catch(error) {
|
||||
console.log(error);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Deploy.Core.Configuration.DebugConfiguration;
|
||||
using Umbraco.Deploy.Core.Configuration.DeployConfiguration;
|
||||
using Umbraco.Deploy.Core.Configuration.DeployProjectConfiguration;
|
||||
using Umbraco.Forms.Core.Configuration;
|
||||
using SecuritySettings = Umbraco.Cms.Core.Configuration.Models.SecuritySettings;
|
||||
|
||||
namespace JsonSchema
|
||||
{
|
||||
internal class AppSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Umbraco
|
||||
/// </summary>
|
||||
public UmbracoDefinition? Umbraco { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configuration of Umbraco CMS and packages
|
||||
/// </summary>
|
||||
internal class UmbracoDefinition
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public CmsDefinition? CMS { get; set; }
|
||||
|
||||
public FormsDefinition? Forms { get; set; }
|
||||
|
||||
public DeployDefinition? Deploy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configurations for the Umbraco CMS
|
||||
/// </summary>
|
||||
public class CmsDefinition
|
||||
{
|
||||
public ContentSettings? Content { get; set; }
|
||||
public CoreDebugSettings? Debug { get; set; }
|
||||
|
||||
public ExceptionFilterSettings? ExceptionFilter { get; set; }
|
||||
|
||||
public ModelsBuilderSettings? ModelsBuilder { get; set; }
|
||||
|
||||
public GlobalSettings? Global { get; set; }
|
||||
|
||||
public HealthChecksSettings? HealthChecks { get; set; }
|
||||
|
||||
public HostingSettings? Hosting { get; set; }
|
||||
|
||||
public ImagingSettings? Imaging { get; set; }
|
||||
|
||||
public IndexCreatorSettings? Examine { get; set; }
|
||||
|
||||
public KeepAliveSettings? KeepAlive { get; set; }
|
||||
|
||||
public LoggingSettings? Logging { get; set; }
|
||||
|
||||
public NuCacheSettings? NuCache { get; set; }
|
||||
|
||||
public RequestHandlerSettings? RequestHandler { get; set; }
|
||||
|
||||
public RuntimeSettings? Runtime { get; set; }
|
||||
|
||||
public SecuritySettings? Security { get; set; }
|
||||
|
||||
public TourSettings? Tours { get; set; }
|
||||
|
||||
public TypeFinderSettings? TypeFinder { get; set; }
|
||||
|
||||
public WebRoutingSettings? WebRouting { get; set; }
|
||||
|
||||
public UmbracoPluginSettings? Plugins { get; set; }
|
||||
|
||||
public UnattendedSettings? Unattended { get; set; }
|
||||
|
||||
public RichTextEditorSettings? RichTextEditor { get; set; }
|
||||
|
||||
public RuntimeMinificationSettings? RuntimeMinification { get; set; }
|
||||
|
||||
public BasicAuthSettings? BasicAuth { get; set; }
|
||||
|
||||
public PackageMigrationSettings? PackageMigration { get; set; }
|
||||
|
||||
public LegacyPasswordMigrationSettings? LegacyPasswordMigration { get; set; }
|
||||
|
||||
public ContentDashboardSettings? ContentDashboard { get; set; }
|
||||
|
||||
public HelpPageSettings? HelpPage { get; set; }
|
||||
|
||||
public InstallDefaultDataSettings? DefaultDataCreation { get; set; }
|
||||
|
||||
public DataTypesSettings? DataTypes { get; set; }
|
||||
|
||||
public MarketplaceSettings? Marketplace { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configurations for the Umbraco Forms package to Umbraco CMS
|
||||
/// </summary>
|
||||
public class FormsDefinition
|
||||
{
|
||||
public FormDesignSettings? FormDesign { get; set; }
|
||||
|
||||
public PackageOptionSettings? Options { get; set; }
|
||||
|
||||
public Umbraco.Forms.Core.Configuration.SecuritySettings? Security { get; set; }
|
||||
|
||||
public FieldTypesDefinition? FieldTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configurations for the Umbraco Forms Field Types
|
||||
/// </summary>
|
||||
public class FieldTypesDefinition
|
||||
{
|
||||
public DatePickerSettings? DatePicker { get; set; }
|
||||
|
||||
public Recaptcha2Settings? Recaptcha2 { get; set; }
|
||||
|
||||
public Recaptcha3Settings? Recaptcha3 { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configurations for the Umbraco Deploy package to Umbraco CMS
|
||||
/// </summary>
|
||||
public class DeployDefinition
|
||||
{
|
||||
public DeploySettings? Settings { get; set; }
|
||||
|
||||
public DeployProjectConfig? Project { get; set; }
|
||||
|
||||
public DebugSettings? Debug { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
<EnablePackageValidation>false</EnablePackageValidation>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="NJsonSchema" Version="10.7.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
|
||||
<PackageReference Include="Umbraco.Deploy.Core" Version="10.1.2" />
|
||||
<PackageReference Include="Umbraco.Forms.Core" Version="10.2.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using NJsonSchema.Generation;
|
||||
|
||||
namespace JsonSchema
|
||||
{
|
||||
internal class NamespacePrefixedSchemaNameGenerator : DefaultSchemaNameGenerator
|
||||
{
|
||||
public override string Generate(Type type) => type.Namespace?.Replace(".", string.Empty) + base.Generate(type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using CommandLine;
|
||||
|
||||
namespace JsonSchema
|
||||
{
|
||||
internal class Options
|
||||
{
|
||||
[Option('o', "outputFile", Required = false, HelpText = "Set path of the output file.", Default = "appsettings-schema.json")]
|
||||
public string OutputFile { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
|
||||
namespace JsonSchema
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Parser.Default.ParseArguments<Options>(args)
|
||||
.WithParsedAsync(Execute);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task Execute(Options options)
|
||||
{
|
||||
var generator = new UmbracoJsonSchemaGenerator();
|
||||
var schema = await generator.Generate();
|
||||
|
||||
var path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, options.OutputFile));
|
||||
Console.WriteLine("Path to use {0}", path);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
Console.WriteLine("Ensured directory exists");
|
||||
await File.WriteAllTextAsync(path, schema);
|
||||
|
||||
Console.WriteLine("File written at {0}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NJsonSchema.Generation;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
namespace JsonSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// Generator of the JsonSchema for AppSettings.json including A specific Umbraco version.
|
||||
/// </summary>
|
||||
public class UmbracoJsonSchemaGenerator
|
||||
{
|
||||
private static readonly HttpClient s_client = new ();
|
||||
private readonly JsonSchemaGenerator _innerGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoJsonSchemaGenerator" /> class.
|
||||
/// </summary>
|
||||
public UmbracoJsonSchemaGenerator()
|
||||
=> _innerGenerator = new JsonSchemaGenerator(new UmbracoJsonSchemaGeneratorSettings());
|
||||
|
||||
/// <summary>
|
||||
/// Generates a json representing the JsonSchema for AppSettings.json including A specific Umbraco version..
|
||||
/// </summary>
|
||||
public async Task<string> Generate()
|
||||
{
|
||||
JObject umbracoSchema = GenerateUmbracoSchema();
|
||||
JObject officialSchema = await GetOfficialAppSettingsSchema();
|
||||
|
||||
officialSchema.Merge(umbracoSchema);
|
||||
|
||||
return officialSchema.ToString();
|
||||
}
|
||||
|
||||
private async Task<JObject> GetOfficialAppSettingsSchema()
|
||||
{
|
||||
HttpResponseMessage response = await s_client.GetAsync("https://json.schemastore.org/appsettings.json")
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonConvert.DeserializeObject<JObject>(result)!;
|
||||
}
|
||||
|
||||
private JObject GenerateUmbracoSchema()
|
||||
{
|
||||
NJsonSchema.JsonSchema schema = _innerGenerator.Generate(typeof(AppSettings));
|
||||
|
||||
// TODO: when the "UmbracoPath" setter is removed from "GlobalSettings" (scheduled for V12), remove this line as well
|
||||
schema.Definitions["UmbracoCmsCoreConfigurationModelsGlobalSettings"]?.Properties?.Remove(nameof(GlobalSettings.UmbracoPath));
|
||||
|
||||
return JsonConvert.DeserializeObject<JObject>(schema.ToJson())!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using NJsonSchema.Generation;
|
||||
|
||||
namespace JsonSchema
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class UmbracoJsonSchemaGeneratorSettings : JsonSchemaGeneratorSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoJsonSchemaGeneratorSettings"/> class.
|
||||
/// </summary>
|
||||
public UmbracoJsonSchemaGeneratorSettings()
|
||||
{
|
||||
AlwaysAllowAdditionalObjectProperties = true;
|
||||
SerializerSettings = new JsonSerializerSettings()
|
||||
{
|
||||
ContractResolver = new WritablePropertiesOnlyResolver()
|
||||
};
|
||||
DefaultReferenceTypeNullHandling = ReferenceTypeNullHandling.NotNull;
|
||||
SchemaNameGenerator = new NamespacePrefixedSchemaNameGenerator();
|
||||
SerializerSettings.Converters.Add(new StringEnumConverter());
|
||||
IgnoreObsoleteProperties = true;
|
||||
GenerateExamples = true;
|
||||
}
|
||||
|
||||
private class WritablePropertiesOnlyResolver : DefaultContractResolver
|
||||
{
|
||||
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
|
||||
{
|
||||
IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);
|
||||
return props.Where(p => p.Writable).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class MapToApiAttribute : Attribute
|
||||
{
|
||||
public MapToApiAttribute(string apiName) => ApiName = apiName;
|
||||
|
||||
public string ApiName { get; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
public class ConfigureApiBehaviorOptions : IConfigureOptions<ApiBehaviorOptions>
|
||||
{
|
||||
public void Configure(ApiBehaviorOptions options) =>
|
||||
// disable ProblemDetails as default result type for every non-success response (i.e. 404)
|
||||
// - see https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmapclienterrors
|
||||
options.SuppressMapClientErrors = true;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
public class ConfigureMvcJsonOptions : IConfigureOptions<MvcOptions>
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
private readonly IOptionsMonitor<JsonOptions> _jsonOptions;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
public ConfigureMvcJsonOptions(
|
||||
string jsonOptionsName,
|
||||
IOptionsMonitor<JsonOptions> jsonOptions,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_jsonOptionsName = jsonOptionsName;
|
||||
_jsonOptions = jsonOptions;
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public void Configure(MvcOptions options)
|
||||
{
|
||||
JsonOptions jsonOptions = _jsonOptions.Get(_jsonOptionsName);
|
||||
ILogger<NamedSystemTextJsonInputFormatter> logger = _loggerFactory.CreateLogger<NamedSystemTextJsonInputFormatter>();
|
||||
options.InputFormatters.Insert(0, new NamedSystemTextJsonInputFormatter(_jsonOptionsName, jsonOptions, logger));
|
||||
options.OutputFormatters.Insert(0, new NamedSystemTextJsonOutputFormatter(_jsonOptionsName, jsonOptions.JsonSerializerOptions));
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private readonly IOptions<ApiVersioningOptions> _apiVersioningOptions;
|
||||
private readonly IOperationIdSelector _operationIdSelector;
|
||||
private readonly ISchemaIdSelector _schemaIdSelector;
|
||||
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOptions<ApiVersioningOptions> apiVersioningOptions,
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector)
|
||||
{
|
||||
_apiVersioningOptions = apiVersioningOptions;
|
||||
_operationIdSelector = operationIdSelector;
|
||||
_schemaIdSelector = schemaIdSelector;
|
||||
}
|
||||
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
{
|
||||
swaggerGenOptions.SwaggerDoc(
|
||||
DefaultApiConfiguration.ApiName,
|
||||
new OpenApiInfo
|
||||
{
|
||||
Title = "Default API",
|
||||
Version = "Latest",
|
||||
Description = "All endpoints not defined under specific APIs"
|
||||
});
|
||||
|
||||
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description, _apiVersioningOptions.Value));
|
||||
swaggerGenOptions.DocInclusionPredicate((name, api) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(api.GroupName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
|
||||
{
|
||||
return controllerActionDescriptor.MethodInfo.HasMapToApiAttribute(name);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName });
|
||||
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
|
||||
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
|
||||
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
|
||||
swaggerGenOptions.SupportNonNullableReferenceTypes();
|
||||
}
|
||||
|
||||
// see https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting
|
||||
private static string ActionOrderBy(ApiDescription apiDesc)
|
||||
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.ActionDescriptor.RouteValues["action"]}_{apiDesc.HttpMethod}";
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
internal static class DefaultApiConfiguration
|
||||
{
|
||||
public const string ApiName = "default";
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
public static class MvcBuilderExtensions
|
||||
{
|
||||
public static IMvcBuilder AddJsonOptions(this IMvcBuilder builder, string settingsName, Action<JsonOptions> configure)
|
||||
{
|
||||
builder.Services.Configure(settingsName, configure);
|
||||
builder.Services.AddSingleton<IConfigureOptions<MvcOptions>>(provider =>
|
||||
{
|
||||
IOptionsMonitor<JsonOptions> options = provider.GetRequiredService<IOptionsMonitor<JsonOptions>>();
|
||||
ILoggerFactory loggerFactory = provider.GetRequiredService<ILoggerFactory>();
|
||||
return new ConfigureMvcJsonOptions(settingsName, options, loggerFactory);
|
||||
});
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.Serialization;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
public static class UmbracoBuilderApiExtensions
|
||||
{
|
||||
public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoSwaggerGenOptions>();
|
||||
builder.Services.AddSingleton<IUmbracoJsonTypeInfoResolver, UmbracoJsonTypeInfoResolver>();
|
||||
builder.Services.AddSingleton<IOperationIdSelector, OperationIdSelector>();
|
||||
builder.Services.AddSingleton<ISchemaIdSelector, SchemaIdSelector>();
|
||||
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Infrastructure.HostedServices;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
public static class UmbracoBuilderAuthExtensions
|
||||
{
|
||||
private static bool _initialized;
|
||||
|
||||
public static IUmbracoBuilder AddUmbracoOpenIddict(this IUmbracoBuilder builder)
|
||||
{
|
||||
if (_initialized is false)
|
||||
{
|
||||
ConfigureOpenIddict(builder);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void ConfigureOpenIddict(IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddOpenIddict()
|
||||
// Register the OpenIddict server components.
|
||||
.AddServer(options =>
|
||||
{
|
||||
// Enable the authorization and token endpoints.
|
||||
// - important: member endpoints MUST be added before backoffice endpoints to ensure that auto-discovery works for members
|
||||
// FIXME: swap paths here so member API is first (see comment above)
|
||||
options
|
||||
.SetAuthorizationEndpointUris(
|
||||
Paths.MemberApi.AuthorizationEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetTokenEndpointUris(
|
||||
Paths.MemberApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetLogoutEndpointUris(
|
||||
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetRevocationEndpointUris(
|
||||
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetUserinfoEndpointUris(
|
||||
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
|
||||
|
||||
// Enable authorization code flow with PKCE
|
||||
options
|
||||
.AllowAuthorizationCodeFlow()
|
||||
.RequireProofKeyForCodeExchange()
|
||||
.AllowRefreshTokenFlow();
|
||||
|
||||
// Register the ASP.NET Core host and configure for custom authentication endpoint.
|
||||
options
|
||||
.UseAspNetCore()
|
||||
.EnableAuthorizationEndpointPassthrough()
|
||||
.EnableLogoutEndpointPassthrough()
|
||||
.EnableUserinfoEndpointPassthrough();
|
||||
|
||||
// Enable reference tokens
|
||||
// - see https://documentation.openiddict.com/configuration/token-storage.html
|
||||
options
|
||||
.UseReferenceAccessTokens()
|
||||
.UseReferenceRefreshTokens();
|
||||
|
||||
// Use ASP.NET Core Data Protection for tokens instead of JWT.
|
||||
// This is more secure, and has the added benefit of having a high throughput
|
||||
// but means that all servers (such as in a load balanced setup)
|
||||
// needs to use the same application name and key ring,
|
||||
// however this is already recommended for load balancing, so should be fine.
|
||||
// See https://documentation.openiddict.com/configuration/token-formats.html#switching-to-data-protection-tokens
|
||||
// and https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-7.0
|
||||
// for more information
|
||||
options.UseDataProtection();
|
||||
|
||||
// Register encryption and signing credentials to protect tokens.
|
||||
// Note that for tokens generated/validated using ASP.NET Core Data Protection,
|
||||
// a separate key ring is used, distinct from the credentials discussed in
|
||||
// https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
|
||||
// More details can be found here: https://github.com/openiddict/openiddict-core/issues/1892#issuecomment-1737308506
|
||||
// "When using ASP.NET Core Data Protection to generate opaque tokens, the signing and encryption credentials
|
||||
// registered via Add*Key/Certificate() are not used". But since OpenIddict requires the registration of such,
|
||||
// we can generate random keys per instance without them taking effect.
|
||||
// - see also https://github.com/openiddict/openiddict-core/issues/1231
|
||||
options
|
||||
.AddEncryptionKey(new SymmetricSecurityKey(RandomNumberGenerator.GetBytes(32))) // generate a cryptographically secure random 256-bits key
|
||||
.AddSigningKey(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048))); // generate RSA key with recommended size of 2048-bits
|
||||
})
|
||||
|
||||
// Register the OpenIddict validation components.
|
||||
.AddValidation(options =>
|
||||
{
|
||||
// Import the configuration from the local OpenIddict server instance.
|
||||
options.UseLocalServer();
|
||||
|
||||
// Register the ASP.NET Core host.
|
||||
options.UseAspNetCore();
|
||||
|
||||
// Enable token entry validation
|
||||
// - see https://documentation.openiddict.com/configuration/token-storage.html#enabling-token-entry-validation-at-the-api-level
|
||||
options.EnableTokenEntryValidation();
|
||||
|
||||
// Use ASP.NET Core Data Protection for tokens instead of JWT. (see note in AddServer)
|
||||
options.UseDataProtection();
|
||||
});
|
||||
|
||||
builder.Services.AddHostedService<OpenIddictCleanup>();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using System.Reflection;
|
||||
using Asp.Versioning;
|
||||
using Umbraco.Cms.Api.Common.Attributes;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
public static class MethodInfoApiCommonExtensions
|
||||
{
|
||||
|
||||
public static string? GetMapToApiVersionAttributeValue(this MethodInfo methodInfo)
|
||||
{
|
||||
MapToApiVersionAttribute[] mapToApis = methodInfo.GetCustomAttributes(typeof(MapToApiVersionAttribute), inherit: true).Cast<MapToApiVersionAttribute>().ToArray();
|
||||
|
||||
return string.Join("|", mapToApis.SelectMany(x=>x.Versions));
|
||||
}
|
||||
|
||||
public static string? GetMapToApiAttributeValue(this MethodInfo methodInfo)
|
||||
{
|
||||
MapToApiAttribute[] mapToApis = (methodInfo.DeclaringType?.GetCustomAttributes(typeof(MapToApiAttribute), inherit: true) ?? Array.Empty<object>()).Cast<MapToApiAttribute>().ToArray();
|
||||
|
||||
return mapToApis.SingleOrDefault()?.ApiName;
|
||||
}
|
||||
|
||||
public static bool HasMapToApiAttribute(this MethodInfo methodInfo, string apiName)
|
||||
{
|
||||
var value = methodInfo.GetMapToApiAttributeValue();
|
||||
|
||||
return value == apiName
|
||||
|| (value is null && apiName == DefaultApiConfiguration.ApiName);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.Filters;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class JsonOptionsNameAttribute : Attribute
|
||||
{
|
||||
public JsonOptionsNameAttribute(string jsonOptionsName) => JsonOptionsName = jsonOptionsName;
|
||||
|
||||
public string JsonOptionsName { get; }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Api.Common.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
public static class HttpContextJsonExtensions
|
||||
{
|
||||
public static string? CurrentJsonOptionsName(this HttpContext context)
|
||||
=> context.GetEndpoint()?.Metadata.GetMetadata<JsonOptionsNameAttribute>()?.JsonOptionsName;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
internal class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
|
||||
public NamedSystemTextJsonInputFormatter(string jsonOptionsName, JsonOptions options, ILogger<NamedSystemTextJsonInputFormatter> logger)
|
||||
: base(options, logger) =>
|
||||
_jsonOptionsName = jsonOptionsName;
|
||||
|
||||
public override bool CanRead(InputFormatterContext context)
|
||||
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanRead(context);
|
||||
|
||||
public override async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await base.ReadAsync(context);
|
||||
}
|
||||
catch (NotSupportedException exception)
|
||||
{
|
||||
// This happens when trying to deserialize to an interface, without sending the $type as part of the request
|
||||
context.ModelState.TryAddModelException(string.Empty, new InputFormatterException(exception.Message, exception));
|
||||
return await InputFormatterResult.FailureAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
|
||||
internal class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
|
||||
public NamedSystemTextJsonOutputFormatter(string jsonOptionsName, JsonSerializerOptions jsonSerializerOptions) : base(jsonSerializerOptions)
|
||||
{
|
||||
_jsonOptionsName = jsonOptionsName;
|
||||
}
|
||||
|
||||
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
|
||||
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanWriteResult(context);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
public class EnumSchemaFilter : ISchemaFilter
|
||||
{
|
||||
public void Apply(OpenApiSchema model, SchemaFilterContext context)
|
||||
{
|
||||
if (context.Type.IsEnum)
|
||||
{
|
||||
model.Type = "string";
|
||||
model.Format = null;
|
||||
model.Enum.Clear();
|
||||
foreach (var name in Enum.GetNames(context.Type))
|
||||
{
|
||||
var actualName = context.Type.GetField(name)?.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? name;
|
||||
model.Enum.Add(new OpenApiString(actualName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
public interface IOperationIdSelector
|
||||
{
|
||||
string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
public interface ISchemaIdSelector
|
||||
{
|
||||
string SchemaId(Type type);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// This filter explicitly removes all other mime types than application/json from a named OpenAPI document when application/json is accepted.
|
||||
/// </summary>
|
||||
public class MimeTypeDocumentFilter : IDocumentFilter
|
||||
{
|
||||
private readonly string _documentName;
|
||||
|
||||
public MimeTypeDocumentFilter(string documentName) => _documentName = documentName;
|
||||
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
if (context.DocumentName != _documentName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OpenApiOperation[] operations = swaggerDoc.Paths
|
||||
.SelectMany(path => path.Value.Operations.Values)
|
||||
.ToArray();
|
||||
|
||||
void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType> content)
|
||||
{
|
||||
if (content.ContainsKey("application/json"))
|
||||
{
|
||||
content.RemoveAll(r => r.Key != "application/json");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OpenApiRequestBody[] requestBodies = operations.Select(operation => operation.RequestBody).WhereNotNull().ToArray();
|
||||
foreach (OpenApiRequestBody requestBody in requestBodies)
|
||||
{
|
||||
RemoveUnwantedMimeTypes(requestBody.Content);
|
||||
}
|
||||
|
||||
OpenApiResponse[] responses = operations.SelectMany(operation => operation.Responses.Values).WhereNotNull().ToArray();
|
||||
foreach (OpenApiResponse response in responses)
|
||||
{
|
||||
RemoveUnwantedMimeTypes(response.Content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// This is the regexes used to generate the operation IDs, the benefit of this being partial with GeneratedRegex
|
||||
/// source generators is that it will be pre-compiled at startup
|
||||
/// See: https://devblogs.microsoft.com/dotnet/regular-expression-improvements-in-dotnet-7/#source-generation for more info.
|
||||
/// </summary>
|
||||
internal static partial class OperationIdRegexes
|
||||
{
|
||||
// Your IDE may be showing errors here, this is because it's a new dotnet 7 feature (it's fixed in the EAP of Rider)
|
||||
[GeneratedRegex(".*?\\/v[1-9]+/")]
|
||||
public static partial Regex VersionPrefixRegex();
|
||||
|
||||
[GeneratedRegex("\\{(.*?)\\:?\\}")]
|
||||
public static partial Regex TemplatePlaceholdersRegex();
|
||||
|
||||
[GeneratedRegex("[\\/\\-](\\w{1})")]
|
||||
public static partial Regex ToCamelCaseRegex();
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
public class OperationIdSelector : IOperationIdSelector
|
||||
{
|
||||
public virtual string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions)
|
||||
{
|
||||
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor
|
||||
|| controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is not true)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return UmbracoOperationId(apiDescription, apiVersioningOptions);
|
||||
}
|
||||
|
||||
protected string? UmbracoOperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions)
|
||||
{
|
||||
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ApiVersion defaultVersion = apiVersioningOptions.DefaultApiVersion;
|
||||
var httpMethod = apiDescription.HttpMethod?.ToLower().ToFirstUpper() ?? "Get";
|
||||
|
||||
// if the route info "Name" is supplied we'll use this explicitly as the operation ID
|
||||
// - usage example: [HttpGet("my-api/route}", Name = "MyCustomRoute")]
|
||||
if (string.IsNullOrWhiteSpace(apiDescription.ActionDescriptor.AttributeRouteInfo?.Name) == false)
|
||||
{
|
||||
var explicitOperationId = apiDescription.ActionDescriptor.AttributeRouteInfo!.Name;
|
||||
return explicitOperationId.InvariantStartsWith(httpMethod)
|
||||
? explicitOperationId
|
||||
: $"{httpMethod}{explicitOperationId}";
|
||||
}
|
||||
|
||||
var relativePath = apiDescription.RelativePath;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(relativePath))
|
||||
{
|
||||
throw new Exception(
|
||||
$"There is no relative path for controller action {apiDescription.ActionDescriptor.RouteValues["controller"]}");
|
||||
}
|
||||
|
||||
// Remove the prefixed base path with version, e.g. /umbraco/management/api/v1/tracked-reference/{id} => tracked-reference/{id}
|
||||
var unprefixedRelativePath = OperationIdRegexes
|
||||
.VersionPrefixRegex()
|
||||
.Replace(relativePath, string.Empty);
|
||||
|
||||
// Remove template placeholders, e.g. tracked-reference/{id} => tracked-reference/Id
|
||||
var formattedOperationId = OperationIdRegexes
|
||||
.TemplatePlaceholdersRegex()
|
||||
.Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}");
|
||||
|
||||
// Remove dashes (-) and slashes (/) and convert the following letter to uppercase with
|
||||
// the word "By" in front, e.g. tracked-reference/Id => TrackedReferenceById
|
||||
formattedOperationId = OperationIdRegexes
|
||||
.ToCamelCaseRegex()
|
||||
.Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper());
|
||||
|
||||
//Get map to version attribute
|
||||
string? version = null;
|
||||
|
||||
var versionAttributeValue = controllerActionDescriptor.MethodInfo.GetMapToApiVersionAttributeValue();
|
||||
|
||||
// We only wanna add a version, if it is not the default one.
|
||||
if (string.Equals(versionAttributeValue, defaultVersion.ToString()) == false)
|
||||
{
|
||||
version = versionAttributeValue;
|
||||
}
|
||||
|
||||
// Return the operation ID with the formatted http method verb in front, e.g. GetTrackedReferenceById
|
||||
return $"{httpMethod}{formattedOperationId.ToFirstUpper()}{version}";
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
public class SchemaIdSelector : ISchemaIdSelector
|
||||
{
|
||||
public virtual string SchemaId(Type type)
|
||||
=> type.Namespace?.StartsWith("Umbraco.Cms") is true ? UmbracoSchemaId(type) : type.Name;
|
||||
|
||||
protected string UmbracoSchemaId(Type type)
|
||||
{
|
||||
string SanitizedTypeName(Type t) => t.Name
|
||||
// first grab the "non generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
|
||||
.Split('`').First()
|
||||
// then remove the "ViewModel" postfix from type names
|
||||
.TrimEnd("ViewModel");
|
||||
|
||||
var name = SanitizedTypeName(type);
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
// append the generic type names, ultimately turning i.e. "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
|
||||
name = $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
|
||||
}
|
||||
|
||||
if (name.EndsWith("Model") == false)
|
||||
{
|
||||
// because some models names clash with common classes in TypeScript (i.e. Document),
|
||||
// we need to add a "Model" postfix to all models
|
||||
name = $"{name}Model";
|
||||
}
|
||||
|
||||
// make absolutely sure we don't pass any invalid named by removing all non-word chars
|
||||
return Regex.Replace(name, @"[^\w]", string.Empty);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
using Umbraco.Extensions;
|
||||
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
|
||||
{
|
||||
public SwaggerRouteTemplatePipelineFilter(string name)
|
||||
: base(name)
|
||||
=> PostPipeline = PostPipelineAction;
|
||||
|
||||
private void PostPipelineAction(IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
if (SwaggerIsEnabled(applicationBuilder) is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IOptions<SwaggerGenOptions> swaggerGenOptions = applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SwaggerGenOptions>>();
|
||||
|
||||
applicationBuilder.UseSwagger(swaggerOptions =>
|
||||
{
|
||||
swaggerOptions.RouteTemplate = SwaggerRouteTemplate(applicationBuilder);
|
||||
});
|
||||
|
||||
applicationBuilder.UseSwaggerUI(swaggerUiOptions => SwaggerUiConfiguration(swaggerUiOptions, swaggerGenOptions.Value, applicationBuilder));
|
||||
}
|
||||
|
||||
protected virtual bool SwaggerIsEnabled(IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
IWebHostEnvironment webHostEnvironment = applicationBuilder.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
|
||||
return webHostEnvironment.IsProduction() is false;
|
||||
}
|
||||
|
||||
protected virtual string SwaggerRouteTemplate(IApplicationBuilder applicationBuilder)
|
||||
=> $"{GetUmbracoPath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json";
|
||||
|
||||
protected virtual string SwaggerUiRoutePrefix(IApplicationBuilder applicationBuilder)
|
||||
=> $"{GetUmbracoPath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger";
|
||||
|
||||
protected virtual void SwaggerUiConfiguration(
|
||||
SwaggerUIOptions swaggerUiOptions,
|
||||
SwaggerGenOptions swaggerGenOptions,
|
||||
IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
swaggerUiOptions.RoutePrefix = SwaggerUiRoutePrefix(applicationBuilder);
|
||||
|
||||
foreach ((var name, OpenApiInfo? apiInfo) in swaggerGenOptions.SwaggerGeneratorOptions.SwaggerDocs
|
||||
.OrderBy(x => x.Value.Title))
|
||||
{
|
||||
swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}");
|
||||
}
|
||||
|
||||
swaggerUiOptions.OAuthUsePkce();
|
||||
}
|
||||
|
||||
private string GetUmbracoPath(IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
GlobalSettings settings = applicationBuilder.ApplicationServices.GetRequiredService<IOptions<GlobalSettings>>().Value;
|
||||
IHostingEnvironment hostingEnvironment = applicationBuilder.ApplicationServices.GetRequiredService<IHostingEnvironment>();
|
||||
|
||||
return settings.GetBackOfficePath(hostingEnvironment);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Common.Security;
|
||||
|
||||
public static class Paths
|
||||
{
|
||||
public static class MemberApi
|
||||
{
|
||||
public const string EndpointTemplate = "security/member";
|
||||
|
||||
public static readonly string AuthorizationEndpoint = EndpointPath($"{EndpointTemplate}/authorize");
|
||||
|
||||
public static readonly string TokenEndpoint = EndpointPath($"{EndpointTemplate}/token");
|
||||
|
||||
public static readonly string LogoutEndpoint = EndpointPath($"{EndpointTemplate}/signout");
|
||||
|
||||
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
|
||||
|
||||
public static readonly string UserinfoEndpoint = EndpointPath($"{EndpointTemplate}/userinfo");
|
||||
|
||||
// NOTE: we're NOT using /api/v1.0/ here because it will clash with the Delivery API docs
|
||||
private static string EndpointPath(string relativePath) => $"/umbraco/delivery/api/v1/{relativePath}";
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Serialization;
|
||||
|
||||
public interface IUmbracoJsonTypeInfoResolver : IJsonTypeInfoResolver
|
||||
{
|
||||
IEnumerable<Type> FindSubTypes(Type type);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Serialization;
|
||||
|
||||
public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, IUmbracoJsonTypeInfoResolver
|
||||
{
|
||||
private readonly ITypeFinder _typeFinder;
|
||||
private readonly ConcurrentDictionary<Type, ISet<Type>> _subTypesCache = new ConcurrentDictionary<Type, ISet<Type>>();
|
||||
|
||||
public UmbracoJsonTypeInfoResolver(ITypeFinder typeFinder)
|
||||
{
|
||||
_typeFinder = typeFinder;
|
||||
}
|
||||
|
||||
public IEnumerable<Type> FindSubTypes(Type type)
|
||||
{
|
||||
if (_subTypesCache.TryGetValue(type, out ISet<Type>? cachedResult))
|
||||
{
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
var result = _typeFinder.FindClassesOfType(type).ToHashSet();
|
||||
_subTypesCache.TryAdd(type, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
|
||||
{
|
||||
JsonTypeInfo result = base.GetTypeInfo(type, options);
|
||||
|
||||
if (type.IsInterface)
|
||||
{
|
||||
return GetTypeInfoForInterface(result, type, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetTypeInfoForClass(result, type, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private JsonTypeInfo GetTypeInfoForClass(JsonTypeInfo result, Type type, JsonSerializerOptions options)
|
||||
{
|
||||
if (result.Kind != JsonTypeInfoKind.Object || !type.GetInterfaces().Any())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
JsonPolymorphismOptions jsonPolymorphismOptions = result.PolymorphismOptions ?? new JsonPolymorphismOptions();
|
||||
|
||||
jsonPolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(type, type.Name));
|
||||
|
||||
result.PolymorphismOptions = jsonPolymorphismOptions;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonTypeInfo GetTypeInfoForInterface(JsonTypeInfo result, Type type, JsonSerializerOptions options)
|
||||
{
|
||||
IEnumerable<Type> subTypes = FindSubTypes(type);
|
||||
|
||||
if (!subTypes.Any())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
JsonPolymorphismOptions jsonPolymorphismOptions = result.PolymorphismOptions ?? new JsonPolymorphismOptions();
|
||||
|
||||
IEnumerable<Type> knownSubTypes = jsonPolymorphismOptions.DerivedTypes.Select(x => x.DerivedType);
|
||||
IEnumerable<Type> subTypesToAdd = subTypes.Except(knownSubTypes);
|
||||
foreach (Type subType in subTypesToAdd)
|
||||
{
|
||||
jsonPolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(
|
||||
subType,
|
||||
subType.Name ?? string.Empty));
|
||||
}
|
||||
|
||||
result.PolymorphismOptions = jsonPolymorphismOptions;
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Title>Umbraco CMS - API Common</Title>
|
||||
<Description>Contains the bits and pieces that are shared between the Umbraco CMS APIs.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="OpenIddict.Abstractions" />
|
||||
<PackageReference Include="OpenIddict.AspNetCore" />
|
||||
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens"/>
|
||||
|
||||
<!-- Take top-level depedendency on OpenIddict.AspNetCore depends on a vulnerable version -->
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Accessors;
|
||||
|
||||
internal sealed class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase<IOutputExpansionStrategy>, IOutputExpansionStrategyAccessor
|
||||
{
|
||||
public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Accessors;
|
||||
|
||||
internal sealed class RequestContextRequestStartItemProviderAccessor : RequestContextServiceAccessorBase<IRequestStartItemProvider>, IRequestStartItemProviderAccessor
|
||||
{
|
||||
public RequestContextRequestStartItemProviderAccessor(IHttpContextAccessor httpContextAccessor)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Accessors;
|
||||
|
||||
public abstract class RequestContextServiceAccessorBase<T>
|
||||
where T : class
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor)
|
||||
=> _httpContextAccessor = httpContextAccessor;
|
||||
|
||||
public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService)
|
||||
{
|
||||
requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService<T>();
|
||||
return requestStartNodeService is not null;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Caching;
|
||||
|
||||
internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
|
||||
{
|
||||
private readonly TimeSpan _duration;
|
||||
private readonly StringValues _varyByHeaderNames;
|
||||
|
||||
public DeliveryApiOutputCachePolicy(TimeSpan duration, StringValues varyByHeaderNames)
|
||||
{
|
||||
_duration = duration;
|
||||
_varyByHeaderNames = varyByHeaderNames;
|
||||
}
|
||||
|
||||
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
IRequestPreviewService requestPreviewService = context
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<IRequestPreviewService>();
|
||||
|
||||
IApiAccessService apiAccessService = context
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<IApiAccessService>();
|
||||
|
||||
context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess();
|
||||
context.ResponseExpirationTimeSpan = _duration;
|
||||
context.CacheVaryByRules.HeaderNames = _varyByHeaderNames;
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
|
||||
=> ValueTask.CompletedTask;
|
||||
|
||||
ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Caching;
|
||||
|
||||
internal sealed class OutputCachePipelineFilter : UmbracoPipelineFilter
|
||||
{
|
||||
public OutputCachePipelineFilter(string name)
|
||||
: base(name)
|
||||
=> PostPipeline = PostPipelineAction;
|
||||
|
||||
private void PostPipelineAction(IApplicationBuilder applicationBuilder)
|
||||
=> applicationBuilder.UseOutputCache();
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Configuration;
|
||||
|
||||
public class ConfigureUmbracoDeliveryApiSwaggerGenOptions: IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
{
|
||||
swaggerGenOptions.SwaggerDoc(
|
||||
DeliveryApiConfiguration.ApiName,
|
||||
new OpenApiInfo
|
||||
{
|
||||
Title = DeliveryApiConfiguration.ApiTitle,
|
||||
Version = "Latest",
|
||||
Description = $"You can find out more about the {DeliveryApiConfiguration.ApiTitle} in [the documentation]({DeliveryApiConfiguration.ApiDocumentationContentArticleLink})."
|
||||
});
|
||||
|
||||
swaggerGenOptions.DocumentFilter<MimeTypeDocumentFilter>(DeliveryApiConfiguration.ApiName);
|
||||
|
||||
swaggerGenOptions.OperationFilter<SwaggerContentDocumentationFilter>();
|
||||
swaggerGenOptions.OperationFilter<SwaggerMediaDocumentationFilter>();
|
||||
swaggerGenOptions.ParameterFilter<SwaggerContentDocumentationFilter>();
|
||||
swaggerGenOptions.ParameterFilter<SwaggerMediaDocumentationFilter>();
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// This configures member authentication for the Delivery API in Swagger. Consult the docs for
|
||||
/// member authentication within the Delivery API for instructions on how to use this.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class is not used by the core CMS due to the required installation dependencies (local login page among other things).
|
||||
/// </remarks>
|
||||
public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private const string AuthSchemeName = "Umbraco Member";
|
||||
|
||||
public void Configure(SwaggerGenOptions options)
|
||||
{
|
||||
options.AddSecurityDefinition(
|
||||
AuthSchemeName,
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Name = AuthSchemeName,
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Description = "Umbraco Member Authentication",
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
AuthorizationCode = new OpenApiOAuthFlow
|
||||
{
|
||||
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
|
||||
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// add security requirements for content API operations
|
||||
options.OperationFilter<DeliveryApiSecurityFilter>();
|
||||
}
|
||||
|
||||
private class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (CanApply(context) is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = AuthSchemeName,
|
||||
}
|
||||
},
|
||||
new string[] { }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Delivery.Configuration;
|
||||
|
||||
internal static class DeliveryApiConfiguration
|
||||
{
|
||||
internal const string ApiTitle = "Umbraco Delivery API";
|
||||
|
||||
internal const string ApiName = "delivery";
|
||||
|
||||
internal const string ApiDocumentationContentArticleLink = "https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api";
|
||||
|
||||
internal const string ApiDocumentationMediaArticleLink = "https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api";
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByIdContentApiController : ContentApiItemControllerBase
|
||||
{
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByIdContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByIdContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestMemberAccessService)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ByIdContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
=> _requestMemberAccessService = requestMemberAccessService;
|
||||
|
||||
[HttpGet("item/{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> ById(Guid id)
|
||||
=> await HandleRequest(id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content item by id.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the content item.</param>
|
||||
/// <returns>The content item or not found result.</returns>
|
||||
[HttpGet("item/{id:guid}")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByIdV20(Guid id)
|
||||
=> await HandleRequest(id);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(Guid id)
|
||||
{
|
||||
IPublishedContent? contentItem = ApiPublishedContentCache.GetById(id);
|
||||
|
||||
if (contentItem is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService);
|
||||
if (deniedAccessResult is not null)
|
||||
{
|
||||
return deniedAccessResult;
|
||||
}
|
||||
|
||||
IApiContentResponse? apiContentResponse = ApiContentResponseBuilder.Build(contentItem);
|
||||
if (apiContentResponse is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(apiContentResponse);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByIdsContentApiController : ContentApiItemControllerBase
|
||||
{
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByIdsContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByIdsContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestMemberAccessService)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ByIdsContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
=> _requestMemberAccessService = requestMemberAccessService;
|
||||
|
||||
[HttpGet("item")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<IApiContentResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
=> await HandleRequest(ids);
|
||||
|
||||
/// <summary>
|
||||
/// Gets content items by ids.
|
||||
/// </summary>
|
||||
/// <param name="ids">The unique identifiers of the content items to retrieve.</param>
|
||||
/// <returns>The content items.</returns>
|
||||
[HttpGet("items")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<IApiContentResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> ItemsV20([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
=> await HandleRequest(ids);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(HashSet<Guid> ids)
|
||||
{
|
||||
IPublishedContent[] contentItems = ApiPublishedContentCache.GetByIds(ids).ToArray();
|
||||
|
||||
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItems, _requestMemberAccessService);
|
||||
if (deniedAccessResult is not null)
|
||||
{
|
||||
return deniedAccessResult;
|
||||
}
|
||||
|
||||
IApiContentResponse[] apiContentItems = contentItems
|
||||
.Select(ApiContentResponseBuilder.Build)
|
||||
.WhereNotNull()
|
||||
.ToArray();
|
||||
|
||||
return await Task.FromResult(Ok(apiContentItems));
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
{
|
||||
private readonly IApiContentPathResolver _apiContentPathResolver;
|
||||
private readonly IRequestRedirectService _requestRedirectService;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
private const string PreviewContentRequestPathPrefix = $"/{Umbraco.Cms.Core.Constants.DeliveryApi.Routing.PreviewContentPathPrefix}";
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService,
|
||||
IRequestRoutingService requestRoutingService,
|
||||
IRequestRedirectService requestRedirectService,
|
||||
IRequestPreviewService requestPreviewService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestRoutingService,
|
||||
requestRedirectService,
|
||||
requestPreviewService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService,
|
||||
IRequestRoutingService requestRoutingService,
|
||||
IRequestRedirectService requestRedirectService,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestRoutingService,
|
||||
requestRedirectService,
|
||||
requestPreviewService,
|
||||
requestMemberAccessService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete($"Please use the constructor that accepts {nameof(IApiContentPathResolver)}. Will be removed in V15.")]
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IRequestRoutingService requestRoutingService,
|
||||
IRequestRedirectService requestRedirectService,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestRedirectService,
|
||||
requestPreviewService,
|
||||
requestMemberAccessService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IApiContentPathResolver>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete($"Please use the non-obsolete constructor. Will be removed in V15.")]
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService,
|
||||
IRequestRoutingService requestRoutingService,
|
||||
IRequestRedirectService requestRedirectService,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IRequestMemberAccessService requestMemberAccessService,
|
||||
IApiContentPathResolver apiContentPathResolver)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestRedirectService,
|
||||
requestPreviewService,
|
||||
requestMemberAccessService,
|
||||
apiContentPathResolver)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IRequestRedirectService requestRedirectService,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IRequestMemberAccessService requestMemberAccessService,
|
||||
IApiContentPathResolver apiContentPathResolver)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
{
|
||||
_requestRedirectService = requestRedirectService;
|
||||
_requestPreviewService = requestPreviewService;
|
||||
_requestMemberAccessService = requestMemberAccessService;
|
||||
_apiContentPathResolver = apiContentPathResolver;
|
||||
}
|
||||
|
||||
[HttpGet("item/{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> ByRoute(string path = "")
|
||||
=> await HandleRequest(path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content item by route.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the content item.</param>
|
||||
/// <remarks>
|
||||
/// Optional URL segment for the root content item
|
||||
/// can be added through the "start-item" header.
|
||||
/// </remarks>
|
||||
/// <returns>The content item or not found result.</returns>
|
||||
[HttpGet("item/{*path}")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByRouteV20(string path = "")
|
||||
=> await HandleRequest(path);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(string path)
|
||||
{
|
||||
path = DecodePath(path);
|
||||
path = path.Length == 0 ? "/" : path;
|
||||
|
||||
if (_apiContentPathResolver.IsResolvablePath(path) is false)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
IPublishedContent? contentItem = GetContent(path);
|
||||
if (contentItem is not null)
|
||||
{
|
||||
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService);
|
||||
if (deniedAccessResult is not null)
|
||||
{
|
||||
return deniedAccessResult;
|
||||
}
|
||||
|
||||
return await Task.FromResult(Ok(ApiContentResponseBuilder.Build(contentItem)));
|
||||
}
|
||||
|
||||
IApiContentRoute? redirectRoute = _requestRedirectService.GetRedirectRoute(path);
|
||||
return redirectRoute != null
|
||||
? RedirectTo(redirectRoute)
|
||||
: NotFound();
|
||||
}
|
||||
|
||||
private IPublishedContent? GetContent(string path)
|
||||
=> path.StartsWith(PreviewContentRequestPathPrefix)
|
||||
? GetPreviewContent(path)
|
||||
: GetPublishedContent(path);
|
||||
|
||||
private IPublishedContent? GetPublishedContent(string path)
|
||||
=> _apiContentPathResolver.ResolveContentPath(path);
|
||||
|
||||
private IPublishedContent? GetPreviewContent(string path)
|
||||
{
|
||||
if (_requestPreviewService.IsPreview() is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Guid.TryParse(path.AsSpan(PreviewContentRequestPathPrefix.Length).TrimEnd("/"), out Guid contentId) is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IPublishedContent? contentItem = ApiPublishedContentCache.GetById(contentId);
|
||||
return contentItem;
|
||||
}
|
||||
|
||||
private IActionResult RedirectTo(IApiContentRoute redirectRoute)
|
||||
{
|
||||
Response.Headers.Add("Location-Start-Item-Path", redirectRoute.StartItem.Path);
|
||||
Response.Headers.Add("Location-Start-Item-Id", redirectRoute.StartItem.Id.ToString("D"));
|
||||
return RedirectPermanent(redirectRoute.Path);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
[DeliveryApiAccess]
|
||||
[VersionedDeliveryApiRoute("content")]
|
||||
[ApiExplorerSettings(GroupName = "Content")]
|
||||
[LocalizeFromAcceptLanguageHeader]
|
||||
[ValidateStartItem]
|
||||
[AddVaryHeader]
|
||||
[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.ContentCachePolicy)]
|
||||
public abstract class ContentApiControllerBase : DeliveryApiControllerBase
|
||||
{
|
||||
protected IApiPublishedContentCache ApiPublishedContentCache { get; }
|
||||
|
||||
protected IApiContentResponseBuilder ApiContentResponseBuilder { get; }
|
||||
|
||||
protected ContentApiControllerBase(IApiPublishedContentCache apiPublishedContentCache, IApiContentResponseBuilder apiContentResponseBuilder)
|
||||
{
|
||||
ApiPublishedContentCache = apiPublishedContentCache;
|
||||
ApiContentResponseBuilder = apiContentResponseBuilder;
|
||||
}
|
||||
|
||||
protected IActionResult ApiContentQueryOperationStatusResult(ApiContentQueryOperationStatus status) =>
|
||||
status switch
|
||||
{
|
||||
ApiContentQueryOperationStatus.FilterOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Filter option not found")
|
||||
.WithDetail("One of the attempted 'filter' options does not exist")
|
||||
.Build()),
|
||||
ApiContentQueryOperationStatus.SelectorOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Selector option not found")
|
||||
.WithDetail("The attempted 'fetch' option does not exist")
|
||||
.Build()),
|
||||
ApiContentQueryOperationStatus.SortOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Sort option not found")
|
||||
.WithDetail("One of the attempted 'sort' options does not exist")
|
||||
.Build()),
|
||||
_ => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Unknown content query status")
|
||||
.WithDetail($"Content query status \"{status}\" was not expected here")
|
||||
.Build()),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 403 Forbidden result.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this method instead of <see cref="ControllerBase.Forbid()"/> on the controller base. The latter will yield
|
||||
/// a redirect to an access denied URL because of the default cookie auth scheme. This method ensures that a proper
|
||||
/// 403 Forbidden status code is returned to the client.
|
||||
/// </remarks>
|
||||
protected IActionResult Forbidden() => new StatusCodeResult(StatusCodes.Status403Forbidden);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
public abstract class ContentApiItemControllerBase : ContentApiControllerBase
|
||||
{
|
||||
// TODO: Remove this in V14 when the obsolete constructors have been removed
|
||||
private readonly IPublicAccessService _publicAccessService;
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
protected ContentApiItemControllerBase(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService)
|
||||
: this(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
protected ContentApiItemControllerBase(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
=> _publicAccessService = StaticServiceProvider.Instance.GetRequiredService<IPublicAccessService>();
|
||||
|
||||
[Obsolete($"Please use {nameof(IPublicAccessService)} to test for content protection. Will be removed in V14.")]
|
||||
protected bool IsProtected(IPublishedContent content) => _publicAccessService.IsProtected(content.Path);
|
||||
|
||||
protected async Task<IActionResult?> HandleMemberAccessAsync(IPublishedContent contentItem, IRequestMemberAccessService requestMemberAccessService)
|
||||
{
|
||||
PublicAccessStatus accessStatus = await requestMemberAccessService.MemberHasAccessToAsync(contentItem);
|
||||
return accessStatus is PublicAccessStatus.AccessAccepted
|
||||
? null
|
||||
: accessStatus is PublicAccessStatus.AccessDenied
|
||||
? Forbidden()
|
||||
: Unauthorized();
|
||||
}
|
||||
|
||||
protected async Task<IActionResult?> HandleMemberAccessAsync(IEnumerable<IPublishedContent> contentItems, IRequestMemberAccessService requestMemberAccessService)
|
||||
{
|
||||
foreach (IPublishedContent content in contentItems)
|
||||
{
|
||||
IActionResult? result = await HandleMemberAccessAsync(content, requestMemberAccessService);
|
||||
// if any of the content items yield an error based on the current member access, return that error
|
||||
if (result is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
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.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class QueryContentApiController : ContentApiControllerBase
|
||||
{
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
private readonly IApiContentQueryService _apiContentQueryService;
|
||||
|
||||
[Obsolete($"Please use the constructor that accepts {nameof(IRequestMemberAccessService)}. Will be removed in V14.")]
|
||||
public QueryContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilderBuilder,
|
||||
IApiContentQueryService apiContentQueryService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilderBuilder,
|
||||
apiContentQueryService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public QueryContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilderBuilder,
|
||||
IApiContentQueryService apiContentQueryService,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilderBuilder)
|
||||
{
|
||||
_apiContentQueryService = apiContentQueryService;
|
||||
_requestMemberAccessService = requestMemberAccessService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IApiContentResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> Query(
|
||||
string? fetch,
|
||||
[FromQuery] string[] filter,
|
||||
[FromQuery] string[] sort,
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
=> await HandleRequest(fetch, filter, sort, skip, take);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paginated list of content item(s) from query.
|
||||
/// </summary>
|
||||
/// <param name="fetch">Optional fetch query parameter value.</param>
|
||||
/// <param name="filter">Optional filter query parameters values.</param>
|
||||
/// <param name="sort">Optional sort query parameters values.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>The paged result of the content item(s).</returns>
|
||||
[HttpGet]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IApiContentResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> QueryV20(
|
||||
string? fetch,
|
||||
[FromQuery] string[] filter,
|
||||
[FromQuery] string[] sort,
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
=> await HandleRequest(fetch, filter, sort, skip, take);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(string? fetch, string[] filter, string[] sort, int skip, int take)
|
||||
{
|
||||
ProtectedAccess protectedAccess = await _requestMemberAccessService.MemberAccessAsync();
|
||||
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> queryAttempt = _apiContentQueryService.ExecuteQuery(fetch, filter, sort, protectedAccess, skip, take);
|
||||
|
||||
if (queryAttempt.Success is false)
|
||||
{
|
||||
return ApiContentQueryOperationStatusResult(queryAttempt.Status);
|
||||
}
|
||||
|
||||
PagedModel<Guid> pagedResult = queryAttempt.Result;
|
||||
IEnumerable<IPublishedContent> contentItems = ApiPublishedContentCache.GetByIds(pagedResult.Items);
|
||||
IApiContentResponse[] apiContentItems = contentItems.Select(ApiContentResponseBuilder.Build).WhereNotNull().ToArray();
|
||||
|
||||
var model = new PagedViewModel<IApiContentResponse>
|
||||
{
|
||||
Total = pagedResult.Total,
|
||||
Items = apiContentItems
|
||||
};
|
||||
|
||||
return Ok(model);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Attributes;
|
||||
using Umbraco.Cms.Api.Common.Filters;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[JsonOptionsName(Constants.JsonOptionsNames.DeliveryApi)]
|
||||
[MapToApi(DeliveryApiConfiguration.ApiName)]
|
||||
public abstract class DeliveryApiControllerBase : Controller
|
||||
{
|
||||
protected string DecodePath(string path)
|
||||
{
|
||||
// OpenAPI does not allow reserved chars as "in:path" parameters, so clients based on the Swagger JSON will URL
|
||||
// encode the path. Normally, ASP.NET Core handles that encoding with an automatic decoding - apparently just not
|
||||
// for forward slashes, for whatever reason... so we need to deal with those. Hopefully this will be addressed in
|
||||
// an upcoming version of ASP.NET Core.
|
||||
// See also https://github.com/dotnet/aspnetcore/issues/11544
|
||||
if (path.Contains("%2F", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = WebUtility.UrlDecode(path);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByIdMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
public ByIdMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
|
||||
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("item/{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> ById(Guid id)
|
||||
=> await HandleRequest(id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a media item by id.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the media item.</param>
|
||||
/// <returns>The media item or not found result.</returns>
|
||||
[HttpGet("item/{id:guid}")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByIdV20(Guid id)
|
||||
=> await HandleRequest(id);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(Guid id)
|
||||
{
|
||||
IPublishedContent? media = PublishedMediaCache.GetById(id);
|
||||
|
||||
if (media is null)
|
||||
{
|
||||
return await Task.FromResult(NotFound());
|
||||
}
|
||||
|
||||
return Ok(BuildApiMediaWithCrops(media));
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByIdsMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
public ByIdsMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
|
||||
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("item")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<IApiMediaWithCropsResponse>), StatusCodes.Status200OK)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
=> await HandleRequest(ids);
|
||||
|
||||
/// <summary>
|
||||
/// Gets media items by ids.
|
||||
/// </summary>
|
||||
/// <param name="ids">The unique identifiers of the media items to retrieve.</param>
|
||||
/// <returns>The media items.</returns>
|
||||
[HttpGet("items")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<IApiMediaWithCropsResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ItemsV20([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
=> await HandleRequest(ids);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(HashSet<Guid> ids)
|
||||
{
|
||||
IPublishedContent[] mediaItems = ids
|
||||
.Select(PublishedMediaCache.GetById)
|
||||
.WhereNotNull()
|
||||
.ToArray();
|
||||
|
||||
IApiMediaWithCropsResponse[] apiMediaItems = mediaItems
|
||||
.Select(BuildApiMediaWithCrops)
|
||||
.ToArray();
|
||||
|
||||
return await Task.FromResult(Ok(apiMediaItems));
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByPathMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
private readonly IApiMediaQueryService _apiMediaQueryService;
|
||||
|
||||
public ByPathMediaApiController(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder,
|
||||
IApiMediaQueryService apiMediaQueryService)
|
||||
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
|
||||
=> _apiMediaQueryService = apiMediaQueryService;
|
||||
|
||||
[HttpGet("item/{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> ByPath(string path)
|
||||
=> await HandleRequest(path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a media item by its path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the media item.</param>
|
||||
/// <returns>The media item or not found result.</returns>
|
||||
[HttpGet("item/{*path}")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByPathV20(string path)
|
||||
=> await HandleRequest(path);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(string path)
|
||||
{
|
||||
path = DecodePath(path);
|
||||
|
||||
IPublishedContent? media = _apiMediaQueryService.GetByPath(path);
|
||||
if (media is null)
|
||||
{
|
||||
return await Task.FromResult(NotFound());
|
||||
}
|
||||
|
||||
return Ok(BuildApiMediaWithCrops(media));
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
|
||||
|
||||
[DeliveryApiMediaAccess]
|
||||
[VersionedDeliveryApiRoute("media")]
|
||||
[ApiExplorerSettings(GroupName = "Media")]
|
||||
[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.MediaCachePolicy)]
|
||||
public abstract class MediaApiControllerBase : DeliveryApiControllerBase
|
||||
{
|
||||
private readonly IApiMediaWithCropsResponseBuilder _apiMediaWithCropsResponseBuilder;
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private IPublishedMediaCache? _publishedMediaCache;
|
||||
|
||||
protected MediaApiControllerBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_apiMediaWithCropsResponseBuilder = apiMediaWithCropsResponseBuilder;
|
||||
}
|
||||
|
||||
protected IPublishedMediaCache PublishedMediaCache => _publishedMediaCache
|
||||
??= _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Media
|
||||
?? throw new InvalidOperationException("Could not obtain the published media cache");
|
||||
|
||||
protected IApiMediaWithCropsResponse BuildApiMediaWithCrops(IPublishedContent media)
|
||||
=> _apiMediaWithCropsResponseBuilder.Build(media);
|
||||
|
||||
protected IActionResult ApiMediaQueryOperationStatusResult(ApiMediaQueryOperationStatus status) =>
|
||||
status switch
|
||||
{
|
||||
ApiMediaQueryOperationStatus.FilterOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Filter option not found")
|
||||
.WithDetail("One of the attempted 'filter' options does not exist")
|
||||
.Build()),
|
||||
ApiMediaQueryOperationStatus.SelectorOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Selector option not found")
|
||||
.WithDetail("The attempted 'fetch' option does not exist")
|
||||
.Build()),
|
||||
ApiMediaQueryOperationStatus.SortOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Sort option not found")
|
||||
.WithDetail("One of the attempted 'sort' options does not exist")
|
||||
.Build()),
|
||||
_ => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Unknown media query status")
|
||||
.WithDetail($"Media query status \"{status}\" was not expected here")
|
||||
.Build()),
|
||||
};
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class QueryMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
private readonly IApiMediaQueryService _apiMediaQueryService;
|
||||
|
||||
public QueryMediaApiController(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder,
|
||||
IApiMediaQueryService apiMediaQueryService)
|
||||
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
|
||||
=> _apiMediaQueryService = apiMediaQueryService;
|
||||
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IApiMediaWithCropsResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> Query(
|
||||
string? fetch,
|
||||
[FromQuery] string[] filter,
|
||||
[FromQuery] string[] sort,
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
=> await HandleRequest(fetch, filter, sort, skip, take);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paginated list of media item(s) from query.
|
||||
/// </summary>
|
||||
/// <param name="fetch">Optional fetch query parameter value.</param>
|
||||
/// <param name="filter">Optional filter query parameters values.</param>
|
||||
/// <param name="sort">Optional sort query parameters values.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>The paged result of the media item(s).</returns>
|
||||
[HttpGet]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IApiMediaWithCropsResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> QueryV20(
|
||||
string? fetch,
|
||||
[FromQuery] string[] filter,
|
||||
[FromQuery] string[] sort,
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
=> await HandleRequest(fetch, filter, sort, skip, take);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(string? fetch, string[] filter, string[] sort, int skip, int take)
|
||||
{
|
||||
Attempt<PagedModel<Guid>, ApiMediaQueryOperationStatus> queryAttempt = _apiMediaQueryService.ExecuteQuery(fetch, filter, sort, skip, take);
|
||||
|
||||
if (queryAttempt.Success is false)
|
||||
{
|
||||
return ApiMediaQueryOperationStatusResult(queryAttempt.Status);
|
||||
}
|
||||
|
||||
PagedModel<Guid> pagedResult = queryAttempt.Result;
|
||||
IPublishedContent[] mediaItems = pagedResult.Items.Select(PublishedMediaCache.GetById).WhereNotNull().ToArray();
|
||||
|
||||
var model = new PagedViewModel<IApiMediaWithCropsResponse>
|
||||
{
|
||||
Total = pagedResult.Total,
|
||||
Items = mediaItems.Select(BuildApiMediaWithCrops)
|
||||
};
|
||||
|
||||
return await Task.FromResult(Ok(model));
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiController]
|
||||
[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
|
||||
public class CurrentMemberController : DeliveryApiControllerBase
|
||||
{
|
||||
private readonly ICurrentMemberClaimsProvider _currentMemberClaimsProvider;
|
||||
|
||||
public CurrentMemberController(ICurrentMemberClaimsProvider currentMemberClaimsProvider)
|
||||
=> _currentMemberClaimsProvider = currentMemberClaimsProvider;
|
||||
|
||||
[HttpGet("userinfo")]
|
||||
public async Task<IActionResult> Userinfo()
|
||||
{
|
||||
Dictionary<string, object> claims = await _currentMemberClaimsProvider.GetClaimsAsync();
|
||||
return Ok(claims);
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult;
|
||||
using IdentitySignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiController]
|
||||
[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class MemberController : DeliveryApiControllerBase
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IMemberSignInManager _memberSignInManager;
|
||||
private readonly IMemberManager _memberManager;
|
||||
private readonly DeliveryApiSettings _deliveryApiSettings;
|
||||
private readonly ILogger<MemberController> _logger;
|
||||
|
||||
public MemberController(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IMemberSignInManager memberSignInManager,
|
||||
IMemberManager memberManager,
|
||||
IOptions<DeliveryApiSettings> deliveryApiSettings,
|
||||
ILogger<MemberController> logger)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_memberSignInManager = memberSignInManager;
|
||||
_memberManager = memberManager;
|
||||
_logger = logger;
|
||||
_deliveryApiSettings = deliveryApiSettings.Value;
|
||||
}
|
||||
|
||||
[HttpGet("authorize")]
|
||||
[MapToApiVersion("1.0")]
|
||||
public async Task<IActionResult> Authorize()
|
||||
{
|
||||
// in principle this is not necessary for now, since the member application has been removed, thus making
|
||||
// the member client ID invalid for the authentication code flow. However, if we ever add additional flows
|
||||
// to the API, we should perform this check, so we might as well include it upfront.
|
||||
if (_deliveryApiSettings.MemberAuthorizationIsEnabled() is false)
|
||||
{
|
||||
return BadRequest("Member authorization is not allowed.");
|
||||
}
|
||||
|
||||
HttpContext context = _httpContextAccessor.GetRequiredHttpContext();
|
||||
OpenIddictRequest? request = context.GetOpenIddictServerRequest();
|
||||
if (request is null)
|
||||
{
|
||||
return BadRequest("Unable to obtain OpenID data from the current request.");
|
||||
}
|
||||
|
||||
// make sure this endpoint ONLY handles member authentication
|
||||
if (request.ClientId is not Constants.OAuthClientIds.Member)
|
||||
{
|
||||
return BadRequest("The specified client ID cannot be used here.");
|
||||
}
|
||||
|
||||
return request.IdentityProvider.IsNullOrWhiteSpace()
|
||||
? await AuthorizeInternal(request)
|
||||
: await AuthorizeExternal(request);
|
||||
}
|
||||
|
||||
[HttpGet("signout")]
|
||||
[MapToApiVersion("1.0")]
|
||||
public async Task<IActionResult> Signout()
|
||||
{
|
||||
await _memberSignInManager.SignOutAsync();
|
||||
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> AuthorizeInternal(OpenIddictRequest request)
|
||||
{
|
||||
// retrieve the user principal stored in the authentication cookie.
|
||||
AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
|
||||
var userName = cookieAuthResult.Succeeded
|
||||
? cookieAuthResult.Principal?.Identity?.Name
|
||||
: null;
|
||||
|
||||
if (userName is null)
|
||||
{
|
||||
return Challenge(IdentityConstants.ApplicationScheme);
|
||||
}
|
||||
|
||||
MemberIdentityUser? member = await _memberManager.FindByNameAsync(userName);
|
||||
if (member is null)
|
||||
{
|
||||
_logger.LogError("The member with username {userName} was successfully authorized, but could not be retrieved by the member manager", userName);
|
||||
return BadRequest("The member could not be found.");
|
||||
}
|
||||
|
||||
return await SignInMember(member, request);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> AuthorizeExternal(OpenIddictRequest request)
|
||||
{
|
||||
var provider = request.IdentityProvider ?? throw new ArgumentException("No identity provider found in request", nameof(request));
|
||||
ExternalLoginInfo? loginInfo = await _memberSignInManager.GetExternalLoginInfoAsync();
|
||||
|
||||
if (loginInfo?.Principal is null)
|
||||
{
|
||||
AuthenticationProperties properties = _memberSignInManager.ConfigureExternalAuthenticationProperties(provider, null);
|
||||
return Challenge(properties, provider);
|
||||
}
|
||||
|
||||
// NOTE: if we're going to support 2FA for members, we need to:
|
||||
// - use SecuritySettings.MemberBypassTwoFactorForExternalLogins instead of the hardcoded value (true) for "bypassTwoFactor".
|
||||
// - handle IdentitySignInResult.TwoFactorRequired
|
||||
IdentitySignInResult result = await _memberSignInManager.ExternalLoginSignInAsync(loginInfo, false, true);
|
||||
if (result == IdentitySignInResult.Success)
|
||||
{
|
||||
// get the member and perform sign-in
|
||||
MemberIdentityUser? member = await _memberManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
|
||||
if (member is null)
|
||||
{
|
||||
_logger.LogError("A member was successfully authorized using external authentication, but could not be retrieved by the member manager");
|
||||
return BadRequest("The member could not be found.");
|
||||
}
|
||||
|
||||
// update member authentication tokens if succeeded
|
||||
await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(loginInfo);
|
||||
return await SignInMember(member, request);
|
||||
}
|
||||
|
||||
var errorProperties = new AuthenticationProperties(new Dictionary<string, string?>
|
||||
{
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.AccessDenied,
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The member is not allowed to access this resource."
|
||||
});
|
||||
return Forbid(errorProperties, provider);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> SignInMember(MemberIdentityUser member, OpenIddictRequest request)
|
||||
{
|
||||
ClaimsPrincipal memberPrincipal = await _memberSignInManager.CreateUserPrincipalAsync(member);
|
||||
memberPrincipal.SetClaim(OpenIddictConstants.Claims.Subject, member.Key.ToString());
|
||||
|
||||
IList<string> roles = await _memberManager.GetRolesAsync(member);
|
||||
memberPrincipal.SetClaim(Constants.OAuthClaims.MemberKey, member.Key.ToString());
|
||||
memberPrincipal.SetClaim(Constants.OAuthClaims.MemberRoles, string.Join(",", roles));
|
||||
|
||||
Claim[] claims = memberPrincipal.Claims.ToArray();
|
||||
foreach (Claim claim in claims.Where(claim => claim.Type is not Constants.Security.SecurityStampClaimType))
|
||||
{
|
||||
claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken);
|
||||
}
|
||||
|
||||
// "openid" and "offline_access" are the only scopes allowed for members; explicitly ensure we only add those
|
||||
// NOTE: the "offline_access" scope is required to use refresh tokens
|
||||
IEnumerable<string> allowedScopes = request
|
||||
.GetScopes()
|
||||
.Intersect(new[] { OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess });
|
||||
memberPrincipal.SetScopes(allowedScopes);
|
||||
|
||||
return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, memberPrincipal);
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Asp.Versioning;
|
||||
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;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.Handlers;
|
||||
using Umbraco.Cms.Api.Delivery.Json;
|
||||
using Umbraco.Cms.Api.Delivery.Rendering;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Api.Delivery.Security;
|
||||
using Umbraco.Cms.Api.Delivery.Services;
|
||||
using Umbraco.Cms.Api.Delivery.Services.QueryBuilders;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
public static class UmbracoBuilderExtensions
|
||||
{
|
||||
public static IUmbracoBuilder AddDeliveryApi(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddScoped<IRequestStartItemProvider, RequestStartItemProvider>();
|
||||
builder.Services.AddScoped<RequestContextOutputExpansionStrategy>();
|
||||
builder.Services.AddScoped<RequestContextOutputExpansionStrategyV2>();
|
||||
|
||||
builder.Services.AddUnique<IOutputExpansionStrategy>(
|
||||
provider =>
|
||||
{
|
||||
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
|
||||
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
|
||||
if (apiVersion is null)
|
||||
{
|
||||
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
}
|
||||
|
||||
// V1 of the Delivery API uses a different expansion strategy than V2+
|
||||
return apiVersion.MajorVersion == 1
|
||||
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
|
||||
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
},
|
||||
ServiceLifetime.Scoped);
|
||||
|
||||
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
|
||||
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
|
||||
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
|
||||
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();
|
||||
|
||||
// Webooks register a more basic implementation, remove it.
|
||||
builder.Services.AddUnique<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>(ServiceLifetime.Singleton);
|
||||
builder.Services.AddSingleton<IRequestStartItemProviderAccessor, RequestContextRequestStartItemProviderAccessor>();
|
||||
|
||||
builder.Services.AddSingleton<IApiAccessService, ApiAccessService>();
|
||||
builder.Services.AddSingleton<IApiContentQueryService, ApiContentQueryService>();
|
||||
builder.Services.AddSingleton<IApiContentQueryProvider, ApiContentQueryProvider>();
|
||||
builder.Services.AddSingleton<IApiContentQueryFactory, ApiContentQueryFactory>();
|
||||
builder.Services.AddSingleton<IApiMediaQueryService, ApiMediaQueryService>();
|
||||
builder.Services.AddTransient<IMemberApplicationManager, MemberApplicationManager>();
|
||||
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
|
||||
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();
|
||||
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
|
||||
builder.AddUmbracoApiOpenApiUI();
|
||||
|
||||
builder
|
||||
.Services
|
||||
.AddControllers()
|
||||
.AddJsonOptions(Constants.JsonOptionsNames.DeliveryApi, options =>
|
||||
{
|
||||
// all Delivery API specific JSON options go here
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver();
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication();
|
||||
builder.AddUmbracoOpenIddict();
|
||||
builder.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, InitializeMemberApplicationNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<MemberSavedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<MemberDeletedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<AssignedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<RemovedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
|
||||
// FIXME: remove this when Delivery API V1 is removed
|
||||
builder.Services.AddSingleton<MatcherPolicy, DeliveryApiItemsEndpointsMatcherPolicy>();
|
||||
|
||||
builder.AddOutputCache();
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static IUmbracoBuilder AddOutputCache(this IUmbracoBuilder builder)
|
||||
{
|
||||
DeliveryApiSettings.OutputCacheSettings outputCacheSettings =
|
||||
builder.Config.GetSection(Constants.Configuration.ConfigDeliveryApi).Get<DeliveryApiSettings>()?.OutputCache
|
||||
?? new DeliveryApiSettings.OutputCacheSettings();
|
||||
|
||||
if (outputCacheSettings.Enabled is false || outputCacheSettings is { ContentDuration.TotalSeconds: <= 0, MediaDuration.TotalSeconds: <= 0 })
|
||||
{
|
||||
return builder;
|
||||
}
|
||||
|
||||
builder.Services.AddOutputCache(options =>
|
||||
{
|
||||
options.AddBasePolicy(_ => { });
|
||||
|
||||
if (outputCacheSettings.ContentDuration.TotalSeconds > 0)
|
||||
{
|
||||
options.AddPolicy(
|
||||
Constants.DeliveryApi.OutputCache.ContentCachePolicy,
|
||||
new DeliveryApiOutputCachePolicy(
|
||||
outputCacheSettings.ContentDuration,
|
||||
new StringValues([Constants.DeliveryApi.HeaderNames.AcceptLanguage, Constants.DeliveryApi.HeaderNames.StartItem])));
|
||||
}
|
||||
|
||||
if (outputCacheSettings.MediaDuration.TotalSeconds > 0)
|
||||
{
|
||||
options.AddPolicy(
|
||||
Constants.DeliveryApi.OutputCache.MediaCachePolicy,
|
||||
new DeliveryApiOutputCachePolicy(
|
||||
outputCacheSettings.MediaDuration,
|
||||
Constants.DeliveryApi.HeaderNames.StartItem));
|
||||
}
|
||||
});
|
||||
|
||||
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new OutputCachePipelineFilter("UmbracoDeliveryApiOutputCache")));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
public sealed class AddVaryHeaderAttribute : ActionFilterAttribute
|
||||
{
|
||||
private const string Vary = "Accept-Language, Preview, Start-Item";
|
||||
|
||||
public override void OnResultExecuting(ResultExecutingContext context)
|
||||
=> context.HttpContext.Response.Headers.Vary = context.HttpContext.Response.Headers.Vary.Count > 0
|
||||
? $"{context.HttpContext.Response.Headers.Vary}, {Vary}"
|
||||
: Vary;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal sealed class DeliveryApiAccessAttribute : TypeFilterAttribute
|
||||
{
|
||||
public DeliveryApiAccessAttribute()
|
||||
: base(typeof(DeliveryApiAccessFilter))
|
||||
{
|
||||
}
|
||||
|
||||
private class DeliveryApiAccessFilter : IActionFilter
|
||||
{
|
||||
private readonly IApiAccessService _apiAccessService;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
|
||||
public DeliveryApiAccessFilter(IApiAccessService apiAccessService, IRequestPreviewService requestPreviewService)
|
||||
{
|
||||
_apiAccessService = apiAccessService;
|
||||
_requestPreviewService = requestPreviewService;
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var hasAccess = _requestPreviewService.IsPreview()
|
||||
? _apiAccessService.HasPreviewAccess()
|
||||
: _apiAccessService.HasPublicAccess();
|
||||
|
||||
if (hasAccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.Result = new UnauthorizedResult();
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal sealed class DeliveryApiMediaAccessAttribute : TypeFilterAttribute
|
||||
{
|
||||
public DeliveryApiMediaAccessAttribute()
|
||||
: base(typeof(DeliveryApiMediaAccessFilter))
|
||||
{
|
||||
}
|
||||
|
||||
private class DeliveryApiMediaAccessFilter : IActionFilter
|
||||
{
|
||||
private readonly IApiAccessService _apiAccessService;
|
||||
|
||||
public DeliveryApiMediaAccessFilter(IApiAccessService apiAccessService)
|
||||
=> _apiAccessService = apiAccessService;
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (_apiAccessService.HasMediaAccess())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.Result = new UnauthorizedResult();
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal sealed class LocalizeFromAcceptLanguageHeaderAttribute : TypeFilterAttribute
|
||||
{
|
||||
public LocalizeFromAcceptLanguageHeaderAttribute()
|
||||
: base(typeof(LocalizeFromAcceptLanguageHeaderAttributeFilter))
|
||||
{
|
||||
}
|
||||
|
||||
private class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
|
||||
{
|
||||
private readonly IRequestCultureService _requestCultureService;
|
||||
|
||||
public LocalizeFromAcceptLanguageHeaderAttributeFilter(IRequestCultureService requestCultureService)
|
||||
=> _requestCultureService = requestCultureService;
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var requestedCulture = _requestCultureService.GetRequestedCulture();
|
||||
if (requestedCulture.IsNullOrWhiteSpace())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_requestCultureService.SetRequestCulture(requestedCulture);
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFilterBase<ContentApiControllerBase>
|
||||
{
|
||||
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationContentArticleLink;
|
||||
|
||||
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Parameters ??= new List<OpenApiParameter>();
|
||||
|
||||
AddExpand(operation, context);
|
||||
|
||||
AddFields(operation, context);
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
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.",
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Default", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{ "English culture", new OpenApiExample { Value = new OpenApiString("en-us") } }
|
||||
}
|
||||
});
|
||||
|
||||
AddApiKey(operation);
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.Preview,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Whether to request draft content.",
|
||||
Schema = new OpenApiSchema { Type = "boolean" }
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.StartItem,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "URL segment or GUID of a root content item.",
|
||||
Schema = new OpenApiSchema { Type = "string" }
|
||||
});
|
||||
}
|
||||
|
||||
protected override void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context)
|
||||
{
|
||||
switch (parameter.Name)
|
||||
{
|
||||
case "fetch":
|
||||
AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the content items to fetch");
|
||||
break;
|
||||
case "filter":
|
||||
AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched content items");
|
||||
break;
|
||||
case "sort":
|
||||
AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found content items");
|
||||
break;
|
||||
case "skip":
|
||||
parameter.Description = PaginationDescription(true, "content");
|
||||
break;
|
||||
case "take":
|
||||
parameter.Description = PaginationDescription(false, "content");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, OpenApiExample> FetchQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Select all", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Select all ancestors of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("ancestors:id") }
|
||||
},
|
||||
{
|
||||
"Select all ancestors of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("ancestors:path") }
|
||||
},
|
||||
{
|
||||
"Select all children of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("children:id") }
|
||||
},
|
||||
{
|
||||
"Select all children of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("children:path") }
|
||||
},
|
||||
{
|
||||
"Select all descendants of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("descendants:id") }
|
||||
},
|
||||
{
|
||||
"Select all descendants of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("descendants:path") }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, OpenApiExample> FilterQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default filter", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Filter by content type (equals)",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("contentType:alias1") } }
|
||||
},
|
||||
{
|
||||
"Filter by name (contains)",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } }
|
||||
},
|
||||
{
|
||||
"Filter by creation date (less than)",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("createDate<2024-01-01") } }
|
||||
},
|
||||
{
|
||||
"Filter by update date (greater than or equal)",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("updateDate>:2023-01-01") } }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, OpenApiExample> SortQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default sort", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Sort by create date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by level",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray { new OpenApiString("level:asc"), new OpenApiString("level:desc") }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by name",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by sort order",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by update date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
[Obsolete($"Superseded by {nameof(SwaggerContentDocumentationFilter)} and {nameof(SwaggerMediaDocumentationFilter)}. Will be removed in V14.")]
|
||||
public class SwaggerDocumentationFilter : IOperationFilter, IParameterFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
// retained for backwards compat
|
||||
}
|
||||
|
||||
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
|
||||
{
|
||||
// retained for backwards compat
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
: SwaggerFilterBase<TBaseController>, IOperationFilter, IParameterFilter
|
||||
where TBaseController : Controller
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (CanApply(context))
|
||||
{
|
||||
ApplyOperation(operation, context);
|
||||
}
|
||||
}
|
||||
|
||||
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
|
||||
{
|
||||
if (CanApply(context))
|
||||
{
|
||||
ApplyParameter(parameter, context);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract string DocumentationLink { get; }
|
||||
|
||||
protected abstract void ApplyOperation(OpenApiOperation operation, OperationFilterContext context);
|
||||
|
||||
protected abstract void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context);
|
||||
|
||||
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, OpenApiExample> examples, string description)
|
||||
{
|
||||
parameter.Description = QueryParameterDescription(description);
|
||||
parameter.Examples = examples;
|
||||
}
|
||||
|
||||
protected void AddExpand(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (IsApiV1(context))
|
||||
{
|
||||
AddExpandV1(operation);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddExpand(operation);
|
||||
}
|
||||
}
|
||||
|
||||
protected void AddFields(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (IsApiV1(context))
|
||||
{
|
||||
// "fields" is not a thing in Delivery API V1
|
||||
return;
|
||||
}
|
||||
|
||||
AddFields(operation);
|
||||
}
|
||||
|
||||
protected void AddApiKey(OpenApiOperation operation) =>
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.ApiKey,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
Schema = new OpenApiSchema { Type = "string" }
|
||||
});
|
||||
|
||||
protected string PaginationDescription(bool skip, string itemType)
|
||||
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
|
||||
|
||||
private string QueryParameterDescription(string description)
|
||||
=> $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
|
||||
|
||||
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
|
||||
private static bool IsApiV1(OperationFilterContext context)
|
||||
=> context.ApiDescription.RelativePath?.Contains("api/v1") is true;
|
||||
|
||||
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
|
||||
private void AddExpandV1(OpenApiOperation operation)
|
||||
=> operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Expand none", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{ "Expand all", new OpenApiExample { Value = new OpenApiString("all") } },
|
||||
{
|
||||
"Expand specific property",
|
||||
new OpenApiExample { Value = new OpenApiString("property:alias1") }
|
||||
},
|
||||
{
|
||||
"Expand specific properties",
|
||||
new OpenApiExample { Value = new OpenApiString("property:alias1,alias2") }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private void AddExpand(OpenApiOperation operation)
|
||||
=> operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Expand none", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{ "Expand all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } },
|
||||
{
|
||||
"Expand specific property",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1]") }
|
||||
},
|
||||
{
|
||||
"Expand specific properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") }
|
||||
},
|
||||
{
|
||||
"Expand nested properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private void AddFields(OpenApiOperation operation)
|
||||
=> operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "fields",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description = QueryParameterDescription("Explicitly defines which properties should be included in the response (by default all properties are included)"),
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Include all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } },
|
||||
{
|
||||
"Include only specific property",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1]") }
|
||||
},
|
||||
{
|
||||
"Include only specific properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") }
|
||||
},
|
||||
{
|
||||
"Include only specific nested properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user