diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3432ac472a..124557d416 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,211 +1,211 @@ -# Contributing to Umbraco CMS - -πŸ‘πŸŽ‰ First off, thanks for taking the time to contribute! πŸŽ‰πŸ‘ - -The following is a set of guidelines, for contributing to Umbraco CMS. - -These are mostly guidelines, not rules. Use your best judgement, and feel free to propose changes to this document in a pull request. - -Remember, we're a friendly bunch and are happy with whatever contribution you might provide. Below are guidelines for success that we've gathered over the years. If you choose to ignore them then we still love you πŸ’–. - -**Code of conduct** - -This project and everyone participating in it, is governed by the [our Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Sebastiaan Janssen - sj@umbraco.dk](mailto:sj@umbraco.dk). - -**Table of contents** - -[Contributing code changes](#contributing-code-changes) - * [Guidelines for contributions we welcome](#guidelines-for-contributions-we-welcome) - * [Ownership and copyright](#ownership-and-copyright) - * [What can I start with?](#what-can-i-start-with) - * [How do I begin?](#how-do-i-begin) - * [Pull requests](#pull-requests) - -[Reviews](#reviews) - * [Styleguides](#styleguides) - * [The Core Contributors](#the-core-contributors-team) - * [Questions?](#questions) - -[Working with the code](#working-with-the-code) - * [Building Umbraco from source code](#building-umbraco-from-source-code) - * [Working with the source code](#working-with-the-source-code) - * [Making changes after the PR is open](#making-changes-after-the-pr-is-open) - * [Which branch should I target for my contributions?](#which-branch-should-i-target-for-my-contributions) - * [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository) - -## Contributing code changes - -This document gives you a quick overview on how to get started. - -### Guidelines for contributions we welcome - -Not all changes are wanted, so on occasion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valuable time. - -We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes, so we can ensure that you don't put all your hard work into something we would not be able to merge. - -Remember, it is always worth working on an issue from the `Up for grabs` list or even asking for some feedback before you send us a PR. This way, your PR will not be closed as unwanted. - -#### 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](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/LICENSE.md) from that time onwards. - -### What can I start with? - -Unsure where to begin contributing to Umbraco? You can start by looking through [these `Up for grabs` issues](https://github.com/umbraco/Umbraco-CMS/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs+) - -### How do I begin? - -Great question! The short version goes like this: - - * **Fork** - create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS) - - ![Fork the repository](img/forkrepository.png) - - * **Clone** - when GitHub has created your fork, you can clone it in your favorite Git tool - - ![Clone the fork](img/clonefork.png) - - * **Switch to the correct branch** - switch to the `v8/contrib` branch - * **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md) - * **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) - * **Commit** - done? Yay! πŸŽ‰ **Important:** 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 `12345`. When you have a branch, commit your changes. Don't commit to `v8/contrib`, create a new branch first. - * **Push** - great, now you can push the changes up to your fork on GitHub - * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here](https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has 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. - - ![Create a pull request](img/createpullrequest.png) - -### Pull requests -The most successful pull requests usually look a like this: - - * Fill in the required template (shown when starting a PR on GitHub), and link your pull request to an issue on the [issue tracker,](https://github.com/umbraco/Umbraco-CMS/issues) if applicable. - * Include screenshots and animated GIFs in your pull request whenever possible. - * Unit tests, while optional, are awesome. Thank you! - * New code is commented with documentation from which [the reference documentation](https://our.umbraco.com/documentation/Reference/) is generated. - -Again, these are guidelines, not strict requirements. However, the more information that you give to us, the more we have to work with when considering your contributions. Good documentation of a pull request can really speed up the time it takes to review and merge your work! - -## Reviews - -You've sent us your first contribution - congratulations! Now what? - -The [pull request team](#the-pr-team) 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. - -We have [a process in place which you can read all about](REVIEW_PROCESS.md). The very abbreviated version is: - -- Your PR will get a reply within 48 hours -- An in-depth reply will be added within at most 2 weeks -- The PR will be either merged or rejected within at most 4 weeks -- Sometimes it is difficult to meet these timelines and we'll talk to you if this is the case. - -### Styleguides - -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. - -### The Core Contributors team - -The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members who have comitted to volunteering their free time: - -- [Nathan Woulfe](https://github.com/nathanwoulfe) -- [Joe Glombek](https://github.com/glombek) -- [Laura Weatherhead](https://github.com/lssweatherhead) -- [Michael Latouche](https://github.com/mikecp) -- [Owain Williams](https://github.com/OwainWilliams) - - -These wonderful people aim to provide you with a first 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. - -### Questions? - -You can get in touch with [the core contributors team](#the-core-contributors-team) 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. -- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.com/forum/contributing-to-umbraco-cms/) forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction. - -## Working with the code - -### Building Umbraco from source code - -In order to build the Umbraco source code locally, first make sure you have the following installed. - - * [Visual Studio 2019 v16.3+ (with .NET Core 3.0)](https://visualstudio.microsoft.com/vs/) - * [Node.js v10+](https://nodejs.org/en/download/) - * npm v6.4.1+ (installed with Node.js) - * [Git command line](https://git-scm.com/download/) - -The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2019 (version 16.3 or higher, [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). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile. - -Alternatively, you can run `build.ps1` from the Powershell command line, which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details. - -![Gulp build in Visual Studio](img/gulpbuild.png) - -After this build completes, you should be able to hit `F5` in Visual Studio to build and run the project. A IISExpress webserver will start and the Umbraco installer will pop up in your browser. Follow the directions there to get a working Umbraco install up and running. - -### Working with the source code - -Some parts of our source code are over 10 years old now. And when we say "old", we mean "mature" of course! - -There are two big areas that you should know about: - - 1. The Umbraco backoffice is a extensible AngularJS app and requires you to run a `gulp dev` command while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes. - You may need to run the following commands to set up gulp properly: - ``` - npm cache clean --force - npm install - npm run build - ``` - The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to disable caching in the browser to help you to see the changes you're making. - - 2. "The rest" is a C# based codebase, which is mostly ASP.NET MVC based. You can make changes, build them in Visual Studio, and hit `F5` to see the result. - -To find the general areas for something you're looking to fix or improve, have a look at the following two parts of the API documentation. - - * [The AngularJS based backoffice files](https://our.umbraco.com/apidocs/ui/#/api) (to be found in `src\Umbraco.Web.UI.Client\src`) - * [The C# application](https://our.umbraco.com/apidocs/csharp/) - -### Which branch should I target for my contributions? - -We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), 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 `v8/contrib`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'. - -Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise. - -![Which branch should I target?](img/defaultbranch.png) - -### Making changes after the PR is open - -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! - -### Keeping your Umbraco fork in sync with the main repository - -We recommend you to sync with our repository before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier. - -Also, if you have submitted a pull request three weeks ago and want to work on something new, you'll want to get the latest code to build against of course. - -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/v8/contrib -``` - -In this command we're syncing with the `v8/contrib` branch, but you can of course choose another one if needed. - -(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated)) - -### And finally - -We welcome all kinds of contributions to this repository. If you don't feel you'd like to make code changes here, you can visit our [documentation repository](https://github.com/umbraco/UmbracoDocs) 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 Contributors 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. +# Contributing to Umbraco CMS + +πŸ‘πŸŽ‰ First off, thanks for taking the time to contribute! πŸŽ‰πŸ‘ + +The following is a set of guidelines, for contributing to Umbraco CMS. + +These are mostly guidelines, not rules. Use your best judgement, and feel free to propose changes to this document in a pull request. + +Remember, we're a friendly bunch and are happy with whatever contribution you might provide. Below are guidelines for success that we've gathered over the years. If you choose to ignore them then we still love you πŸ’–. + +**Code of conduct** + +This project and everyone participating in it, is governed by the [our Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Sebastiaan Janssen - sj@umbraco.dk](mailto:sj@umbraco.dk). + +**Table of contents** + +[Contributing code changes](#contributing-code-changes) + * [Guidelines for contributions we welcome](#guidelines-for-contributions-we-welcome) + * [Ownership and copyright](#ownership-and-copyright) + * [What can I start with?](#what-can-i-start-with) + * [How do I begin?](#how-do-i-begin) + * [Pull requests](#pull-requests) + +[Reviews](#reviews) + * [Styleguides](#styleguides) + * [The Core Contributors](#the-core-contributors-team) + * [Questions?](#questions) + +[Working with the code](#working-with-the-code) + * [Building Umbraco from source code](#building-umbraco-from-source-code) + * [Working with the source code](#working-with-the-source-code) + * [Making changes after the PR is open](#making-changes-after-the-pr-is-open) + * [Which branch should I target for my contributions?](#which-branch-should-i-target-for-my-contributions) + * [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository) + +## Contributing code changes + +This document gives you a quick overview on how to get started. + +### Guidelines for contributions we welcome + +Not all changes are wanted, so on occasion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valuable time. + +We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes, so we can ensure that you don't put all your hard work into something we would not be able to merge. + +Remember, it is always worth working on an issue from the `Up for grabs` list or even asking for some feedback before you send us a PR. This way, your PR will not be closed as unwanted. + +#### 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](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/LICENSE.md) from that time onwards. + +### What can I start with? + +Unsure where to begin contributing to Umbraco? You can start by looking through [these `Up for grabs` issues](https://github.com/umbraco/Umbraco-CMS/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs+) + +### How do I begin? + +Great question! The short version goes like this: + + * **Fork** - create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS) + + ![Fork the repository](img/forkrepository.png) + + * **Clone** - when GitHub has created your fork, you can clone it in your favorite Git tool + + ![Clone the fork](img/clonefork.png) + + * **Switch to the correct branch** - switch to the `v8/contrib` branch + * **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md) + * **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) + * **Commit** - done? Yay! πŸŽ‰ **Important:** 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 `12345`. When you have a branch, commit your changes. Don't commit to `v8/contrib`, create a new branch first. + * **Push** - great, now you can push the changes up to your fork on GitHub + * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here](https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has 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. + + ![Create a pull request](img/createpullrequest.png) + +### Pull requests +The most successful pull requests usually look a like this: + + * Fill in the required template (shown when starting a PR on GitHub), and link your pull request to an issue on the [issue tracker,](https://github.com/umbraco/Umbraco-CMS/issues) if applicable. + * Include screenshots and animated GIFs in your pull request whenever possible. + * Unit tests, while optional, are awesome. Thank you! + * New code is commented with documentation from which [the reference documentation](https://our.umbraco.com/documentation/Reference/) is generated. + +Again, these are guidelines, not strict requirements. However, the more information that you give to us, the more we have to work with when considering your contributions. Good documentation of a pull request can really speed up the time it takes to review and merge your work! + +## Reviews + +You've sent us your first contribution - congratulations! Now what? + +The [pull request team](#the-pr-team) 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. + +We have [a process in place which you can read all about](REVIEW_PROCESS.md). The very abbreviated version is: + +- Your PR will get a reply within 48 hours +- An in-depth reply will be added within at most 2 weeks +- The PR will be either merged or rejected within at most 4 weeks +- Sometimes it is difficult to meet these timelines and we'll talk to you if this is the case. + +### Styleguides + +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. + +### The Core Contributors team + +The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members who have comitted to volunteering their free time: + +- [Nathan Woulfe](https://github.com/nathanwoulfe) +- [Joe Glombek](https://github.com/glombek) +- [Laura Weatherhead](https://github.com/lssweatherhead) +- [Michael Latouche](https://github.com/mikecp) +- [Owain Williams](https://github.com/OwainWilliams) + + +These wonderful people aim to provide you with a first 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. + +### Questions? + +You can get in touch with [the core contributors team](#the-core-contributors-team) 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. +- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.com/forum/contributing-to-umbraco-cms/) forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction. + +## Working with the code + +### Building Umbraco from source code + +In order to build the Umbraco source code locally, first make sure you have the following installed. + + * [Visual Studio 2019 v16.3+ (with .NET Core 3.0)](https://visualstudio.microsoft.com/vs/) + * [Node.js v10+](https://nodejs.org/en/download/) + * npm v6.4.1+ (installed with Node.js) + * [Git command line](https://git-scm.com/download/) + +The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2019 (version 16.3 or higher, [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). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile. + +Alternatively, you can run `build.ps1` from the Powershell command line, which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details. + +![Gulp build in Visual Studio](img/gulpbuild.png) + +After this build completes, you should be able to hit `F5` in Visual Studio to build and run the project. A IISExpress webserver will start and the Umbraco installer will pop up in your browser. Follow the directions there to get a working Umbraco install up and running. + +### Working with the source code + +Some parts of our source code are over 10 years old now. And when we say "old", we mean "mature" of course! + +There are two big areas that you should know about: + + 1. The Umbraco backoffice is a extensible AngularJS app and requires you to run a `gulp dev` command while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes. + You may need to run the following commands to set up gulp properly: + ``` + npm cache clean --force + npm install + npm run build + ``` + The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to disable caching in the browser to help you to see the changes you're making. + + 2. "The rest" is a C# based codebase, which is mostly ASP.NET MVC based. You can make changes, build them in Visual Studio, and hit `F5` to see the result. + +To find the general areas for something you're looking to fix or improve, have a look at the following two parts of the API documentation. + + * [The AngularJS based backoffice files](https://apidocs.umbraco.com/v9/ui#/api) (to be found in `src\Umbraco.Web.UI.Client\src`) + * [The C# application](https://apidocs.umbraco.com/v9/csharp/) + +### Which branch should I target for my contributions? + +We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), 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 `v8/contrib`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'. + +Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise. + +![Which branch should I target?](img/defaultbranch.png) + +### Making changes after the PR is open + +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! + +### Keeping your Umbraco fork in sync with the main repository + +We recommend you to sync with our repository before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier. + +Also, if you have submitted a pull request three weeks ago and want to work on something new, you'll want to get the latest code to build against of course. + +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/v8/contrib +``` + +In this command we're syncing with the `v8/contrib` branch, but you can of course choose another one if needed. + +(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated)) + +### And finally + +We welcome all kinds of contributions to this repository. If you don't feel you'd like to make code changes here, you can visit our [documentation repository](https://github.com/umbraco/UmbracoDocs) 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 Contributors 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. diff --git a/.gitignore b/.gitignore index e38c71eef8..f5249bb70b 100644 --- a/.gitignore +++ b/.gitignore @@ -207,6 +207,7 @@ src/Umbraco.Tests/TEMP/ src/Umbraco.Tests.UnitTests/umbraco/Data/TEMP/ /src/Umbraco.Web.UI.NetCore/appsettings.Local.json -src/Umbraco.Tests.Integration/DatabaseContextTests.sdf +src/Umbraco.Tests.Integration.SqlCe/umbraco/Data/TEMP/ +src/Umbraco.Tests.Integration.SqlCe/DatabaseContextTests.sdf -src/Umbraco.Web.UI.NetCore/umbraco/config/appsettings-schema.json +src/Umbraco.Web.UI.NetCore/umbraco/config/appsettings-schema.json diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index d95949d865..f82d90c1b2 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -123,7 +123,7 @@ stages: displayName: dotnet test inputs: command: test - projects: '**\Umbraco.Tests.Integration.csproj' + projects: '**\Umbraco.Tests.Integration*.csproj' - stage: Acceptance_Tests displayName: Acceptance Tests dependsOn: [] diff --git a/build/build.ps1 b/build/build.ps1 index b800d45fa3..a5f8b6a3e4 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -503,7 +503,7 @@ Pop-Location # change baseUrl - $BaseUrl = "https://our.umbraco.com/apidocs/v8/ui/" + $BaseUrl = "https://apidocs.umbraco.com/v9/ui/" $IndexPath = "./api/index.html" (Get-Content $IndexPath).replace('origin + location.href.substr(origin.length).replace(rUrl, indexFile)', "`'" + $BaseUrl + "`'") | Set-Content $IndexPath diff --git a/build/templates/UmbracoPackage/.template.config/template.json b/build/templates/UmbracoPackage/.template.config/template.json index 0a4c8f2f2b..b2bc86399a 100644 --- a/build/templates/UmbracoPackage/.template.config/template.json +++ b/build/templates/UmbracoPackage/.template.config/template.json @@ -24,7 +24,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.0.0-beta003", + "defaultValue": "9.0.0-rc001", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/build/templates/UmbracoProject/.template.config/template.json b/build/templates/UmbracoProject/.template.config/template.json index 8a8c396dcb..ba75721bc4 100644 --- a/build/templates/UmbracoProject/.template.config/template.json +++ b/build/templates/UmbracoProject/.template.config/template.json @@ -57,7 +57,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.0.0-beta004", + "defaultValue": "9.0.0-rc001", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/build/templates/UmbracoProject/UmbracoProject.csproj b/build/templates/UmbracoProject/UmbracoProject.csproj index 86562f7957..2310f789e2 100644 --- a/build/templates/UmbracoProject/UmbracoProject.csproj +++ b/build/templates/UmbracoProject/UmbracoProject.csproj @@ -1,4 +1,4 @@ - +ο»Ώ net5.0 Umbraco.Cms.Web.UI.NetCore @@ -15,6 +15,11 @@ + + + + + @@ -26,9 +31,6 @@ - - - true Always @@ -37,11 +39,9 @@ true Always - - - - - + + + diff --git a/build/templates/UmbracoProject/Views/_ViewImports.cshtml b/build/templates/UmbracoProject/Views/_ViewImports.cshtml index 3aaf133335..25e6e66533 100644 --- a/build/templates/UmbracoProject/Views/_ViewImports.cshtml +++ b/build/templates/UmbracoProject/Views/_ViewImports.cshtml @@ -1,4 +1,6 @@ ο»Ώ@using Umbraco.Web.UI.NetCore @using Umbraco.Extensions @using Umbraco.Web.PublishedModels +@using Umbraco.Cms.Core.Models.PublishedContent +@using Microsoft.AspNetCore.Html @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 431bd07fd3..b45e31a4a6 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ 9.0.0 9.0.0 - 9.0.0-rc001.pre001 + 9.0.0-rc001 9.0.0 9.0 en-US diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index 616d641751..4045421eb1 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -46,6 +46,7 @@ namespace JsonSchema public UnattendedSettings Unattended { get; set; } public RichTextEditorSettings RichTextEditor { get; set; } public RuntimeMinificationSettings RuntimeMinification { get; set; } + public BasicAuthSettings BasicAuth { get; set; } } /// diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index 25d4fa5498..3b64612b62 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Umbraco.Core/Composing/CompositionExtensions.cs b/src/Umbraco.Core/Composing/CompositionExtensions.cs index 74f30b81b6..d087af77d8 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions.cs @@ -11,9 +11,10 @@ namespace Umbraco.Extensions /// /// The builder. /// A function creating a published snapshot service. - public static void SetPublishedSnapshotService(this IUmbracoBuilder builder, Func factory) + public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder, Func factory) { builder.Services.AddUnique(factory); + return builder; } /// @@ -21,10 +22,11 @@ namespace Umbraco.Extensions /// /// The type of the published snapshot service. /// The builder. - public static void SetPublishedSnapshotService(this IUmbracoBuilder builder) + public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder) where T : class, IPublishedSnapshotService { builder.Services.AddUnique(); + return builder; } /// @@ -32,9 +34,10 @@ namespace Umbraco.Extensions /// /// The builder. /// A published snapshot service. - public static void SetPublishedSnapshotService(this IUmbracoBuilder builder, IPublishedSnapshotService service) + public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder, IPublishedSnapshotService service) { builder.Services.AddUnique(service); + return builder; } } } diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index 4299328b5d..07a2eb72c9 100644 --- a/src/Umbraco.Core/Composing/TypeFinder.cs +++ b/src/Umbraco.Core/Composing/TypeFinder.cs @@ -146,6 +146,7 @@ namespace Umbraco.Cms.Core.Composing "HtmlDiff,", "ICSharpCode.", "Iesi.Collections,", // used by NHibernate + "JetBrains.Annotations,", "LightInject.", // DI "LightInject,", "Lucene.", diff --git a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs index dab615da51..e88d1f4d01 100644 --- a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs +++ b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs @@ -17,48 +17,57 @@ namespace Umbraco.Cms.Core.Configuration public string ProviderName { get; } public string Name { get; } - private string ParseProvider(string connectionString) + private static bool IsSqlCe(DbConnectionStringBuilder builder) => (builder.TryGetValue("Data Source", out var ds) + || builder.TryGetValue("DataSource", out ds)) && + ds is string dataSource && + dataSource.EndsWith(".sdf"); + + private static bool IsSqlServer(DbConnectionStringBuilder builder) => + !string.IsNullOrEmpty(GetServer(builder)) && + ((builder.TryGetValue("Database", out var db) && db is string database && + !string.IsNullOrEmpty(database)) || + (builder.TryGetValue("AttachDbFileName", out var a) && a is string attachDbFileName && + !string.IsNullOrEmpty(attachDbFileName)) || + (builder.TryGetValue("Initial Catalog", out var i) && i is string initialCatalog && + !string.IsNullOrEmpty(initialCatalog))); + + private static string GetServer(DbConnectionStringBuilder builder) + { + if(builder.TryGetValue("Server", out var s) && s is string server) + { + return server; + } + + if ((builder.TryGetValue("Data Source", out var ds) + || builder.TryGetValue("DataSource", out ds)) && ds is string dataSource) + { + return dataSource; + } + + return ""; + } + + private static string ParseProvider(string connectionString) { if (string.IsNullOrEmpty(connectionString)) { return null; } - var builder = new DbConnectionStringBuilder + var builder = new DbConnectionStringBuilder {ConnectionString = connectionString}; + if (IsSqlCe(builder)) { - ConnectionString = connectionString - }; - - if ( - (builder.TryGetValue("Data Source", out var ds) - || builder.TryGetValue("DataSource", out ds)) && ds is string dataSource) - { - if (dataSource.EndsWith(".sdf")) - { - return Umbraco.Cms.Core.Constants.DbProviderNames.SqlCe; - } + return Constants.DbProviderNames.SqlCe; } - if (builder.TryGetValue("Server", out var s) && s is string server && !string.IsNullOrEmpty(server)) + if (IsSqlServer(builder)) { - if (builder.TryGetValue("Database", out var db) && db is string database && !string.IsNullOrEmpty(database)) - { - return Umbraco.Cms.Core.Constants.DbProviderNames.SqlServer; - } - - if (builder.TryGetValue("AttachDbFileName", out var a) && a is string attachDbFileName && !string.IsNullOrEmpty(attachDbFileName)) - { - return Umbraco.Cms.Core.Constants.DbProviderNames.SqlServer; - } - - if (builder.TryGetValue("Initial Catalog", out var i) && i is string initialCatalog && !string.IsNullOrEmpty(initialCatalog)) - { - return Umbraco.Cms.Core.Constants.DbProviderNames.SqlServer; - } + return Constants.DbProviderNames.SqlServer; } - throw new ArgumentException("Cannot determine provider name from connection string", nameof(connectionString)); + throw new ArgumentException("Cannot determine provider name from connection string", + nameof(connectionString)); } } } diff --git a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs b/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs index 6840d974aa..7bef36dba4 100644 --- a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs +++ b/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs @@ -1,10 +1,10 @@ ο»Ώ -namespace Umbraco.Core.Dashboards +namespace Umbraco.Cms.Core.Configuration { public class ContentDashboardSettings { private const string DefaultContentDashboardPath = "cms"; - + /// /// Gets a value indicating whether the content dashboard should be available to all users. /// diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index ab6a7e9396..f1a4f0643c 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -64,7 +64,7 @@ namespace Umbraco.Cms.Core.Configuration.Grid } // add manifest editors, skip duplicates - foreach (var gridEditor in _manifestParser.Manifest.GridEditors) + foreach (var gridEditor in _manifestParser.CombinedManifest.GridEditors) { if (editors.Contains(gridEditor) == false) editors.Add(gridEditor); } diff --git a/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs new file mode 100644 index 0000000000..054619d843 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs @@ -0,0 +1,27 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.ComponentModel; +using System.Net; + +namespace Umbraco.Cms.Core.Configuration.Models +{ + /// + /// Typed configuration options for basic authentication settings. + /// + [UmbracoOptions(Constants.Configuration.ConfigBasicAuth)] + public class BasicAuthSettings + { + private const bool StaticEnabled = false; + + /// + /// Gets or sets a value indicating whether to keep the user logged in. + /// + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; + + + public string[] AllowedIPs { get; set; } = Array.Empty(); + } +} diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs index bd92b4ba3c..7767d1dbdc 100644 --- a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs @@ -30,5 +30,7 @@ namespace Umbraco.Cms.Core.Configuration.Models /// [DefaultValue(StaticSqlPageSize)] public int SqlPageSize { get; set; } = StaticSqlPageSize; + + public bool UnPublishedContentCompression { get; set; } = false; } } diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index 48e08d596a..7d4dd45fb8 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -15,6 +15,7 @@ namespace Umbraco.Cms.Core.Configuration.Models internal const bool StaticHideDisabledUsersInBackOffice = false; internal const bool StaticAllowPasswordReset = true; internal const string StaticAuthCookieName = "UMB_UCONTEXT"; + internal const string StaticAllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\"; /// /// Gets or sets a value indicating whether to keep the user logged in. @@ -50,6 +51,12 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public bool UsernameIsEmail { get; set; } = true; + /// + /// Gets or sets the set of allowed characters for a username + /// + [DefaultValue(StaticAllowedUserNameCharacters)] + public string AllowedUserNameCharacters { get; set; } = StaticAllowedUserNameCharacters; + /// /// Gets or sets a value for the user password settings. /// diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index d596d3feec..0c7657d07e 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -46,6 +46,7 @@ public const string ConfigRuntimeMinification = ConfigPrefix + "RuntimeMinification"; public const string ConfigRuntimeMinificationVersion = ConfigRuntimeMinification + ":Version"; public const string ConfigSecurity = ConfigPrefix + "Security"; + public const string ConfigBasicAuth = ConfigPrefix + "BasicAuth"; public const string ConfigTours = ConfigPrefix + "Tours"; public const string ConfigTypeFinder = ConfigPrefix + "TypeFinder"; public const string ConfigWebRouting = ConfigPrefix + "WebRouting"; diff --git a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs index ff1223d983..a80c79a3ef 100644 --- a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs +++ b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs @@ -33,7 +33,7 @@ namespace Umbraco.Cms.Core.ContentApps // its dependencies too, and that can create cycles or other oddities var manifestParser = factory.GetRequiredService(); var ioHelper = factory.GetRequiredService(); - return base.CreateItems(factory).Concat(manifestParser.Manifest.ContentApps.Select(x => new ManifestContentAppFactory(x, ioHelper))); + return base.CreateItems(factory).Concat(manifestParser.CombinedManifest.ContentApps.Select(x => new ManifestContentAppFactory(x, ioHelper))); } } } diff --git a/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs b/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs index 55e840ad8e..348e81e383 100644 --- a/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs +++ b/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs @@ -1,4 +1,4 @@ -ο»Ώusing System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Dashboards // its dependencies too, and that can create cycles or other oddities var manifestParser = factory.GetRequiredService(); - var dashboardSections = Merge(base.CreateItems(factory), manifestParser.Manifest.Dashboards); + var dashboardSections = Merge(base.CreateItems(factory), manifestParser.CombinedManifest.Dashboards); return dashboardSections; } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index b5edfdf818..77902cc5c1 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -71,6 +71,7 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() + .AddUmbracoOptions() .AddUmbracoOptions(); return builder; diff --git a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs index 2666d81310..e0e5c1ab6a 100644 --- a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; - +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Install.InstallSteps { /// @@ -39,7 +39,7 @@ namespace Umbraco.Cms.Core.Install.InstallSteps var currentState = FormatGuidState(_runtimeState.CurrentMigrationState); var newState = FormatGuidState(_runtimeState.FinalMigrationState); - var newVersion = _umbracoVersion.SemanticVersion.ToString(); + var newVersion = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); var oldVersion = new SemVersion(_umbracoVersion.SemanticVersion.Major, 0, 0).ToString(); //TODO can we find the old version somehow? e.g. from current state var reportUrl = $"https://our.umbraco.com/contribute/releases/compare?from={oldVersion}&to={newVersion}¬es=1"; diff --git a/src/Umbraco.Core/Manifest/BundleOptions.cs b/src/Umbraco.Core/Manifest/BundleOptions.cs new file mode 100644 index 0000000000..810efb6c45 --- /dev/null +++ b/src/Umbraco.Core/Manifest/BundleOptions.cs @@ -0,0 +1,26 @@ +namespace Umbraco.Cms.Core.Manifest +{ + public enum BundleOptions + { + /// + /// The default bundling behavior for assets in the package folder. + /// + /// + /// The assets will be bundled with the typical packages bundle. + /// + Default = 0, + + /// + /// The assets in the package will not be processed at all and will all be requested as individual assets. + /// + /// + /// This will essentially be a bundle that has composite processing turned off for both debug and production. + /// + None = 1, + + /// + /// The packages assets will be processed as it's own separate bundle. (in debug, files will not be processed) + /// + Independent = 2 + } +} diff --git a/src/Umbraco.Core/Manifest/CompositePackageManifest.cs b/src/Umbraco.Core/Manifest/CompositePackageManifest.cs new file mode 100644 index 0000000000..939d635fc3 --- /dev/null +++ b/src/Umbraco.Core/Manifest/CompositePackageManifest.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Core.Manifest +{ + + /// + /// A package manifest made up of all combined manifests + /// + public class CompositePackageManifest + { + public CompositePackageManifest( + IReadOnlyList propertyEditors, + IReadOnlyList parameterEditors, + IReadOnlyList gridEditors, + IReadOnlyList contentApps, + IReadOnlyList dashboards, + IReadOnlyList sections, + IReadOnlyDictionary> scripts, + IReadOnlyDictionary> stylesheets) + { + PropertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + ParameterEditors = parameterEditors ?? throw new ArgumentNullException(nameof(parameterEditors)); + GridEditors = gridEditors ?? throw new ArgumentNullException(nameof(gridEditors)); + ContentApps = contentApps ?? throw new ArgumentNullException(nameof(contentApps)); + Dashboards = dashboards ?? throw new ArgumentNullException(nameof(dashboards)); + Sections = sections ?? throw new ArgumentNullException(nameof(sections)); + Scripts = scripts ?? throw new ArgumentNullException(nameof(scripts)); + Stylesheets = stylesheets ?? throw new ArgumentNullException(nameof(stylesheets)); + } + + /// + /// Gets or sets the property editors listed in the manifest. + /// + public IReadOnlyList PropertyEditors { get; } + + /// + /// Gets or sets the parameter editors listed in the manifest. + /// + public IReadOnlyList ParameterEditors { get; } + + /// + /// Gets or sets the grid editors listed in the manifest. + /// + public IReadOnlyList GridEditors { get; } + + /// + /// Gets or sets the content apps listed in the manifest. + /// + public IReadOnlyList ContentApps { get; } + + /// + /// Gets or sets the dashboards listed in the manifest. + /// + public IReadOnlyList Dashboards { get; } + + /// + /// Gets or sets the sections listed in the manifest. + /// + public IReadOnlyCollection Sections { get; } + + public IReadOnlyDictionary> Scripts { get; } + + public IReadOnlyDictionary> Stylesheets { get; } + } +} diff --git a/src/Umbraco.Core/Manifest/IManifestParser.cs b/src/Umbraco.Core/Manifest/IManifestParser.cs index dc3a19714e..09d3ccbe1c 100644 --- a/src/Umbraco.Core/Manifest/IManifestParser.cs +++ b/src/Umbraco.Core/Manifest/IManifestParser.cs @@ -4,13 +4,13 @@ namespace Umbraco.Cms.Core.Manifest { public interface IManifestParser { - string Path { get; set; } + string AppPluginsPath { get; set; } /// /// Gets all manifests, merged into a single manifest object. /// /// - PackageManifest Manifest { get; } + CompositePackageManifest CombinedManifest { get; } /// /// Parses a manifest. diff --git a/src/Umbraco.Core/Manifest/ManifestAssets.cs b/src/Umbraco.Core/Manifest/ManifestAssets.cs new file mode 100644 index 0000000000..ad5dfaa0f0 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestAssets.cs @@ -0,0 +1,17 @@ +ο»Ώusing System; +using System.Collections.Generic; + +namespace Umbraco.Cms.Core.Manifest +{ + public class ManifestAssets + { + public ManifestAssets(string packageName, IReadOnlyList assets) + { + PackageName = packageName ?? throw new ArgumentNullException(nameof(packageName)); + Assets = assets ?? throw new ArgumentNullException(nameof(assets)); + } + + public string PackageName { get; } + public IReadOnlyList Assets { get; } + } +} diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 56c69ebb15..753ec0613a 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -6,6 +6,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Manifest { + /// /// Represents the content of a package manifest. /// @@ -47,6 +48,8 @@ namespace Umbraco.Cms.Core.Manifest /// [IgnoreDataMember] public string Source { get; set; } + [DataMember(Name = "bundleOptions")] + public BundleOptions BundleOptions { get; set; } /// /// Gets or sets the scripts listed in the manifest. diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs new file mode 100644 index 0000000000..a9651e4f7a --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs @@ -0,0 +1,27 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models.ContentEditing +{ + /// + /// A model for retrieving multiple content types based on their keys. + /// + [DataContract(Name = "contentTypes", Namespace = "")] + public class ContentTypesByKeys + { + /// + /// ID of the parent of the content type. + /// + [DataMember(Name = "parentId")] + [Required] + public int ParentId { get; set; } + + /// + /// The id of every content type to get. + /// + [DataMember(Name = "contentTypeKeys")] + [Required] + public Guid[] ContentTypeKeys { get; set; } + } +} diff --git a/src/Umbraco.Core/Notifications/SendEmailNotification.cs b/src/Umbraco.Core/Notifications/SendEmailNotification.cs index 3c9caabb0e..4ca6dc80c0 100644 --- a/src/Umbraco.Core/Notifications/SendEmailNotification.cs +++ b/src/Umbraco.Core/Notifications/SendEmailNotification.cs @@ -7,5 +7,15 @@ namespace Umbraco.Cms.Core.Notifications public SendEmailNotification(NotificationEmailModel message) => Message = message; public NotificationEmailModel Message { get; } + + /// + /// Call to tell Umbraco that the email sending is handled. + /// + public void HandleEmail() => IsHandled = true; + + /// + /// Returns true if the email sending is handled. + /// + public bool IsHandled { get; private set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs index f0973f3157..61f31a85c9 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs @@ -9,7 +9,12 @@ namespace Umbraco.Cms.Core.PropertyEditors /// /// public interface IPropertyCacheCompression - { - bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias); + {/// + /// Whether a property on the content is/should be compressed + /// + /// The content + /// The property to compress or not + /// Whether this content is the published version + bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias, bool published); } } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs index 86bda9e799..a63029fc3d 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs @@ -4,6 +4,13 @@ namespace Umbraco.Cms.Core.PropertyEditors { public interface IPropertyCacheCompressionOptions { - bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor); + /// + /// Whether a property on the content is/should be compressed + /// + /// The content + /// The property to compress or not + /// The datatype of the property to compress or not + /// Whether this content is the published version + bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published); } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs index 7e01f6b1cb..f3adb6a05f 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs @@ -12,6 +12,14 @@ namespace Umbraco.Cms.Core.PropertyEditors public bool TryGetMediaPath(string propertyEditorAlias, object value, out string mediaPath) { + // We can't get a media path from a null value + // The value will be null when uploading a brand new image, since we try to get the "old path" which doesn't exist yet. + if (value is null) + { + mediaPath = null; + return false; + } + foreach(IMediaUrlGenerator generator in this) { if (generator.TryGetMediaPath(propertyEditorAlias, value, out var mp)) diff --git a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs index f2020ecbca..7e91d8e3ee 100644 --- a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs @@ -7,6 +7,6 @@ namespace Umbraco.Cms.Core.PropertyEditors /// public sealed class NoopPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions { - public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor) => false; + public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) => false; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs index 039de8cb6a..39647bb753 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public ParameterEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) : base(() => dataEditors .Where(x => (x.Type & EditorType.MacroParameter) > 0) - .Union(manifestParser.Manifest.PropertyEditors)) + .Union(manifestParser.CombinedManifest.PropertyEditors)) { } // note: virtual so it can be mocked diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs index 5216e3158f..3d6c0adff6 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs @@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors { /// @@ -16,13 +16,13 @@ namespace Umbraco.Core.PropertyEditors private readonly IPropertyCacheCompressionOptions _compressionOptions; private readonly IReadOnlyDictionary _contentTypes; private readonly PropertyEditorCollection _propertyEditors; - private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias), bool> _isCompressedCache; + private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias, bool published), bool> _isCompressedCache; public PropertyCacheCompression( IPropertyCacheCompressionOptions compressionOptions, IReadOnlyDictionary contentTypes, PropertyEditorCollection propertyEditors, - ConcurrentDictionary<(int, string), bool> compressedStoragePropertyEditorCache) + ConcurrentDictionary<(int, string, bool), bool> compressedStoragePropertyEditorCache) { _compressionOptions = compressionOptions; _contentTypes = contentTypes ?? throw new System.ArgumentNullException(nameof(contentTypes)); @@ -30,9 +30,9 @@ namespace Umbraco.Core.PropertyEditors _isCompressedCache = compressedStoragePropertyEditorCache; } - public bool IsCompressed(IReadOnlyContentBase content, string alias) + public bool IsCompressed(IReadOnlyContentBase content, string alias, bool published) { - var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias), x => + var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias, published), x => { if (!_contentTypes.TryGetValue(x.contentTypeId, out var ct)) return false; @@ -44,7 +44,7 @@ namespace Umbraco.Core.PropertyEditors if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) return false; - return _compressionOptions.IsCompressed(content, propertyType, propertyEditor); + return _compressionOptions.IsCompressed(content, propertyType, propertyEditor, published); }); return compressedStorage; diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs index 4b551d3257..1ddf150f93 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public PropertyEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) : base(() => dataEditors .Where(x => (x.Type & EditorType.PropertyValue) > 0) - .Union(manifestParser.Manifest.PropertyEditors)) + .Union(manifestParser.CombinedManifest.PropertyEditors)) { } public PropertyEditorCollection(DataEditorCollection dataEditors) diff --git a/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs new file mode 100644 index 0000000000..dc7f080acf --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs @@ -0,0 +1,20 @@ +ο»Ώusing Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.PropertyEditors +{ + /// + /// Compress large, non published text properties + /// + public class UnPublishedContentPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions + { + public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) + { + if (!published && propertyType.SupportsPublishing && propertyType.ValueStorageType == ValueStorageType.Ntext) + { + //Only compress non published content that supports publishing and the property is text + return true; + } + return false; + } + } +} diff --git a/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs b/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs index 0c5b2d7ba9..219d634261 100644 --- a/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs +++ b/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs @@ -18,7 +18,7 @@ namespace Umbraco.Cms.Core.Sections // its dependencies too, and that can create cycles or other oddities var manifestParser = factory.GetRequiredService(); - return base.CreateItems(factory).Concat(manifestParser.Manifest.Sections); + return base.CreateItems(factory).Concat(manifestParser.CombinedManifest.Sections); } } } diff --git a/src/Umbraco.Core/Services/IBasicAuthService.cs b/src/Umbraco.Core/Services/IBasicAuthService.cs new file mode 100644 index 0000000000..84173a629a --- /dev/null +++ b/src/Umbraco.Core/Services/IBasicAuthService.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace Umbraco.Cms.Core.Services +{ + public interface IBasicAuthService + { + bool IsBasicAuthEnabled(); + bool IsIpAllowListed(IPAddress clientIpAddress); + } +} diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs index ebb09f62c2..e0d10668ea 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs @@ -34,17 +34,6 @@ namespace Umbraco.Extensions public static string Localize(this ILocalizedTextService manager, string area, string alias, string[] tokens) => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens)); - /// - /// Localize using the current thread culture - /// - /// - /// - /// - /// - /// - public static string Localize(this ILocalizedTextService manager, string area, string alias, IDictionary tokens = null) - => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, tokens); - /// /// Localize a key without any variables /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 2a30778d80..f43ac533a7 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -17,7 +17,7 @@ - + @@ -59,4 +59,8 @@ + + + + diff --git a/src/Umbraco.Core/WebAssets/BundlingOptions.cs b/src/Umbraco.Core/WebAssets/BundlingOptions.cs new file mode 100644 index 0000000000..6a3c0b9bd1 --- /dev/null +++ b/src/Umbraco.Core/WebAssets/BundlingOptions.cs @@ -0,0 +1,44 @@ +using System; + +namespace Umbraco.Cms.Core.WebAssets +{ + public struct BundlingOptions : IEquatable + { + public static BundlingOptions OptimizedAndComposite => new BundlingOptions(true, true); + public static BundlingOptions OptimizedNotComposite => new BundlingOptions(true, false); + public static BundlingOptions NotOptimizedNotComposite => new BundlingOptions(false, false); + public static BundlingOptions NotOptimizedAndComposite => new BundlingOptions(false, true); + + public BundlingOptions(bool optimizeOutput = true, bool enabledCompositeFiles = true) + { + OptimizeOutput = optimizeOutput; + EnabledCompositeFiles = enabledCompositeFiles; + } + + /// + /// If true, the files in the bundle will be minified + /// + public bool OptimizeOutput { get; } + + /// + /// If true, the files in the bundle will be combined, if false the files + /// will be served as individual files. + /// + public bool EnabledCompositeFiles { get; } + + public override bool Equals(object obj) => obj is BundlingOptions options && Equals(options); + public bool Equals(BundlingOptions other) => OptimizeOutput == other.OptimizeOutput && EnabledCompositeFiles == other.EnabledCompositeFiles; + + public override int GetHashCode() + { + int hashCode = 2130304063; + hashCode = hashCode * -1521134295 + OptimizeOutput.GetHashCode(); + hashCode = hashCode * -1521134295 + EnabledCompositeFiles.GetHashCode(); + return hashCode; + } + + public static bool operator ==(BundlingOptions left, BundlingOptions right) => left.Equals(right); + + public static bool operator !=(BundlingOptions left, BundlingOptions right) => !(left == right); + } +} diff --git a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs index f287c722ad..7ca1cd883b 100644 --- a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs +++ b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs @@ -25,7 +25,7 @@ namespace Umbraco.Cms.Core.WebAssets /// /// Thrown if any of the paths specified are not absolute /// - void CreateCssBundle(string bundleName, bool optimizeOutput, params string[] filePaths); + void CreateCssBundle(string bundleName, BundlingOptions bundleOptions, params string[] filePaths); /// /// Renders the html link tag for the bundle @@ -48,7 +48,7 @@ namespace Umbraco.Cms.Core.WebAssets /// /// Thrown if any of the paths specified are not absolute /// - void CreateJsBundle(string bundleName, bool optimizeOutput, params string[] filePaths); + void CreateJsBundle(string bundleName, BundlingOptions bundleOptions, params string[] filePaths); /// /// Renders the html script tag for the bundle diff --git a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj index 5dd6d9bde6..7473ce1bab 100644 --- a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj +++ b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj @@ -30,11 +30,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - 3.5.4 - runtime; build; native; contentfiles; analyzers - all - all diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs index 05dba2cc0f..71ea85d80f 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs @@ -2,12 +2,8 @@ using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Infrastructure.Sync; using Umbraco.Extensions; @@ -40,49 +36,67 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// The type of the server registrar. /// The builder. - public static void SetServerRegistrar(this IUmbracoBuilder builder) + public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder) where T : class, IServerRoleAccessor - => builder.Services.AddUnique(); + { + builder.Services.AddUnique(); + return builder; + } /// /// Sets the server registrar. /// /// The builder. /// A function creating a server registrar. - public static void SetServerRegistrar(this IUmbracoBuilder builder, Func factory) - => builder.Services.AddUnique(factory); + public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } /// /// Sets the server registrar. /// /// The builder. /// A server registrar. - public static void SetServerRegistrar(this IUmbracoBuilder builder, IServerRoleAccessor registrar) - => builder.Services.AddUnique(registrar); + public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, IServerRoleAccessor registrar) + { + builder.Services.AddUnique(registrar); + return builder; + } /// /// Sets the server messenger. /// /// The type of the server registrar. /// The builder. - public static void SetServerMessenger(this IUmbracoBuilder builder) + public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder) where T : class, IServerMessenger - => builder.Services.AddUnique(); + { + builder.Services.AddUnique(); + return builder; + } /// /// Sets the server messenger. /// /// The builder. /// A function creating a server messenger. - public static void SetServerMessenger(this IUmbracoBuilder builder, Func factory) - => builder.Services.AddUnique(factory); + public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } /// /// Sets the server messenger. /// /// The builder. /// A server messenger. - public static void SetServerMessenger(this IUmbracoBuilder builder, IServerMessenger registrar) - => builder.Services.AddUnique(registrar); + public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, IServerMessenger registrar) + { + builder.Services.AddUnique(registrar); + return builder; + } } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 8b34289c9c..e535b399e4 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -40,6 +40,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs index cbbaa6a3e0..b311b1f0da 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs @@ -1,6 +1,5 @@ using System; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.IO; @@ -21,10 +20,11 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// The type of the factory. /// The builder. - public static void SetCultureDictionaryFactory(this IUmbracoBuilder builder) + public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder) where T : class, ICultureDictionaryFactory { builder.Services.AddUnique(); + return builder; } /// @@ -32,9 +32,10 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// The builder. /// A function creating a culture dictionary factory. - public static void SetCultureDictionaryFactory(this IUmbracoBuilder builder, Func factory) + public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder, Func factory) { builder.Services.AddUnique(factory); + return builder; } /// @@ -42,9 +43,10 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// The builder. /// A factory. - public static void SetCultureDictionaryFactory(this IUmbracoBuilder builder, ICultureDictionaryFactory factory) + public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder, ICultureDictionaryFactory factory) { builder.Services.AddUnique(factory); + return builder; } /// @@ -52,10 +54,11 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// The type of the factory. /// The builder. - public static void SetPublishedContentModelFactory(this IUmbracoBuilder builder) + public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder) where T : class, IPublishedModelFactory { builder.Services.AddUnique(); + return builder; } /// @@ -63,9 +66,10 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// The builder. /// A function creating a published content model factory. - public static void SetPublishedContentModelFactory(this IUmbracoBuilder builder, Func factory) + public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder, Func factory) { builder.Services.AddUnique(factory); + return builder; } /// @@ -73,9 +77,10 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// The builder. /// A published content model factory. - public static void SetPublishedContentModelFactory(this IUmbracoBuilder builder, IPublishedModelFactory factory) + public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder, IPublishedModelFactory factory) { builder.Services.AddUnique(factory); + return builder; } /// @@ -83,10 +88,11 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// The type of the short string helper. /// The builder. - public static void SetShortStringHelper(this IUmbracoBuilder builder) + public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder) where T : class, IShortStringHelper { builder.Services.AddUnique(); + return builder; } /// @@ -94,9 +100,10 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// The builder. /// A function creating a short string helper. - public static void SetShortStringHelper(this IUmbracoBuilder builder, Func factory) + public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, Func factory) { builder.Services.AddUnique(factory); + return builder; } /// @@ -104,9 +111,10 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// A builder. /// A short string helper. - public static void SetShortStringHelper(this IUmbracoBuilder builder, IShortStringHelper helper) + public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, IShortStringHelper helper) { builder.Services.AddUnique(helper); + return builder; } /// @@ -114,19 +122,23 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// A builder. /// Factory method to create an IFileSystem implementation used in the MediaFileManager - public static void SetMediaFileSystem(this IUmbracoBuilder builder, - Func filesystemFactory) => builder.Services.AddUnique( - provider => - { - IFileSystem filesystem = filesystemFactory(provider); - // We need to use the Filesystems to create a shadow wrapper, - // because shadow wrapper requires the IsScoped delegate from the FileSystems. - // This is used by the scope provider when taking control of the filesystems. - FileSystems fileSystems = provider.GetRequiredService(); - IFileSystem shadow = fileSystems.CreateShadowWrapper(filesystem, "media"); + public static IUmbracoBuilder SetMediaFileSystem(this IUmbracoBuilder builder, + Func filesystemFactory) + { + builder.Services.AddUnique( + provider => + { + IFileSystem filesystem = filesystemFactory(provider); + // We need to use the Filesystems to create a shadow wrapper, + // because shadow wrapper requires the IsScoped delegate from the FileSystems. + // This is used by the scope provider when taking control of the filesystems. + FileSystems fileSystems = provider.GetRequiredService(); + IFileSystem shadow = fileSystems.CreateShadowWrapper(filesystem, "media"); - return provider.CreateInstance(shadow); - }); + return provider.CreateInstance(shadow); + }); + return builder; + } /// /// Register FileSystems with a method to configure the . @@ -135,7 +147,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// Method that configures the . /// Throws exception if is null. /// Throws exception if full path can't be resolved successfully. - public static void ConfigureFileSystems(this IUmbracoBuilder builder, + public static IUmbracoBuilder ConfigureFileSystems(this IUmbracoBuilder builder, Action configure) { if (configure == null) @@ -150,6 +162,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection configure(provider, fileSystems); return fileSystems; }); + return builder; } /// @@ -157,10 +170,11 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// The type of the log viewer. /// The builder. - public static void SetLogViewer(this IUmbracoBuilder builder) + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder) where T : class, ILogViewer { builder.Services.AddUnique(); + return builder; } /// @@ -168,19 +182,21 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// /// The builder. /// A function creating a log viewer. - public static void SetLogViewer(this IUmbracoBuilder builder, Func factory) + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, Func factory) { builder.Services.AddUnique(factory); + return builder; } /// /// Sets the log viewer. /// /// A builder. - /// A log viewer. - public static void SetLogViewer(this IUmbracoBuilder builder, ILogViewer viewer) + /// A log viewer. + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, ILogViewer viewer) { builder.Services.AddUnique(viewer); + return builder; } } } diff --git a/src/Umbraco.Infrastructure/EmailSender.cs b/src/Umbraco.Infrastructure/EmailSender.cs index 72daad7de1..20c48a3b04 100644 --- a/src/Umbraco.Infrastructure/EmailSender.cs +++ b/src/Umbraco.Infrastructure/EmailSender.cs @@ -3,6 +3,7 @@ using System.Net.Mail; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; @@ -23,12 +24,14 @@ namespace Umbraco.Cms.Infrastructure private readonly IEventAggregator _eventAggregator; private readonly GlobalSettings _globalSettings; private readonly bool _notificationHandlerRegistered; + private readonly ILogger _logger; - public EmailSender(IOptions globalSettings, IEventAggregator eventAggregator) + public EmailSender( + ILogger logger, + IOptions globalSettings, + IEventAggregator eventAggregator) : this(globalSettings, eventAggregator, null) - { - - } + => _logger = logger; public EmailSender(IOptions globalSettings, IEventAggregator eventAggregator, INotificationHandler handler) { @@ -49,39 +52,47 @@ namespace Umbraco.Cms.Infrastructure private async Task SendAsyncInternal(EmailMessage message, bool enableNotification) { + if (enableNotification) + { + var notification = new SendEmailNotification(message.ToNotificationEmail(_globalSettings.Smtp?.From)); + await _eventAggregator.PublishAsync(notification); + + // if a handler handled sending the email then don't continue. + if (notification.IsHandled) + { + _logger.LogDebug("The email sending for {Subject} was handled by a notification handler", notification.Message.Subject); + return; + } + } + if (_globalSettings.IsSmtpServerConfigured == false) { - if (enableNotification) - { - await _eventAggregator.PublishAsync( - new SendEmailNotification(message.ToNotificationEmail(_globalSettings.Smtp?.From))); - } + _logger.LogDebug("Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.", message.Subject); return; } - using (var client = new SmtpClient()) + using var client = new SmtpClient(); + + await client.ConnectAsync(_globalSettings.Smtp.Host, + _globalSettings.Smtp.Port, + (MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); + + if (!(_globalSettings.Smtp.Username is null && _globalSettings.Smtp.Password is null)) { - await client.ConnectAsync(_globalSettings.Smtp.Host, - _globalSettings.Smtp.Port, - (MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); - - if (!(_globalSettings.Smtp.Username is null && _globalSettings.Smtp.Password is null)) - { - await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); - } - - var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From); - if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network) - { - await client.SendAsync(mailMessage); - } - else - { - client.Send(mailMessage); - } - - await client.DisconnectAsync(true); + await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); } + + var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From); + if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network) + { + await client.SendAsync(mailMessage); + } + else + { + client.Send(mailMessage); + } + + await client.DisconnectAsync(true); } /// diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index 3cc19b364f..2e1a2bcd4d 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -1,5 +1,4 @@ -ο»Ώusing System; -using System.IO; +using System; using Microsoft.Extensions.Configuration; using Serilog; using Serilog.Events; @@ -15,17 +14,6 @@ namespace Umbraco.Cms.Core.Logging.Serilog { public global::Serilog.ILogger SerilogLog { get; } - /// - /// Initialize a new instance of the class with a configuration file. - /// - /// - public SerilogLogger(FileInfo logConfigFile) - { - SerilogLog = new LoggerConfiguration() - .ReadFrom.AppSettings(filePath: logConfigFile.FullName) - .CreateLogger(); - } - public SerilogLogger(LoggerConfiguration logConfig) { //Configure Serilog static global logger with config passed in diff --git a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs index 7d98a19091..529a148093 100644 --- a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs +++ b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs @@ -62,22 +62,22 @@ namespace Umbraco.Cms.Core.Manifest /// /// Initializes a new instance of the class. /// - private ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, ManifestFilterCollection filters, string path, ILogger logger, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment) + private ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, ManifestFilterCollection filters, string appPluginsPath, ILogger logger, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment) { if (appCaches == null) throw new ArgumentNullException(nameof(appCaches)); _cache = appCaches.RuntimeCache; _validators = validators ?? throw new ArgumentNullException(nameof(validators)); _filters = filters ?? throw new ArgumentNullException(nameof(filters)); - if (path == null) throw new ArgumentNullException(nameof(path)); - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(path)); + if (appPluginsPath == null) throw new ArgumentNullException(nameof(appPluginsPath)); + if (string.IsNullOrWhiteSpace(appPluginsPath)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(appPluginsPath)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _ioHelper = ioHelper; _hostingEnvironment = hostingEnvironment; - Path = path; + AppPluginsPath = appPluginsPath; } - public string Path + public string AppPluginsPath { get => _path; set => _path = value.StartsWith("~/") ? _hostingEnvironment.MapPathContentRoot(value) : value; @@ -87,11 +87,12 @@ namespace Umbraco.Cms.Core.Manifest /// Gets all manifests, merged into a single manifest object. /// /// - public PackageManifest Manifest - => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => + public CompositePackageManifest CombinedManifest + => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => { - var manifests = GetManifests(); + IEnumerable manifests = GetManifests(); return MergeManifests(manifests); + }, new TimeSpan(0, 4, 0)); /// @@ -130,10 +131,10 @@ namespace Umbraco.Cms.Core.Manifest /// /// Merges all manifests into one. /// - private static PackageManifest MergeManifests(IEnumerable manifests) + private static CompositePackageManifest MergeManifests(IEnumerable manifests) { - var scripts = new HashSet(); - var stylesheets = new HashSet(); + var scripts = new Dictionary>(); + var stylesheets = new Dictionary>(); var propertyEditors = new List(); var parameterEditors = new List(); var gridEditors = new List(); @@ -141,10 +142,28 @@ namespace Umbraco.Cms.Core.Manifest var dashboards = new List(); var sections = new List(); - foreach (var manifest in manifests) + foreach (PackageManifest manifest in manifests) { - if (manifest.Scripts != null) foreach (var script in manifest.Scripts) scripts.Add(script); - if (manifest.Stylesheets != null) foreach (var stylesheet in manifest.Stylesheets) stylesheets.Add(stylesheet); + if (manifest.Scripts != null) + { + if (!scripts.TryGetValue(manifest.BundleOptions, out List scriptsPerBundleOption)) + { + scriptsPerBundleOption = new List(); + scripts[manifest.BundleOptions] = scriptsPerBundleOption; + } + scriptsPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Scripts)); + } + + if (manifest.Stylesheets != null) + { + if (!stylesheets.TryGetValue(manifest.BundleOptions, out List stylesPerBundleOption)) + { + stylesPerBundleOption = new List(); + stylesheets[manifest.BundleOptions] = stylesPerBundleOption; + } + stylesPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Stylesheets)); + } + if (manifest.PropertyEditors != null) propertyEditors.AddRange(manifest.PropertyEditors); if (manifest.ParameterEditors != null) parameterEditors.AddRange(manifest.ParameterEditors); if (manifest.GridEditors != null) gridEditors.AddRange(manifest.GridEditors); @@ -153,17 +172,15 @@ namespace Umbraco.Cms.Core.Manifest if (manifest.Sections != null) sections.AddRange(manifest.Sections.DistinctBy(x => x.Alias.ToLowerInvariant())); } - return new PackageManifest - { - Scripts = scripts.ToArray(), - Stylesheets = stylesheets.ToArray(), - PropertyEditors = propertyEditors.ToArray(), - ParameterEditors = parameterEditors.ToArray(), - GridEditors = gridEditors.ToArray(), - ContentApps = contentApps.ToArray(), - Dashboards = dashboards.ToArray(), - Sections = sections.ToArray() - }; + return new CompositePackageManifest( + propertyEditors, + parameterEditors, + gridEditors, + contentApps, + dashboards, + sections, + scripts.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value), + stylesheets.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value)); } // gets all manifest files (recursively) diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs index 5f699d66d5..ae2098b75a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs @@ -9,6 +9,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table public class CreateTableOfDtoBuilder : IExecutableBuilder { private readonly IMigrationContext _context; + + // TODO: This doesn't do anything. private readonly DatabaseType[] _supportedDatabaseTypes; public CreateTableOfDtoBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 83adbc6e8a..a7cf92e2a9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1,4 +1,4 @@ -ο»Ώusing System; +using System; using System.Linq; using Microsoft.Extensions.Logging; using NPoco; @@ -151,7 +151,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install //New UDI pickers with newer Ids _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picke (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Multi URL Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 954ec0bf0a..4eb9c5ae38 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -211,8 +211,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade Merge() .To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}") .To("{4695D0C9-0729-4976-985B-048D503665D8}") - - // to 9.0.0 + .To("{5C424554-A32D-4852-8ED1-A13508187901}") + // to 9.0.0 RC .With() .To("{22D801BA-A1FF-4539-BFCC-2139B55594F8}") .To("{50A43237-A6F4-49E2-A7A6-5DAD65C84669}") @@ -222,6 +222,17 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade //FINAL .As("{5060F3D2-88BE-4D30-8755-CF51F28EAD12}"); + + // TO 9.0.0 + + + // This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step. + // - 8.15 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8} + // - 8.15 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901} + // - 9.0 RC1 - Current state: {5060F3D2-88BE-4D30-8755-CF51F28EAD12} + To("{622E5172-42E1-4662-AD80-9504AF5A4E53}"); + + To("{10F7BB61-C550-426B-830B-7F954F689CDF}"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs index 2f0caa4939..a63cb7c1e5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs @@ -1,6 +1,9 @@ +using NPoco; using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 { @@ -14,12 +17,46 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 protected override void Migrate() { + // allow null for the `data` field + if (DatabaseType.IsSqlCe()) + { + // SQLCE does not support altering NTEXT, so we have to jump through some hoops to do it + // All column ordering must remain the same as what is defined in the DTO so we need to create a temp table, + // drop orig and then re-create/copy. + Create.Table(withoutKeysAndIndexes: true).Do(); + Execute.Sql($"INSERT INTO [{TempTableName}] SELECT nodeId, published, data, rv FROM [{Constants.DatabaseSchema.Tables.NodeData}]").Do(); + Delete.Table(Constants.DatabaseSchema.Tables.NodeData).Do(); + Create.Table().Do(); + Execute.Sql($"INSERT INTO [{Constants.DatabaseSchema.Tables.NodeData}] SELECT nodeId, published, data, rv, NULL FROM [{TempTableName}]").Do(); + } + else + { + AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); + } + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "dataRaw"); + } - // allow null - AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); + private const string TempTableName = Constants.DatabaseSchema.TableNamePrefix + "cms" + "ContentNuTEMP"; + + [TableName(TempTableName)] + [ExplicitColumns] + private class ContentNuDtoTemp + { + [Column("nodeId")] + public int NodeId { get; set; } + + [Column("published")] + public bool Published { get; set; } + + [Column("data")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string Data { get; set; } + + [Column("rv")] + public long Rv { get; set; } } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs new file mode 100644 index 0000000000..868343374d --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs @@ -0,0 +1,19 @@ +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 +{ + public class UpdateCmsPropertyGroupIdSeed : MigrationBase + { + public UpdateCmsPropertyGroupIdSeed(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + if (DatabaseType.IsSqlCe()) + { + Database.Execute(Sql("ALTER TABLE [cmsPropertyTypeGroup] ALTER COLUMN [id] IDENTITY (56,1)")); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs index ef29207093..f350ed633c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 { + public class ExternalLoginTableIndexes : MigrationBase { public ExternalLoginTableIndexes(IMigrationContext context) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs new file mode 100644 index 0000000000..5efb914eb7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs @@ -0,0 +1,59 @@ +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +{ + /// + /// Fixes up the original for post RC release to ensure that + /// the correct indexes are applied. + /// + public class ExternalLoginTableIndexesFixup : MigrationBase + { + public ExternalLoginTableIndexesFixup(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + var indexName1 = "IX_" + ExternalLoginDto.TableName + "_LoginProvider"; + var indexName2 = "IX_" + ExternalLoginDto.TableName + "_ProviderKey"; + + if (IndexExists(indexName1)) + { + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexName1).OnTable(ExternalLoginDto.TableName).Do(); + } + + if (IndexExists(indexName2)) + { + // drop since it's using a column we're about to modify + Delete.Index(indexName2).OnTable(ExternalLoginDto.TableName).Do(); + } + + // then fixup the length of the loginProvider column + AlterColumn(ExternalLoginDto.TableName, "loginProvider"); + + // create it with the correct definition + Create + .Index(indexName1) + .OnTable(ExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("userId").Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); + + // re-create the original + Create + .Index(indexName2) + .OnTable(ExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("providerKey").Ascending() + .WithOptions() + .NonClustered() + .Do(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs index 14cc8e31f8..9e5101550a 100644 --- a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs +++ b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs @@ -1,7 +1,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -namespace Umbraco.Core.Models +namespace Umbraco.Cms.Core.Models { /// /// Represents a media item with local crops. diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs index 9084cc902f..a5186963ab 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs @@ -98,6 +98,13 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building sb.AppendFormat("{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n", tabs, ApiVersion.Current.Version); } + // writes an attribute that specifies that an output may be null. + // (useful for consuming projects with nullable reference types enabled) + private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) + { + sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs, isReturn ? "return: " : ""); + } + private void WriteContentType(StringBuilder sb, TypeModel type) { string sep; @@ -185,9 +192,11 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", itemType); WriteGeneratedCodeAttribute(sb, "\t\t"); + WriteMaybeNullAttribute(sb, "\t\t", true); sb.Append("\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)\n"); sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);\n"); WriteGeneratedCodeAttribute(sb, "\t\t"); + WriteMaybeNullAttribute(sb, "\t\t", true); sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector)\n", type.ClrName); sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);\n"); @@ -305,6 +314,8 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building } WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + WriteMaybeNullAttribute(sb, "\t\t"); sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); if (mixinStatic) @@ -349,6 +360,8 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + WriteMaybeNullAttribute(sb, "\t\t", true); sb.Append("\t\tpublic static "); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0}(I{1} that, IPublishedValueFallback publishedValueFallback) => that.Value", @@ -404,6 +417,9 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building if (!string.IsNullOrWhiteSpace(property.Name)) sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + WriteMaybeNullAttribute(sb, "\t\t"); + sb.Append("\t\t"); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0} {{ get; }}\n", diff --git a/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs index 4ce6abe7a0..a5524b44de 100644 --- a/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs @@ -13,10 +13,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { - var recordsA = records.ToArray(); - if (recordsA.Length == 0) return 0; + if (!records.Any()) return 0; - return BulkInsertRecordsWithCommands(database, recordsA); + return BulkInsertRecordsWithCommands(database, records.ToArray()); } /// diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs new file mode 100644 index 0000000000..00be5c51ab --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +{ + /// + /// Allows for specifying custom DB types that are not natively mapped. + /// + public struct SpecialDbType : IEquatable + { + private readonly string _dbType; + + public SpecialDbType(string dbType) + { + if (string.IsNullOrWhiteSpace(dbType)) + { + throw new ArgumentException($"'{nameof(dbType)}' cannot be null or whitespace.", nameof(dbType)); + } + + _dbType = dbType; + } + + public SpecialDbType(SpecialDbTypes specialDbTypes) + => _dbType = specialDbTypes.ToString(); + + public static SpecialDbType NTEXT { get; } = new SpecialDbType(SpecialDbTypes.NTEXT); + public static SpecialDbType NCHAR { get; } = new SpecialDbType(SpecialDbTypes.NCHAR); + public static SpecialDbType NVARCHARMAX { get; } = new SpecialDbType(SpecialDbTypes.NVARCHARMAX); + + public override bool Equals(object obj) => obj is SpecialDbType types && Equals(types); + public bool Equals(SpecialDbType other) => _dbType == other._dbType; + public override int GetHashCode() => 1038481724 + EqualityComparer.Default.GetHashCode(_dbType); + + public override string ToString() => _dbType.ToString(); + + // Make this directly castable to string + public static implicit operator string(SpecialDbType dbType) => dbType.ToString(); + + // direct equality operators with SpecialDbTypes enum + public static bool operator ==(SpecialDbTypes x, SpecialDbType y) => x.ToString() == y; + public static bool operator !=(SpecialDbTypes x, SpecialDbType y) => x.ToString() != y; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs index 158a7ccb9b..d7fd2ff34f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs @@ -1,4 +1,4 @@ -ο»Ώusing System; +using System; namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations { @@ -12,13 +12,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations public class SpecialDbTypeAttribute : Attribute { public SpecialDbTypeAttribute(SpecialDbTypes databaseType) - { - DatabaseType = databaseType; - } + => DatabaseType = new SpecialDbType(databaseType); + + public SpecialDbTypeAttribute(string databaseType) + => DatabaseType = new SpecialDbType(databaseType); /// - /// Gets or sets the for this column + /// Gets or sets the for this column /// - public SpecialDbTypes DatabaseType { get; private set; } + public SpecialDbType DatabaseType { get; private set; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs index 9d07395743..d867d6f682 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs @@ -1,13 +1,12 @@ ο»Ώnamespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations { /// - /// Enum with the two special types that has to be supported because - /// of the current umbraco db schema. + /// Known special DB types required for Umbraco. /// public enum SpecialDbTypes { NTEXT, NCHAR, - NVARCHARMAX + NVARCHARMAX, } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs index 2c22863ae5..dee560a40d 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs @@ -1,4 +1,4 @@ -ο»Ώusing System; +using System; using System.Data; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; @@ -12,9 +12,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions //When DbType isn't set explicitly the Type will be used to find the right DbType in the SqlSyntaxProvider. //This type is typically used as part of an initial table creation public Type PropertyType { get; set; } - //Only used for special cases as part of an initial table creation - public bool HasSpecialDbType { get; set; } - public SpecialDbTypes DbType { get; set; } + + /// + /// Used for column types that cannot be natively mapped. + /// + public SpecialDbType? CustomDbType { get; set; } + public virtual int Seeding { get; set; } public virtual int Size { get; set; } public virtual int Precision { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 407672c995..34ad767b04 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -1,4 +1,4 @@ -ο»Ώusing System; +using System; using System.Linq; using System.Reflection; using NPoco; @@ -75,8 +75,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions var databaseTypeAttribute = propertyInfo.FirstAttribute(); if (databaseTypeAttribute != null) { - definition.HasSpecialDbType = true; - definition.DbType = databaseTypeAttribute.DatabaseType; + definition.CustomDbType = databaseTypeAttribute.DatabaseType; } else { diff --git a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs index 797400b7cc..942368f5cb 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -24,15 +24,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence IEnumerable providerSpecificMapperFactories) { _getFactory = getFactory; - _embeddedDatabaseCreators = embeddedDatabaseCreators.ToDictionary(x=>x.ProviderName); - _syntaxProviders = syntaxProviders.ToDictionary(x=>x.ProviderName); - _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x=>x.ProviderName); - _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x=>x.ProviderName); + _embeddedDatabaseCreators = embeddedDatabaseCreators.ToDictionary(x => x.ProviderName); + _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName); + _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName); + _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x => x.ProviderName); } public DbProviderFactory CreateFactory(string providerName) { - if (string.IsNullOrEmpty(providerName)) return null; + if (string.IsNullOrEmpty(providerName)) + return null; return _getFactory(providerName); } @@ -40,7 +41,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) { - if(!_syntaxProviders.TryGetValue(providerName, out var result)) + if (!_syntaxProviders.TryGetValue(providerName, out var result)) { throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); } @@ -51,7 +52,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) { - if(!_bulkSqlInsertProviders.TryGetValue(providerName, out var result)) + if (!_bulkSqlInsertProviders.TryGetValue(providerName, out var result)) { return new BasicBulkSqlInsertProvider(); } @@ -61,7 +62,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence public void CreateDatabase(string providerName) { - if(_embeddedDatabaseCreators.TryGetValue(providerName, out var creator)) + if (_embeddedDatabaseCreators.TryGetValue(providerName, out var creator)) { creator.Create(); } @@ -69,7 +70,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence public NPocoMapperCollection ProviderSpecificMappers(string providerName) { - if(_providerSpecificMapperFactories.TryGetValue(providerName, out var mapperFactory)) + if (_providerSpecificMapperFactories.TryGetValue(providerName, out var mapperFactory)) { return mapperFactory.Mappers; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs index 3fc65b28a5..69bf1b837e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs @@ -24,12 +24,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos [Index(IndexTypes.NonClustered)] public int UserId { get; set; } + /// + /// Used to store the name of the provider (i.e. Facebook, Google) + /// [Column("loginProvider")] - [Length(4000)] // TODO: This value seems WAY too high, this is just a name + [Length(400)] [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_LoginProvider")] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userId", Name = "IX_" + TableName + "_LoginProvider")] public string LoginProvider { get; set; } + /// + /// Stores the key the provider uses to lookup the login + /// [Column("providerKey")] [Length(4000)] [NullSetting(NullSetting = NullSettings.NotNull)] diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs index 5f3b2a9572..1fbc1b734c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs @@ -12,7 +12,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos internal class PropertyTypeGroupDto { [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 12)] + [PrimaryKeyColumn(IdentitySeed = 56)] public int Id { get; set; } [Column("contenttypeNodeId")] diff --git a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs b/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs index 71e22a4837..c3875d3770 100644 --- a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs +++ b/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs @@ -1,4 +1,4 @@ -ο»Ώusing System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -6,6 +6,7 @@ using NPoco; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence { @@ -40,9 +41,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence _tableDefinition = DefinitionFactory.GetTableDefinition(pd.Type, sqlSyntaxProvider); if (_tableDefinition == null) throw new InvalidOperationException("No table definition found for type " + pd.Type); - // only real columns, exclude result columns + // only real columns, exclude result/computed columns + // Like NPoco does: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L59 _readerColumns = pd.Columns - .Where(x => x.Value.ResultColumn == false) + .Where(x => x.Value.ResultColumn == false && x.Value.ComputedColumn == false) .Select(x => x.Value) .ToArray(); @@ -68,22 +70,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence foreach (var col in _columnDefinitions) { SqlDbType sqlDbType; - if (col.HasSpecialDbType) + if (col.CustomDbType.HasValue) { //get the SqlDbType from the 'special type' - switch (col.DbType) + switch (col.CustomDbType) { - case SpecialDbTypes.NTEXT: + case var x when x == SpecialDbType.NTEXT: sqlDbType = SqlDbType.NText; break; - case SpecialDbTypes.NCHAR: + case var x when x == SpecialDbType.NCHAR: sqlDbType = SqlDbType.NChar; break; - case SpecialDbTypes.NVARCHARMAX: + case var x when x == SpecialDbType.NVARCHARMAX: sqlDbType = SqlDbType.NVarChar; break; default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException("The custom DB type " + col.CustomDbType + " is not supported for bulk import statements."); } } else if (col.Type.HasValue) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 0dd6e2d43c..71bc5b8d33 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -432,11 +432,12 @@ ORDER BY colName"; { var list = new List { - "DELETE FROM umbracoUser2UserGroup WHERE userId = @id", - "DELETE FROM umbracoUser2NodeNotify WHERE userId = @id", - "DELETE FROM umbracoUserStartNode WHERE userId = @id", - "DELETE FROM umbracoUser WHERE id = @id", - "DELETE FROM umbracoExternalLogin WHERE id = @id" + $"DELETE FROM {Constants.DatabaseSchema.Tables.UserLogin} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User2UserGroup} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User2NodeNotify} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.UserStartNode} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User} WHERE id = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.ExternalLogin} WHERE id = @id" }; return list; } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs index 8b80010335..d67c97f2c4 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs @@ -39,6 +39,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence /// The number of records that were inserted. private int BulkInsertRecordsSqlServer(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) { + // TODO: The main reason this exists is because the NPoco InsertBulk method doesn't return the number of items. + // It is worth investigating the performance of this vs NPoco's because we use a custom BulkDataReader + // which in theory should be more efficient than NPocos way of building up an in-memory DataTable. + // create command against the original database.Connection using (var command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty)) { @@ -50,7 +54,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence var syntax = database.SqlContext.SqlSyntax as SqlServerSyntaxProvider; if (syntax == null) throw new NotSupportedException("SqlSyntax must be SqlServerSyntaxProvider."); - using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) { BulkCopyTimeout = 10000, DestinationTableName = tableName }) + using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) + { + BulkCopyTimeout = 0, // 0 = no bulk copy timeout. If a timeout occurs it will be an connection/command timeout. + DestinationTableName = tableName, + // be consistent with NPoco: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L50 + BatchSize = 4096 + }) using (var bulkReader = new PocoDataDataReader(records, pocoData, syntax)) { //we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs index 004c4f11f4..18e4791d0b 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs @@ -1,4 +1,4 @@ -ο»Ώusing System; +using System; using System.Collections.Generic; using System.Data; @@ -6,24 +6,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax { public class DbTypes { - public DbType DbType; - public string TextDefinition; - public bool ShouldQuoteValue; - public Dictionary ColumnTypeMap = new Dictionary(); - public Dictionary ColumnDbTypeMap = new Dictionary(); - - public void Set(DbType dbType, string fieldDefinition) + public DbTypes(IReadOnlyDictionary columnTypeMap, IReadOnlyDictionary columnDbTypeMap) { - DbType = dbType; - TextDefinition = fieldDefinition; - ShouldQuoteValue = fieldDefinition != "INTEGER" - && fieldDefinition != "BIGINT" - && fieldDefinition != "DOUBLE" - && fieldDefinition != "DECIMAL" - && fieldDefinition != "BOOL"; - - ColumnTypeMap[typeof(T)] = fieldDefinition; - ColumnDbTypeMap[typeof(T)] = dbType; + ColumnTypeMap = columnTypeMap; + ColumnDbTypeMap = columnDbTypeMap; } + + public IReadOnlyDictionary ColumnTypeMap { get; } + public IReadOnlyDictionary ColumnDbTypeMap { get; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs new file mode 100644 index 0000000000..bf1e0989f5 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs @@ -0,0 +1,20 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Data; + +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +{ + internal class DbTypesFactory + { + private readonly Dictionary _columnTypeMap = new Dictionary(); + private readonly Dictionary _columnDbTypeMap = new Dictionary(); + + public void Set(DbType dbType, string fieldDefinition) + { + _columnTypeMap[typeof(T)] = fieldDefinition; + _columnDbTypeMap[typeof(T)] = dbType; + } + + public DbTypes Create() => new DbTypes(_columnTypeMap, _columnDbTypeMap); + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 6c551648b7..75d348df1a 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -1,4 +1,4 @@ -ο»Ώusing System; +using System; using System.Collections.Generic; using System.Data; using System.Text.RegularExpressions; @@ -28,7 +28,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax string GetQuotedName(string name); bool DoesTableExist(IDatabase db, string tableName); string GetIndexType(IndexTypes indexTypes); - string GetSpecialDbType(SpecialDbTypes dbTypes); + string GetSpecialDbType(SpecialDbType dbType); string CreateTable { get; } string DropTable { get; } string AddColumn { get; } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs index 4c75128926..0093ee14ce 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs @@ -22,8 +22,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax DecimalColumnDefinition = "DECIMAL(38,6)"; TimeColumnDefinition = "TIME"; //SQLSERVER 2008+ BlobColumnDefinition = "VARBINARY(MAX)"; - - InitColumnTypeMap(); } public override string RenameTable => "sp_rename '{0}', '{1}'"; @@ -78,7 +76,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax /// public virtual SqlDbType GetSqlDbType(Type clrType) { - var dbType = DbTypeMap.ColumnDbTypeMap.First(x => x.Key == clrType).Value; + var dbType = DbTypeMap.ColumnDbTypeMap[clrType]; return GetSqlDbType(dbType); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 24548fd36b..753a372e82 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -24,6 +24,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider where TSyntax : ISqlSyntaxProvider { + private readonly Lazy _dbTypes; + protected SqlSyntaxProviderBase() { ClauseOrder = new List> @@ -42,94 +44,96 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); DecimalColumnDefinition = string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); - InitColumnTypeMap(); - // ReSharper disable VirtualMemberCallInConstructor // ok to call virtual GetQuotedXxxName here - they don't depend on any state var col = Regex.Escape(GetQuotedColumnName("column")).Replace("column", @"\w+"); var fld = Regex.Escape(GetQuotedTableName("table") + ".").Replace("table", @"\w+") + col; // ReSharper restore VirtualMemberCallInConstructor AliasRegex = new Regex("(" + fld + @")\s+AS\s+(" + col + ")", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); + + _dbTypes = new Lazy(InitColumnTypeMap); } public Regex AliasRegex { get; } - public string GetWildcardPlaceholder() - { - return "%"; - } + public string GetWildcardPlaceholder() => "%"; - public string StringLengthNonUnicodeColumnDefinitionFormat = "VARCHAR({0})"; - public string StringLengthUnicodeColumnDefinitionFormat = "NVARCHAR({0})"; - public string DecimalColumnDefinitionFormat = "DECIMAL({0},{1})"; + public string StringLengthNonUnicodeColumnDefinitionFormat { get; } = "VARCHAR({0})"; + public string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; + public string DecimalColumnDefinitionFormat { get; } = "DECIMAL({0},{1})"; - public string DefaultValueFormat = "DEFAULT ({0})"; - public int DefaultStringLength = 255; - public int DefaultDecimalPrecision = 20; - public int DefaultDecimalScale = 9; + public string DefaultValueFormat { get; } = "DEFAULT ({0})"; + public int DefaultStringLength { get; } = 255; + public int DefaultDecimalPrecision { get; } = 20; + public int DefaultDecimalScale { get; } = 9; //Set by Constructor - public string StringColumnDefinition; - public string StringLengthColumnDefinitionFormat; + public string StringColumnDefinition { get; } + public string StringLengthColumnDefinitionFormat { get; } - public string AutoIncrementDefinition = "AUTOINCREMENT"; - public string IntColumnDefinition = "INTEGER"; - public string LongColumnDefinition = "BIGINT"; - public string GuidColumnDefinition = "GUID"; - public string BoolColumnDefinition = "BOOL"; - public string RealColumnDefinition = "DOUBLE"; - public string DecimalColumnDefinition; - public string BlobColumnDefinition = "BLOB"; - public string DateTimeColumnDefinition = "DATETIME"; - public string TimeColumnDefinition = "DATETIME"; + public string AutoIncrementDefinition { get; protected set; } = "AUTOINCREMENT"; + public string IntColumnDefinition { get; protected set; } = "INTEGER"; + public string LongColumnDefinition { get; protected set; } = "BIGINT"; + public string GuidColumnDefinition { get; protected set; } = "GUID"; + public string BoolColumnDefinition { get; protected set; } = "BOOL"; + public string RealColumnDefinition { get; protected set; } = "DOUBLE"; + public string DecimalColumnDefinition { get; protected set; } + public string BlobColumnDefinition { get; protected set; } = "BLOB"; + public string DateTimeColumnDefinition { get; protected set; } = "DATETIME"; + public string DateTimeOffsetColumnDefinition { get; protected set; } = "DATETIMEOFFSET(7)"; + public string TimeColumnDefinition { get; protected set; } = "DATETIME"; protected IList> ClauseOrder { get; } - protected DbTypes DbTypeMap = new DbTypes(); - protected void InitColumnTypeMap() + protected DbTypes DbTypeMap => _dbTypes.Value; + + private DbTypes InitColumnTypeMap() { - DbTypeMap.Set(DbType.String, StringColumnDefinition); - DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - DbTypeMap.Set(DbType.String, StringColumnDefinition); - DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); + var dbTypeMap = new DbTypesFactory(); + dbTypeMap.Set(DbType.String, StringColumnDefinition); + dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + dbTypeMap.Set(DbType.String, StringColumnDefinition); + dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeColumnDefinition); + dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); + dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); - DbTypeMap.Set(DbType.Byte, IntColumnDefinition); - DbTypeMap.Set(DbType.Byte, IntColumnDefinition); - DbTypeMap.Set(DbType.SByte, IntColumnDefinition); - DbTypeMap.Set(DbType.SByte, IntColumnDefinition); - DbTypeMap.Set(DbType.Int16, IntColumnDefinition); - DbTypeMap.Set(DbType.Int16, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - DbTypeMap.Set(DbType.Int32, IntColumnDefinition); - DbTypeMap.Set(DbType.Int32, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + dbTypeMap.Set(DbType.Byte, IntColumnDefinition); + dbTypeMap.Set(DbType.Byte, IntColumnDefinition); + dbTypeMap.Set(DbType.SByte, IntColumnDefinition); + dbTypeMap.Set(DbType.SByte, IntColumnDefinition); + dbTypeMap.Set(DbType.Int16, IntColumnDefinition); + dbTypeMap.Set(DbType.Int16, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + dbTypeMap.Set(DbType.Int32, IntColumnDefinition); + dbTypeMap.Set(DbType.Int32, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - DbTypeMap.Set(DbType.Int64, LongColumnDefinition); - DbTypeMap.Set(DbType.Int64, LongColumnDefinition); - DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + dbTypeMap.Set(DbType.Int64, LongColumnDefinition); + dbTypeMap.Set(DbType.Int64, LongColumnDefinition); + dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - DbTypeMap.Set(DbType.Single, RealColumnDefinition); - DbTypeMap.Set(DbType.Single, RealColumnDefinition); - DbTypeMap.Set(DbType.Double, RealColumnDefinition); - DbTypeMap.Set(DbType.Double, RealColumnDefinition); + dbTypeMap.Set(DbType.Single, RealColumnDefinition); + dbTypeMap.Set(DbType.Single, RealColumnDefinition); + dbTypeMap.Set(DbType.Double, RealColumnDefinition); + dbTypeMap.Set(DbType.Double, RealColumnDefinition); - DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); + dbTypeMap.Set(DbType.Binary, BlobColumnDefinition); + + return dbTypeMap.Create(); } public abstract string ProviderName { get; } @@ -193,17 +197,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax return indexType; } - public virtual string GetSpecialDbType(SpecialDbTypes dbTypes) + public virtual string GetSpecialDbType(SpecialDbType dbType) { - if (dbTypes == SpecialDbTypes.NCHAR) + if (dbType == SpecialDbType.NCHAR) { - return "NCHAR"; + return SpecialDbType.NCHAR; } - else if (dbTypes == SpecialDbTypes.NTEXT) + else if (dbType == SpecialDbType.NTEXT) { - return "NTEXT"; + return SpecialDbType.NTEXT; } - else if (dbTypes == SpecialDbTypes.NVARCHARMAX) + else if (dbType == SpecialDbType.NVARCHARMAX) { return "NVARCHAR(MAX)"; } @@ -470,14 +474,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) return column.CustomType; - if (column.HasSpecialDbType) + if (column.CustomDbType.HasValue) { - if (column.Size != default(int)) + if (column.Size != default) { - return $"{GetSpecialDbType(column.DbType)}({column.Size})"; + return $"{GetSpecialDbType(column.CustomDbType.Value)}({column.Size})"; } - return GetSpecialDbType(column.DbType); + return GetSpecialDbType(column.CustomDbType.Value); } var type = column.Type.HasValue @@ -486,19 +490,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax if (type == typeof(string)) { - var valueOrDefault = column.Size != default(int) ? column.Size : DefaultStringLength; + var valueOrDefault = column.Size != default ? column.Size : DefaultStringLength; return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); } if (type == typeof(decimal)) { - var precision = column.Size != default(int) ? column.Size : DefaultDecimalPrecision; - var scale = column.Precision != default(int) ? column.Precision : DefaultDecimalScale; + var precision = column.Size != default ? column.Size : DefaultDecimalPrecision; + var scale = column.Precision != default ? column.Precision : DefaultDecimalScale; return string.Format(DecimalColumnDefinitionFormat, precision, scale); } - var definition = DbTypeMap.ColumnTypeMap.First(x => x.Key == type).Value; - var dbTypeDefinition = column.Size != default(int) + var definition = DbTypeMap.ColumnTypeMap[type]; + var dbTypeDefinition = column.Size != default ? $"{definition}({column.Size})" : definition; //NOTE Precision is left out diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs index 51bf3c477c..ae1290d6a4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs @@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Serialization; -using Umbraco.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters diff --git a/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs b/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs index 7b3b3f86ed..ed1370e5a5 100644 --- a/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs +++ b/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; @@ -29,6 +30,7 @@ namespace Umbraco.Cms.Core.Routing INotificationHandler, INotificationHandler { + private readonly ILogger _logger; private readonly IOptionsMonitor _webRoutingSettings; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IRedirectUrlService _redirectUrlService; @@ -36,8 +38,14 @@ namespace Umbraco.Cms.Core.Routing private const string NotificationStateKey = "Umbraco.Cms.Core.Routing.RedirectTrackingHandler"; - public RedirectTrackingHandler(IOptionsMonitor webRoutingSettings, IPublishedSnapshotAccessor publishedSnapshotAccessor, IRedirectUrlService redirectUrlService, IVariationContextAccessor variationContextAccessor) + public RedirectTrackingHandler( + ILogger logger, + IOptionsMonitor webRoutingSettings, + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IRedirectUrlService redirectUrlService, + IVariationContextAccessor variationContextAccessor) { + _logger = logger; _webRoutingSettings = webRoutingSettings; _publishedSnapshotAccessor = publishedSnapshotAccessor; _redirectUrlService = redirectUrlService; @@ -87,8 +95,8 @@ namespace Umbraco.Cms.Core.Routing private void StoreOldRoute(IContent entity, OldRoutesDictionary oldRoutes) { - var contentCache = _publishedSnapshotAccessor.PublishedSnapshot.Content; - var entityContent = contentCache.GetById(entity.Id); + var contentCache = _publishedSnapshotAccessor.PublishedSnapshot?.Content; + var entityContent = contentCache?.GetById(entity.Id); if (entityContent == null) return; @@ -112,7 +120,13 @@ namespace Umbraco.Cms.Core.Routing private void CreateRedirects(OldRoutesDictionary oldRoutes) { - var contentCache = _publishedSnapshotAccessor.PublishedSnapshot.Content; + var contentCache = _publishedSnapshotAccessor.PublishedSnapshot?.Content; + + if (contentCache == null) + { + _logger.LogWarning("Could not track redirects because there is no current published snapshot available."); + return; + } foreach (var oldRoute in oldRoutes) { diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index 9ddb67d611..e3ddc69e6f 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -18,6 +18,7 @@ namespace Umbraco.Cms.Core.Security private string[] _allowedSections; private int[] _startMediaIds; private int[] _startContentIds; + private DateTime? _inviteDateUtc; private static readonly DelegateEqualityComparer s_startIdsComparer = new DelegateEqualityComparer( (groups, enumerable) => groups.UnsortedSequenceEqual(enumerable), @@ -75,6 +76,15 @@ namespace Umbraco.Cms.Core.Security public int[] CalculatedMediaStartNodeIds { get; set; } public int[] CalculatedContentStartNodeIds { get; set; } + /// + /// Gets or sets invite date + /// + public DateTime? InviteDateUtc + { + get => _inviteDateUtc; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _inviteDateUtc, nameof(InviteDateUtc)); + } + /// /// Gets or sets content start nodes assigned to the User (not ones assigned to the user's groups) /// diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index e6a58efa88..deb85ff496 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -438,6 +438,13 @@ namespace Umbraco.Cms.Core.Security user.LastLoginDate = dt; } + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.InviteDateUtc)) + || (user.InvitedDate?.ToUniversalTime() != identityUser.InviteDateUtc)) + { + anythingChanged = true; + user.InvitedDate = identityUser.InviteDateUtc?.ToLocalTime(); + } + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastPasswordChangeDateUtc)) || (user.LastPasswordChangeDate != default && identityUser.LastPasswordChangeDateUtc.HasValue == false) || (identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value)) diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 0d0b9fc156..5addc73a3f 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -74,6 +74,7 @@ namespace Umbraco.Cms.Core.Security target.UserName = source.Username; target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime(); + target.InviteDateUtc = source.InvitedDate?.ToUniversalTime(); target.EmailConfirmed = source.EmailConfirmedDate.HasValue; target.Name = source.Name; target.AccessFailedCount = source.FailedPasswordAttempts; @@ -87,7 +88,7 @@ namespace Umbraco.Cms.Core.Security target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; } - // TODO: We need to validate this mapping is OK, we need to get Umbraco.Code working + // Umbraco.Code.MapAll -Id -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -TwoFactorEnabled -ConcurrencyStamp -NormalizedEmail -NormalizedUserName -Roles private void Map(IMember source, MemberIdentityUser target) { target.Email = source.Email; diff --git a/src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs b/src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs new file mode 100644 index 0000000000..9e413b7162 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs @@ -0,0 +1,33 @@ +using System.Net; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Core.Services.Implement +{ + public class BasicAuthService : IBasicAuthService + { + private BasicAuthSettings _basicAuthSettings; + + public BasicAuthService(IOptionsMonitor optionsMonitor) + { + _basicAuthSettings = optionsMonitor.CurrentValue; + + optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings); + } + + public bool IsBasicAuthEnabled() => _basicAuthSettings.Enabled; + + public bool IsIpAllowListed(IPAddress clientIpAddress) + { + foreach (var allowedIpString in _basicAuthSettings.AllowedIPs) + { + if (IPNetwork.TryParse(allowedIpString, out IPNetwork allowedIp) && allowedIp.Contains(clientIpAddress)) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 712323656d..2f709d5d27 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -19,15 +19,16 @@ - + + - + - + @@ -40,12 +41,12 @@ - - - + + + diff --git a/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs b/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs index 74668d3090..c7e6df8cb2 100644 --- a/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs +++ b/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs @@ -21,6 +21,8 @@ namespace Umbraco.Cms.Infrastructure.WebAssets public const string UmbracoInitCssBundleName = "umbraco-backoffice-init-css"; public const string UmbracoCoreJsBundleName = "umbraco-backoffice-js"; public const string UmbracoExtensionsJsBundleName = "umbraco-backoffice-extensions-js"; + public const string UmbracoNonOptimizedPackageJsBundleName = "umbraco-backoffice-non-optimized-js"; + public const string UmbracoNonOptimizedPackageCssBundleName = "umbraco-backoffice-non-optimized-css"; public const string UmbracoTinyMceJsBundleName = "umbraco-tinymce-js"; public const string UmbracoUpgradeCssBundleName = "umbraco-authorize-upgrade-css"; @@ -51,47 +53,134 @@ namespace Umbraco.Cms.Infrastructure.WebAssets { // Create bundles - _runtimeMinifier.CreateCssBundle(UmbracoInitCssBundleName, false, - FormatPaths("lib/bootstrap-social/bootstrap-social.css", - "assets/css/umbraco.min.css", - "lib/font-awesome/css/font-awesome.min.css")); + _runtimeMinifier.CreateCssBundle(UmbracoInitCssBundleName, + BundlingOptions.NotOptimizedAndComposite, + FormatPaths( + "assets/css/umbraco.min.css", + "lib/bootstrap-social/bootstrap-social.css", + "lib/font-awesome/css/font-awesome.min.css")); - _runtimeMinifier.CreateCssBundle(UmbracoUpgradeCssBundleName, false, - FormatPaths("assets/css/umbraco.min.css", - "lib/bootstrap-social/bootstrap-social.css", - "lib/font-awesome/css/font-awesome.min.css")); + _runtimeMinifier.CreateCssBundle(UmbracoUpgradeCssBundleName, + BundlingOptions.NotOptimizedAndComposite, + FormatPaths( + "assets/css/umbraco.min.css", + "lib/bootstrap-social/bootstrap-social.css", + "lib/font-awesome/css/font-awesome.min.css")); - _runtimeMinifier.CreateCssBundle(UmbracoPreviewCssBundleName, false, + _runtimeMinifier.CreateCssBundle(UmbracoPreviewCssBundleName, + BundlingOptions.NotOptimizedAndComposite, FormatPaths("assets/css/canvasdesigner.min.css")); - _runtimeMinifier.CreateJsBundle(UmbracoPreviewJsBundleName, false, + _runtimeMinifier.CreateJsBundle(UmbracoPreviewJsBundleName, + BundlingOptions.NotOptimizedAndComposite, FormatPaths(GetScriptsForPreview())); - _runtimeMinifier.CreateJsBundle(UmbracoTinyMceJsBundleName, false, + _runtimeMinifier.CreateJsBundle(UmbracoTinyMceJsBundleName, + BundlingOptions.NotOptimizedAndComposite, FormatPaths(GetScriptsForTinyMce())); - _runtimeMinifier.CreateJsBundle(UmbracoCoreJsBundleName, false, + _runtimeMinifier.CreateJsBundle(UmbracoCoreJsBundleName, + BundlingOptions.NotOptimizedAndComposite, FormatPaths(GetScriptsForBackOfficeCore())); + + // get the property editor assets var propertyEditorAssets = ScanPropertyEditors() .GroupBy(x => x.AssetType) .ToDictionary(x => x.Key, x => x.Select(c => c.FilePath)); + // get the back office custom assets var customAssets = _customBackOfficeAssetsCollection.GroupBy(x => x.DependencyType).ToDictionary(x => x.Key, x => x.Select(c => c.FilePath)); - var jsAssets = (customAssets.TryGetValue(AssetType.Javascript, out var customScripts) ? customScripts : Enumerable.Empty()) - .Union(propertyEditorAssets.TryGetValue(AssetType.Javascript, out var scripts) ? scripts : Enumerable.Empty()); + // This bundle includes all scripts from property editor assets, + // custom back office assets, and any scripts found in package manifests + // that have the default bundle options. + + IEnumerable jsAssets = (customAssets.TryGetValue(AssetType.Javascript, out IEnumerable customScripts) ? customScripts : Enumerable.Empty()) + .Union(propertyEditorAssets.TryGetValue(AssetType.Javascript, out IEnumerable scripts) ? scripts : Enumerable.Empty()); + _runtimeMinifier.CreateJsBundle( - UmbracoExtensionsJsBundleName, true, + UmbracoExtensionsJsBundleName, + BundlingOptions.OptimizedAndComposite, FormatPaths( GetScriptsForBackOfficeExtensions(jsAssets))); - var cssAssets = (customAssets.TryGetValue(AssetType.Css, out var customStyles) ? customStyles : Enumerable.Empty()) - .Union(propertyEditorAssets.TryGetValue(AssetType.Css, out var styles) ? styles : Enumerable.Empty()); + // Create a bundle per package manifest that is declaring an Independent bundle type + RegisterPackageBundlesForIndependentOptions(_parser.CombinedManifest.Scripts, AssetType.Javascript); + + // Create a single non-optimized (no file processing) bundle for all manifests declaring None as a bundle option + RegisterPackageBundlesForNoneOption(_parser.CombinedManifest.Scripts, UmbracoNonOptimizedPackageJsBundleName); + + // This bundle includes all CSS from property editor assets, + // custom back office assets, and any CSS found in package manifests + // that have the default bundle options. + + IEnumerable cssAssets = (customAssets.TryGetValue(AssetType.Css, out IEnumerable customStyles) ? customStyles : Enumerable.Empty()) + .Union(propertyEditorAssets.TryGetValue(AssetType.Css, out IEnumerable styles) ? styles : Enumerable.Empty()); + _runtimeMinifier.CreateCssBundle( - UmbracoCssBundleName, true, + UmbracoCssBundleName, + BundlingOptions.OptimizedAndComposite, FormatPaths( GetStylesheetsForBackOffice(cssAssets))); + + // Create a bundle per package manifest that is declaring an Independent bundle type + RegisterPackageBundlesForIndependentOptions(_parser.CombinedManifest.Stylesheets, AssetType.Css); + + // Create a single non-optimized (no file processing) bundle for all manifests declaring None as a bundle option + RegisterPackageBundlesForNoneOption(_parser.CombinedManifest.Stylesheets, UmbracoNonOptimizedPackageCssBundleName); + } + + public static string GetIndependentPackageBundleName(ManifestAssets manifestAssets, AssetType assetType) + => $"{manifestAssets.PackageName.ToLowerInvariant()}-{(assetType == AssetType.Css ? "css" : "js")}"; + + private void RegisterPackageBundlesForNoneOption( + IReadOnlyDictionary> combinedPackageManifestAssets, + string bundleName) + { + var assets = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + // Create a bundle per package manifest that is declaring the matching BundleOptions + if (combinedPackageManifestAssets.TryGetValue(BundleOptions.None, out IReadOnlyList manifestAssetList)) + { + foreach(var asset in manifestAssetList.SelectMany(x => x.Assets)) + { + assets.Add(asset); + } + } + + _runtimeMinifier.CreateJsBundle( + bundleName, + // no optimization, no composite files, just render individual files + BundlingOptions.NotOptimizedNotComposite, + FormatPaths(assets.ToArray())); + } + + private void RegisterPackageBundlesForIndependentOptions( + IReadOnlyDictionary> combinedPackageManifestAssets, + AssetType assetType) + { + // Create a bundle per package manifest that is declaring the matching BundleOptions + if (combinedPackageManifestAssets.TryGetValue(BundleOptions.Independent, out IReadOnlyList manifestAssetList)) + { + foreach (ManifestAssets manifestAssets in manifestAssetList) + { + string bundleName = GetIndependentPackageBundleName(manifestAssets, assetType); + string[] filePaths = FormatPaths(manifestAssets.Assets.ToArray()); + + switch (assetType) + { + case AssetType.Javascript: + _runtimeMinifier.CreateJsBundle(bundleName, BundlingOptions.OptimizedAndComposite, filePaths); + break; + case AssetType.Css: + _runtimeMinifier.CreateCssBundle(bundleName, BundlingOptions.OptimizedAndComposite, filePaths); + break; + default: + throw new IndexOutOfRangeException(); + } + } + } } /// @@ -100,10 +189,15 @@ namespace Umbraco.Cms.Infrastructure.WebAssets /// private string[] GetScriptsForBackOfficeExtensions(IEnumerable propertyEditorScripts) { - var scripts = new HashSet(); - foreach (string script in _parser.Manifest.Scripts) + var scripts = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + // only include scripts with the default bundle options here + if (_parser.CombinedManifest.Scripts.TryGetValue(BundleOptions.Default, out IReadOnlyList manifestAssets)) { - scripts.Add(script); + foreach (string script in manifestAssets.SelectMany(x => x.Assets)) + { + scripts.Add(script); + } } foreach (string script in propertyEditorScripts) @@ -130,11 +224,15 @@ namespace Umbraco.Cms.Infrastructure.WebAssets /// private string[] GetStylesheetsForBackOffice(IEnumerable propertyEditorStyles) { - var stylesheets = new HashSet(); + var stylesheets = new HashSet(StringComparer.InvariantCultureIgnoreCase); - foreach (string script in _parser.Manifest.Stylesheets) + // only include css with the default bundle options here + if (_parser.CombinedManifest.Stylesheets.TryGetValue(BundleOptions.Default, out IReadOnlyList manifestAssets)) { - stylesheets.Add(script); + foreach (string script in manifestAssets.SelectMany(x => x.Assets)) + { + stylesheets.Add(script); + } } foreach (string stylesheet in propertyEditorStyles) diff --git a/src/Umbraco.Infrastructure/WebAssets/RuntimeMinifierExtensions.cs b/src/Umbraco.Infrastructure/WebAssets/RuntimeMinifierExtensions.cs deleted file mode 100644 index f33d48d8fd..0000000000 --- a/src/Umbraco.Infrastructure/WebAssets/RuntimeMinifierExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -ο»Ώusing System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.WebAssets; -using Umbraco.Cms.Infrastructure.WebAssets; - -namespace Umbraco.Extensions -{ - public static class RuntimeMinifierExtensions - { - /// - /// Returns the JavaScript to load the back office's assets - /// - /// - public static async Task GetScriptForLoadingBackOfficeAsync(this IRuntimeMinifier minifier, GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) - { - var coreScripts = await minifier.GetJsAssetPathsAsync(BackOfficeWebAssets.UmbracoCoreJsBundleName); - var extensionsScripts = await minifier.GetJsAssetPathsAsync(BackOfficeWebAssets.UmbracoExtensionsJsBundleName); - var result = BackOfficeJavaScriptInitializer.GetJavascriptInitialization(coreScripts.Union(extensionsScripts), "umbraco", globalSettings, hostingEnvironment); - result += await GetStylesheetInitializationAsync(minifier); - - return result; - } - - /// - /// Gets the back office css bundle paths and formats a JS call to lazy load them - /// - private static async Task GetStylesheetInitializationAsync(IRuntimeMinifier minifier) - { - var files = await minifier.GetCssAssetPathsAsync(BackOfficeWebAssets.UmbracoCssBundleName); - var sb = new StringBuilder(); - foreach (var file in files) - sb.AppendFormat("{0}LazyLoad.css('{1}');", Environment.NewLine, file); - return sb.ToString(); - } - - } -} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs index 667c5262d5..e6ed41548d 100644 --- a/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs +++ b/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs @@ -17,13 +17,12 @@ namespace Umbraco.Cms.Persistence.SqlCe public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { - var recordsA = records.ToArray(); - if (recordsA.Length == 0) return 0; + if (!records.Any()) return 0; var pocoData = database.PocoDataFactory.ForType(typeof(T)); if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); - return BulkInsertRecordsSqlCe(database, pocoData, recordsA); + return BulkInsertRecordsSqlCe(database, pocoData, records.ToArray()); } diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs index e81b6135da..62aa933a04 100644 --- a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs @@ -1,4 +1,4 @@ -ο»Ώusing System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -25,10 +25,10 @@ namespace Umbraco.Cms.Persistence.SqlCe { _globalSettings = globalSettings; BlobColumnDefinition = "IMAGE"; - // This is silly to have to do this but the way these inherited classes are structured it's the easiest - // way without an overhaul in type map initialization - DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); - + // NOTE: if this column type is used in sqlce, it will prob result in errors since + // SQLCE cannot support this type correctly without 2x columns and a lot of work arounds. + // We don't use this natively within Umbraco but 3rd parties might with SQL server. + DateTimeOffsetColumnDefinition = "DATETIME"; } public override string ProviderName => Constants.DatabaseProviders.SqlCe; @@ -300,10 +300,14 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() GetQuotedTableName(index.TableName), columns); } - public override string GetSpecialDbType(SpecialDbTypes dbTypes) + public override string GetSpecialDbType(SpecialDbType dbTypes) { - if (dbTypes == SpecialDbTypes.NVARCHARMAX) // SqlCE does not have nvarchar(max) for now + // SqlCE does not have nvarchar(max) for now + if (dbTypes == SpecialDbType.NVARCHARMAX) + { return "NTEXT"; + } + return base.GetSpecialDbType(dbTypes); } public override SqlDbType GetSqlDbType(DbType dbType) diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs index 59b73c21b0..99ce9365fc 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs @@ -1,4 +1,4 @@ -ο»Ώusing System.IO; +using System.IO; using CSharpTest.Net.Serialization; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource /// /// Serializes/Deserializes data to BTree data source for /// - internal class ContentDataSerializer : ISerializer + public class ContentDataSerializer : ISerializer { public ContentDataSerializer(IDictionaryOfPropertyDataSerializer dictionaryOfPropertyDataSerializer = null) { diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs index 1b8089d8ba..c813e428d2 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs @@ -1,11 +1,11 @@ -ο»Ώusing System.Configuration; +using System.Configuration; using CSharpTest.Net.Collections; using CSharpTest.Net.Serialization; using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { - internal class BTree + public class BTree { public static BPlusTree GetTree(string filepath, bool exists, NuCacheSettings settings, ContentDataSerializer contentDataSerializer = null) { diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs index 65412f10d2..cbc40a9630 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs @@ -14,12 +14,12 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource /// /// Deserialize the data into a /// - ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData); + ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published); /// - /// Serializes the + /// Serializes the /// - ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model); + ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published); } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs index bffa66898d..702689a995 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs @@ -3,7 +3,7 @@ using System.IO; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { - internal interface IDictionaryOfPropertyDataSerializer + public interface IDictionaryOfPropertyDataSerializer { IDictionary ReadFrom(Stream stream); void WriteTo(IDictionary value, Stream stream); diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs index 8fc953f353..9f1bfd39e2 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -24,7 +24,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource DateFormatString = "o" }; private readonly JsonNameTable _propertyNameTable = new DefaultJsonNameTable(); - public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published) { if (stringData == null && byteData != null) throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); @@ -39,7 +39,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource } } - public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) { // note that numeric values (which are Int32) are serialized without their // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 66c01cf1dc..7caf6eac7d 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -39,7 +39,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource _options = defaultOptions .WithResolver(resolver) - .WithCompression(MessagePackCompression.Lz4BlockArray); + .WithCompression(MessagePackCompression.Lz4BlockArray) + .WithSecurity(MessagePackSecurity.UntrustedData); } public string ToJson(byte[] bin) @@ -48,12 +49,12 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource return json; } - public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published) { if (byteData != null) { var cacheModel = MessagePackSerializer.Deserialize(byteData, _options); - Expand(content, cacheModel); + Expand(content, cacheModel, published); return cacheModel; } else if (stringData != null) @@ -61,7 +62,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) var bin = Convert.FromBase64String(stringData); var cacheModel = MessagePackSerializer.Deserialize(bin, _options); - Expand(content, cacheModel); + Expand(content, cacheModel, published); return cacheModel; } else @@ -70,9 +71,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource } } - public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) { - Compress(content, model); + Compress(content, model, published); var bytes = MessagePackSerializer.Serialize(model, _options); return new ContentCacheDataSerializationResult(null, bytes); } @@ -80,7 +81,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource /// /// Used during serialization to compress properties /// + /// /// + /// /// /// This will essentially 'double compress' property data. The MsgPack data as a whole will already be compressed /// but this will go a step further and double compress property data so that it is stored in the nucache file @@ -88,11 +91,11 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource /// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant /// memory savings but could also affect performance of first rendering pages while decompression occurs. /// - private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model) + private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) { foreach(var propertyAliasToData in model.PropertyData) { - if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published)) { foreach(var property in propertyAliasToData.Value.Where(x => x.Value != null && x.Value is string)) { @@ -105,12 +108,14 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource /// /// Used during deserialization to map the property data as lazy or expand the value /// + /// /// - private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData) + /// + private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData, bool published) { foreach (var propertyAliasToData in nestedData.PropertyData) { - if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key,published)) { foreach (var property in propertyAliasToData.Value.Where(x => x.Value != null)) { diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs index f93cd71ad2..732d51b3b1 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; -using Umbraco.Core.PropertyEditors; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { @@ -14,7 +13,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource private readonly IMemberTypeService _memberTypeService; private readonly PropertyEditorCollection _propertyEditors; private readonly IPropertyCacheCompressionOptions _compressionOptions; - private readonly ConcurrentDictionary<(int, string), bool> _isCompressedCache = new ConcurrentDictionary<(int, string), bool>(); + private readonly ConcurrentDictionary<(int, string, bool), bool> _isCompressedCache = new ConcurrentDictionary<(int, string, bool), bool>(); public MsgPackContentNestedDataSerializerFactory( IContentTypeService contentTypeService, diff --git a/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs index 7ef655b2a8..c52c1271ad 100644 --- a/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs @@ -68,7 +68,20 @@ namespace Umbraco.Extensions throw new IndexOutOfRangeException(); } }); - builder.Services.AddSingleton(); + + builder.Services.AddSingleton(s => + { + IOptions options = s.GetRequiredService>(); + + if (options.Value.NuCacheSerializerType == NuCacheSerializerType.MessagePack && + options.Value.UnPublishedContentCompression) + { + return new UnPublishedContentPropertyCacheCompressionOptions(); + } + + return new NoopPropertyCacheCompressionOptions(); + }); + builder.Services.AddSingleton(s => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); // add the NuCache health check (hidden from type finder) diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs index a141872957..2919edb8c3 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs @@ -415,7 +415,7 @@ AND cmsContentNu.nodeId IS NULL UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders) }; - var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData); + var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData, published); var dto = new ContentNuDto { @@ -817,12 +817,13 @@ AND cmsContentNu.nodeId IS NULL } else { - var deserializedContent = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw); + bool published = false; + var deserializedContent = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published); d = new ContentData { Name = dto.EditName, - Published = false, + Published = published, TemplateId = dto.EditTemplateId, VersionId = dto.VersionId, VersionDate = dto.EditVersionDate, @@ -847,13 +848,14 @@ AND cmsContentNu.nodeId IS NULL } else { - var deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw); + bool published = true; + var deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw, published); p = new ContentData { Name = dto.PubName, UrlSegment = deserializedContent.UrlSegment, - Published = true, + Published = published, TemplateId = dto.PubTemplateId, VersionId = dto.VersionId, VersionDate = dto.PubVersionDate, @@ -883,12 +885,13 @@ AND cmsContentNu.nodeId IS NULL if (dto.EditData == null && dto.EditDataRaw == null) throw new InvalidOperationException("No data for media " + dto.Id); - var deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw); + bool published = true; + var deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published); var p = new ContentData { Name = dto.EditName, - Published = true, + Published = published, TemplateId = -1, VersionId = dto.VersionId, VersionDate = dto.EditVersionDate, diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj index bedc6b7137..7d6ff54c25 100644 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj @@ -19,8 +19,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all diff --git a/src/Umbraco.TestData/Configuration/TestDataSettings.cs b/src/Umbraco.TestData/Configuration/TestDataSettings.cs new file mode 100644 index 0000000000..78084f726a --- /dev/null +++ b/src/Umbraco.TestData/Configuration/TestDataSettings.cs @@ -0,0 +1,16 @@ +namespace Umbraco.TestData.Configuration +{ + public class TestDataSettings + { + /// + /// Gets or sets a value indicating whether the test data generation is enabled. + /// + public bool Enabled { get; set; } = false; + + /// + /// Gets or sets a value indicating whether persisted local database cache files for content and media are disabled. + /// + /// The URL path. + public bool IgnoreLocalDb { get; set; } = false; + } +} diff --git a/src/Umbraco.TestData/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.TestData/Extensions/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..7e3e2a70b1 --- /dev/null +++ b/src/Umbraco.TestData/Extensions/UmbracoBuilderExtensions.cs @@ -0,0 +1,54 @@ +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure.PublishedCache; +using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.TestData.Configuration; + +namespace Umbraco.TestData.Extensions +{ + public static class UmbracoBuilderExtensions + { + public static IUmbracoBuilder AddUmbracoTestData(this IUmbracoBuilder builder) + { + if (builder.Services.Any(x => x.ServiceType == typeof(LoadTestController))) + { + // We assume the test data project is composed if any implementations of LoadTestController exist. + return builder; + } + + IConfigurationSection testDataSection = builder.Config.GetSection("Umbraco:CMS:TestData"); + TestDataSettings config = testDataSection.Get(); + if (config == null || config.Enabled == false) + { + return builder; + } + + builder.Services.Configure(testDataSection); + + if (config.IgnoreLocalDb) + { + builder.Services.AddSingleton(factory => new PublishedSnapshotServiceOptions + { + IgnoreLocalDb = true + }); + } + + builder.Services.Configure(options => + options.AddFilter(new UmbracoPipelineFilter(nameof(LoadTestController)) + { + Endpoints = app => app.UseEndpoints(endpoints => + endpoints.MapControllerRoute( + "LoadTest", + "/LoadTest/{action}", + new { controller = "LoadTest", Action = "Index" })) + })); + + builder.Services.AddScoped(typeof(LoadTestController)); + + return builder; + } + } +} diff --git a/src/Umbraco.TestData/LoadTestComponent.cs b/src/Umbraco.TestData/LoadTestComponent.cs deleted file mode 100644 index cfd923cd07..0000000000 --- a/src/Umbraco.TestData/LoadTestComponent.cs +++ /dev/null @@ -1,35 +0,0 @@ -ο»Ώusing System.Web.Mvc; -using System.Web.Routing; -using System.Configuration; -using Umbraco.Cms.Core.Composing; - -// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting - -namespace Umbraco.TestData -{ - public class LoadTestComponent : IComponent - { - public void Initialize() - { - if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") - return; - - - - RouteTable.Routes.MapRoute( - name: "LoadTest", - url: "LoadTest/{action}", - defaults: new - { - controller = "LoadTest", - action = "Index" - }, - namespaces: new[] { "Umbraco.TestData" } - ); - } - - public void Terminate() - { - } - } -} diff --git a/src/Umbraco.TestData/LoadTestComposer.cs b/src/Umbraco.TestData/LoadTestComposer.cs index e5b16e5ab1..8d66c4965d 100644 --- a/src/Umbraco.TestData/LoadTestComposer.cs +++ b/src/Umbraco.TestData/LoadTestComposer.cs @@ -1,31 +1,13 @@ -ο»Ώusing System.Configuration; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Infrastructure.PublishedCache; +using Umbraco.TestData.Extensions; // see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting namespace Umbraco.TestData { - public class LoadTestComposer : ComponentComposer, IUserComposer + public class LoadTestComposer : IUserComposer { - public override void Compose(IUmbracoBuilder builder) - { - base.Compose(builder); - - if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") - return; - - builder.Services.AddScoped(typeof(LoadTestController), typeof(LoadTestController)); - - if (ConfigurationManager.AppSettings["Umbraco.TestData.IgnoreLocalDb"] == "true") - { - builder.Services.AddSingleton(factory => new PublishedSnapshotServiceOptions - { - IgnoreLocalDb = true - }); - } - } + public void Compose(IUmbracoBuilder builder) => builder.AddUmbracoTestData(); } } diff --git a/src/Umbraco.TestData/LoadTestController.cs b/src/Umbraco.TestData/LoadTestController.cs index e1494fbdab..3033d2febb 100644 --- a/src/Umbraco.TestData/LoadTestController.cs +++ b/src/Umbraco.TestData/LoadTestController.cs @@ -1,21 +1,15 @@ -ο»Ώusing System; -using System.Configuration; +using System; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Text; using System.Threading; -using System.Web; -using System.Web.Hosting; -using System.Web.Mvc; -using System.Web.Routing; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.DependencyInjection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using System.IO; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Hosting; // see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting @@ -23,27 +17,17 @@ namespace Umbraco.TestData { public class LoadTestController : Controller { - public LoadTestController( - ServiceContext serviceContext, - IShortStringHelper shortStringHelper, - IHostingEnvironment hostingEnvironment) - { - _serviceContext = serviceContext; - _shortStringHelper = shortStringHelper; - _hostingEnvironment = hostingEnvironment; - } + private static readonly Random s_random = new Random(); + private static readonly object s_locko = new object(); - private static readonly Random _random = new Random(); - private static readonly object _locko = new object(); + private static volatile int s_containerId = -1; - private static volatile int _containerId = -1; + private const string ContainerAlias = "LoadTestContainer"; + private const string ContentAlias = "LoadTestContent"; + private const int TextboxDefinitionId = -88; + private const int MaxCreate = 1000; - private const string _containerAlias = "LoadTestContainer"; - private const string _contentAlias = "LoadTestContent"; - private const int _textboxDefinitionId = -88; - private const int _maxCreate = 1000; - - private static readonly string HeadHtml = @" + private static readonly string s_headHtml = @" LoadTest