diff --git a/.github/BUILD.md b/.github/BUILD.md
index c89a1be460..a9c26f3a9b 100644
--- a/.github/BUILD.md
+++ b/.github/BUILD.md
@@ -1,4 +1,4 @@
-# Umbraco Cms Build
+# Umbraco CMS Build
## Are you sure?
@@ -66,7 +66,7 @@ The Visual Studio object is `null` when Visual Studio has not been detected (eg
* `Path`: Visual Studio installation path (eg some place under `Program Files`)
* `Major`: Visual Studio major version (eg `15` for VS 2017)
* `Minor`: Visual Studio minor version
-* `MsBUild`: the absolute path to the MsBuild executable
+* `MsBuild`: the absolute path to the MsBuild executable
#### GetUmbracoVersion
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
index 0e79851c0b..1526c54656 100644
--- a/.github/CODE_OF_CONDUCT.md
+++ b/.github/CODE_OF_CONDUCT.md
@@ -29,4 +29,4 @@ Don't rest on your laurels and never accept the status quo. Contribute and give
## Friendly
-Don’t judge upon mistakes made but rather upon the speed and quality with which mistakes are corrected. Friendly posts and contributions generate smiles and builds long lasting relationships.
\ No newline at end of file
+Don’t judge upon mistakes made but rather upon the speed and quality with which mistakes are corrected. Friendly posts and contributions generate smiles and build long lasting relationships.
\ No newline at end of file
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 84115b946a..2679eaa411 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -2,15 +2,15 @@
👍🎉 First off, thanks for taking the time to contribute! 🎉👍
-The following is a set of guidelines for contributing to Umbraco CMS.
+The following is a set of guidelines, for contributing to Umbraco CMS.
-These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
+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).
+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**
@@ -38,11 +38,11 @@ This document gives you a quick overview on how to get started.
### Guidelines for contributions we welcome
-Not all changes are wanted, so on occassion 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.
+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.
+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, if an issue is in the `Up for grabs` list or you've asked for some feedback before you sent us a PR, your PR will not be closed as unwanted.
+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.
### What can I start with?
@@ -64,32 +64,32 @@ Great question! The short version goes like this:
* **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/dev`, 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). 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 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.

### Pull requests
The most successful pull requests usually look a like this:
- * Fill in the required template
+ * Fill in the required template, linking 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
+ * 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.
+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?
+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 you to make some additional changes.
+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
+- Sometimes it is difficult to meet these timelines and we'll talk to you if this is the case.
### Styleguides
@@ -99,21 +99,21 @@ That said, the Umbraco development team likes to follow the hints that ReSharper
### The PR team
-The pull request team consists of a member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members
+The pull request 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:
- [Anders Bjerner](https://github.com/abjerner)
- [Dave Woestenborghs](https://github.com/dawoe)
- [Emma Burstow](https://github.com/emmaburstow)
- [Poornima Nayar](https://github.com/poornimanayar)
-These wonderful volunteers will provide you with a first reply to your PR, review and test out your changes and might ask more questions. After that they'll let Umbraco HQ know if everything seems okay.
+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 PR team](#the-pr-team) in multiple ways, we love open conversations and we are a friendly bunch. No question you have is stupid. Any questions you have usually helps out multiple people with the same question. Ask away:
+You can get in touch with [the PR team](#the-pr-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
+- 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
@@ -125,19 +125,19 @@ In order to build the Umbraco source code locally, first make sure you have the
* Node v10+
* npm v6.4.1+
-The easiest way to get started is to run `build.ps1` 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.
+The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2017 (version 15.9.7 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 open `src\umbraco.sln` in Visual Studio 2017 (version 15.9.7 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.

-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.
+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's two big areas that you should know about:
+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:
@@ -146,30 +146,34 @@ There's two big areas that you should know about:
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 of something you're looking to fix or improve, have a look at the following two parts of the API documentation.
+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/), 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/dev`. Whatever the default is, that's where we'd like you to target your 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/dev`. 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.

-### Making changes after the PR was opened
+### 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 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.
+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've 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.
+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:
+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
@@ -185,3 +189,7 @@ git rebase upstream/v8/dev
In this command we're syncing with the `v8/dev` 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 PR team 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/.github/CONTRIBUTION_GUIDELINES.md b/.github/CONTRIBUTION_GUIDELINES.md
new file mode 100644
index 0000000000..0ac35e6897
--- /dev/null
+++ b/.github/CONTRIBUTION_GUIDELINES.md
@@ -0,0 +1,35 @@
+# Contributing to Umbraco CMS
+
+When you’re considering creating a pull request for Umbraco CMS, we will categorize them in two different sizes, small and large.
+
+The process for both sizes is very similar, as [explained in the contribution document](CONTRIBUTING.md#how-do-i-begin).
+
+## Small PRs
+Bug fixes and small improvements - can be recognized by seeing a small number of changes and possibly a small number of new files.
+
+We’re usually able to handle small PRs pretty quickly. A community volunteer will do the initial review and flag it for Umbraco HQ as “community tested”. If everything looks good, it will be merged pretty quickly [as per the described process](REVIEW_PROCESS.md).
+
+### Up for grabs
+
+Umbraco HQ will regularly mark newly created issues on the issue tracker with the `Up for grabs` tag. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out.
+
+If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has.
+
+## Large PRs
+New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGet’s packages.config, NPM’s packages.json, etc.).
+
+We would love to follow the same process for larger PRs but this is not always possible due to time limitations and priorities that need to be aligned. We don’t want to put up any barriers, but this document should set the correct expectations.
+
+Please make sure to describe your idea in an issue, it helps to put in mockup screenshots or videos.
+
+If the change makes sense for HQ to include in Umbraco CMS we will leave you some feedback on how we’d like to see it being implemented.
+
+If a larger pull request is encouraged by Umbraco HQ, the process will be similar to what is described in the [small PRs process](#small-prs) above, we strive to feedback within 14 days. Finalizing and merging the PR might take longer though as it will likely need to be picked up by the development team to make sure everything is in order. We’ll keep you posted on the progress.
+
+It is highly recommended that you speak to the HQ before making large, complex changes.
+
+### Pull request or package?
+
+If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and fix bugs.
+
+Eventually, a package could "graduate" to be included in the CMS.
diff --git a/.github/ISSUE_TEMPLATE/1_Bug.md b/.github/ISSUE_TEMPLATE/1_Bug.md
index 619452f700..d388af0d39 100644
--- a/.github/ISSUE_TEMPLATE/1_Bug.md
+++ b/.github/ISSUE_TEMPLATE/1_Bug.md
@@ -7,15 +7,15 @@ A brief description of the issue goes here.
+## Umbraco version
+
+I am seeing this issue on Umbraco version:
Reproduction
diff --git a/.github/README.md b/.github/README.md
index bdf9ef9f67..d6d978c3d6 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -1,4 +1,4 @@
-# [Umbraco CMS](https://umbraco.com) · [](../LICENSE.md) [](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [](CONTRIBUTING.md) [](https://pullreminders.com?ref=badge)
+# [Umbraco CMS](https://umbraco.com) · [](../LICENSE.md) [](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [](CONTRIBUTING.md) [](https://twitter.com/intent/follow?screen_name=umbraco)
Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
@@ -21,7 +21,7 @@ Please also see our [Code of Conduct](CODE_OF_CONDUCT.md).
[Umbraco Cloud](https://umbraco.com/cloud) is the easiest and fastest way to use Umbraco yet, with full support for all your custom .NET code and integrations. You're up and running in less than a minute, and your life will be made easier with automated upgrades and a built-in deployment engine. We offer a free 14-day trial, no credit card needed.
-If you want to DIY, you can [download Umbraco]((https://our.umbraco.com/download)) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Cloud, but you'll need to find a place to host it yourself, and handling deployments and upgrades will be all up to you.
+If you want to DIY, then you can [download Umbraco]((https://our.umbraco.com/download)) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Cloud, but you'll need to find a place to host it yourself, and handling deployments and upgrades will be all up to you.
## Documentation
@@ -29,7 +29,7 @@ The documentation for Umbraco CMS can be found [on Our Umbraco](https://our.umbr
## Join the Umbraco community
-Our friendly community is available 24/7 at the community hub we call ["Our Umbraco"](https://our.umbraco.com/). Our Umbraco features forums for questions and answers, documentation, downloadable plugins for Umbraco, and a rich collection of community resources.
+Our friendly community is available 24/7 at the community hub, we call ["Our Umbraco"](https://our.umbraco.com/). Our Umbraco features forums for questions and answers, documentation, downloadable plugins for Umbraco, and a rich collection of community resources.
Besides "Our", we all support each other also via Twitter: [Umbraco HQ](https://twitter.com/umbraco), [Release Updates](https://twitter.com/umbracoproject), [#umbraco](https://twitter.com/hashtag/umbraco)
diff --git a/.github/REVIEW_PROCESS.md b/.github/REVIEW_PROCESS.md
new file mode 100644
index 0000000000..917d25b090
--- /dev/null
+++ b/.github/REVIEW_PROCESS.md
@@ -0,0 +1,25 @@
+# Review process
+
+You're an awesome person and have sent us your contribution in the form of a pull request! It's now time to relax for a bit and wait for our response.
+
+In order to set some expectations, here's what happens next.
+
+## Review process
+
+You will get an initial reply within 48 hours (workdays) to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can.
+
+You will get feedback within at most 14 days after opening the PR. You’ll most likely get feedback sooner though. Then there are a few possible outcomes:
+
+- Your proposed change is awesome! We merge it in and it will be included in the next minor release of Umbraco
+- If the change is a high priority bug fix, we will cherry-pick it into the next patch release as well so that we can release it as soon as possible
+- Your proposed change is awesome but needs a bit more work, we’ll give you feedback on the changes we’d like to see
+- Your proposed change is awesome but.. not something we’re looking to include at this point. We’ll close your PR and the related issue (we’ll be nice about it!)
+
+## Are you still available?
+
+We understand you have other things to do and can't just drop everything to help us out.
+So if we’re asking for your help to improve the PR we’ll wait for two weeks to give you a fair chance to make changes. We’ll ask for an update if we don’t hear back from you after that time.
+
+If we don’t hear back from you for 4 weeks, we’ll close the PR so that it doesn’t just hang around forever. You’re very welcome to re-open it once you have some more time to spend on it.
+
+There will be times that we really like your proposed changes and we’ll finish the final improvements we’d like to see ourselves. You still get the credits and your commits will live on in the git repository.
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 585bd855b7..a0ff4d5b27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -132,7 +132,8 @@ src/Umbraco.Web.UI.Client/bower_components/*
preserve.belle
#Ignore Rule for output of generated documentation files from Grunt docserve
-src/Umbraco.Web.UI.Client/docs/api
+src/Umbraco.Web.UI.Docs/api
+src/Umbraco.Web.UI.Docs/package-lock.json
src/*.boltdata/
src/umbraco.sln.ide/*
src/.vs/
diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec
index 4aa354eba2..c8374bc2f7 100644
--- a/build/NuSpecs/UmbracoCms.Web.nuspec
+++ b/build/NuSpecs/UmbracoCms.Web.nuspec
@@ -25,10 +25,10 @@
not want this to happen as the alpha of the next major is, really, the next major already.
-->
-
-
+
+
-
+
diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec
index b7bfaaff5b..97e9ef3df2 100644
--- a/build/NuSpecs/UmbracoCms.nuspec
+++ b/build/NuSpecs/UmbracoCms.nuspec
@@ -30,7 +30,6 @@
-
diff --git a/build/NuSpecs/tools/Readme.txt b/build/NuSpecs/tools/Readme.txt
index e40b0dbc7e..b0583a2b4d 100644
--- a/build/NuSpecs/tools/Readme.txt
+++ b/build/NuSpecs/tools/Readme.txt
@@ -1,11 +1,11 @@
- 888
- 888
-888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b.
-888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b
-888 888 888 888 888 888 888 888 .d888888 888 888 888
-Y88b 888 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P
- "Y88888 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P"
+ 888
+ 888
+888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b.
+888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b
+888 888 888 888 888 888 888 888 .d888888 888 888 888
+Y88 88Y 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P
+ "Y888P" 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P"
------------------------------------------------------------------
diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt
index df364c64ed..ba88f808c0 100644
--- a/build/NuSpecs/tools/ReadmeUpgrade.txt
+++ b/build/NuSpecs/tools/ReadmeUpgrade.txt
@@ -1,12 +1,13 @@
- _ _ __ __ ____ _____ _____ ____
- | | | | \/ | _ \| __ \ /\ / ____/ __ \
- | | | | \ / | |_) | |__) | / \ | | | | | |
- | | | | |\/| | _ <| _ / / /\ \| | | | | |
- | |__| | | | | |_) | | \ \ / ____ | |___| |__| |
- \____/|_| |_|____/|_| \_/_/ \_\_____\____/
+ 888
+ 888
+888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b.
+888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b
+888 888 888 888 888 888 888 888 .d888888 888 888 888
+Y88 88Y 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P
+ "Y888P" 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P"
-----------------------------------------------------
+------------------------------------------------------------------
Don't forget to build!
@@ -25,6 +26,6 @@ The following items will now be automatically included when creating a deploy pa
system: umbraco, config\splashes and global.asax.
Please read the release notes on our.umbraco.com:
-http://our.umbraco.com/contribute/releases
+https://our.umbraco.com/contribute/releases
- Umbraco
diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt
index 14778e0f10..2b79f95c70 100644
--- a/build/NuSpecs/tools/Web.config.install.xdt
+++ b/build/NuSpecs/tools/Web.config.install.xdt
@@ -53,7 +53,7 @@
-
+
@@ -76,7 +76,7 @@
-
+
@@ -128,6 +128,7 @@
+
diff --git a/build/build-docs.ps1 b/build/build-docs.ps1
deleted file mode 100644
index 8cd3f090c7..0000000000
--- a/build/build-docs.ps1
+++ /dev/null
@@ -1,44 +0,0 @@
-$uenv=build/build.ps1 -get
-
-$src = "$($uenv.SolutionRoot)\src"
-$tmp = $uenv.BuildTemp
-$out = $uenv.BuildOutput
-$DocFxJson = "$src\ApiDocs\docfx.json"
-$DocFxSiteOutput = "$tmp\_site\*.*"
-
-################ Do the UI docs
-$uenv.CompileBelle()
-
-"Moving to Umbraco.Web.UI.Client folder"
-cd .\src\Umbraco.Web.UI.Client
-
-"Generating the docs and waiting before executing the next commands"
-& gulp docs | Out-Null
-
-# change baseUrl
-$BaseUrl = "https://our.umbraco.com/apidocs/v8/ui/"
-$IndexPath = "./docs/api/index.html"
-(Get-Content $IndexPath).replace('location.href.replace(rUrl, indexFile)', "`'" + $BaseUrl + "`'") | Set-Content $IndexPath
-
-# zip it
-& $uenv.BuildEnv.Zip a -tzip -r "$out\ui-docs.zip" "$src\Umbraco.Web.UI.Client\docs\api\*.*"
-
-
-################ Do the c# docs
-
-# Build the solution in debug mode
-$SolutionPath = Join-Path -Path $src -ChildPath "umbraco.sln"
-#$uenv.CompileUmbraco()
-
-#restore nuget packages
-$uenv.RestoreNuGet()
-
-# run DocFx
-$DocFx = $uenv.BuildEnv.DocFx
-
-Write-Host "$DocFxJson"
-& $DocFx metadata $DocFxJson
-& $DocFx build $DocFxJson
-
-# zip it
-& $uenv.BuildEnv.Zip a -tzip -r "$out\csharp-docs.zip" $DocFxSiteOutput
diff --git a/build/build.ps1 b/build/build.ps1
index e4994d2c4a..892654d3cd 100644
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -456,25 +456,24 @@
$ubuild.DefineMethod("PrepareAngularDocs",
{
Write-Host "Prepare Angular Documentation"
-
+
$src = "$($this.SolutionRoot)\src"
- $out = $this.BuildOutput
-
- $this.CompileBelle()
-
- "Moving to Umbraco.Web.UI.Client folder"
- cd .\src\Umbraco.Web.UI.Client
+ $out = $this.BuildOutput
+
+ "Moving to Umbraco.Web.UI.Docs folder"
+ cd ..\src\Umbraco.Web.UI.Docs
"Generating the docs and waiting before executing the next commands"
- & gulp docs | Out-Null
+ & npm install
+ & npx gulp docs
# change baseUrl
$BaseUrl = "https://our.umbraco.com/apidocs/v8/ui/"
- $IndexPath = "./docs/api/index.html"
+ $IndexPath = "./api/index.html"
(Get-Content $IndexPath).replace('location.href.replace(rUrl, indexFile)', "`'" + $BaseUrl + "`'") | Set-Content $IndexPath
# zip it
- & $this.BuildEnv.Zip a -tzip -r "$out\ui-docs.zip" "$src\Umbraco.Web.UI.Client\docs\api\*.*"
+ & $this.BuildEnv.Zip a -tzip -r "$out\ui-docs.zip" "$src\Umbraco.Web.UI.Docs\api\*.*"
})
$ubuild.DefineMethod("Build",
diff --git a/src/ApiDocs/umbracotemplate/styles/main.css b/src/ApiDocs/umbracotemplate/styles/main.css
index d74d51b150..802d42bc0a 100644
--- a/src/ApiDocs/umbracotemplate/styles/main.css
+++ b/src/ApiDocs/umbracotemplate/styles/main.css
@@ -1,65 +1,11 @@
body {
- color: rgba(0,0,0,.8);
-}
-.navbar-inverse {
- background: #a3db78;
-}
-.navbar-inverse .navbar-nav>li>a, .navbar-inverse .navbar-text {
- color: rgba(0,0,0,.8);
-}
-
-.navbar-inverse {
- border-color: transparent;
-}
-
-.sidetoc {
- background-color: #f5fbf1;
-}
-body .toc {
- background-color: #f5fbf1;
-}
-.sidefilter {
- background-color: #daf0c9;
-}
-.subnav {
- background-color: #f5fbf1;
-}
-
-.navbar-inverse .navbar-nav>.active>a {
- color: rgba(0,0,0,.8);
- background-color: #daf0c9;
-}
-
-.navbar-inverse .navbar-nav>.active>a:focus, .navbar-inverse .navbar-nav>.active>a:hover {
- color: rgba(0,0,0,.8);
- background-color: #daf0c9;
-}
-
-.btn-primary {
- color: rgba(0,0,0,.8);
- background-color: #fff;
- border-color: rgba(0,0,0,.8);
-}
-.btn-primary:hover {
- background-color: #daf0c9;
- color: rgba(0,0,0,.8);
- border-color: rgba(0,0,0,.8);
-}
-
-.toc .nav > li > a {
- color: rgba(0,0,0,.8);
-}
-
-button, a {
- color: #f36f21;
-}
-
-button:hover,
-button:focus,
-a:hover,
-a:focus {
- color: #143653;
- text-decoration: none;
+ color: #34393e;
+ font-family: 'Roboto', sans-serif;
+ line-height: 1.5;
+ font-size: 16px;
+ -ms-text-size-adjust: 100%;
+ -webkit-text-size-adjust: 100%;
+ word-wrap: break-word
}
.navbar-header .navbar-brand {
@@ -68,6 +14,202 @@ a:focus {
width:50px;
}
-.toc .nav > li.active > a {
- color: #f36f21;
+#_content>a {
+ margin-top: 5px;
}
+/* HEADINGS */
+
+h1 {
+ font-weight: 600;
+ font-size: 32px;
+}
+
+h2 {
+ font-weight: 600;
+ font-size: 24px;
+ line-height: 1.8;
+}
+
+h3 {
+ font-weight: 600;
+ font-size: 20px;
+ line-height: 1.8;
+}
+
+h5 {
+ font-size: 14px;
+ padding: 10px 0px;
+}
+
+article h1,
+article h2,
+article h3,
+article h4 {
+ margin-top: 35px;
+ margin-bottom: 15px;
+}
+
+article h4 {
+ padding-bottom: 8px;
+ border-bottom: 2px solid #ddd;
+}
+
+/* NAVBAR */
+
+.navbar-brand>img {
+ color: #fff;
+}
+
+.navbar {
+ border: none;
+ /* Both navbars use box-shadow */
+ -webkit-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5);
+ -moz-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5);
+ box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5);
+}
+
+.subnav {
+ border-top: 1px solid #ddd;
+ background-color: #fff;
+}
+
+.navbar-inverse {
+ background-color: #303ea1;
+ z-index: 100;
+}
+
+.navbar-inverse .navbar-nav>li>a,
+.navbar-inverse .navbar-text {
+ color: #fff;
+ background-color: #303ea1;
+ border-bottom: 3px solid transparent;
+ padding-bottom: 12px;
+}
+
+.navbar-inverse .navbar-nav>li>a:focus,
+.navbar-inverse .navbar-nav>li>a:hover {
+ color: #fff;
+ background-color: #303ea1;
+ border-bottom: 3px solid white;
+}
+
+.navbar-inverse .navbar-nav>.active>a,
+.navbar-inverse .navbar-nav>.active>a:focus,
+.navbar-inverse .navbar-nav>.active>a:hover {
+ color: #fff;
+ background-color: #303ea1;
+ border-bottom: 3px solid white;
+}
+
+.navbar-form .form-control {
+ border: none;
+ border-radius: 20px;
+}
+
+/* SIDEBAR */
+
+.toc .level1>li {
+ font-weight: 400;
+}
+
+.toc .nav>li>a {
+ color: #34393e;
+}
+
+.sidefilter {
+ background-color: #fff;
+ border-left: none;
+ border-right: none;
+}
+
+.sidefilter {
+ background-color: #fff;
+ border-left: none;
+ border-right: none;
+}
+
+.toc-filter {
+ padding: 10px;
+ margin: 0;
+}
+
+.toc-filter>input {
+ border: 2px solid #ddd;
+ border-radius: 20px;
+}
+
+.toc-filter>.filter-icon {
+ display: none;
+}
+
+.sidetoc>.toc {
+ background-color: #fff;
+ overflow-x: hidden;
+}
+
+.sidetoc {
+ background-color: #fff;
+ border: none;
+}
+
+/* ALERTS */
+
+.alert {
+ padding: 0px 0px 5px 0px;
+ color: inherit;
+ background-color: inherit;
+ border: none;
+ box-shadow: 0px 2px 2px 0px rgba(100, 100, 100, 0.4);
+}
+
+.alert>p {
+ margin-bottom: 0;
+ padding: 5px 10px;
+}
+
+.alert>ul {
+ margin-bottom: 0;
+ padding: 5px 40px;
+}
+
+.alert>h5 {
+ padding: 10px 15px;
+ margin-top: 0;
+ text-transform: uppercase;
+ font-weight: bold;
+ border-radius: 4px 4px 0 0;
+}
+
+.alert-info>h5 {
+ color: #1976d2;
+ border-bottom: 4px solid #1976d2;
+ background-color: #e3f2fd;
+}
+
+.alert-warning>h5 {
+ color: #f57f17;
+ border-bottom: 4px solid #f57f17;
+ background-color: #fff3e0;
+}
+
+.alert-danger>h5 {
+ color: #d32f2f;
+ border-bottom: 4px solid #d32f2f;
+ background-color: #ffebee;
+}
+
+/* CODE HIGHLIGHT */
+pre {
+ padding: 9.5px;
+ margin: 0 0 10px;
+ font-size: 13px;
+ word-break: break-all;
+ word-wrap: break-word;
+ background-color: #fffaef;
+ border-radius: 4px;
+ box-shadow: 0px 1px 4px 1px rgba(100, 100, 100, 0.4);
+}
+
+.sideaffix {
+ overflow: visible;
+}
\ No newline at end of file
diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs
index a4e859988e..bf3a271d32 100644
--- a/src/SolutionInfo.cs
+++ b/src/SolutionInfo.cs
@@ -18,5 +18,5 @@ using System.Resources;
[assembly: AssemblyVersion("8.0.0")]
// these are FYI and changed automatically
-[assembly: AssemblyFileVersion("8.2.0")]
-[assembly: AssemblyInformationalVersion("8.2.0")]
+[assembly: AssemblyFileVersion("8.3.0")]
+[assembly: AssemblyInformationalVersion("8.3.0")]
diff --git a/src/Umbraco.Core/AsyncLock.cs b/src/Umbraco.Core/AsyncLock.cs
index 158b132f26..6dd866705e 100644
--- a/src/Umbraco.Core/AsyncLock.cs
+++ b/src/Umbraco.Core/AsyncLock.cs
@@ -67,31 +67,34 @@ namespace Umbraco.Core
: new NamedSemaphoreReleaser(_semaphore2);
}
- public Task LockAsync()
- {
- var wait = _semaphore != null
- ? _semaphore.WaitAsync()
- : _semaphore2.WaitOneAsync();
+ //NOTE: We don't use the "Async" part of this lock at all
+ //TODO: Remove this and rename this class something like SystemWideLock, then we can re-instate this logic if we ever need an Async lock again
- return wait.IsCompleted
- ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named
- : wait.ContinueWith((_, state) => (((AsyncLock) state).CreateReleaser()),
- this, CancellationToken.None,
- TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
- }
+ //public Task LockAsync()
+ //{
+ // var wait = _semaphore != null
+ // ? _semaphore.WaitAsync()
+ // : _semaphore2.WaitOneAsync();
- public Task LockAsync(int millisecondsTimeout)
- {
- var wait = _semaphore != null
- ? _semaphore.WaitAsync(millisecondsTimeout)
- : _semaphore2.WaitOneAsync(millisecondsTimeout);
+ // return wait.IsCompleted
+ // ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named
+ // : wait.ContinueWith((_, state) => (((AsyncLock) state).CreateReleaser()),
+ // this, CancellationToken.None,
+ // TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
+ //}
- return wait.IsCompleted
- ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named
- : wait.ContinueWith((_, state) => (((AsyncLock)state).CreateReleaser()),
- this, CancellationToken.None,
- TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
- }
+ //public Task LockAsync(int millisecondsTimeout)
+ //{
+ // var wait = _semaphore != null
+ // ? _semaphore.WaitAsync(millisecondsTimeout)
+ // : _semaphore2.WaitOneAsync(millisecondsTimeout);
+
+ // return wait.IsCompleted
+ // ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named
+ // : wait.ContinueWith((_, state) => (((AsyncLock)state).CreateReleaser()),
+ // this, CancellationToken.None,
+ // TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
+ //}
public IDisposable Lock()
{
diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs
index e8f93d636a..b8ee0e97c4 100644
--- a/src/Umbraco.Core/Cache/CacheKeys.cs
+++ b/src/Umbraco.Core/Cache/CacheKeys.cs
@@ -11,5 +11,6 @@
public const string TemplateFrontEndCacheKey = "template";
public const string MacroContentCacheKey = "macroContent_"; // used in MacroRenderers
+ public const string MacroFromAliasCacheKey = "macroFromAlias_";
}
}
diff --git a/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs b/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs
index 078a505be9..8518d907b5 100644
--- a/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs
+++ b/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs
@@ -90,7 +90,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions
// register the IFileSystem supporting the IMediaFileSystem
// THIS IS THE ONLY THING THAT NEEDS TO CHANGE, IN ORDER TO REPLACE THE UNDERLYING FILESYSTEM
// and, SupportingFileSystem.For() returns the underlying filesystem
- composition.SetMediaFileSystem(() => new PhysicalFileSystem("~/media"));
+ composition.SetMediaFileSystem(() => new PhysicalFileSystem(SystemDirectories.Media));
return composition;
}
diff --git a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs
index 0baefe104b..d252c58730 100644
--- a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs
+++ b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs
@@ -96,7 +96,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions
var pluginLangFolders = appPlugins.Exists == false
? Enumerable.Empty()
: appPlugins.GetDirectories()
- .SelectMany(x => x.GetDirectories("Lang"))
+ .SelectMany(x => x.GetDirectories("Lang", SearchOption.AllDirectories))
.SelectMany(x => x.GetFiles("*.xml", SearchOption.TopDirectoryOnly))
.Select(x => new LocalizedTextServiceSupplementaryFileSource(x, false));
diff --git a/src/Umbraco.Core/Composing/Lifetime.cs b/src/Umbraco.Core/Composing/Lifetime.cs
index 1a2cc3119a..012555be5e 100644
--- a/src/Umbraco.Core/Composing/Lifetime.cs
+++ b/src/Umbraco.Core/Composing/Lifetime.cs
@@ -12,31 +12,62 @@
/// or MS.DI, PerDependency in Autofac.
Transient,
+ // TODO: We need to fix this up, currently LightInject is the only one that behaves differently from all other containers.
+ // ... the simple fix would be to map this to PerScopeLifetime in LI but need to wait on a response here https://github.com/seesharper/LightInject/issues/494#issuecomment-518942625
+ //
+ // we use it for controllers, httpContextBase and other request scoped objects: MembershpHelper, TagQuery, UmbracoTreeSearcher and ISearchableTree
+ // - so that they are automatically disposed at the end of the scope (ie request)
+ // - not sure they should not be simply 'scoped'?
+
///
/// One unique instance per request.
///
- // TODO: review lifetimes for LightInject vs other containers
- // currently, corresponds to 'Request' in LightInject which is 'Transient + disposed by Scope'
- // but NOT (in LightInject) a per-web-request lifetime, more a TransientScoped
- //
- // we use it for controllers, httpContextBase and umbracoContext
- // - so that they are automatically disposed at the end of the scope (ie request)
- // - not sure they should not be simply 'scoped'?
- //
- // Castle has an extra PerWebRequest something, and others use scope
- // what about Request before first request ie during application startup?
- // see http://blog.ploeh.dk/2009/11/17/UsingCastleWindsor'sPerWebRequestlifestylewithASP.NETMVConIIS7/
- // Castle ends up requiring a special scope manager too
- // see https://groups.google.com/forum/#!topic/castle-project-users/1E2W9LVIYR4
- //
- // but maybe also - why are we requiring scoped services at startup?
+ ///
+ ///
+ /// Any instance created with this lifetime will be disposed at the end of a request.
+ ///
+ /// Corresponds to
+ ///
+ /// PerRequestLifeTime in LightInject - means transient but disposed at the end of the current web request.
+ /// see: https://github.com/seesharper/LightInject/issues/494#issuecomment-518493262
+ ///
+ ///
+ /// Scoped in MS.DI - means one per web request.
+ /// see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2#service-lifetimes
+ ///
+ /// InstancePerRequest in Autofac - means one per web request.
+ /// see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-request
+ /// But "Behind the scenes, though, it’s still just instance per matching lifetime scope."
+ ///
+ ///
+ /// LifestylePerWebRequest in Castle Windsor - means one per web request.
+ /// see https://github.com/castleproject/Windsor/blob/master/docs/mvc-tutorial-part-7-lifestyles.md#the-perwebrequest-lifestyle
+ ///
+ ///
Request,
///
- /// One unique instance per container scope.
+ /// One unique instance per scope.
///
- /// Corresponds to Scope in LightInject, Scoped in MS.DI
- /// or Castle Windsor, PerLifetimeScope in Autofac.
+ ///
+ ///
+ /// Any instance created with this lifetime will be disposed at the end of the current scope.
+ ///
+ /// Corresponds to
+ /// PerScopeLifetime in LightInject (when in a request, means one per web request)
+ ///
+ /// Scoped in MS.DI (when in a request, means one per web request)
+ /// see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2#service-lifetimes
+ ///
+ /// InstancePerLifetimeScope in Autofac (when in a request, means one per web request)
+ /// see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-lifetime-scope
+ /// Also note that Autofac's InstancePerRequest is the same as this, see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-request
+ /// it says "Behind the scenes, though, it’s still just instance per matching lifetime scope."
+ ///
+ ///
+ /// LifestyleScoped in Castle Windsor
+ ///
+ ///
Scope,
///
diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs
index 49f4481a59..a888e3c42b 100644
--- a/src/Umbraco.Core/Configuration/GlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs
@@ -33,7 +33,6 @@ namespace Umbraco.Core.Configuration
///
private static void ResetInternal()
{
- GlobalSettingsExtensions.Reset();
_reservedPaths = null;
_reservedUrls = null;
HasSmtpServer = null;
diff --git a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs
index 6bfb7ea904..bc76caacee 100644
--- a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs
+++ b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using System.Web;
using System.Web.Routing;
using Umbraco.Core.IO;
@@ -9,22 +11,9 @@ namespace Umbraco.Core.Configuration
{
public static class GlobalSettingsExtensions
{
- ///
- /// Used in unit testing to reset all config items, this is automatically called by GlobalSettings.Reset()
- ///
- internal static void Reset()
- {
- _reservedUrlsCache = null;
- _mvcArea = null;
- }
-
- private static readonly object Locker = new object();
- //make this volatile so that we can ensure thread safety with a double check lock
- private static volatile string _reservedUrlsCache;
- private static string _reservedPathsCache;
- private static HashSet _reservedList = new HashSet();
private static string _mvcArea;
+
///
/// This returns the string of the MVC Area route.
///
@@ -40,6 +29,13 @@ namespace Umbraco.Core.Configuration
{
if (_mvcArea != null) return _mvcArea;
+ _mvcArea = GetUmbracoMvcAreaNoCache(globalSettings);
+
+ return _mvcArea;
+ }
+
+ internal static string GetUmbracoMvcAreaNoCache(this IGlobalSettings globalSettings)
+ {
if (globalSettings.Path.IsNullOrWhiteSpace())
{
throw new InvalidOperationException("Cannot create an MVC Area path without the umbracoPath specified");
@@ -48,95 +44,8 @@ namespace Umbraco.Core.Configuration
var path = globalSettings.Path;
if (path.StartsWith(SystemDirectories.Root)) // beware of TrimStart, see U4-2518
path = path.Substring(SystemDirectories.Root.Length);
- _mvcArea = path.TrimStart('~').TrimStart('/').Replace('/', '-').Trim().ToLower();
- return _mvcArea;
+ return path.TrimStart('~').TrimStart('/').Replace('/', '-').Trim().ToLower();
}
- ///
- /// Determines whether the specified URL is reserved or is inside a reserved path.
- ///
- ///
- /// The URL to check.
- ///
- /// true if the specified URL is reserved; otherwise, false.
- ///
- internal static bool IsReservedPathOrUrl(this IGlobalSettings globalSettings, string url)
- {
- if (_reservedUrlsCache == null)
- {
- lock (Locker)
- {
- if (_reservedUrlsCache == null)
- {
- // store references to strings to determine changes
- _reservedPathsCache = globalSettings.ReservedPaths;
- _reservedUrlsCache = globalSettings.ReservedUrls;
-
- // add URLs and paths to a new list
- var newReservedList = new HashSet();
- foreach (var reservedUrlTrimmed in _reservedUrlsCache
- .Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Trim().ToLowerInvariant())
- .Where(x => x.IsNullOrWhiteSpace() == false)
- .Select(reservedUrl => IOHelper.ResolveUrl(reservedUrl).Trim().EnsureStartsWith("/"))
- .Where(reservedUrlTrimmed => reservedUrlTrimmed.IsNullOrWhiteSpace() == false))
- {
- newReservedList.Add(reservedUrlTrimmed);
- }
-
- foreach (var reservedPathTrimmed in _reservedPathsCache
- .Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Trim().ToLowerInvariant())
- .Where(x => x.IsNullOrWhiteSpace() == false)
- .Select(reservedPath => IOHelper.ResolveUrl(reservedPath).Trim().EnsureStartsWith("/").EnsureEndsWith("/"))
- .Where(reservedPathTrimmed => reservedPathTrimmed.IsNullOrWhiteSpace() == false))
- {
- newReservedList.Add(reservedPathTrimmed);
- }
-
- // use the new list from now on
- _reservedList = newReservedList;
- }
- }
- }
-
- //The url should be cleaned up before checking:
- // * If it doesn't contain an '.' in the path then we assume it is a path based URL, if that is the case we should add an trailing '/' because all of our reservedPaths use a trailing '/'
- // * We shouldn't be comparing the query at all
- var pathPart = url.Split(new[] {'?'}, StringSplitOptions.RemoveEmptyEntries)[0].ToLowerInvariant();
- if (pathPart.Contains(".") == false)
- {
- pathPart = pathPart.EnsureEndsWith('/');
- }
-
- // return true if url starts with an element of the reserved list
- return _reservedList.Any(x => pathPart.InvariantStartsWith(x));
- }
-
- ///
- /// Determines whether the current request is reserved based on the route table and
- /// whether the specified URL is reserved or is inside a reserved path.
- ///
- ///
- ///
- ///
- /// The route collection to lookup the request in
- ///
- internal static bool IsReservedPathOrUrl(this IGlobalSettings globalSettings, string url, HttpContextBase httpContext, RouteCollection routes)
- {
- if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
- if (routes == null) throw new ArgumentNullException(nameof(routes));
-
- //check if the current request matches a route, if so then it is reserved.
- //TODO: This value should be cached! Else this is doing double routing in MVC every request!
- var route = routes.GetRouteData(httpContext);
- if (route != null)
- return true;
-
- //continue with the standard ignore routine
- return globalSettings.IsReservedPathOrUrl(url);
- }
-
-
}
}
diff --git a/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs b/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs
index e447f7f493..9a11b0ef3e 100644
--- a/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs
+++ b/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs
@@ -6,6 +6,7 @@ namespace Umbraco.Core.Configuration.Grid
public interface IGridEditorConfig
{
string Name { get; }
+ string NameTemplate { get; }
string Alias { get; }
string View { get; }
string Render { get; }
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs
index 5163dda1f6..77ad7df0dc 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs
@@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
{
internal class ContentElement : UmbracoConfigurationElement, IContentSection
{
- private const string DefaultPreviewBadge = @"In Preview Mode - click to end";
+ private const string DefaultPreviewBadge = @"";
[ConfigurationProperty("imaging")]
internal ContentImagingElement Imaging => (ContentImagingElement) this["imaging"];
diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs
index 0c2e246721..eb2b3525a7 100644
--- a/src/Umbraco.Core/Constants-PropertyEditors.cs
+++ b/src/Umbraco.Core/Constants-PropertyEditors.cs
@@ -14,6 +14,23 @@ namespace Umbraco.Core
///
public const string InternalGenericPropertiesPrefix = "_umb_";
+ public static class Legacy
+ {
+ public static class Aliases
+ {
+ public const string Textbox = "Umbraco.Textbox";
+ public const string Date = "Umbraco.Date";
+ public const string ContentPicker2 = "Umbraco.ContentPicker2";
+ public const string MediaPicker2 = "Umbraco.MediaPicker2";
+ public const string MemberPicker2 = "Umbraco.MemberPicker2";
+ public const string MultiNodeTreePicker2 = "Umbraco.MultiNodeTreePicker2";
+ public const string TextboxMultiple = "Umbraco.TextboxMultiple";
+ public const string RelatedLinks2 = "Umbraco.RelatedLinks2";
+ public const string RelatedLinks = "Umbraco.RelatedLinks";
+
+ }
+ }
+
///
/// Defines Umbraco built-in property editor aliases.
///
@@ -68,17 +85,17 @@ namespace Umbraco.Core
/// ListView.
///
public const string ListView = "Umbraco.ListView";
-
- ///
- /// Macro Container.
- ///
- public const string MacroContainer = "Umbraco.MacroContainer";
-
+
///
/// Media Picker.
///
public const string MediaPicker = "Umbraco.MediaPicker";
+ ///
+ /// Multiple Media Picker.
+ ///
+ public const string MultipleMediaPicker = "Umbraco.MultipleMediaPicker";
+
///
/// Member Picker.
///
@@ -186,6 +203,24 @@ namespace Umbraco.Core
/// Must be a valid value.
public const string DataValueType = "umbracoDataValueType";
}
+
+ ///
+ /// Defines Umbraco's built-in property editor groups.
+ ///
+ public static class Groups
+ {
+ public const string Common = "Common";
+
+ public const string Lists = "Lists";
+
+ public const string Media = "Media";
+
+ public const string People = "People";
+
+ public const string Pickers = "Pickers";
+
+ public const string RichContent = "Rich Content";
+ }
}
}
}
diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs
index 9fdc5f0b90..5b157307ab 100644
--- a/src/Umbraco.Core/ContentVariationExtensions.cs
+++ b/src/Umbraco.Core/ContentVariationExtensions.cs
@@ -150,39 +150,46 @@ namespace Umbraco.Core
culture = culture.NullOrWhiteSpaceAsNull();
segment = segment.NullOrWhiteSpaceAsNull();
- bool Validate(bool variesBy, string value)
- {
- if (variesBy)
- {
- // varies by
- // in exact mode, the value cannot be null (but it can be a wildcard)
- // in !wildcards mode, the value cannot be a wildcard (but it can be null)
- if ((exact && value == null) || (!wildcards && value == "*"))
- return false;
- }
- else
- {
- // does not vary by value
- // the value cannot have a value
- // unless wildcards and it's "*"
- if (value != null && (!wildcards || value != "*"))
- return false;
- }
-
- return true;
- }
-
- if (!Validate(variation.VariesByCulture(), culture))
+ // if wildcards are disabled, do not allow "*"
+ if (!wildcards && (culture == "*" || segment == "*"))
{
if (throwIfInvalid)
- throw new NotSupportedException($"Culture value \"{culture ?? ""}\" is invalid.");
+ throw new NotSupportedException($"Variation wildcards are not supported.");
return false;
}
- if (!Validate(variation.VariesBySegment(), segment))
+ if (variation.VariesByCulture())
+ {
+ // varies by culture
+ // in exact mode, the culture cannot be null
+ if (exact && culture == null)
+ {
+ if (throwIfInvalid)
+ throw new NotSupportedException($"Culture may not be null because culture variation is enabled.");
+ return false;
+ }
+ }
+ else
+ {
+ // does not vary by culture
+ // the culture cannot have a value
+ // unless wildcards and it's "*"
+ if (culture != null && !(wildcards && culture == "*"))
+ {
+ if (throwIfInvalid)
+ throw new NotSupportedException($"Culture \"{culture}\" is invalid because culture variation is disabled.");
+ return false;
+ }
+ }
+
+ // if it does not vary by segment
+ // the segment cannot have a value
+ // segment may always be null, even when the ContentVariation.Segment flag is set for this variation,
+ // therefore the exact parameter is not used in segment validation.
+ if (!variation.VariesBySegment() && segment != null && !(wildcards && segment == "*"))
{
if (throwIfInvalid)
- throw new NotSupportedException($"Segment value \"{segment ?? ""}\" is invalid.");
+ throw new NotSupportedException($"Segment \"{segment}\" is invalid because segment variation is disabled.");
return false;
}
diff --git a/src/Umbraco.Core/DateTimeExtensions.cs b/src/Umbraco.Core/DateTimeExtensions.cs
index 66b788f9c8..f665aaa8ed 100644
--- a/src/Umbraco.Core/DateTimeExtensions.cs
+++ b/src/Umbraco.Core/DateTimeExtensions.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text;
@@ -15,7 +16,7 @@ namespace Umbraco.Core
///
public static string ToIsoString(this DateTime dt)
{
- return dt.ToString("yyyy-MM-dd HH:mm:ss");
+ return dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
}
public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo)
diff --git a/src/Umbraco.Core/Exceptions/PanicException.cs b/src/Umbraco.Core/Exceptions/PanicException.cs
new file mode 100644
index 0000000000..c3564d2834
--- /dev/null
+++ b/src/Umbraco.Core/Exceptions/PanicException.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Core.Exceptions
+{
+ ///
+ /// Represents an internal exception that in theory should never been thrown, it is only thrown in circumstances that should never happen.
+ ///
+ ///
+ [Serializable]
+ internal class PanicException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PanicException()
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public PanicException(string message)
+ : base(message)
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified.
+ public PanicException(string message, Exception innerException)
+ : base(message, innerException)
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The that holds the serialized object data about the exception being thrown.
+ /// The that contains contextual information about the source or destination.
+ protected PanicException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
+ }
+}
diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs
index 2f33d82bdc..d6fb63b0a1 100644
--- a/src/Umbraco.Core/IO/SystemDirectories.cs
+++ b/src/Umbraco.Core/IO/SystemDirectories.cs
@@ -15,6 +15,8 @@ namespace Umbraco.Core.IO
public static string TempFileUploads => TempData + "/FileUploads";
+ public static string TempImageUploads => TempFileUploads + "/rte";
+
public static string Install => "~/install";
public static string AppCode => "~/App_Code";
diff --git a/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs
index b39a3f38df..dbdd7842ba 100644
--- a/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs
+++ b/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs
@@ -42,6 +42,12 @@ namespace Umbraco.Core.Logging.Viewer
bool CheckCanOpenLogs(LogTimePeriod logTimePeriod);
+ ///
+ /// Gets the current Serilog minimum log level
+ ///
+ ///
+ string GetLogLevel();
+
///
/// Returns the collection of logs
///
diff --git a/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs b/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs
index acb2f5dbf9..607c20e601 100644
--- a/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs
+++ b/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs
@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Xml;
using Newtonsoft.Json;
+using Serilog;
using Serilog.Events;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
+using Formatting = Newtonsoft.Json.Formatting;
namespace Umbraco.Core.Logging.Viewer
{
@@ -89,6 +92,16 @@ namespace Umbraco.Core.Logging.Viewer
return errorCounter.Count;
}
+ ///
+ /// Get the Serilog minimum-level value from the config file.
+ ///
+ ///
+ public string GetLogLevel()
+ {
+ var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.Logger.IsEnabled)?.Min() ?? null;
+ return logLevel?.ToString() ?? "";
+ }
+
public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod)
{
var counter = new CountingFilter();
diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs
index ca875c2167..5da1062275 100644
--- a/src/Umbraco.Core/MainDom.cs
+++ b/src/Umbraco.Core/MainDom.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Security.Cryptography;
using System.Threading;
using System.Web.Hosting;
using Umbraco.Core.Logging;
@@ -64,7 +65,7 @@ namespace Umbraco.Core
// a new process for the same application path
var appPath = HostingEnvironment.ApplicationPhysicalPath;
- var hash = (appId + ":::" + appPath).ToSHA1();
+ var hash = (appId + ":::" + appPath).GenerateHash();
var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK";
_asyncLock = new AsyncLock(lockName);
@@ -113,7 +114,7 @@ namespace Umbraco.Core
lock (_locko)
{
- _logger.Debug("Signaled {Signaled} ({SignalSource})", _signaled ? "(again)" : string.Empty, source);
+ _logger.Debug("Signaled ({Signaled}) ({SignalSource})", _signaled ? "again" : "first", source);
if (_signaled) return;
if (_isMainDom == false) return; // probably not needed
_signaled = true;
@@ -171,6 +172,7 @@ namespace Umbraco.Core
// if more than 1 instance reach that point, one will get the lock
// and the other one will timeout, which is accepted
+ //TODO: This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset?
_asyncLocker = _asyncLock.Lock(LockTimeoutMilliseconds);
_isMainDom = true;
@@ -181,6 +183,9 @@ namespace Umbraco.Core
// which is accepted
_signal.Reset();
+
+ //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread
+
_signal.WaitOneAsync()
.ContinueWith(_ => OnSignal("signal"));
diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs
index 8915ebcf74..e62825101c 100644
--- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs
+++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs
@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
+using Umbraco.Core.Exceptions;
namespace Umbraco.Core.Mapping
{
@@ -231,10 +232,10 @@ namespace Umbraco.Core.Mapping
if (ctor != null && map != null)
{
// register (for next time) and do it now (for this time)
- object NCtor(object s, MapperContext c) => MapEnumerableInternal((IEnumerable) s, targetGenericArg, ctor, map, c);
+ object NCtor(object s, MapperContext c) => MapEnumerableInternal((IEnumerable)s, targetGenericArg, ctor, map, c);
DefineCtors(sourceType)[targetType] = NCtor;
DefineMaps(sourceType)[targetType] = Identity;
- return (TTarget) NCtor(source, context);
+ return (TTarget)NCtor(source, context);
}
throw new InvalidOperationException($"Don't know how to map {sourceGenericArg.FullName} to {targetGenericArg.FullName}, so don't know how to map {sourceType.FullName} to {targetType.FullName}.");
@@ -259,13 +260,13 @@ namespace Umbraco.Core.Mapping
if (typeof(TTarget).IsArray)
{
var elementType = typeof(TTarget).GetElementType();
- if (elementType == null) throw new Exception("panic");
+ if (elementType == null) throw new PanicException("elementType == null which should never occur");
var targetArray = Array.CreateInstance(elementType, targetList.Count);
targetList.CopyTo(targetArray, 0);
target = targetArray;
}
- return (TTarget) target;
+ return (TTarget)target;
}
///
@@ -342,7 +343,21 @@ namespace Umbraco.Core.Mapping
if (ctor == null) return null;
- _ctors[sourceType] = sourceCtor;
+ _ctors.AddOrUpdate(sourceType, sourceCtor, (k, v) =>
+ {
+ // Add missing constructors
+ foreach (var c in sourceCtor)
+ {
+ if (!v.ContainsKey(c.Key))
+ {
+ v.Add(c.Key, c.Value);
+ }
+ }
+
+ return v;
+ });
+
+
return ctor;
}
@@ -367,7 +382,17 @@ namespace Umbraco.Core.Mapping
if (map == null) return null;
- _maps[sourceType] = sourceMap;
+ if (_maps.ContainsKey(sourceType))
+ {
+ foreach (var m in sourceMap)
+ {
+ if (!_maps[sourceType].TryGetValue(m.Key, out _))
+ _maps[sourceType].Add(m.Key, m.Value);
+ }
+ }
+ else
+ _maps[sourceType] = sourceMap;
+
return map;
}
@@ -382,7 +407,7 @@ namespace Umbraco.Core.Mapping
{
if (type.IsArray) return type.GetElementType();
if (type.IsGenericType) return type.GenericTypeArguments[0];
- throw new Exception("panic");
+ throw new PanicException($"Could not get enumerable or array type from {type}");
}
///
diff --git a/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs
index 9b13457b76..df74bf7c87 100644
--- a/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs
+++ b/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs
@@ -1,5 +1,7 @@
-using System.Linq;
+using System.Collections.Generic;
+using System.Linq;
using NPoco;
+using Umbraco.Core;
using Umbraco.Core.Migrations.Expressions.Common;
using Umbraco.Core.Persistence.SqlSyntax;
@@ -27,31 +29,57 @@ namespace Umbraco.Core.Migrations.Expressions.Delete.KeysAndIndexes
{
_context.BuildingExpression = false;
+ //get a list of all constraints - this will include all PK, FK and unique constraints
+ var tableConstraints = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList();
+
+ //get a list of defined indexes - this will include all indexes, unique indexes and unique constraint indexes
+ var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList();
+
+ var uniqueConstraintNames = tableConstraints.Where(x => !x.Item2.InvariantStartsWith("PK_") && !x.Item2.InvariantStartsWith("FK_")).Select(x => x.Item2);
+ var indexNames = indexes.Select(x => x.IndexName).ToList();
+
// drop keys
if (DeleteLocal || DeleteForeign)
{
// table, constraint
- var tableKeys = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList();
+
if (DeleteForeign)
{
- foreach (var key in tableKeys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("FK_")))
+ //In some cases not all FK's are prefixed with "FK" :/ mostly with old upgraded databases so we need to check if it's either:
+ // * starts with FK OR
+ // * doesn't start with PK_ and doesn't exist in the list of indexes
+
+ foreach (var key in tableConstraints.Where(x => x.Item1 == TableName
+ && (x.Item2.InvariantStartsWith("FK_") || (!x.Item2.InvariantStartsWith("PK_") && !indexNames.InvariantContains(x.Item2)))))
+ {
Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do();
+ }
+
}
if (DeleteLocal)
{
- foreach (var key in tableKeys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("PK_")))
+ foreach (var key in tableConstraints.Where(x => x.Item1 == TableName && x.Item2.InvariantStartsWith("PK_")))
Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do();
- // note: we do *not* delete the DEFAULT constraints
+ // note: we do *not* delete the DEFAULT constraints and if we wanted to we'd have to deal with that in interesting ways
+ // since SQL server has a specific way to handle that, see SqlServerSyntaxProvider.GetDefaultConstraintsPerColumn
}
}
// drop indexes
if (DeleteLocal)
- {
- var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList();
+ {
foreach (var index in indexes.Where(x => x.TableName == TableName))
- Delete.Index(index.IndexName).OnTable(index.TableName).Do();
+ {
+ //if this is a unique constraint we need to drop the constraint, else drop the index
+ //to figure this out, the index must be tagged as unique and it must exist in the tableConstraints
+
+ if (index.IsUnique && uniqueConstraintNames.InvariantContains(index.IndexName))
+ Delete.UniqueConstraint(index.IndexName).FromTable(index.TableName).Do();
+ else
+ Delete.Index(index.IndexName).OnTable(index.TableName).Do();
+ }
+
}
}
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
index dd5c17713a..94d8cfbc62 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
@@ -190,6 +190,7 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Settings });
_database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Users });
_database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Forms });
+ _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Translation });
_database.Insert(new UserGroup2AppDto { UserGroupId = 2, AppAlias = Constants.Applications.Content });
@@ -226,8 +227,8 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
- _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
- _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
+ _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
+ _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs
index 2f20f01728..309f8acbc3 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using System.Linq;
using NPoco;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
@@ -71,10 +72,21 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
// flip known property types
- var intPropertyTypes = new[] { 7, 8, 29 };
- var bigintPropertyTypes = new[] { 9, 26 };
- var dtPropertyTypes = new[] { 32, 33, 34 };
+ var labelPropertyTypes = Database.Fetch(Sql()
+ .Select(x => x.Id, x => x.Alias)
+ .From()
+ .Where(x => x.DataTypeId == Constants.DataTypes.LabelString)
+ );
+ var intPropertyAliases = new[] { Constants.Conventions.Media.Width, Constants.Conventions.Media.Height, Constants.Conventions.Member.FailedPasswordAttempts };
+ var bigintPropertyAliases = new[] { Constants.Conventions.Media.Bytes };
+ var dtPropertyAliases = new[] { Constants.Conventions.Member.LastLockoutDate, Constants.Conventions.Member.LastLoginDate, Constants.Conventions.Member.LastPasswordChangeDate };
+
+ var intPropertyTypes = labelPropertyTypes.Where(pt => intPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray();
+ var bigintPropertyTypes = labelPropertyTypes.Where(pt => bigintPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray();
+ var dtPropertyTypes = labelPropertyTypes.Where(pt => dtPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray();
+
+ Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelInt)).WhereIn(x => x.Id, intPropertyTypes));
Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelInt)).WhereIn(x => x.Id, intPropertyTypes));
Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelBigint)).WhereIn(x => x.Id, bigintPropertyTypes));
Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelDateTime)).WhereIn(x => x.Id, dtPropertyTypes));
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs
index 44f7affe8e..1956876402 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs
@@ -19,8 +19,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
var sqlDataTypes = Sql()
.Select()
.From()
- .Where(x => x.EditorAlias == "Umbraco.RelatedLinks"
- || x.EditorAlias == "Umbraco.RelatedLinks2");
+ .Where(x => x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks
+ || x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2);
var dataTypes = Database.Fetch(sqlDataTypes);
var dataTypeIds = dataTypes.Select(x => x.NodeId).ToList();
@@ -72,6 +72,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
{
var sqlNodeData = Sql()
.Select()
+ .From()
.Where(x => x.NodeId == intId);
var node = Database.Fetch(sqlNodeData).FirstOrDefault();
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs
index 438b02385b..95b272dcb4 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Umbraco.Core.Composing;
@@ -18,6 +19,18 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
private readonly PropertyEditorCollection _propertyEditors;
private readonly ILogger _logger;
+ private static readonly ISet LegacyAliases = new HashSet()
+ {
+ Constants.PropertyEditors.Legacy.Aliases.Date,
+ Constants.PropertyEditors.Legacy.Aliases.Textbox,
+ Constants.PropertyEditors.Legacy.Aliases.ContentPicker2,
+ Constants.PropertyEditors.Legacy.Aliases.MediaPicker2,
+ Constants.PropertyEditors.Legacy.Aliases.MemberPicker2,
+ Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2,
+ Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple,
+ Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2,
+ };
+
public DataTypeMigration(IMigrationContext context, PreValueMigratorCollection preValueMigrators, PropertyEditorCollection propertyEditors, ILogger logger)
: base(context)
{
@@ -61,25 +74,41 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
.From()
.Where(x => x.NodeId == group.Key)).First();
+ // check for duplicate aliases
+ var aliases = group.Select(x => x.Alias).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
+ if (aliases.Distinct().Count() != aliases.Length)
+ throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataType.NodeId}, editor={dataType.EditorAlias}: duplicate alias.");
+
+ // handle null/empty aliases
+ int index = 0;
+ var dictionary = group.ToDictionary(x => string.IsNullOrWhiteSpace(x.Alias) ? index++.ToString() : x.Alias);
+
// migrate the preValues to configuration
var migrator = _preValueMigrators.GetMigrator(dataType.EditorAlias) ?? new DefaultPreValueMigrator();
- var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, group.ToDictionary(x => x.Alias, x => x));
+ var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, dictionary);
var json = JsonConvert.SerializeObject(config);
// validate - and kill the migration if it fails
var newAlias = migrator.GetNewAlias(dataType.EditorAlias);
if (newAlias == null)
{
- _logger.Warn("Skipping validation of configuration for data type {NodeId} : {EditorAlias}."
- + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.",
- dataType.NodeId, dataType.EditorAlias);
+ if (!LegacyAliases.Contains(dataType.EditorAlias))
+ {
+ _logger.Warn(
+ "Skipping validation of configuration for data type {NodeId} : {EditorAlias}."
+ + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.",
+ dataType.NodeId, dataType.EditorAlias);
+ }
}
else if (!_propertyEditors.TryGet(newAlias, out var propertyEditor))
{
- _logger.Warn("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})"
- + " because no property editor with that alias was found."
- + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.",
- dataType.NodeId, newAlias, dataType.EditorAlias);
+ if (!LegacyAliases.Contains(newAlias))
+ {
+ _logger.Warn("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})"
+ + " because no property editor with that alias was found."
+ + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.",
+ dataType.NodeId, newAlias, dataType.EditorAlias);
+ }
}
else
{
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs
index 2e341ad091..f445742aa9 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs
@@ -3,7 +3,7 @@
class ContentPickerPreValueMigrator : DefaultPreValueMigrator
{
public override bool CanMigrate(string editorAlias)
- => editorAlias == "Umbraco.ContentPicker2";
+ => editorAlias == Constants.PropertyEditors.Legacy.Aliases.ContentPicker2;
public override string GetNewAlias(string editorAlias)
=> null;
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs
index 7112679de2..0c8161c9ef 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs
@@ -24,8 +24,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes
}
// assuming we don't want to fall back to array
- if (aliases.Length != preValuesA.Count || aliases.Any(string.IsNullOrWhiteSpace))
- throw new InvalidOperationException($"Cannot migrate datatype w/ id={dataTypeId} preValues: duplicate or null/empty alias.");
+ if (aliases.Any(string.IsNullOrWhiteSpace))
+ throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataTypeId}, editor={editorAlias}: null/empty alias.");
// dictionary-base prevalues
return GetPreValues(preValuesA).ToDictionary(x => x.Alias, GetPreValueValue);
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs
new file mode 100644
index 0000000000..87a1fd6504
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes
+{
+ class MarkdownEditorPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase
+ {
+ public override bool CanMigrate(string editorAlias)
+ => editorAlias == Constants.PropertyEditors.Aliases.MarkdownEditor;
+
+ protected override object GetPreValueValue(PreValueDto preValue)
+ {
+ if (preValue.Alias == "preview")
+ return preValue.Value == "1";
+
+ return base.GetPreValueValue(preValue);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs
index a46b1eefb7..922d886595 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs
@@ -6,15 +6,15 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes
{
private readonly string[] _editors =
{
- "Umbraco.MediaPicker2",
- "Umbraco.MediaPicker"
+ Constants.PropertyEditors.Legacy.Aliases.MediaPicker2,
+ Constants.PropertyEditors.Aliases.MediaPicker
};
public override bool CanMigrate(string editorAlias)
=> _editors.Contains(editorAlias);
public override string GetNewAlias(string editorAlias)
- => "Umbraco.MediaPicker";
+ => Constants.PropertyEditors.Aliases.MediaPicker;
// you wish - but MediaPickerConfiguration lives in Umbraco.Web
/*
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs
index 8dfa464508..db9021d653 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs
@@ -20,7 +20,8 @@ public class PreValueMigratorComposer : ICoreComposer
.Append()
.Append()
.Append()
- .Append();
+ .Append()
+ .Append();
}
}
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs
index c04e7c8fda..89a71fdaf4 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using Umbraco.Core.Exceptions;
namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes
{
@@ -20,7 +21,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes
case "Umbraco.NoEdit":
return Constants.PropertyEditors.Aliases.Label;
default:
- throw new Exception("panic");
+ throw new PanicException($"The alias {editorAlias} is not supported");
}
}
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs
index 7249ebd6ec..07fefc8e85 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs
@@ -9,6 +9,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes
private readonly string[] _editors =
{
"Umbraco.RadioButtonList",
+ "Umbraco.CheckBoxList",
"Umbraco.DropDown",
"Umbraco.DropdownlistPublishingKeys",
"Umbraco.DropDownMultiple",
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs
index a434b9f1c1..0d451e8460 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs
@@ -16,7 +16,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
public override void Migrate()
{
- var dataTypes = GetDataTypes("Umbraco.Date");
+ var dataTypes = GetDataTypes(Constants.PropertyEditors.Legacy.Aliases.Date);
foreach (var dataType in dataTypes)
{
@@ -25,6 +25,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
{
config = (DateTimeConfiguration) new CustomDateTimeConfigurationEditor().FromDatabase(
dataType.Configuration);
+
+ // If the Umbraco.Date type is the default from V7 and it has never been updated, then the
+ // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that
+ // after the migration, so we force the format to the default from V7.
+ if (string.IsNullOrEmpty(dataType.Configuration))
+ {
+ config.Format = "YYYY-MM-DD";
+ };
}
catch (Exception ex)
{
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs
index dac62abb75..89a8f010ec 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs
@@ -12,12 +12,12 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
public override void Migrate()
{
- RenameDataType(Constants.PropertyEditors.Aliases.ContentPicker + "2", Constants.PropertyEditors.Aliases.ContentPicker);
- RenameDataType(Constants.PropertyEditors.Aliases.MediaPicker + "2", Constants.PropertyEditors.Aliases.MediaPicker);
- RenameDataType(Constants.PropertyEditors.Aliases.MemberPicker + "2", Constants.PropertyEditors.Aliases.MemberPicker);
- RenameDataType(Constants.PropertyEditors.Aliases.MultiNodeTreePicker + "2", Constants.PropertyEditors.Aliases.MultiNodeTreePicker);
- RenameDataType("Umbraco.TextboxMultiple", Constants.PropertyEditors.Aliases.TextArea, false);
- RenameDataType("Umbraco.Textbox", Constants.PropertyEditors.Aliases.TextBox, false);
+ RenameDataType(Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, Constants.PropertyEditors.Aliases.ContentPicker);
+ RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, Constants.PropertyEditors.Aliases.MediaPicker);
+ RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, Constants.PropertyEditors.Aliases.MemberPicker);
+ RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, Constants.PropertyEditors.Aliases.MultiNodeTreePicker);
+ RenameDataType(Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, Constants.PropertyEditors.Aliases.TextArea, false);
+ RenameDataType(Constants.PropertyEditors.Legacy.Aliases.Textbox, Constants.PropertyEditors.Aliases.TextBox, false);
}
private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true)
@@ -43,7 +43,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
$"Property Editors. Before upgrading to v8, any Data Types using property editors that are named with the prefix '(Obsolete)' must be migrated " +
$"to the non-obsolete v7 property editors of the same type.");
}
-
+
}
Database.Execute(Sql()
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs
index bd08b53877..58ec0e30c2 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs
@@ -15,21 +15,17 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
{
//special trick to add the column without constraints and return the sql to add them later
AddColumn("macroType", out var sqls1);
- //now we need to update the new column with some values because this column doesn't allow NULL values
- Update.Table(Constants.DatabaseSchema.Tables.Macro).Set(new { macroType = (int)MacroTypes.Unknown}).AllRows().Do();
- //now apply constraints (NOT NULL) to new table
- foreach (var sql in sqls1) Execute.Sql(sql).Do();
-
- //special trick to add the column without constraints and return the sql to add them later
AddColumn("macroSource", out var sqls2);
- //populate the new macroSource column with legacy data
- Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = {(int)MacroTypes.Unknown} WHERE macroXSLT IS NOT NULL").Do();
- Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptAssembly IS NOT NULL").Do();
- Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptType IS NOT NULL").Do();
- Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = {(int)MacroTypes.PartialView} WHERE macroPython IS NOT NULL").Do();
+ //populate the new columns with legacy data
+ Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = '', macroType = {(int)MacroTypes.Unknown}").Do();
+ Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = {(int)MacroTypes.Unknown} WHERE macroXSLT != '' AND macroXSLT IS NOT NULL").Do();
+ Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptAssembly != '' AND macroScriptAssembly IS NOT NULL").Do();
+ Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptType != '' AND macroScriptType IS NOT NULL").Do();
+ Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = {(int)MacroTypes.PartialView} WHERE macroPython != '' AND macroPython IS NOT NULL").Do();
//now apply constraints (NOT NULL) to new table
+ foreach (var sql in sqls1) Execute.Sql(sql).Do();
foreach (var sql in sqls2) Execute.Sql(sql).Do();
//now remove these old columns
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs
index 64ac20d175..9026f15fc1 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs
@@ -30,6 +30,7 @@
Database.Execute("set identity_insert umbracoUser off;");
Database.Execute("update umbracoUser2UserGroup set userId=-1 where userId=0;");
+ Database.Execute("update umbracoUser2NodeNotify set userId=-1 where userId=0;");
Database.Execute("update umbracoNode set nodeUser=-1 where nodeUser=0;");
Database.Execute("update umbracoUserLogin set userId=-1 where userId=0;");
Database.Execute($"update {Constants.DatabaseSchema.Tables.ContentVersion} set userId=-1 where userId=0;");
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs
index bf048bf2bd..b68aa23d78 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
@@ -36,6 +37,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0
var properties = Database.Fetch(sqlPropertyData);
+ var exceptions = new List();
foreach (var property in properties)
{
var value = property.TextValue;
@@ -43,19 +45,34 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0
if (property.PropertyTypeDto.DataTypeDto.EditorAlias == Constants.PropertyEditors.Aliases.Grid)
{
- var obj = JsonConvert.DeserializeObject(value);
- var allControls = obj.SelectTokens("$.sections..rows..areas..controls");
-
- foreach (var control in allControls.SelectMany(c => c))
+ try
{
- var controlValue = control["value"];
- if (controlValue.Type == JTokenType.String)
+ var obj = JsonConvert.DeserializeObject(value);
+ var allControls = obj.SelectTokens("$.sections..rows..areas..controls");
+
+ foreach (var control in allControls.SelectMany(c => c).OfType())
{
- control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value());
+ var controlValue = control["value"];
+ if (controlValue?.Type == JTokenType.String)
+ {
+ control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value());
+ }
}
+
+ property.TextValue = JsonConvert.SerializeObject(obj);
+ }
+ catch (JsonException e)
+ {
+ exceptions.Add(new InvalidOperationException(
+ "Cannot deserialize the value as json. This can be because the property editor " +
+ "type is changed from another type into a grid. Old versions of the value in this " +
+ "property can have the structure from the old property editor type. This needs to be " +
+ "changed manually before updating the database.\n" +
+ $"Property info: Id = {property.Id}, LanguageId = {property.LanguageId}, VersionId = {property.VersionId}, Value = {property.Value}"
+ , e));
+ continue;
}
- property.TextValue = JsonConvert.SerializeObject(obj);
}
else
{
@@ -65,6 +82,12 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0
Database.Update(property);
}
+
+ if (exceptions.Any())
+ {
+ throw new AggregateException("One or more errors related to unexpected data in grid values occurred.", exceptions);
+ }
+
Context.AddPostMigration();
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs
index bf28c28c9e..64e4b41186 100644
--- a/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs
@@ -67,6 +67,12 @@ namespace Umbraco.Core.Models.ContentEditing
///
[DataMember(Name = "active")]
public bool Active { get; set; }
+
+ ///
+ /// Gets or sets the content app badge.
+ ///
+ [DataMember(Name = "badge")]
+ public ContentAppBadge Badge { get; set; }
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs
new file mode 100644
index 0000000000..ba11fd338d
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs
@@ -0,0 +1,39 @@
+namespace Umbraco.Core.Models.ContentEditing
+{
+ using System.Runtime.Serialization;
+
+ using Umbraco.Core.Events;
+
+ ///
+ /// Represents a content app badge
+ ///
+ [DataContract(Name = "badge", Namespace = "")]
+ public class ContentAppBadge
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContentAppBadge()
+ {
+ this.Type = ContentAppBadgeType.Default;
+ }
+
+ ///
+ /// Gets or sets the number displayed in the badge
+ ///
+ [DataMember(Name = "count")]
+ public int Count { get; set; }
+
+ ///
+ /// Gets or sets the type of badge to display
+ ///
+ ///
+ /// This controls the background color of the badge.
+ /// Warning will display a dark yellow badge
+ /// Alert will display a red badge
+ /// Default will display a turquoise badge
+ ///
+ [DataMember(Name = "type")]
+ public ContentAppBadgeType Type { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs
new file mode 100644
index 0000000000..c3217099b6
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs
@@ -0,0 +1,24 @@
+namespace Umbraco.Core.Models.ContentEditing
+{
+ using System.Runtime.Serialization;
+
+ using Newtonsoft.Json;
+ using Newtonsoft.Json.Converters;
+
+ ///
+ /// Represent the content app badge types
+ ///
+ [DataContract(Name = "contentAppBadgeType")]
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum ContentAppBadgeType
+ {
+ [EnumMember(Value = "default")]
+ Default = 0,
+
+ [EnumMember(Value = "warning")]
+ Warning = 1,
+
+ [EnumMember(Value = "alert")]
+ Alert = 2
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
index 644e60a961..af8c781072 100644
--- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
+++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
@@ -221,7 +221,13 @@ namespace Umbraco.Core.Models
return true;
}
- public static void UnpublishCulture(this IContent content, string culture = "*")
+ ///
+ /// Returns false if the culture is already unpublished
+ ///
+ ///
+ ///
+ ///
+ public static bool UnpublishCulture(this IContent content, string culture = "*")
{
culture = culture.NullOrWhiteSpaceAsNull();
@@ -229,16 +235,31 @@ namespace Umbraco.Core.Models
if (!content.ContentType.SupportsPropertyVariation(culture, "*", true))
throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\".");
- if (culture == "*") // all cultures
+
+ var keepProcessing = true;
+
+ if (culture == "*")
+ {
+ // all cultures
content.ClearPublishInfos();
- else // one single culture
- content.ClearPublishInfo(culture);
+ }
+ else
+ {
+ // one single culture
+ keepProcessing = content.ClearPublishInfo(culture);
+ }
+
- // property.PublishValues only publishes what is valid, variation-wise
- foreach (var property in content.Properties)
- property.UnpublishValues(culture);
+ if (keepProcessing)
+ {
+ // property.PublishValues only publishes what is valid, variation-wise
+ foreach (var property in content.Properties)
+ property.UnpublishValues(culture);
- content.PublishedState = PublishedState.Publishing;
+ content.PublishedState = PublishedState.Publishing;
+ }
+
+ return keepProcessing;
}
public static void ClearPublishInfos(this IContent content)
@@ -246,15 +267,24 @@ namespace Umbraco.Core.Models
content.PublishCultureInfos = null;
}
- public static void ClearPublishInfo(this IContent content, string culture)
+ ///
+ /// Returns false if the culture is already unpublished
+ ///
+ ///
+ ///
+ ///
+ public static bool ClearPublishInfo(this IContent content, string culture)
{
if (culture == null) throw new ArgumentNullException(nameof(culture));
if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture));
- content.PublishCultureInfos.Remove(culture);
-
- // set the culture to be dirty - it's been modified
- content.TouchCulture(culture);
+ var removed = content.PublishCultureInfos.Remove(culture);
+ if (removed)
+ {
+ // set the culture to be dirty - it's been modified
+ content.TouchCulture(culture);
+ }
+ return removed;
}
///
diff --git a/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs
new file mode 100644
index 0000000000..050a999cc2
--- /dev/null
+++ b/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs
@@ -0,0 +1,7 @@
+namespace Umbraco.Core.Models.Entities
+{
+ public interface IMemberEntitySlim : IContentEntitySlim
+ {
+
+ }
+}
diff --git a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs
new file mode 100644
index 0000000000..335e269467
--- /dev/null
+++ b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs
@@ -0,0 +1,13 @@
+namespace Umbraco.Core.Models.Entities
+{
+ public class MemberEntitySlim : EntitySlim, IMemberEntitySlim
+ {
+ public string ContentTypeAlias { get; set; }
+
+ ///
+ public string ContentTypeIcon { get; set; }
+
+ ///
+ public string ContentTypeThumbnail { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs
index 5f1fe6ed49..ed87c5f320 100644
--- a/src/Umbraco.Core/Models/IContentTypeBase.cs
+++ b/src/Umbraco.Core/Models/IContentTypeBase.cs
@@ -101,7 +101,7 @@ namespace Umbraco.Core.Models
PropertyGroupCollection PropertyGroups { get; set; }
///
- /// Gets all local property types belonging to a group, across all local property groups.
+ /// Gets all local property types all local property groups or ungrouped.
///
IEnumerable PropertyTypes { get; }
diff --git a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs
index 3903fe405b..b1d0189c56 100644
--- a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs
+++ b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs
@@ -15,6 +15,12 @@ namespace Umbraco.Core.Models.Membership
return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, group.StartContentId, group.StartMediaId, group.Alias, group.AllowedSections, group.Permissions);
}
+ public static bool IsSystemUserGroup(this IUserGroup group) =>
+ IsSystemUserGroup(group.Alias);
+
+ public static bool IsSystemUserGroup(this IReadOnlyUserGroup group) =>
+ IsSystemUserGroup(group.Alias);
+
public static IReadOnlyUserGroup ToReadOnlyGroup(this UserGroupDto group)
{
return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon,
@@ -22,5 +28,12 @@ namespace Umbraco.Core.Models.Membership
group.UserGroup2AppDtos.Select(x => x.AppAlias).ToArray(),
group.DefaultPermissions == null ? Enumerable.Empty() : group.DefaultPermissions.ToCharArray().Select(x => x.ToString()));
}
+
+ private static bool IsSystemUserGroup(this string groupAlias)
+ {
+ return groupAlias == Constants.Security.AdminGroupAlias
+ || groupAlias == Constants.Security.SensitiveDataGroupAlias
+ || groupAlias == Constants.Security.TranslatorGroupAlias;
+ }
}
}
diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs
index 816bfdbb01..89009ac7b8 100644
--- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs
@@ -30,6 +30,16 @@
/// Is used by constructor to create special property types.
IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations);
+ ///
+ /// Creates a core (non-user) published property type.
+ ///
+ /// The published content type owning the property.
+ /// The property type alias.
+ /// The datatype identifier.
+ /// The variations.
+ /// Is used by constructor to create special property types.
+ IPublishedPropertyType CreateCorePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations);
+
///
/// Gets a published datatype.
///
diff --git a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs
index 6f42715f16..dd60eb9beb 100644
--- a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
+using Umbraco.Core.Exceptions;
namespace Umbraco.Core.Models.PublishedContent
{
@@ -76,7 +77,7 @@ namespace Umbraco.Core.Models.PublishedContent
return type;
var def = type.GetGenericTypeDefinition();
if (def == null)
- throw new InvalidOperationException("panic");
+ throw new PanicException($"The type {type} has not generic type definition");
var args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray();
return def.MakeGenericType(args);
@@ -115,7 +116,7 @@ namespace Umbraco.Core.Models.PublishedContent
return type.FullName;
var def = type.GetGenericTypeDefinition();
if (def == null)
- throw new InvalidOperationException("panic");
+ throw new PanicException($"The type {type} has not generic type definition");
var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray();
var defFullName = def.FullName.Substring(0, def.FullName.IndexOf('`'));
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs
index 3b03cfc9ea..b11e991118 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs
@@ -92,26 +92,26 @@ namespace Umbraco.Core.Models.PublishedContent
{
var aliases = new HashSet(propertyTypes.Select(x => x.Alias), StringComparer.OrdinalIgnoreCase);
- foreach ((var alias, (var dataTypeId, var editorAlias)) in BuiltinMemberProperties)
+ foreach (var (alias, dataTypeId) in BuiltinMemberProperties)
{
if (aliases.Contains(alias)) continue;
- propertyTypes.Add(factory.CreatePropertyType(this, alias, dataTypeId, ContentVariation.Nothing));
+ propertyTypes.Add(factory.CreateCorePropertyType(this, alias, dataTypeId, ContentVariation.Nothing));
}
}
// TODO: this list somehow also exists in constants, see memberTypeRepository => remove duplicate!
- private static readonly Dictionary BuiltinMemberProperties = new Dictionary
+ private static readonly Dictionary BuiltinMemberProperties = new Dictionary
{
- { "Email", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) },
- { "Username", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) },
- { "PasswordQuestion", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) },
- { "Comments", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) },
- { "IsApproved", (Constants.DataTypes.Boolean, Constants.PropertyEditors.Aliases.Boolean) },
- { "IsLockedOut", (Constants.DataTypes.Boolean, Constants.PropertyEditors.Aliases.Boolean) },
- { "LastLockoutDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) },
- { "CreateDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) },
- { "LastLoginDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) },
- { "LastPasswordChangeDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) },
+ { "Email", Constants.DataTypes.Textbox },
+ { "Username", Constants.DataTypes.Textbox },
+ { "PasswordQuestion", Constants.DataTypes.Textbox },
+ { "Comments", Constants.DataTypes.Textbox },
+ { "IsApproved", Constants.DataTypes.Boolean },
+ { "IsLockedOut", Constants.DataTypes.Boolean },
+ { "LastLockoutDate", Constants.DataTypes.DateTime },
+ { "CreateDate", Constants.DataTypes.DateTime },
+ { "LastLoginDate", Constants.DataTypes.DateTime },
+ { "LastPasswordChangeDate", Constants.DataTypes.DateTime },
};
#region Content type
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs
index 17a15a2536..34094508c3 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs
@@ -61,6 +61,12 @@ namespace Umbraco.Core.Models.PublishedContent
return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this);
}
+ ///
+ public IPublishedPropertyType CreateCorePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing)
+ {
+ return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, false, variations, _propertyValueConverters, _publishedModelFactory, this);
+ }
+
///
/// This method is for tests and is not intended to be used directly from application code.
///
diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs
index cf7df4fb86..e00ac4ba15 100644
--- a/src/Umbraco.Core/Models/UserExtensions.cs
+++ b/src/Umbraco.Core/Models/UserExtensions.cs
@@ -67,7 +67,7 @@ namespace Umbraco.Core.Models
if (user.Avatar.IsNullOrWhiteSpace())
{
- var gravatarHash = user.Email.ToMd5();
+ var gravatarHash = user.Email.GenerateHash();
var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404";
//try Gravatar
diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs
index 68bc9c923d..78ad60f763 100644
--- a/src/Umbraco.Core/ObjectExtensions.cs
+++ b/src/Umbraco.Core/ObjectExtensions.cs
@@ -542,7 +542,7 @@ namespace Umbraco.Core
{
return "\"{0}\"".InvariantFormat(obj);
}
- if (obj is int || obj is Int16 || obj is Int64 || obj is float || obj is double || obj is bool || obj is int? || obj is Int16? || obj is Int64? || obj is float? || obj is double? || obj is bool?)
+ if (obj is int || obj is short || obj is long || obj is float || obj is double || obj is bool || obj is int? || obj is float? || obj is double? || obj is bool?)
{
return "{0}".InvariantFormat(obj);
}
@@ -723,7 +723,7 @@ namespace Umbraco.Core
{
return typeConverter;
}
-
+
var converter = TypeDescriptor.GetConverter(target);
if (converter.CanConvertFrom(source))
{
@@ -788,6 +788,6 @@ namespace Umbraco.Core
return BoolConvertCache[type] = false;
}
-
+
}
}
diff --git a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs
index c811f484bc..281cc2c396 100644
--- a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs
+++ b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using System.Text.RegularExpressions;
using System.Web;
using System.Xml.Linq;
using System.Xml.XPath;
@@ -12,7 +11,9 @@ using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Packaging;
+using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
+using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
@@ -26,13 +27,14 @@ namespace Umbraco.Core.Packaging
private readonly ILocalizationService _localizationService;
private readonly IDataTypeService _dataTypeService;
private readonly PropertyEditorCollection _propertyEditors;
+ private readonly IScopeProvider _scopeProvider;
private readonly IEntityService _entityService;
private readonly IContentTypeService _contentTypeService;
private readonly IContentService _contentService;
public PackageDataInstallation(ILogger logger, IFileService fileService, IMacroService macroService, ILocalizationService localizationService,
IDataTypeService dataTypeService, IEntityService entityService, IContentTypeService contentTypeService,
- IContentService contentService, PropertyEditorCollection propertyEditors)
+ IContentService contentService, PropertyEditorCollection propertyEditors, IScopeProvider scopeProvider)
{
_logger = logger;
_fileService = fileService;
@@ -40,12 +42,13 @@ namespace Umbraco.Core.Packaging
_localizationService = localizationService;
_dataTypeService = dataTypeService;
_propertyEditors = propertyEditors;
+ _scopeProvider = scopeProvider;
_entityService = entityService;
_contentTypeService = contentTypeService;
_contentService = contentService;
}
- #region Uninstall
+ #region Install/Uninstall
public UninstallationSummary UninstallPackageData(PackageDefinition package, int userId)
{
@@ -58,93 +61,97 @@ namespace Umbraco.Core.Packaging
var removedDataTypes = new List();
var removedLanguages = new List();
-
- //Uninstall templates
- foreach (var item in package.Templates.ToArray())
+ using (var scope = _scopeProvider.CreateScope())
{
- if (int.TryParse(item, out var nId) == false) continue;
- var found = _fileService.GetTemplate(nId);
- if (found != null)
+ //Uninstall templates
+ foreach (var item in package.Templates.ToArray())
{
- removedTemplates.Add(found);
- _fileService.DeleteTemplate(found.Alias, userId);
+ if (int.TryParse(item, out var nId) == false) continue;
+ var found = _fileService.GetTemplate(nId);
+ if (found != null)
+ {
+ removedTemplates.Add(found);
+ _fileService.DeleteTemplate(found.Alias, userId);
+ }
+ package.Templates.Remove(nId.ToString());
}
- package.Templates.Remove(nId.ToString());
- }
- //Uninstall macros
- foreach (var item in package.Macros.ToArray())
- {
- if (int.TryParse(item, out var nId) == false) continue;
- var macro = _macroService.GetById(nId);
- if (macro != null)
+ //Uninstall macros
+ foreach (var item in package.Macros.ToArray())
{
- removedMacros.Add(macro);
- _macroService.Delete(macro, userId);
+ if (int.TryParse(item, out var nId) == false) continue;
+ var macro = _macroService.GetById(nId);
+ if (macro != null)
+ {
+ removedMacros.Add(macro);
+ _macroService.Delete(macro, userId);
+ }
+ package.Macros.Remove(nId.ToString());
}
- package.Macros.Remove(nId.ToString());
- }
- //Remove Document Types
- var contentTypes = new List();
- var contentTypeService = _contentTypeService;
- foreach (var item in package.DocumentTypes.ToArray())
- {
- if (int.TryParse(item, out var nId) == false) continue;
- var contentType = contentTypeService.Get(nId);
- if (contentType == null) continue;
- contentTypes.Add(contentType);
- package.DocumentTypes.Remove(nId.ToString(CultureInfo.InvariantCulture));
- }
-
- //Order the DocumentTypes before removing them
- if (contentTypes.Any())
- {
- // TODO: I don't think this ordering is necessary
- var orderedTypes = (from contentType in contentTypes
- orderby contentType.ParentId descending, contentType.Id descending
- select contentType).ToList();
- removedContentTypes.AddRange(orderedTypes);
- contentTypeService.Delete(orderedTypes, userId);
- }
-
- //Remove Dictionary items
- foreach (var item in package.DictionaryItems.ToArray())
- {
- if (int.TryParse(item, out var nId) == false) continue;
- var di = _localizationService.GetDictionaryItemById(nId);
- if (di != null)
+ //Remove Document Types
+ var contentTypes = new List();
+ var contentTypeService = _contentTypeService;
+ foreach (var item in package.DocumentTypes.ToArray())
{
- removedDictionaryItems.Add(di);
- _localizationService.Delete(di, userId);
+ if (int.TryParse(item, out var nId) == false) continue;
+ var contentType = contentTypeService.Get(nId);
+ if (contentType == null) continue;
+ contentTypes.Add(contentType);
+ package.DocumentTypes.Remove(nId.ToString(CultureInfo.InvariantCulture));
}
- package.DictionaryItems.Remove(nId.ToString());
- }
- //Remove Data types
- foreach (var item in package.DataTypes.ToArray())
- {
- if (int.TryParse(item, out var nId) == false) continue;
- var dtd = _dataTypeService.GetDataType(nId);
- if (dtd != null)
+ //Order the DocumentTypes before removing them
+ if (contentTypes.Any())
{
- removedDataTypes.Add(dtd);
- _dataTypeService.Delete(dtd, userId);
+ // TODO: I don't think this ordering is necessary
+ var orderedTypes = (from contentType in contentTypes
+ orderby contentType.ParentId descending, contentType.Id descending
+ select contentType).ToList();
+ removedContentTypes.AddRange(orderedTypes);
+ contentTypeService.Delete(orderedTypes, userId);
}
- package.DataTypes.Remove(nId.ToString());
- }
- //Remove Langs
- foreach (var item in package.Languages.ToArray())
- {
- if (int.TryParse(item, out var nId) == false) continue;
- var lang = _localizationService.GetLanguageById(nId);
- if (lang != null)
+ //Remove Dictionary items
+ foreach (var item in package.DictionaryItems.ToArray())
{
- removedLanguages.Add(lang);
- _localizationService.Delete(lang, userId);
+ if (int.TryParse(item, out var nId) == false) continue;
+ var di = _localizationService.GetDictionaryItemById(nId);
+ if (di != null)
+ {
+ removedDictionaryItems.Add(di);
+ _localizationService.Delete(di, userId);
+ }
+ package.DictionaryItems.Remove(nId.ToString());
}
- package.Languages.Remove(nId.ToString());
+
+ //Remove Data types
+ foreach (var item in package.DataTypes.ToArray())
+ {
+ if (int.TryParse(item, out var nId) == false) continue;
+ var dtd = _dataTypeService.GetDataType(nId);
+ if (dtd != null)
+ {
+ removedDataTypes.Add(dtd);
+ _dataTypeService.Delete(dtd, userId);
+ }
+ package.DataTypes.Remove(nId.ToString());
+ }
+
+ //Remove Langs
+ foreach (var item in package.Languages.ToArray())
+ {
+ if (int.TryParse(item, out var nId) == false) continue;
+ var lang = _localizationService.GetLanguageById(nId);
+ if (lang != null)
+ {
+ removedLanguages.Add(lang);
+ _localizationService.Delete(lang, userId);
+ }
+ package.Languages.Remove(nId.ToString());
+ }
+
+ scope.Complete();
}
// create a summary of what was actually removed, for PackagingService.UninstalledPackage
@@ -165,14 +172,40 @@ namespace Umbraco.Core.Packaging
}
+ public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId)
+ {
+ using (var scope = _scopeProvider.CreateScope())
+ {
+ var installationSummary = new InstallationSummary
+ {
+ DataTypesInstalled = ImportDataTypes(compiledPackage.DataTypes.ToList(), userId),
+ LanguagesInstalled = ImportLanguages(compiledPackage.Languages, userId),
+ DictionaryItemsInstalled = ImportDictionaryItems(compiledPackage.DictionaryItems, userId),
+ MacrosInstalled = ImportMacros(compiledPackage.Macros, userId),
+ TemplatesInstalled = ImportTemplates(compiledPackage.Templates.ToList(), userId),
+ DocumentTypesInstalled = ImportDocumentTypes(compiledPackage.DocumentTypes, userId)
+ };
+
+ //we need a reference to the imported doc types to continue
+ var importedDocTypes = installationSummary.DocumentTypesInstalled.ToDictionary(x => x.Alias, x => x);
+
+ installationSummary.StylesheetsInstalled = ImportStylesheets(compiledPackage.Stylesheets, userId);
+ installationSummary.ContentInstalled = ImportContent(compiledPackage.Documents, importedDocTypes, userId);
+
+ scope.Complete();
+
+ return installationSummary;
+ }
+ }
+
#endregion
#region Content
- public IEnumerable ImportContent(IEnumerable docs, IDictionary importedDocumentTypes, int userId)
+ public IReadOnlyList ImportContent(IEnumerable docs, IDictionary importedDocumentTypes, int userId)
{
- return docs.SelectMany(x => ImportContent(x, -1, importedDocumentTypes, userId));
+ return docs.SelectMany(x => ImportContent(x, -1, importedDocumentTypes, userId)).ToList();
}
///
@@ -353,7 +386,7 @@ namespace Umbraco.Core.Packaging
#region DocumentTypes
- public IEnumerable ImportDocumentType(XElement docTypeElement, int userId)
+ public IReadOnlyList ImportDocumentType(XElement docTypeElement, int userId)
{
return ImportDocumentTypes(new[] { docTypeElement }, userId);
}
@@ -364,7 +397,7 @@ namespace Umbraco.Core.Packaging
/// Xml to import
/// Optional id of the User performing the operation. Default is zero (admin).
/// An enumerable list of generated ContentTypes
- public IEnumerable ImportDocumentTypes(IEnumerable docTypeElements, int userId)
+ public IReadOnlyList ImportDocumentTypes(IEnumerable docTypeElements, int userId)
{
return ImportDocumentTypes(docTypeElements.ToList(), true, userId);
}
@@ -376,7 +409,7 @@ namespace Umbraco.Core.Packaging
/// Boolean indicating whether or not to import the
/// Optional id of the User performing the operation. Default is zero (admin).
/// An enumerable list of generated ContentTypes
- public IEnumerable ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, bool importStructure, int userId)
+ public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, bool importStructure, int userId)
{
var importedContentTypes = new Dictionary();
@@ -575,12 +608,11 @@ namespace Umbraco.Core.Packaging
contentType.Thumbnail = infoElement.Element("Thumbnail").Value;
contentType.Description = infoElement.Element("Description").Value;
- //NOTE AllowAtRoot is a new property in the package xml so we need to verify it exists before using it.
+ //NOTE AllowAtRoot, IsListView, IsElement and Variations are new properties in the package xml so we need to verify it exists before using it.
var allowAtRoot = infoElement.Element("AllowAtRoot");
if (allowAtRoot != null)
contentType.AllowedAsRoot = allowAtRoot.Value.InvariantEquals("true");
- //NOTE IsListView is a new property in the package xml so we need to verify it exists before using it.
var isListView = infoElement.Element("IsListView");
if (isListView != null)
contentType.IsContainer = isListView.Value.InvariantEquals("true");
@@ -589,6 +621,10 @@ namespace Umbraco.Core.Packaging
if (isElement != null)
contentType.IsElement = isElement.Value.InvariantEquals("true");
+ var variationsElement = infoElement.Element("Variations");
+ if (variationsElement != null)
+ contentType.Variations = (ContentVariation)Enum.Parse(typeof(ContentVariation), variationsElement.Value);
+
//Name of the master corresponds to the parent and we need to ensure that the Parent Id is set
var masterElement = infoElement.Element("Master");
if (masterElement != null)
@@ -614,7 +650,7 @@ namespace Umbraco.Core.Packaging
var compositionContentType = importedContentTypes.ContainsKey(compositionAlias)
? importedContentTypes[compositionAlias]
: _contentTypeService.Get(compositionAlias);
- var added = contentType.AddContentType(compositionContentType);
+ contentType.AddContentType(compositionContentType);
}
}
}
@@ -748,9 +784,14 @@ namespace Umbraco.Core.Packaging
{
Name = property.Element("Name").Value,
Description = (string)property.Element("Description"),
- Mandatory = property.Element("Mandatory") != null ? property.Element("Mandatory").Value.ToLowerInvariant().Equals("true") : false,
+ Mandatory = property.Element("Mandatory") != null
+ ? property.Element("Mandatory").Value.ToLowerInvariant().Equals("true")
+ : false,
ValidationRegExp = (string)property.Element("Validation"),
- SortOrder = sortOrder
+ SortOrder = sortOrder,
+ Variations = property.Element("Variations") != null
+ ? (ContentVariation)Enum.Parse(typeof(ContentVariation), property.Element("Variations").Value)
+ : ContentVariation.Nothing
};
var tab = (string)property.Element("Tab");
@@ -817,7 +858,7 @@ namespace Umbraco.Core.Packaging
/// Xml to import
/// Optional id of the user
/// An enumerable list of generated DataTypeDefinitions
- public IEnumerable ImportDataTypes(IReadOnlyCollection dataTypeElements, int userId)
+ public IReadOnlyList ImportDataTypes(IReadOnlyCollection dataTypeElements, int userId)
{
var dataTypes = new List();
@@ -946,13 +987,13 @@ namespace Umbraco.Core.Packaging
/// Xml to import
///
/// An enumerable list of dictionary items
- public IEnumerable ImportDictionaryItems(IEnumerable dictionaryItemElementList, int userId)
+ public IReadOnlyList ImportDictionaryItems(IEnumerable dictionaryItemElementList, int userId)
{
var languages = _localizationService.GetAllLanguages().ToList();
return ImportDictionaryItems(dictionaryItemElementList, languages, null, userId);
}
- private IEnumerable ImportDictionaryItems(IEnumerable dictionaryItemElementList, List languages, Guid? parentId, int userId)
+ private IReadOnlyList ImportDictionaryItems(IEnumerable dictionaryItemElementList, List languages, Guid? parentId, int userId)
{
var items = new List();
foreach (var dictionaryItemElement in dictionaryItemElementList)
@@ -1029,7 +1070,7 @@ namespace Umbraco.Core.Packaging
/// Xml to import
/// Optional id of the User performing the operation
/// An enumerable list of generated languages
- public IEnumerable ImportLanguages(IEnumerable languageElements, int userId)
+ public IReadOnlyList ImportLanguages(IEnumerable languageElements, int userId)
{
var list = new List();
foreach (var languageElement in languageElements)
@@ -1058,7 +1099,7 @@ namespace Umbraco.Core.Packaging
/// Xml to import
/// Optional id of the User performing the operation
///
- public IEnumerable ImportMacros(IEnumerable macroElements, int userId)
+ public IReadOnlyList ImportMacros(IEnumerable macroElements, int userId)
{
var macros = macroElements.Select(ParseMacroElement).ToList();
@@ -1148,7 +1189,7 @@ namespace Umbraco.Core.Packaging
#region Stylesheets
- public IEnumerable ImportStylesheets(IEnumerable stylesheetElements, int userId)
+ public IReadOnlyList ImportStylesheets(IEnumerable stylesheetElements, int userId)
{
var result = new List();
@@ -1216,7 +1257,7 @@ namespace Umbraco.Core.Packaging
/// Xml to import
/// Optional user id
/// An enumerable list of generated Templates
- public IEnumerable ImportTemplates(IReadOnlyCollection templateElements, int userId)
+ public IReadOnlyList ImportTemplates(IReadOnlyCollection templateElements, int userId)
{
var templates = new List();
diff --git a/src/Umbraco.Core/Packaging/PackageInstallation.cs b/src/Umbraco.Core/Packaging/PackageInstallation.cs
index d791295b38..a42ee1aeb2 100644
--- a/src/Umbraco.Core/Packaging/PackageInstallation.cs
+++ b/src/Umbraco.Core/Packaging/PackageInstallation.cs
@@ -90,21 +90,8 @@ namespace Umbraco.Core.Packaging
public InstallationSummary InstallPackageData(PackageDefinition packageDefinition, CompiledPackage compiledPackage, int userId)
{
- var installationSummary = new InstallationSummary
- {
- DataTypesInstalled = _packageDataInstallation.ImportDataTypes(compiledPackage.DataTypes.ToList(), userId),
- LanguagesInstalled = _packageDataInstallation.ImportLanguages(compiledPackage.Languages, userId),
- DictionaryItemsInstalled = _packageDataInstallation.ImportDictionaryItems(compiledPackage.DictionaryItems, userId),
- MacrosInstalled = _packageDataInstallation.ImportMacros(compiledPackage.Macros, userId),
- TemplatesInstalled = _packageDataInstallation.ImportTemplates(compiledPackage.Templates.ToList(), userId),
- DocumentTypesInstalled = _packageDataInstallation.ImportDocumentTypes(compiledPackage.DocumentTypes, userId)
- };
+ var installationSummary = _packageDataInstallation.InstallPackageData(compiledPackage, userId);
- //we need a reference to the imported doc types to continue
- var importedDocTypes = installationSummary.DocumentTypesInstalled.ToDictionary(x => x.Alias, x => x);
-
- installationSummary.StylesheetsInstalled = _packageDataInstallation.ImportStylesheets(compiledPackage.Stylesheets, userId);
- installationSummary.ContentInstalled = _packageDataInstallation.ImportContent(compiledPackage.Documents, importedDocTypes, userId);
installationSummary.Actions = CompiledPackageXmlParser.GetPackageActions(XElement.Parse(compiledPackage.Actions), compiledPackage.Name);
installationSummary.MetaData = compiledPackage;
installationSummary.FilesInstalled = packageDefinition.Files;
diff --git a/src/Umbraco.Core/Persistence/BulkDataReader.cs b/src/Umbraco.Core/Persistence/BulkDataReader.cs
index 1eaa88ee88..7dbe74922a 100644
--- a/src/Umbraco.Core/Persistence/BulkDataReader.cs
+++ b/src/Umbraco.Core/Persistence/BulkDataReader.cs
@@ -470,7 +470,7 @@ namespace Umbraco.Core.Persistence
break;
case SqlDbType.SmallInt:
- dataType = typeof(Int16);
+ dataType = typeof(short);
dataTypeName = "smallint";
break;
@@ -688,34 +688,34 @@ namespace Umbraco.Core.Persistence
DataColumnCollection columns = _schemaTable.Columns;
- columns.Add(SchemaTableColumn.ColumnName, typeof(System.String));
- columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(System.Int32));
- columns.Add(SchemaTableColumn.ColumnSize, typeof(System.Int32));
- columns.Add(SchemaTableColumn.NumericPrecision, typeof(System.Int16));
- columns.Add(SchemaTableColumn.NumericScale, typeof(System.Int16));
- columns.Add(SchemaTableColumn.IsUnique, typeof(System.Boolean));
- columns.Add(SchemaTableColumn.IsKey, typeof(System.Boolean));
- columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(System.String));
- columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(System.String));
- columns.Add(SchemaTableColumn.BaseColumnName, typeof(System.String));
- columns.Add(SchemaTableColumn.BaseSchemaName, typeof(System.String));
- columns.Add(SchemaTableColumn.BaseTableName, typeof(System.String));
- columns.Add(SchemaTableColumn.DataType, typeof(System.Type));
- columns.Add(SchemaTableColumn.AllowDBNull, typeof(System.Boolean));
- columns.Add(SchemaTableColumn.ProviderType, typeof(System.Int32));
- columns.Add(SchemaTableColumn.IsAliased, typeof(System.Boolean));
- columns.Add(SchemaTableColumn.IsExpression, typeof(System.Boolean));
- columns.Add(BulkDataReader.IsIdentitySchemaColumn, typeof(System.Boolean));
- columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(System.Boolean));
- columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(System.Boolean));
- columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(System.Boolean));
- columns.Add(SchemaTableColumn.IsLong, typeof(System.Boolean));
- columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(System.Boolean));
- columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(System.Type));
- columns.Add(BulkDataReader.DataTypeNameSchemaColumn, typeof(System.String));
- columns.Add(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, typeof(System.String));
- columns.Add(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(System.String));
- columns.Add(BulkDataReader.XmlSchemaCollectionNameSchemaColumn, typeof(System.String));
+ columns.Add(SchemaTableColumn.ColumnName, typeof(string));
+ columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int));
+ columns.Add(SchemaTableColumn.ColumnSize, typeof(int));
+ columns.Add(SchemaTableColumn.NumericPrecision, typeof(short));
+ columns.Add(SchemaTableColumn.NumericScale, typeof(short));
+ columns.Add(SchemaTableColumn.IsUnique, typeof(bool));
+ columns.Add(SchemaTableColumn.IsKey, typeof(bool));
+ columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string));
+ columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string));
+ columns.Add(SchemaTableColumn.BaseColumnName, typeof(string));
+ columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string));
+ columns.Add(SchemaTableColumn.BaseTableName, typeof(string));
+ columns.Add(SchemaTableColumn.DataType, typeof(Type));
+ columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool));
+ columns.Add(SchemaTableColumn.ProviderType, typeof(int));
+ columns.Add(SchemaTableColumn.IsAliased, typeof(bool));
+ columns.Add(SchemaTableColumn.IsExpression, typeof(bool));
+ columns.Add(BulkDataReader.IsIdentitySchemaColumn, typeof(bool));
+ columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool));
+ columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool));
+ columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool));
+ columns.Add(SchemaTableColumn.IsLong, typeof(bool));
+ columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool));
+ columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(Type));
+ columns.Add(BulkDataReader.DataTypeNameSchemaColumn, typeof(string));
+ columns.Add(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, typeof(string));
+ columns.Add(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(string));
+ columns.Add(BulkDataReader.XmlSchemaCollectionNameSchemaColumn, typeof(string));
}
#endregion
@@ -1090,7 +1090,7 @@ namespace Umbraco.Core.Persistence
///
public decimal GetDecimal(int i)
{
- return (Decimal)GetValue(i);
+ return (decimal)GetValue(i);
}
///
diff --git a/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs b/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs
deleted file mode 100644
index 48edee3c94..0000000000
--- a/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Data;
-using System.Runtime.CompilerServices;
-
-namespace Umbraco.Core.Persistence
-{
- internal static class DatabaseNodeLockExtensions
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void ValidateDatabase(IUmbracoDatabase database)
- {
- if (database == null)
- throw new ArgumentNullException("database");
- if (database.GetCurrentTransactionIsolationLevel() < IsolationLevel.RepeatableRead)
- throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required.");
- }
-
- // updating a record within a repeatable-read transaction gets an exclusive lock on
- // that record which will be kept until the transaction is ended, effectively locking
- // out all other accesses to that record - thus obtaining an exclusive lock over the
- // protected resources.
- public static void AcquireLockNodeWriteLock(this IUmbracoDatabase database, int nodeId)
- {
- ValidateDatabase(database);
-
- database.Execute("UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id",
- new { @id = nodeId });
- }
-
- // reading a record within a repeatable-read transaction gets a shared lock on
- // that record which will be kept until the transaction is ended, effectively preventing
- // other write accesses to that record - thus obtaining a shared lock over the protected
- // resources.
- public static void AcquireLockNodeReadLock(this IUmbracoDatabase database, int nodeId)
- {
- ValidateDatabase(database);
-
- database.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id",
- new { @id = nodeId });
- }
- }
-}
diff --git a/src/Umbraco.Core/Persistence/EntityNotFoundException.cs b/src/Umbraco.Core/Persistence/EntityNotFoundException.cs
index 1d075339c0..ea6d5142f0 100644
--- a/src/Umbraco.Core/Persistence/EntityNotFoundException.cs
+++ b/src/Umbraco.Core/Persistence/EntityNotFoundException.cs
@@ -3,8 +3,6 @@ using System.Runtime.Serialization;
namespace Umbraco.Core.Persistence
{
- // TODO: Would be good to use this exception type anytime we cannot find an entity
-
///
/// An exception used to indicate that an Umbraco entity could not be found.
///
diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs
index f1473b5888..33dabe1b24 100644
--- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs
@@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Factories
public static IEnumerable BuildEntities(PropertyType[] propertyTypes, IReadOnlyCollection dtos, int publishedVersionId, ILanguageRepository languageRepository)
{
var properties = new List();
- var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable) x);
+ var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable)x);
foreach (var propertyType in propertyTypes)
{
@@ -104,10 +104,14 @@ namespace Umbraco.Core.Persistence.Factories
/// The properties to map
///
/// out parameter indicating that one or more properties have been edited
- /// out parameter containing a collection of edited cultures when the contentVariation varies by culture
+ ///
+ /// Out parameter containing a collection of edited cultures when the contentVariation varies by culture.
+ /// The value of this will be used to populate the edited cultures in the umbracoDocumentCultureVariation table.
+ ///
///
public static IEnumerable BuildDtos(ContentVariation contentVariation, int currentVersionId, int publishedVersionId, IEnumerable properties,
- ILanguageRepository languageRepository, out bool edited, out HashSet editedCultures)
+ ILanguageRepository languageRepository, out bool edited,
+ out HashSet editedCultures)
{
var propertyDataDtos = new List();
edited = false;
@@ -130,6 +134,9 @@ namespace Umbraco.Core.Persistence.Factories
// publishing = deal with edit and published values
foreach (var propertyValue in property.Values)
{
+ var isInvariantValue = propertyValue.Culture == null;
+ var isCultureValue = propertyValue.Culture != null && propertyValue.Segment == null;
+
// deal with published value
if (propertyValue.PublishedValue != null && publishedVersionId > 0)
propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue));
@@ -138,26 +145,36 @@ namespace Umbraco.Core.Persistence.Factories
if (propertyValue.EditedValue != null)
propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue));
+ // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the
+ // administrator has previously changed the property type to be variant vs invariant.
+ // We need to check for this scenario here because otherwise the editedCultures and edited flags
+ // will end up incorrectly set in the umbracoDocumentCultureVariation table so here we need to
+ // only process edited cultures based on the current value type and how the property varies.
+ // The above logic will still persist the currently saved property value for each culture in case the admin
+ // decides to swap the property's variance again, in which case the edited flag will be recalculated.
+
+ if (property.PropertyType.VariesByCulture() && isInvariantValue || !property.PropertyType.VariesByCulture() && isCultureValue)
+ continue;
+
// use explicit equals here, else object comparison fails at comparing eg strings
var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue);
+
edited |= !sameValues;
- if (entityVariesByCulture // cultures can be edited, ie CultureNeutral is supported
- && propertyValue.Culture != null && propertyValue.Segment == null // and value is CultureNeutral
- && !sameValues) // and edited and published are different
+ if (entityVariesByCulture && !sameValues)
{
- editedCultures.Add(propertyValue.Culture); // report culture as edited
- }
+ if (isCultureValue)
+ {
+ editedCultures.Add(propertyValue.Culture); // report culture as edited
+ }
+ else if (isInvariantValue)
+ {
+ // flag culture as edited if it contains an edited invariant property
+ if (defaultCulture == null)
+ defaultCulture = languageRepository.GetDefaultIsoCode();
- // flag culture as edited if it contains an edited invariant property
- if (propertyValue.Culture == null //invariant property
- && !sameValues // and edited and published are different
- && entityVariesByCulture) //only when the entity is variant
- {
- if (defaultCulture == null)
- defaultCulture = languageRepository.GetDefaultIsoCode();
-
- editedCultures.Add(defaultCulture);
+ editedCultures.Add(defaultCulture);
+ }
}
}
}
@@ -167,7 +184,7 @@ namespace Umbraco.Core.Persistence.Factories
{
// not publishing = only deal with edit values
if (propertyValue.EditedValue != null)
- propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue));
+ propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue));
}
edited = true;
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs
index afb419ebd6..3a44cb10b4 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs
@@ -7,5 +7,12 @@ namespace Umbraco.Core.Persistence.Repositories
public interface IDataTypeRepository : IReadWriteQueryRepository
{
IEnumerable> Move(IDataType toMove, EntityContainer container);
+
+ ///
+ /// Returns a dictionary of content type s and the property type aliases that use a
+ ///
+ ///
+ ///
+ IReadOnlyDictionary> FindUsages(int id);
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs
index c25328b10c..0788594e3a 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs
@@ -174,7 +174,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
totalRecords = page.TotalItems;
var items = page.Items.Select(
- dto => new AuditItem(dto.Id, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList();
+ dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList();
// map the DateStamp
for (var i = 0; i < items.Count; i++)
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
index aeb4c3774f..7ab73f3f2d 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
@@ -512,31 +512,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
foreach (var a in allPropertyDataDtos)
a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId];
- // prefetch configuration for tag properties
- var tagEditors = new Dictionary();
- foreach (var propertyTypeDto in indexedPropertyTypeDtos.Values)
- {
- var editorAlias = propertyTypeDto.DataTypeDto.EditorAlias;
- var editorAttribute = PropertyEditors[editorAlias].GetTagAttribute();
- if (editorAttribute == null) continue;
- var tagConfigurationSource = propertyTypeDto.DataTypeDto.Configuration;
- var tagConfiguration = string.IsNullOrWhiteSpace(tagConfigurationSource)
- ? new TagConfiguration()
- : JsonConvert.DeserializeObject(tagConfigurationSource);
- if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = editorAttribute.Delimiter;
- tagEditors[editorAlias] = tagConfiguration;
- }
-
// now we have
// - the definitions
// - all property data dtos
- // - tag editors
+ // - tag editors (Actually ... no we don't since i removed that code, but we don't need them anyways it seems)
// and we need to build the proper property collections
- return GetPropertyCollections(temps, allPropertyDataDtos, tagEditors);
+ return GetPropertyCollections(temps, allPropertyDataDtos);
}
- private IDictionary GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos, Dictionary tagConfigurations)
+ private IDictionary GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos)
where T : class, IContentBase
{
var result = new Dictionary();
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
index 6b751eb8ff..4393d365f8 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using NPoco;
using Umbraco.Core.Cache;
+using Umbraco.Core.Exceptions;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Factories;
@@ -90,7 +91,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
contentType = ContentTypeFactory.BuildContentTypeEntity(contentTypeDto);
else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MemberType)
contentType = ContentTypeFactory.BuildMemberTypeEntity(contentTypeDto);
- else throw new Exception("panic");
+ else throw new PanicException($"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported");
contentTypes.Add(contentType.Id, contentType);
// map allowed content types
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs
index 9d77eb0990..359b967dab 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using NPoco;
using Umbraco.Core.Cache;
+using Umbraco.Core.Exceptions;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
@@ -18,8 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
///
internal class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository
{
- public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository)
- : base(scopeAccessor, cache, logger, commonRepository)
+ public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository)
+ : base(scopeAccessor, cache, logger, commonRepository, languageRepository)
{ }
protected override bool SupportsPublishing => ContentType.SupportsPublishingConst;
@@ -56,7 +57,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
// the cache policy will always want everything
// even GetMany(ids) gets everything and filters afterwards
- if (ids.Any()) throw new Exception("panic");
+ if (ids.Any()) throw new PanicException("There can be no ids specified");
return CommonRepository.GetAllTypes().OfType();
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
index 22c9244d8f..6385482686 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
@@ -1,4 +1,5 @@
-using System;
+
+using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
@@ -15,6 +16,7 @@ using Umbraco.Core.Persistence.Factories;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
+using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
@@ -26,14 +28,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
internal abstract class ContentTypeRepositoryBase : NPocoRepositoryBase, IReadRepository
where TEntity : class, IContentTypeComposition
{
- protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository)
+ protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository)
: base(scopeAccessor, cache, logger)
{
CommonRepository = commonRepository;
+ LanguageRepository = languageRepository;
}
protected IContentTypeCommonRepository CommonRepository { get; }
-
+ protected ILanguageRepository LanguageRepository { get; }
protected abstract bool SupportsPublishing { get; }
public IEnumerable> Move(TEntity moving, EntityContainer container)
@@ -98,6 +101,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected void PersistNewBaseContentType(IContentTypeComposition entity)
{
+ ValidateVariations(entity);
+
var dto = ContentTypeFactory.BuildContentTypeDto(entity);
//Cannot add a duplicate content type
@@ -163,11 +168,11 @@ AND umbracoNode.nodeObjectType = @objectType",
foreach (var allowedContentType in entity.AllowedContentTypes)
{
Database.Insert(new ContentTypeAllowedContentTypeDto
- {
- Id = entity.Id,
- AllowedId = allowedContentType.Id.Value,
- SortOrder = allowedContentType.SortOrder
- });
+ {
+ Id = entity.Id,
+ AllowedId = allowedContentType.Id.Value,
+ SortOrder = allowedContentType.SortOrder
+ });
}
@@ -214,6 +219,9 @@ AND umbracoNode.nodeObjectType = @objectType",
protected void PersistUpdatedBaseContentType(IContentTypeComposition entity)
{
+ CorrectPropertyTypeVariations(entity);
+ ValidateVariations(entity);
+
var dto = ContentTypeFactory.BuildContentTypeDto(entity);
// ensure the alias is not used already
@@ -370,7 +378,7 @@ AND umbracoNode.id <> @id",
foreach (var propertyGroup in entity.PropertyGroups)
{
// insert or update group
- var groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup,entity.Id);
+ var groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, entity.Id);
var groupId = propertyGroup.HasIdentity
? Database.Update(groupDto)
: Convert.ToInt32(Database.Insert(groupDto));
@@ -388,7 +396,7 @@ AND umbracoNode.id <> @id",
//check if the content type variation has been changed
var contentTypeVariationDirty = entity.IsPropertyDirty("Variations");
- var oldContentTypeVariation = (ContentVariation) dtoPk.Variations;
+ var oldContentTypeVariation = (ContentVariation)dtoPk.Variations;
var newContentTypeVariation = entity.Variations;
var contentTypeVariationChanging = contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation;
if (contentTypeVariationChanging)
@@ -404,26 +412,7 @@ AND umbracoNode.id <> @id",
// note: this only deals with *local* property types, we're dealing w/compositions later below
foreach (var propertyType in entity.PropertyTypes)
{
- if (contentTypeVariationChanging)
- {
- // content type is changing
- switch (newContentTypeVariation)
- {
- case ContentVariation.Nothing: // changing to Nothing
- // all property types must change to Nothing
- propertyType.Variations = ContentVariation.Nothing;
- break;
- case ContentVariation.Culture: // changing to Culture
- // all property types can remain Nothing
- break;
- case ContentVariation.CultureAndSegment:
- case ContentVariation.Segment:
- default:
- throw new NotSupportedException(); // TODO: Support this
- }
- }
-
- // then, track each property individually
+ // track each property individually
if (propertyType.IsPropertyDirty("Variations"))
{
// allocate the list only when needed
@@ -449,23 +438,19 @@ AND umbracoNode.id <> @id",
// via composition, with their original variations (ie not filtered by this
// content type variations - we need this true value to make decisions.
- foreach (var propertyType in ((ContentTypeCompositionBase) entity).RawComposedPropertyTypes)
+ propertyTypeVariationChanges = propertyTypeVariationChanges ?? new Dictionary();
+
+ foreach (var composedPropertyType in ((ContentTypeCompositionBase)entity).RawComposedPropertyTypes)
{
- if (propertyType.VariesBySegment() || newContentTypeVariation.VariesBySegment())
- throw new NotSupportedException(); // TODO: support this
+ if (composedPropertyType.Variations == ContentVariation.Nothing) continue;
- if (propertyType.Variations == ContentVariation.Culture)
- {
- if (propertyTypeVariationChanges == null)
- propertyTypeVariationChanges = new Dictionary();
+ // Determine target variation of the composed property type.
+ // The composed property is only considered culture variant when the base content type is also culture variant.
+ // The composed property is only considered segment variant when the base content type is also segment variant.
+ // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture
+ var target = newContentTypeVariation & composedPropertyType.Variations;
- // if content type moves to Culture, property type becomes Culture here again
- // if content type moves to Nothing, property type becomes Nothing here
- if (newContentTypeVariation == ContentVariation.Culture)
- propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Nothing, ContentVariation.Culture);
- else if (newContentTypeVariation == ContentVariation.Nothing)
- propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Culture, ContentVariation.Nothing);
- }
+ propertyTypeVariationChanges[composedPropertyType.Id] = (composedPropertyType.Variations, target);
}
}
@@ -506,7 +491,7 @@ AND umbracoNode.id <> @id",
var impacted = GetImpactedContentTypes(entity, all);
// if some property types have actually changed, move their variant data
- if (propertyTypeVariationChanges != null)
+ if (propertyTypeVariationChanges?.Count > 0)
MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted);
// deal with orphan properties: those that were in a deleted tab,
@@ -518,6 +503,42 @@ AND umbracoNode.id <> @id",
CommonRepository.ClearCache(); // always
}
+ ///
+ /// Corrects the property type variations for the given entity
+ /// to make sure the property type variation is compatible with the
+ /// variation set on the entity itself.
+ ///
+ /// Entity to correct properties for
+ private void CorrectPropertyTypeVariations(IContentTypeComposition entity)
+ {
+ // Update property variations based on the content type variation
+ foreach (var propertyType in entity.PropertyTypes)
+ {
+ // Determine variation for the property type.
+ // The property is only considered culture variant when the base content type is also culture variant.
+ // The property is only considered segment variant when the base content type is also segment variant.
+ // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture
+ propertyType.Variations = entity.Variations & propertyType.Variations;
+ }
+ }
+
+ ///
+ /// Ensures that no property types are flagged for a variance that is not supported by the content type itself
+ ///
+ /// The entity for which the property types will be validated
+ private void ValidateVariations(IContentTypeComposition entity)
+ {
+ foreach (var prop in entity.PropertyTypes)
+ {
+ // The variation of a property is only allowed if all its variation flags
+ // are also set on the entity itself. It cannot set anything that is not also set by the content type.
+ // For example, when entity.Variations is set to Culture a property cannot be set to Segment.
+ var isValid = entity.Variations.HasFlag(prop.Variations);
+ if (!isValid)
+ throw new InvalidOperationException($"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}");
+ }
+ }
+
private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, IEnumerable all)
{
var impact = new List();
@@ -525,12 +546,12 @@ AND umbracoNode.id <> @id",
var tree = new Dictionary>();
foreach (var x in all)
- foreach (var y in x.ContentTypeComposition)
- {
- if (!tree.TryGetValue(y.Id, out var list))
- list = tree[y.Id] = new List();
- list.Add(x);
- }
+ foreach (var y in x.ContentTypeComposition)
+ {
+ if (!tree.TryGetValue(y.Id, out var list))
+ list = tree[y.Id] = new List();
+ list.Add(x);
+ }
var nset = new List();
do
@@ -572,7 +593,7 @@ AND umbracoNode.id <> @id",
// new property type, ignore
if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB))
continue;
- var oldVariation = (ContentVariation) oldVariationB; // NPoco cannot fetch directly
+ var oldVariation = (ContentVariation)oldVariationB; // NPoco cannot fetch directly
// only those property types that *actually* changed
var newVariation = propertyType.Variations;
@@ -636,25 +657,27 @@ AND umbracoNode.id <> @id",
var impactedL = impacted.Select(x => x.Id).ToList();
//Group by the "To" variation so we can bulk update in the correct batches
- foreach(var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation))
+ foreach (var grouping in propertyTypeChanges.GroupBy(x => x.Value))
{
var propertyTypeIds = grouping.Select(x => x.Key).ToList();
- var toVariation = grouping.Key;
+ var (FromVariation, ToVariation) = grouping.Key;
- switch (toVariation)
+ var fromCultureEnabled = FromVariation.HasFlag(ContentVariation.Culture);
+ var toCultureEnabled = ToVariation.HasFlag(ContentVariation.Culture);
+
+ if (!fromCultureEnabled && toCultureEnabled)
{
- case ContentVariation.Culture:
- CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL);
- CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL);
- break;
- case ContentVariation.Nothing:
- CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL);
- CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL);
- break;
- case ContentVariation.CultureAndSegment:
- case ContentVariation.Segment:
- default:
- throw new NotSupportedException(); // TODO: Support this
+ // Culture has been enabled
+ CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL);
+ CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL);
+ RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL);
+ }
+ else if (fromCultureEnabled && !toCultureEnabled)
+ {
+ // Culture has been disabled
+ CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL);
+ CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL);
+ RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL);
}
}
}
@@ -666,78 +689,72 @@ AND umbracoNode.id <> @id",
{
var defaultLanguageId = GetDefaultLanguageId();
- switch (toVariation)
+ var cultureIsNotEnabled = !fromVariation.HasFlag(ContentVariation.Culture);
+ var cultureWillBeEnabled = toVariation.HasFlag(ContentVariation.Culture);
+
+ if (cultureIsNotEnabled && cultureWillBeEnabled)
{
- case ContentVariation.Culture:
+ //move the names
+ //first clear out any existing names that might already exists under the default lang
+ //there's 2x tables to update
- //move the names
- //first clear out any existing names that might already exists under the default lang
- //there's 2x tables to update
+ //clear out the versionCultureVariation table
+ var sqlSelect = Sql().Select(x => x.Id)
+ .From()
+ .InnerJoin().On(x => x.Id, x => x.VersionId)
+ .InnerJoin().On(x => x.NodeId, x => x.NodeId)
+ .Where(x => x.ContentTypeId == contentType.Id)
+ .Where(x => x.LanguageId == defaultLanguageId);
+ var sqlDelete = Sql()
+ .Delete()
+ .WhereIn(x => x.Id, sqlSelect);
- //clear out the versionCultureVariation table
- var sqlSelect = Sql().Select(x => x.Id)
- .From()
- .InnerJoin().On(x => x.Id, x => x.VersionId)
- .InnerJoin().On(x => x.NodeId, x => x.NodeId)
- .Where(x => x.ContentTypeId == contentType.Id)
- .Where(x => x.LanguageId == defaultLanguageId);
- var sqlDelete = Sql()
- .Delete()
- .WhereIn(x => x.Id, sqlSelect);
+ Database.Execute(sqlDelete);
- Database.Execute(sqlDelete);
+ //clear out the documentCultureVariation table
+ sqlSelect = Sql().Select(x => x.Id)
+ .From()
+ .InnerJoin().On(x => x.NodeId, x => x.NodeId)
+ .Where(x => x.ContentTypeId == contentType.Id)
+ .Where(x => x.LanguageId == defaultLanguageId);
+ sqlDelete = Sql()
+ .Delete()
+ .WhereIn(x => x.Id, sqlSelect);
- //clear out the documentCultureVariation table
- sqlSelect = Sql().Select(x => x.Id)
- .From()
- .InnerJoin().On(x => x.NodeId, x => x.NodeId)
- .Where(x => x.ContentTypeId == contentType.Id)
- .Where(x => x.LanguageId == defaultLanguageId);
- sqlDelete = Sql()
- .Delete()
- .WhereIn(x => x.Id, sqlSelect);
+ Database.Execute(sqlDelete);
- Database.Execute(sqlDelete);
+ //now we need to insert names into these 2 tables based on the invariant data
- //now we need to insert names into these 2 tables based on the invariant data
+ //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang
+ var cols = Sql().Columns(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId);
+ sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate)
+ .Append($", {defaultLanguageId}") //default language ID
+ .From()
+ .InnerJoin().On(x => x.NodeId, x => x.NodeId)
+ .Where(x => x.ContentTypeId == contentType.Id);
+ var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect);
- //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang
- var cols = Sql().Columns(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId);
- sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate)
- .Append($", {defaultLanguageId}") //default language ID
- .From()
- .InnerJoin().On(x => x.NodeId, x => x.NodeId)
- .Where(x => x.ContentTypeId == contentType.Id);
- var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect);
+ Database.Execute(sqlInsert);
- Database.Execute(sqlInsert);
+ //insert rows into the documentCultureVariation table
+ cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId);
+ sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published)
+ .AndSelect(x => x.Text)
+ .Append($", 1, {defaultLanguageId}") //make Available + default language ID
+ .From()
+ .InnerJoin().On(x => x.NodeId, x => x.NodeId)
+ .InnerJoin().On(x => x.NodeId, x => x.NodeId)
+ .Where(x => x.ContentTypeId == contentType.Id);
+ sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect);
- //insert rows into the documentCultureVariation table
- cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId);
- sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published)
- .AndSelect(x => x.Text)
- .Append($", 1, {defaultLanguageId}") //make Available + default language ID
- .From()
- .InnerJoin().On(x => x.NodeId, x => x.NodeId)
- .InnerJoin().On(x => x.NodeId, x => x.NodeId)
- .Where(x => x.ContentTypeId == contentType.Id);
- sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect);
+ Database.Execute(sqlInsert);
+ }
+ else
+ {
+ //we don't need to move the names! this is because we always keep the invariant names with the name of the default language.
- Database.Execute(sqlInsert);
-
- break;
- case ContentVariation.Nothing:
-
- //we don't need to move the names! this is because we always keep the invariant names with the name of the default language.
-
- //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :(
- // if we want these SQL statements back, look into GIT history
-
- break;
- case ContentVariation.CultureAndSegment:
- case ContentVariation.Segment:
- default:
- throw new NotSupportedException(); // TODO: Support this
+ //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :(
+ // if we want these SQL statements back, look into GIT history
}
}
@@ -963,6 +980,205 @@ AND umbracoNode.id <> @id",
Database.Execute(sqlDelete);
}
+
+ }
+
+ ///
+ /// Re-normalizes the edited value in the umbracoDocumentCultureVariation and umbracoDocument table when variations are changed
+ ///
+ ///
+ ///
+ ///
+ /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true when it should be false
+ /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values stored for each
+ /// property, culture and current/published version.
+ ///
+ private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null)
+ {
+ var defaultLang = LanguageRepository.GetDefaultId();
+
+ //This will build up a query to get the property values of both the current and the published version so that we can check
+ //based on the current variance of each item to see if it's 'edited' value should be true/false.
+
+ var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0);
+ if (whereInArgsCount > 2000)
+ throw new NotSupportedException("Too many property/content types.");
+
+ var propertySql = Sql()
+ .Select()
+ .AndSelect(x => x.NodeId, x => x.Current)
+ .AndSelect(x => x.Published)
+ .AndSelect(x => x.Variations)
+ .From()
+ .InnerJoin().On((left, right) => left.Id == right.VersionId)
+ .InnerJoin().On((left, right) => left.Id == right.PropertyTypeId);
+
+ if (contentTypeIds != null)
+ {
+ propertySql.InnerJoin().On((c, cversion) => c.NodeId == cversion.NodeId);
+ }
+
+ propertySql.LeftJoin().On((docversion, cversion) => cversion.Id == docversion.Id)
+ .Where((docversion, cversion) => cversion.Current || docversion.Published)
+ .WhereIn(x => x.PropertyTypeId, propertyTypeIds);
+
+ if (contentTypeIds != null)
+ {
+ propertySql.WhereIn(x => x.ContentTypeId, contentTypeIds);
+ }
+
+ propertySql
+ .OrderBy(x => x.NodeId)
+ .OrderBy(x => x.PropertyTypeId, x => x.LanguageId, x => x.VersionId);
+
+ //keep track of this node/lang to mark or unmark a culture as edited
+ var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>();
+ //keep track of which node to mark or unmark as edited
+ var editedDocument = new Dictionary();
+ var nodeId = -1;
+ var propertyTypeId = -1;
+
+ PropertyValueVersionDto pubRow = null;
+
+ //This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data.
+ //Published data will always come before Current data based on the version id sort.
+ //There will only be one published row (max) and one current row per property.
+ foreach (var row in Database.Query(propertySql))
+ {
+ //make sure to reset on each node/property change
+ if (nodeId != row.NodeId || propertyTypeId != row.PropertyTypeId)
+ {
+ nodeId = row.NodeId;
+ propertyTypeId = row.PropertyTypeId;
+ pubRow = null;
+ }
+
+ if (row.Published)
+ pubRow = row;
+
+ if (row.Current)
+ {
+ var propVariations = (ContentVariation)row.Variations;
+
+ //if this prop doesn't vary but the row has a lang assigned or vice versa, flag this as not edited
+ if (!propVariations.VariesByCulture() && row.LanguageId.HasValue
+ || propVariations.VariesByCulture() && !row.LanguageId.HasValue)
+ {
+ //Flag this as not edited for this node/lang if the key doesn't exist
+ if (!editedLanguageVersions.TryGetValue((row.NodeId, row.LanguageId), out _))
+ editedLanguageVersions.Add((row.NodeId, row.LanguageId), false);
+
+ //mark as false if the item doesn't exist, else coerce to true
+ editedDocument[row.NodeId] = editedDocument.TryGetValue(row.NodeId, out var edited) ? (edited |= false) : false;
+ }
+ else if (pubRow == null)
+ {
+ //this would mean that that this property is 'edited' since there is no published version
+ editedLanguageVersions[(row.NodeId, row.LanguageId)] = true;
+ editedDocument[row.NodeId] = true;
+ }
+ //compare the property values, if they differ from versions then flag the current version as edited
+ else if (IsPropertyValueChanged(pubRow, row))
+ {
+ //Here we would check if the property is invariant, in which case the edited language should be indicated by the default lang
+ editedLanguageVersions[(row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true;
+ editedDocument[row.NodeId] = true;
+ }
+
+ //reset
+ pubRow = null;
+ }
+ }
+
+ //lookup all matching rows in umbracoDocumentCultureVariation
+ var docCultureVariationsToUpdate = editedLanguageVersions.InGroupsOf(2000)
+ .SelectMany(_ => Database.Fetch(
+ Sql().Select().From()
+ .WhereIn(x => x.LanguageId, editedLanguageVersions.Keys.Select(x => x.langId).ToList())
+ .WhereIn(x => x.NodeId, editedLanguageVersions.Keys.Select(x => x.nodeId))))
+ //convert to dictionary with the same key type
+ .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), x => x);
+
+ var toUpdate = new List();
+ foreach (var ev in editedLanguageVersions)
+ {
+ if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out var docVariations))
+ {
+ //check if it needs updating
+ if (docVariations.Edited != ev.Value)
+ {
+ docVariations.Edited = ev.Value;
+ toUpdate.Add(docVariations);
+ }
+ }
+ else if (ev.Key.langId.HasValue)
+ {
+ //This should never happen! If a property culture is flagged as edited then the culture must exist at the document level
+ throw new PanicException($"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}");
+ }
+ }
+
+ //Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false
+ foreach (var editValue in toUpdate.GroupBy(x => x.Edited))
+ {
+ Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key))
+ .WhereIn(x => x.Id, editValue.Select(x => x.Id)));
+ }
+
+ //Now bulk update the umbracoDocument table
+ foreach (var editValue in editedDocument.GroupBy(x => x.Value))
+ {
+ Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key))
+ .WhereIn(x => x.NodeId, editValue.Select(x => x.Key)));
+ }
+ }
+
+ private static bool IsPropertyValueChanged(PropertyValueVersionDto pubRow, PropertyValueVersionDto row)
+ {
+ return !pubRow.TextValue.IsNullOrWhiteSpace() && pubRow.TextValue != row.TextValue
+ || !pubRow.VarcharValue.IsNullOrWhiteSpace() && pubRow.VarcharValue != row.VarcharValue
+ || pubRow.DateValue.HasValue && pubRow.DateValue != row.DateValue
+ || pubRow.DecimalValue.HasValue && pubRow.DecimalValue != row.DecimalValue
+ || pubRow.IntValue.HasValue && pubRow.IntValue != row.IntValue;
+ }
+
+ private class NameCompareDto
+ {
+ public int NodeId { get; set; }
+ public int CurrentVersion { get; set; }
+ public int LanguageId { get; set; }
+ public string CurrentName { get; set; }
+ public string PublishedName { get; set; }
+ public int? PublishedVersion { get; set; }
+ public int Id { get; set; } // the Id of the DocumentCultureVariationDto
+ public bool Edited { get; set; }
+ }
+
+ private class PropertyValueVersionDto
+ {
+ public int VersionId { get; set; }
+ public int PropertyTypeId { get; set; }
+ public int? LanguageId { get; set; }
+ public string Segment { get; set; }
+ public int? IntValue { get; set; }
+
+ private decimal? _decimalValue;
+ [Column("decimalValue")]
+ public decimal? DecimalValue
+ {
+ get => _decimalValue;
+ set => _decimalValue = value?.Normalize();
+ }
+
+ public DateTime? DateValue { get; set; }
+ public string VarcharValue { get; set; }
+ public string TextValue { get; set; }
+
+ public int NodeId { get; set; }
+ public bool Current { get; set; }
+ public bool Published { get; set; }
+
+ public byte Variations { get; set; }
}
private void DeletePropertyType(int contentTypeId, int propertyTypeId)
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs
index dac8fda5ec..9ccf6e9623 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs
@@ -279,6 +279,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return moveInfo;
}
+ public IReadOnlyDictionary> FindUsages(int id)
+ {
+ if (id == default)
+ return new Dictionary>();
+
+ var sql = Sql()
+ .Select(ct => ct.Select(node => node.NodeDto))
+ .AndSelect(pt => Alias(pt.Alias, "ptAlias"), pt => Alias(pt.Name, "ptName"))
+ .From()
+ .InnerJoin().On(ct => ct.NodeId, pt => pt.ContentTypeId)
+ .InnerJoin().On(n => n.NodeId, ct => ct.NodeId)
+ .Where(pt => pt.DataTypeId == id)
+ .OrderBy(node => node.NodeId)
+ .AndBy