diff --git a/.gitattributes b/.gitattributes
index a664be3a85..c8987ade67 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -13,7 +13,7 @@
*.png binary
*.gif binary
-*.cs text=auto diff=csharp
+*.cs text=auto diff=csharp
*.vb text=auto
*.c text=auto
*.cpp text=auto
@@ -41,9 +41,13 @@
*.fs text=auto
*.fsx text=auto
*.hs text=auto
+*.json text=auto
+*.xml text=auto
-*.csproj text=auto merge=union
-*.vbproj text=auto merge=union
-*.fsproj text=auto merge=union
-*.dbproj text=auto merge=union
-*.sln text=auto eol=crlf merge=union
+*.csproj text=auto merge=union
+*.vbproj text=auto merge=union
+*.fsproj text=auto merge=union
+*.dbproj text=auto merge=union
+*.sln text=auto eol=crlf merge=union
+
+*.gitattributes text=auto
diff --git a/.github/BUILD.md b/.github/BUILD.md
index c89a1be460..c6e870f396 100644
--- a/.github/BUILD.md
+++ b/.github/BUILD.md
@@ -1,4 +1,4 @@
-# Umbraco Cms Build
+# Umbraco CMS Build
## Are you sure?
@@ -39,6 +39,8 @@ To build Umbraco, fire up PowerShell and move to Umbraco's repository root (the
build/build.ps1
+If you only see a build.bat-file, you're probably on the wrong branch. If you switch to the correct branch (dev-v8) the file will appear and you can build it.
+
You might run into [Powershell quirks](#powershell-quirks).
### Build Infrastructure
@@ -66,7 +68,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
@@ -209,4 +211,4 @@ The best solution is to unblock the Zip file before un-zipping: right-click the
### Git Quirks
-Git might have issues dealing with long file paths during build. You may want/need to enable `core.longpaths` support (see [this page](https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path) for details).
\ No newline at end of file
+Git might have issues dealing with long file paths during build. You may want/need to enable `core.longpaths` support (see [this page](https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path) for details).
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..cea5859486 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?
@@ -59,37 +59,38 @@ Great question! The short version goes like this:
* **Clone** - when GitHub has created your fork, you can clone it in your favorite Git tool

-
+
+ * **Switch to the correct branch** - switch to the `v8/contrib` branch
* **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md)
* **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions)
- * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/dev`, create a new branch first.
+ * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/contrib`, create a new branch first.
* **Push** - great, now you can push the changes up to your fork on GitHub
- * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress). 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 +100,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 +126,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 +147,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/contrib`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'.
+
+Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise.

-### 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
@@ -179,9 +184,13 @@ Then when you want to get the changes from the main repository:
```
git fetch upstream
-git rebase upstream/v8/dev
+git rebase upstream/v8/contrib
```
-In this command we're syncing with the `v8/dev` branch, but you can of course choose another one if needed.
+In this command we're syncing with the `v8/contrib` branch, but you can of course choose another one if needed.
(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated))
+
+### And finally
+
+We welcome all kinds of contributions to this repository. If you don't feel you'd like to make code changes here, you can visit our [documentation repository](https://github.com/umbraco/UmbracoDocs) and use your experience to contribute to making the docs we have, even better. We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the 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..467ca6e5e6 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/.github/img/defaultbranch.png b/.github/img/defaultbranch.png
index f3a5b9efbc..3550b5c34c 100644
Binary files a/.github/img/defaultbranch.png and b/.github/img/defaultbranch.png differ
diff --git a/.gitignore b/.gitignore
index 585bd855b7..12ad3299ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,6 +55,7 @@ App_Data/TEMP/*
src/Umbraco.Web.UI/[Cc]ss/*
src/Umbraco.Web.UI/App_Code/*
src/Umbraco.Web.UI/App_Data/*
+src/Umbraco.Web.UI/data/*
src/Umbraco.Tests/App_Data/*
src/Umbraco.Web.UI/[Mm]edia/*
src/Umbraco.Web.UI/[Mm]aster[Pp]ages/*
@@ -132,7 +133,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..347bde139e 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.
-->
-
-
+
+
-
+
@@ -52,14 +52,17 @@
+
+
+
diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec
index b7bfaaff5b..a6b06d9964 100644
--- a/build/NuSpecs/UmbracoCms.nuspec
+++ b/build/NuSpecs/UmbracoCms.nuspec
@@ -26,11 +26,9 @@
not want this to happen as the alpha of the next major is, really, the next major already.
-->
-
-
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..91d49d896c 100644
--- a/build/NuSpecs/tools/ReadmeUpgrade.txt
+++ b/build/NuSpecs/tools/ReadmeUpgrade.txt
@@ -1,30 +1,26 @@
- _ _ __ __ ____ _____ _____ ____
- | | | | \/ | _ \| __ \ /\ / ____/ __ \
- | | | | \ / | |_) | |__) | / \ | | | | | |
- | | | | |\/| | _ <| _ / / /\ \| | | | | |
- | |__| | | | | |_) | | \ \ / ____ | |___| |__| |
- \____/|_| |_|____/|_| \_/_/ \_\_____\____/
+ 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!
-We've done our best to transform your configuration files but in case something is not quite right: remember we
-backed up your files in App_Data\NuGetBackup so you can find the original files before they were transformed.
-
-We've overwritten all the files in the Umbraco folder, these have been backed up in
-App_Data\NuGetBackup. We didn't overwrite the UI.xml file nor did we remove any files or folders that you or
-a package might have added. Only the existing files were overwritten. If you customized anything then make
-sure to do a compare and merge with the NuGetBackup folder.
+We've done our best to transform your configuration files but in case something is not quite right: we recommmend you look in source control for the previous version so you can find the original files before they were transformed.
This NuGet package includes build targets that extend the creation of a deploy package, which is generated by
Publishing from Visual Studio. The targets will only work once Publishing is configured, so if you don't use
Publish this won't affect you.
+
The following items will now be automatically included when creating a deploy package or publishing to the file
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..ea07e4516f 100644
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -458,23 +458,22 @@
Write-Host "Prepare Angular Documentation"
$src = "$($this.SolutionRoot)\src"
- $out = $this.BuildOutput
+ $out = $this.BuildOutput
- $this.CompileBelle()
-
- "Moving to Umbraco.Web.UI.Client folder"
- cd .\src\Umbraco.Web.UI.Client
+ "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..a0d969c1b8 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.7.0")]
+[assembly: AssemblyInformationalVersion("8.7.0")]
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/Collections/ConcurrentHashSet.cs b/src/Umbraco.Core/Collections/ConcurrentHashSet.cs
index 54367ed588..c4dba51acd 100644
--- a/src/Umbraco.Core/Collections/ConcurrentHashSet.cs
+++ b/src/Umbraco.Core/Collections/ConcurrentHashSet.cs
@@ -70,7 +70,23 @@ namespace Umbraco.Core.Collections
/// The number of elements contained in the .
///
/// 2
- public int Count => GetThreadSafeClone().Count;
+ public int Count
+ {
+ get
+ {
+ try
+ {
+ _instanceLocker.EnterReadLock();
+ return _innerSet.Count;
+ }
+ finally
+ {
+ if (_instanceLocker.IsReadLockHeld)
+ _instanceLocker.ExitReadLock();
+ }
+
+ }
+ }
///
/// Gets a value indicating whether the is read-only.
@@ -105,8 +121,7 @@ namespace Umbraco.Core.Collections
///
public bool TryAdd(T item)
{
- var clone = GetThreadSafeClone();
- if (clone.Contains(item)) return false;
+ if (Contains(item)) return false;
try
{
_instanceLocker.EnterWriteLock();
@@ -150,7 +165,16 @@ namespace Umbraco.Core.Collections
/// The object to locate in the .
public bool Contains(T item)
{
- return GetThreadSafeClone().Contains(item);
+ try
+ {
+ _instanceLocker.EnterReadLock();
+ return _innerSet.Contains(item);
+ }
+ finally
+ {
+ if (_instanceLocker.IsReadLockHeld)
+ _instanceLocker.ExitReadLock();
+ }
}
///
@@ -168,13 +192,13 @@ namespace Umbraco.Core.Collections
HashSet clone = null;
try
{
- _instanceLocker.EnterWriteLock();
+ _instanceLocker.EnterReadLock();
clone = new HashSet(_innerSet, _innerSet.Comparer);
}
finally
{
- if (_instanceLocker.IsWriteLockHeld)
- _instanceLocker.ExitWriteLock();
+ if (_instanceLocker.IsReadLockHeld)
+ _instanceLocker.ExitReadLock();
}
return clone;
}
diff --git a/src/Umbraco.Core/Collections/ObservableDictionary.cs b/src/Umbraco.Core/Collections/ObservableDictionary.cs
index 40269aa4eb..c6aedab377 100644
--- a/src/Umbraco.Core/Collections/ObservableDictionary.cs
+++ b/src/Umbraco.Core/Collections/ObservableDictionary.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
+using System.Runtime.Serialization;
namespace Umbraco.Core.Collections
{
@@ -26,7 +27,7 @@ namespace Umbraco.Core.Collections
/// The equality comparer to use when comparing keys, or null to use the default comparer.
public ObservableDictionary(Func keySelector, IEqualityComparer equalityComparer = null)
{
- KeySelector = keySelector ?? throw new ArgumentException("keySelector");
+ KeySelector = keySelector ?? throw new ArgumentException(nameof(keySelector));
Indecies = new Dictionary(equalityComparer);
}
@@ -36,7 +37,7 @@ namespace Umbraco.Core.Collections
{
var key = KeySelector(item);
if (Indecies.ContainsKey(key))
- throw new DuplicateKeyException(key.ToString());
+ throw new ArgumentException($"An element with the same key '{key}' already exists in the dictionary.", nameof(item));
if (index != Count)
{
@@ -91,7 +92,7 @@ namespace Umbraco.Core.Collections
{
//confirm key matches
if (!KeySelector(value).Equals(key))
- throw new InvalidOperationException("Key of new value does not match");
+ throw new InvalidOperationException("Key of new value does not match.");
if (!Indecies.ContainsKey(key))
{
@@ -118,7 +119,7 @@ namespace Umbraco.Core.Collections
//confirm key matches
if (!KeySelector(value).Equals(key))
- throw new InvalidOperationException("Key of new value does not match");
+ throw new InvalidOperationException("Key of new value does not match.");
this[Indecies[key]] = value;
return true;
@@ -155,12 +156,12 @@ namespace Umbraco.Core.Collections
{
if (!Indecies.ContainsKey(currentKey))
{
- throw new InvalidOperationException("No item with the key " + currentKey + "was found in the collection");
+ throw new InvalidOperationException($"No item with the key '{currentKey}' was found in the dictionary.");
}
if (ContainsKey(newKey))
{
- throw new DuplicateKeyException(newKey.ToString());
+ throw new ArgumentException($"An element with the same key '{newKey}' already exists in the dictionary.", nameof(newKey));
}
var currentIndex = Indecies[currentKey];
@@ -234,16 +235,5 @@ namespace Umbraco.Core.Collections
}
#endregion
-
- internal class DuplicateKeyException : Exception
- {
- public DuplicateKeyException(string key)
- : base("Attempted to insert duplicate key \"" + key + "\" in collection.")
- {
- Key = key;
- }
-
- public string Key { get; }
- }
}
}
diff --git a/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs b/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs
index 63a7e170da..b56ff8b87e 100644
--- a/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs
+++ b/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs
@@ -26,10 +26,11 @@ namespace Umbraco.Core.Compose
if (relationType == null)
{
- relationType = new RelationType(Constants.ObjectTypes.Document,
+ relationType = new RelationType(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias,
+ Constants.Conventions.RelationTypes.RelateDocumentOnCopyName,
+ true,
Constants.ObjectTypes.Document,
- Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias,
- Constants.Conventions.RelationTypes.RelateDocumentOnCopyName) { IsBidirectional = true };
+ Constants.ObjectTypes.Document);
relationService.Save(relationType);
}
diff --git a/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs b/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs
index 8371f9b279..4e01c50fc6 100644
--- a/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs
+++ b/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs
@@ -63,7 +63,7 @@ namespace Umbraco.Core.Compose
var documentObjectType = Constants.ObjectTypes.Document;
const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName;
- relationType = new RelationType(documentObjectType, documentObjectType, relationTypeAlias, relationTypeName);
+ relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType);
relationService.Save(relationType);
}
@@ -106,7 +106,7 @@ namespace Umbraco.Core.Compose
{
var documentObjectType = Constants.ObjectTypes.Document;
const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName;
- relationType = new RelationType(documentObjectType, documentObjectType, relationTypeAlias, relationTypeName);
+ relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType);
relationService.Save(relationType);
}
foreach (var item in e.MoveInfoCollection)
diff --git a/src/Umbraco.Core/Composing/ComponentCollection.cs b/src/Umbraco.Core/Composing/ComponentCollection.cs
index 9b5319dc41..62b240f10f 100644
--- a/src/Umbraco.Core/Composing/ComponentCollection.cs
+++ b/src/Umbraco.Core/Composing/ComponentCollection.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Logging;
@@ -43,8 +44,15 @@ namespace Umbraco.Core.Composing
var componentType = component.GetType();
using (_logger.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
- component.Terminate();
- component.DisposeIfDisposable();
+ try
+ {
+ component.Terminate();
+ component.DisposeIfDisposable();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Error while terminating component {ComponentType}.", componentType.FullName);
+ }
}
}
}
diff --git a/src/Umbraco.Core/Composing/Composers.cs b/src/Umbraco.Core/Composing/Composers.cs
index 0510740e42..b2e6c9d068 100644
--- a/src/Umbraco.Core/Composing/Composers.cs
+++ b/src/Umbraco.Core/Composing/Composers.cs
@@ -18,19 +18,52 @@ namespace Umbraco.Core.Composing
private readonly Composition _composition;
private readonly IProfilingLogger _logger;
private readonly IEnumerable _composerTypes;
+ private readonly IEnumerable _enableDisableAttributes;
private const int LogThresholdMilliseconds = 100;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The composition.
- /// The composer types.
- /// A profiling logger.
+ /// The types.
+ /// The profiling logger.
+ [Obsolete("This overload only gets the EnableComposer/DisableComposer attributes from the composerTypes assemblies.")]
public Composers(Composition composition, IEnumerable composerTypes, IProfilingLogger logger)
+ : this(composition, composerTypes, Enumerable.Empty(), logger)
+ {
+ var enableDisableAttributes = new List();
+
+ var assemblies = composerTypes.Select(t => t.Assembly).Distinct();
+ foreach (var assembly in assemblies)
+ {
+ enableDisableAttributes.AddRange(assembly.GetCustomAttributes(typeof(EnableComposerAttribute)));
+ enableDisableAttributes.AddRange(assembly.GetCustomAttributes(typeof(DisableComposerAttribute)));
+ }
+
+ _enableDisableAttributes = enableDisableAttributes;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The composition.
+ /// The types.
+ /// The and/or attributes.
+ /// The profiling logger.
+ /// composition
+ /// or
+ /// composerTypes
+ /// or
+ /// enableDisableAttributes
+ /// or
+ /// logger
+
+ public Composers(Composition composition, IEnumerable composerTypes, IEnumerable enableDisableAttributes, IProfilingLogger logger)
{
_composition = composition ?? throw new ArgumentNullException(nameof(composition));
_composerTypes = composerTypes ?? throw new ArgumentNullException(nameof(composerTypes));
+ _enableDisableAttributes = enableDisableAttributes ?? throw new ArgumentNullException(nameof(enableDisableAttributes));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -103,7 +136,7 @@ namespace Umbraco.Core.Composing
.ToList();
// enable or disable composers
- EnableDisableComposers(composerTypeList);
+ EnableDisableComposers(_enableDisableAttributes, composerTypeList);
void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2)
where TAttribute : Attribute
@@ -218,7 +251,7 @@ namespace Umbraco.Core.Composing
return text.ToString();
}
- private static void EnableDisableComposers(ICollection types)
+ private static void EnableDisableComposers(IEnumerable enableDisableAttributes, ICollection types)
{
var enabled = new Dictionary();
@@ -240,20 +273,16 @@ namespace Umbraco.Core.Composing
enableInfo.Weight = weight2;
}
- var assemblies = types.Select(x => x.Assembly).Distinct();
- foreach (var assembly in assemblies)
+ foreach (var attr in enableDisableAttributes.OfType())
{
- foreach (var attr in assembly.GetCustomAttributes())
- {
- var type = attr.EnabledType;
- UpdateEnableInfo(type, 2, enabled, true);
- }
+ var type = attr.EnabledType;
+ UpdateEnableInfo(type, 2, enabled, true);
+ }
- foreach (var attr in assembly.GetCustomAttributes())
- {
- var type = attr.DisabledType;
- UpdateEnableInfo(type, 2, enabled, false);
- }
+ foreach (var attr in enableDisableAttributes.OfType())
+ {
+ var type = attr.DisabledType;
+ UpdateEnableInfo(type, 2, enabled, false);
}
foreach (var composerType in types)
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/Current.cs b/src/Umbraco.Core/Composing/Current.cs
index f12bf0dd63..846331a16e 100644
--- a/src/Umbraco.Core/Composing/Current.cs
+++ b/src/Umbraco.Core/Composing/Current.cs
@@ -154,6 +154,9 @@ namespace Umbraco.Core.Composing
public static DataEditorCollection DataEditors
=> Factory.GetInstance();
+ public static DataValueReferenceFactoryCollection DataValueReferenceFactories
+ => Factory.GetInstance();
+
public static PropertyEditorCollection PropertyEditors
=> Factory.GetInstance();
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/Composing/LightInject/LightInjectException.cs b/src/Umbraco.Core/Composing/LightInject/LightInjectException.cs
index fa0aed21ca..e1344468f9 100644
--- a/src/Umbraco.Core/Composing/LightInject/LightInjectException.cs
+++ b/src/Umbraco.Core/Composing/LightInject/LightInjectException.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.Serialization;
using System.Text;
namespace Umbraco.Core.Composing.LightInject
@@ -6,20 +7,51 @@ namespace Umbraco.Core.Composing.LightInject
///
/// Represents errors that occur due to LightInject.
///
+ ///
+ [Serializable]
public class LightInjectException : Exception
{
- public LightInjectException(string message)
- : base(message)
- { }
-
- public LightInjectException(string message, Exception innerException)
- : base(message, innerException)
- { }
-
private const string LightInjectUnableToResolveType = "Unable to resolve type:";
private const string LightInjectUnresolvedDependency = "Unresolved dependency ";
private const string LightInjectRequestedDependency = "[Requested dependency:";
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public LightInjectException()
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public LightInjectException(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 LightInjectException(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 LightInjectException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
+
+ ///
+ /// Tries to throw the exception with additional details.
+ ///
+ /// The exception.
+ ///
public static void TryThrow(Exception e)
{
var ex = e as InvalidOperationException;
@@ -32,6 +64,12 @@ namespace Umbraco.Core.Composing.LightInject
throw new LightInjectException(sb.ToString(), e);
}
+ ///
+ /// Tries to throw the exception with additional details.
+ ///
+ /// The exception.
+ /// The implementing type.
+ ///
public static void TryThrow(Exception e, Type implementingType)
{
var ex = e as InvalidOperationException;
@@ -45,6 +83,11 @@ namespace Umbraco.Core.Composing.LightInject
throw new LightInjectException(sb.ToString(), e);
}
+ ///
+ /// Writes the details.
+ ///
+ /// The exception.
+ /// The to write the details to.
private static void WriteDetails(InvalidOperationException ex, StringBuilder sb)
{
ex = ex.InnerException as InvalidOperationException;
diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs
index fe7a561eca..6d0b1a0514 100644
--- a/src/Umbraco.Core/Composing/TypeLoader.cs
+++ b/src/Umbraco.Core/Composing/TypeLoader.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using System.Web;
@@ -505,6 +506,49 @@ namespace Umbraco.Core.Composing
#endregion
+ #region Get Assembly Attributes
+
+ ///
+ /// Gets the assembly attributes of the specified type .
+ ///
+ /// The attribute type.
+ ///
+ /// The assembly attributes of the specified type .
+ ///
+ public IEnumerable GetAssemblyAttributes()
+ where T : Attribute
+ {
+ return AssembliesToScan.SelectMany(a => a.GetCustomAttributes()).ToList();
+ }
+
+ ///
+ /// Gets all the assembly attributes.
+ ///
+ ///
+ /// All assembly attributes.
+ ///
+ public IEnumerable GetAssemblyAttributes()
+ {
+ return AssembliesToScan.SelectMany(a => a.GetCustomAttributes()).ToList();
+ }
+
+ ///
+ /// Gets the assembly attributes of the specified .
+ ///
+ /// The attribute types.
+ ///
+ /// The assembly attributes of the specified types.
+ ///
+ /// attributeTypes
+ public IEnumerable GetAssemblyAttributes(params Type[] attributeTypes)
+ {
+ if (attributeTypes == null) throw new ArgumentNullException(nameof(attributeTypes));
+
+ return AssembliesToScan.SelectMany(a => attributeTypes.SelectMany(at => a.GetCustomAttributes(at))).ToList();
+ }
+
+ #endregion
+
#region Get Types
///
@@ -813,11 +857,44 @@ namespace Umbraco.Core.Composing
}
///
- /// Represents the error that occurs when a type was not found in the cache type
- /// list with the specified TypeResolutionKind.
+ /// Represents the error that occurs when a type was not found in the cache type list with the specified TypeResolutionKind.
///
+ ///
+ [Serializable]
internal class CachedTypeNotFoundInFileException : Exception
- { }
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CachedTypeNotFoundInFileException()
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public CachedTypeNotFoundInFileException(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 CachedTypeNotFoundInFileException(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 CachedTypeNotFoundInFileException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
+ }
#endregion
}
diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs
index 5dd33c2a60..ced9a9386a 100644
--- a/src/Umbraco.Core/CompositionExtensions.cs
+++ b/src/Umbraco.Core/CompositionExtensions.cs
@@ -49,6 +49,13 @@ namespace Umbraco.Core
public static DataEditorCollectionBuilder DataEditors(this Composition composition)
=> composition.WithCollectionBuilder();
+ ///
+ /// Gets the data value reference factory collection builder.
+ ///
+ /// The composition.
+ public static DataValueReferenceFactoryCollectionBuilder DataValueReferenceFactories(this Composition composition)
+ => composition.WithCollectionBuilder();
+
///
/// Gets the property value converters collection builder.
///
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/Configuration/UmbracoSettings/IKeepAliveSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IKeepAliveSection.cs
new file mode 100644
index 0000000000..3a0ad258c5
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IKeepAliveSection.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Core.Configuration.UmbracoSettings
+{
+ public interface IKeepAliveSection : IUmbracoConfigurationSection
+ {
+ bool DisableKeepAliveTask { get; }
+ string KeepAlivePingUrl { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs
index 81d27e7bae..acd9c92588 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs
@@ -15,5 +15,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
ILoggingSection Logging { get; }
IWebRoutingSection WebRouting { get; }
+
+ IKeepAliveSection KeepAlive { get; }
}
}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/KeepAliveElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/KeepAliveElement.cs
new file mode 100644
index 0000000000..89ba9be54d
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/KeepAliveElement.cs
@@ -0,0 +1,13 @@
+using System.Configuration;
+
+namespace Umbraco.Core.Configuration.UmbracoSettings
+{
+ internal class KeepAliveElement : ConfigurationElement, IKeepAliveSection
+ {
+ [ConfigurationProperty("disableKeepAliveTask", DefaultValue = "false")]
+ public bool DisableKeepAliveTask => (bool)base["disableKeepAliveTask"];
+
+ [ConfigurationProperty("keepAlivePingUrl", DefaultValue = "{umbracoApplicationUrl}/api/keepalive/ping")]
+ public string KeepAlivePingUrl => (string)base["keepAlivePingUrl"];
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs
index 7cf8096345..17430c81eb 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs
@@ -19,10 +19,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
[ConfigurationProperty("logging")]
internal LoggingElement Logging => (LoggingElement)this["logging"];
-
[ConfigurationProperty("web.routing")]
internal WebRoutingElement WebRouting => (WebRoutingElement)this["web.routing"];
+ [ConfigurationProperty("keepAlive")]
+ internal KeepAliveElement KeepAlive => (KeepAliveElement)this["keepAlive"];
+
IContentSection IUmbracoSettingsSection.Content => Content;
ISecuritySection IUmbracoSettingsSection.Security => Security;
@@ -34,5 +36,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
ILoggingSection IUmbracoSettingsSection.Logging => Logging;
IWebRoutingSection IUmbracoSettingsSection.WebRouting => WebRouting;
+
+ IKeepAliveSection IUmbracoSettingsSection.KeepAlive => KeepAlive;
}
}
diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs
index 509be46b61..704617d90c 100644
--- a/src/Umbraco.Core/Constants-AppSettings.cs
+++ b/src/Umbraco.Core/Constants-AppSettings.cs
@@ -9,6 +9,8 @@ namespace Umbraco.Core
///
public static class AppSettings
{
+ public const string MainDomLock = "Umbraco.Core.MainDom.Lock";
+
// TODO: Kill me - still used in Umbraco.Core.IO.SystemFiles:27
[Obsolete("We need to kill this appsetting as we do not use XML content cache umbraco.config anymore due to NuCache")]
public const string ContentXML = "Umbraco.Core.ContentXML"; //umbracoContentXML
@@ -41,6 +43,21 @@ namespace Umbraco.Core
///
public const string Path = "Umbraco.Core.Path";
+ ///
+ /// Gets the path to the css directory (/css by default).
+ ///
+ public const string CssPath = "umbracoCssPath";
+
+ ///
+ /// Gets the path to the scripts directory (/scripts by default).
+ ///
+ public const string ScriptsPath = "umbracoScriptsPath";
+
+ ///
+ /// Gets the path to media directory (/media by default).
+ ///
+ public const string MediaPath = "umbracoMediaPath";
+
///
/// The reserved urls from web.config.
///
diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs
index 6c9407667a..c1d7103a1c 100644
--- a/src/Umbraco.Core/Constants-Conventions.cs
+++ b/src/Umbraco.Core/Constants-Conventions.cs
@@ -221,7 +221,8 @@ namespace Umbraco.Core
FailedPasswordAttempts,
new PropertyType(PropertyEditors.Aliases.Label, ValueStorageType.Integer, true, FailedPasswordAttempts)
{
- Name = FailedPasswordAttemptsLabel
+ Name = FailedPasswordAttemptsLabel,
+ DataTypeId = Constants.DataTypes.LabelInt
}
},
{
@@ -242,35 +243,40 @@ namespace Umbraco.Core
LastLockoutDate,
new PropertyType(PropertyEditors.Aliases.Label, ValueStorageType.Date, true, LastLockoutDate)
{
- Name = LastLockoutDateLabel
+ Name = LastLockoutDateLabel,
+ DataTypeId = Constants.DataTypes.LabelDateTime
}
},
{
LastLoginDate,
new PropertyType(PropertyEditors.Aliases.Label, ValueStorageType.Date, true, LastLoginDate)
{
- Name = LastLoginDateLabel
+ Name = LastLoginDateLabel,
+ DataTypeId = Constants.DataTypes.LabelDateTime
}
},
{
LastPasswordChangeDate,
new PropertyType(PropertyEditors.Aliases.Label, ValueStorageType.Date, true, LastPasswordChangeDate)
{
- Name = LastPasswordChangeDateLabel
+ Name = LastPasswordChangeDateLabel,
+ DataTypeId = Constants.DataTypes.LabelDateTime
}
},
{
PasswordAnswer,
new PropertyType(PropertyEditors.Aliases.Label, ValueStorageType.Nvarchar, true, PasswordAnswer)
{
- Name = PasswordAnswerLabel
+ Name = PasswordAnswerLabel,
+ DataTypeId = Constants.DataTypes.LabelString
}
},
{
PasswordQuestion,
new PropertyType(PropertyEditors.Aliases.Label, ValueStorageType.Nvarchar, true, PasswordQuestion)
{
- Name = PasswordQuestionLabel
+ Name = PasswordQuestionLabel,
+ DataTypeId = Constants.DataTypes.LabelString
}
}
};
@@ -309,34 +315,65 @@ namespace Umbraco.Core
public static class RelationTypes
{
///
- /// ContentType name for default relation type "Relate Document On Copy".
+ /// Name for default relation type "Related Media".
+ ///
+ public const string RelatedMediaName = "Related Media";
+
+ ///
+ /// Alias for default relation type "Related Media"
+ ///
+ public const string RelatedMediaAlias = "umbMedia";
+
+ ///
+ /// Name for default relation type "Related Document".
+ ///
+ public const string RelatedDocumentName = "Related Document";
+
+ ///
+ /// Alias for default relation type "Related Document"
+ ///
+ public const string RelatedDocumentAlias = "umbDocument";
+
+ ///
+ /// Name for default relation type "Relate Document On Copy".
///
public const string RelateDocumentOnCopyName = "Relate Document On Copy";
///
- /// ContentType alias for default relation type "Relate Document On Copy".
+ /// Alias for default relation type "Relate Document On Copy".
///
public const string RelateDocumentOnCopyAlias = "relateDocumentOnCopy";
///
- /// ContentType name for default relation type "Relate Parent Document On Delete".
+ /// Name for default relation type "Relate Parent Document On Delete".
///
public const string RelateParentDocumentOnDeleteName = "Relate Parent Document On Delete";
///
- /// ContentType alias for default relation type "Relate Parent Document On Delete".
+ /// Alias for default relation type "Relate Parent Document On Delete".
///
public const string RelateParentDocumentOnDeleteAlias = "relateParentDocumentOnDelete";
///
- /// ContentType name for default relation type "Relate Parent Media Folder On Delete".
+ /// Name for default relation type "Relate Parent Media Folder On Delete".
///
public const string RelateParentMediaFolderOnDeleteName = "Relate Parent Media Folder On Delete";
///
- /// ContentType alias for default relation type "Relate Parent Media Folder On Delete".
+ /// Alias for default relation type "Relate Parent Media Folder On Delete".
///
public const string RelateParentMediaFolderOnDeleteAlias = "relateParentMediaFolderOnDelete";
+
+ ///
+ /// Returns the types of relations that are automatically tracked
+ ///
+ ///
+ /// Developers should not manually use these relation types since they will all be cleared whenever an entity
+ /// (content, media or member) is saved since they are auto-populated based on property values.
+ ///
+ public static string[] AutomaticRelationTypes = new[] { RelatedMediaAlias, RelatedDocumentAlias };
+
+ //TODO: return a list of built in types so we can use that to prevent deletion in the uI
}
}
}
diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs
index b48286f197..eb2b3525a7 100644
--- a/src/Umbraco.Core/Constants-PropertyEditors.cs
+++ b/src/Umbraco.Core/Constants-PropertyEditors.cs
@@ -85,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.
///
@@ -203,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/UdiEntityType.cs b/src/Umbraco.Core/Contants-UdiEntityType.cs
similarity index 100%
rename from src/Umbraco.Core/UdiEntityType.cs
rename to src/Umbraco.Core/Contants-UdiEntityType.cs
diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs
index a7d40b0b7d..3edad0c963 100644
--- a/src/Umbraco.Core/ContentExtensions.cs
+++ b/src/Umbraco.Core/ContentExtensions.cs
@@ -13,6 +13,7 @@ using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Membership;
+using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
@@ -23,6 +24,8 @@ namespace Umbraco.Core
// this ain't pretty
private static IMediaFileSystem _mediaFileSystem;
private static IMediaFileSystem MediaFileSystem => _mediaFileSystem ?? (_mediaFileSystem = Current.MediaFileSystem);
+ private static readonly PropertyEditorCollection _propertyEditors;
+ private static PropertyEditorCollection PropertyEditors = _propertyEditors ?? (_propertyEditors = Current.PropertyEditors);
#region IContent
@@ -162,14 +165,12 @@ namespace Umbraco.Core
// Fixes https://github.com/umbraco/Umbraco-CMS/issues/3937 - Assigning a new file to an
// existing IMedia with extension SetValue causes exception 'Illegal characters in path'
string oldpath = null;
- if (property.GetValue(culture, segment) is string svalue)
+ var value = property.GetValue(culture, segment);
+
+ if (PropertyEditors.TryGet(propertyTypeAlias, out var editor)
+ && editor is IDataEditorWithMediaPath dataEditor)
{
- if (svalue.DetectIsJson())
- {
- // the property value is a JSON serialized image crop data set - grab the "src" property as the file source
- var jObject = JsonConvert.DeserializeObject(svalue);
- svalue = jObject != null ? jObject.GetValueAsString("src") : svalue;
- }
+ var svalue = dataEditor.GetMediaPath(value);
oldpath = MediaFileSystem.GetRelativePath(svalue);
}
diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs
index 9fdc5f0b90..29442b78e6 100644
--- a/src/Umbraco.Core/ContentVariationExtensions.cs
+++ b/src/Umbraco.Core/ContentVariationExtensions.cs
@@ -12,121 +12,260 @@ namespace Umbraco.Core
///
/// Determines whether the content type is invariant.
///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type is invariant.
+ ///
public static bool VariesByNothing(this ISimpleContentType contentType) => contentType.Variations.VariesByNothing();
- ///
- /// Determines whether the content type varies by culture.
- ///
- public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture();
-
///
/// Determines whether the content type is invariant.
///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type is invariant.
+ ///
public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing();
- ///
- /// Determines whether the content type varies by culture.
- ///
- /// And then it could also vary by segment.
- public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture();
-
- ///
- /// Determines whether the content type varies by segment.
- ///
- /// And then it could also vary by culture.
- public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment();
-
- ///
- /// Determines whether the content type varies by culture and segment.
- ///
- public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment();
-
- ///
- /// Determines whether the property type is invariant.
- ///
- public static bool VariesByNothing(this PropertyType propertyType) => propertyType.Variations.VariesByNothing();
-
- ///
- /// Determines whether the property type varies by culture.
- ///
- /// And then it could also vary by segment.
- public static bool VariesByCulture(this PropertyType propertyType) => propertyType.Variations.VariesByCulture();
-
- ///
- /// Determines whether the property type varies by segment.
- ///
- /// And then it could also vary by culture.
- public static bool VariesBySegment(this PropertyType propertyType) => propertyType.Variations.VariesBySegment();
-
- ///
- /// Determines whether the property type varies by culture and segment.
- ///
- public static bool VariesByCultureAndSegment(this PropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
-
///
/// Determines whether the content type is invariant.
///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type is invariant.
+ ///
public static bool VariesByNothing(this IPublishedContentType contentType) => contentType.Variations.VariesByNothing();
///
- /// Determines whether the content type varies by culture.
+ /// Determines whether the property type is invariant.
///
- /// And then it could also vary by segment.
- public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture();
-
- ///
- /// Determines whether the content type varies by segment.
- ///
- /// And then it could also vary by culture.
- public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment();
-
- ///
- /// Determines whether the content type varies by culture and segment.
- ///
- public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment();
+ /// The property type.
+ ///
+ /// A value indicating whether the property type is invariant.
+ ///
+ public static bool VariesByNothing(this PropertyType propertyType) => propertyType.Variations.VariesByNothing();
///
/// Determines whether the property type is invariant.
///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type is invariant.
+ ///
public static bool VariesByNothing(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing();
- ///
- /// Determines whether the property type varies by culture.
- ///
- public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture();
-
- ///
- /// Determines whether the property type varies by segment.
- ///
- public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment();
-
- ///
- /// Determines whether the property type varies by culture and segment.
- ///
- public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
-
///
/// Determines whether a variation is invariant.
///
+ /// The variation.
+ ///
+ /// A value indicating whether the variation is invariant.
+ ///
public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing;
+ ///
+ /// Determines whether the content type varies by culture.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture.
+ ///
+ public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture();
+
+ ///
+ /// Determines whether the content type varies by culture.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture.
+ ///
+ public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture();
+
+ ///
+ /// Determines whether the content type varies by culture.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture.
+ ///
+ public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture();
+
+ ///
+ /// Determines whether the property type varies by culture.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by culture.
+ ///
+ public static bool VariesByCulture(this PropertyType propertyType) => propertyType.Variations.VariesByCulture();
+
+ ///
+ /// Determines whether the property type varies by culture.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by culture.
+ ///
+ public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture();
+
///
/// Determines whether a variation varies by culture.
///
- /// And then it could also vary by segment.
+ /// The variation.
+ ///
+ /// A value indicating whether the variation varies by culture.
+ ///
public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0;
+ ///
+ /// Determines whether the content type varies by segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by segment.
+ ///
+ public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment();
+
+ ///
+ /// Determines whether the content type varies by segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by segment.
+ ///
+ public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment();
+
+ ///
+ /// Determines whether the content type varies by segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by segment.
+ ///
+ public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment();
+
+ ///
+ /// Determines whether the property type varies by segment.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by segment.
+ ///
+ public static bool VariesBySegment(this PropertyType propertyType) => propertyType.Variations.VariesBySegment();
+
+ ///
+ /// Determines whether the property type varies by segment.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by segment.
+ ///
+ public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment();
+
///
/// Determines whether a variation varies by segment.
///
- /// And then it could also vary by culture.
+ /// The variation.
+ ///
+ /// A value indicating whether the variation varies by segment.
+ ///
public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0;
+ ///
+ /// Determines whether the content type varies by culture and segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture and segment.
+ ///
+ public static bool VariesByCultureAndSegment(this ISimpleContentType contentType) => contentType.Variations.VariesByCultureAndSegment();
+
+ ///
+ /// Determines whether the content type varies by culture and segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture and segment.
+ ///
+ public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment();
+
+ ///
+ /// Determines whether the content type varies by culture and segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture and segment.
+ ///
+ public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment();
+
+ ///
+ /// Determines whether the property type varies by culture and segment.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by culture and segment.
+ ///
+ public static bool VariesByCultureAndSegment(this PropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
+
+ ///
+ /// Determines whether the property type varies by culture and segment.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by culture and segment.
+ ///
+ public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
+
///
/// Determines whether a variation varies by culture and segment.
///
+ /// The variation.
+ ///
+ /// A value indicating whether the variation varies by culture and segment.
+ ///
public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment;
+ ///
+ /// Sets or removes the content type variation depending on the specified value.
+ ///
+ /// The content type.
+ /// The variation to set or remove.
+ /// If set to true sets the variation; otherwise, removes the variation.
+ ///
+ /// This method does not support setting the variation to nothing.
+ ///
+ public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetFlag(variation, value);
+
+ ///
+ /// Sets or removes the property type variation depending on the specified value.
+ ///
+ /// The property type.
+ /// The variation to set or remove.
+ /// If set to true sets the variation; otherwise, removes the variation.
+ ///
+ /// This method does not support setting the variation to nothing.
+ ///
+ public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetFlag(variation, value);
+
+ ///
+ /// Returns the variations with the variation set or removed depending on the specified value.
+ ///
+ /// The existing variations.
+ /// The variation to set or remove.
+ /// If set to true sets the variation; otherwise, removes the variation.
+ ///
+ /// The variations with the variation set or removed.
+ ///
+ ///
+ /// This method does not support setting the variation to nothing.
+ ///
+ public static ContentVariation SetFlag(this ContentVariation variations, ContentVariation variation, bool value = true)
+ {
+ return value
+ ? variations | variation // Set flag using bitwise logical OR
+ : variations & ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit)
+ }
+
///
/// Validates that a combination of culture and segment is valid for the variation.
///
@@ -135,54 +274,66 @@ namespace Umbraco.Core
/// The segment.
/// A value indicating whether to perform exact validation.
/// A value indicating whether to support wildcards.
- /// A value indicating whether to throw a when the combination is invalid.
- /// True if the combination is valid; otherwise false.
+ /// A value indicating whether to throw a when the combination is invalid.
+ ///
+ /// true if the combination is valid; otherwise false.
+ ///
+ /// Occurs when the combination is invalid, and is true.
///
/// When validation is exact, the combination must match the variation exactly. For instance, if the variation is Culture, then
/// a culture is required. When validation is not strict, the combination must be equivalent, or more restrictive: if the variation is
/// Culture, an invariant combination is ok.
/// Basically, exact is for one content type, or one property type, and !exact is for "all property types" of one content type.
- /// Both and can be "*" to indicate "all of them".
+ /// Both and can be "*" to indicate "all of them".
///
- /// Occurs when the combination is invalid, and is true.
public static bool ValidateVariation(this ContentVariation variation, string culture, string segment, bool exact, bool wildcards, bool throwIfInvalid)
{
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/EnumExtensions.cs b/src/Umbraco.Core/EnumExtensions.cs
index 1443a27876..9097432f64 100644
--- a/src/Umbraco.Core/EnumExtensions.cs
+++ b/src/Umbraco.Core/EnumExtensions.cs
@@ -3,43 +3,42 @@
namespace Umbraco.Core
{
///
- /// Provides extension methods to enums.
+ /// Provides extension methods to .
///
public static class EnumExtensions
{
- // note:
- // - no need to HasFlagExact, that's basically an == test
- // - HasFlagAll cannot be named HasFlag because ext. methods never take priority over instance methods
-
///
- /// Determines whether a flag enum has all the specified values.
+ /// Determines whether all the flags/bits are set within the enum value.
///
- ///
- /// True when all bits set in are set in , though other bits may be set too.
- /// This is the behavior of the original method.
- ///
- public static bool HasFlagAll(this T use, T uses)
+ /// The enum type.
+ /// The enum value.
+ /// The flags.
+ ///
+ /// true if all the flags/bits are set within the enum value; otherwise, false.
+ ///
+ [Obsolete("Use Enum.HasFlag() or bitwise operations (if performance is important) instead.")]
+ public static bool HasFlagAll(this T value, T flags)
where T : Enum
{
- var num = Convert.ToUInt64(use);
- var nums = Convert.ToUInt64(uses);
-
- return (num & nums) == nums;
+ return value.HasFlag(flags);
}
///
- /// Determines whether a flag enum has any of the specified values.
+ /// Determines whether any of the flags/bits are set within the enum value.
///
- ///
- /// True when at least one of the bits set in is set in .
- ///
- public static bool HasFlagAny(this T use, T uses)
+ /// The enum type.
+ /// The value.
+ /// The flags.
+ ///
+ /// true if any of the flags/bits are set within the enum value; otherwise, false.
+ ///
+ public static bool HasFlagAny(this T value, T flags)
where T : Enum
{
- var num = Convert.ToUInt64(use);
- var nums = Convert.ToUInt64(uses);
+ var v = Convert.ToUInt64(value);
+ var f = Convert.ToUInt64(flags);
- return (num & nums) > 0;
+ return (v & f) > 0;
}
}
}
diff --git a/src/Umbraco.Core/Exceptions/ArgumentNullOrEmptyException.cs b/src/Umbraco.Core/Exceptions/ArgumentNullOrEmptyException.cs
index 90cc20c404..037d35d0ee 100644
--- a/src/Umbraco.Core/Exceptions/ArgumentNullOrEmptyException.cs
+++ b/src/Umbraco.Core/Exceptions/ArgumentNullOrEmptyException.cs
@@ -1,16 +1,24 @@
using System;
+using System.Runtime.Serialization;
namespace Umbraco.Core.Exceptions
{
///
- /// The exception that is thrown when a null reference, or an empty argument,
- /// is passed to a method that does not accept it as a valid argument.
+ /// The exception that is thrown when a null reference, or an empty argument, is passed to a method that does not accept it as a valid argument.
///
+ ///
+ [Obsolete("Throw an ArgumentNullException when the parameter is null or an ArgumentException when its empty instead.")]
+ [Serializable]
public class ArgumentNullOrEmptyException : ArgumentNullException
{
///
- /// Initializes a new instance of the class
- /// with the name of the parameter that caused this exception.
+ /// Initializes a new instance of the class.
+ ///
+ public ArgumentNullOrEmptyException()
+ { }
+
+ ///
+ /// Initializes a new instance of the class with the name of the parameter that caused this exception.
///
/// The named of the parameter that caused the exception.
public ArgumentNullOrEmptyException(string paramName)
@@ -18,13 +26,30 @@ namespace Umbraco.Core.Exceptions
{ }
///
- /// Initializes a new instance of the class
- /// with a specified error message and the name of the parameter that caused this exception.
+ /// Initializes a new instance of the class with a specified error message and the name of the parameter that caused this exception.
///
/// The named of the parameter that caused the exception.
/// A message that describes the error.
public ArgumentNullOrEmptyException(string paramName, string message)
: base(paramName, message)
{ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for this 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 ArgumentNullOrEmptyException(string message, Exception innerException)
+ : base(message, innerException)
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that holds the serialized object data.
+ /// An object that describes the source or destination of the serialized data.
+ protected ArgumentNullOrEmptyException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
}
}
diff --git a/src/Umbraco.Core/Exceptions/AuthorizationException.cs b/src/Umbraco.Core/Exceptions/AuthorizationException.cs
index 955fec270b..b87a8da8b8 100644
--- a/src/Umbraco.Core/Exceptions/AuthorizationException.cs
+++ b/src/Umbraco.Core/Exceptions/AuthorizationException.cs
@@ -1,14 +1,45 @@
using System;
+using System.Runtime.Serialization;
namespace Umbraco.Core.Exceptions
{
+ ///
+ /// The exception that is thrown when authorization failed.
+ ///
+ ///
+ [Serializable]
public class AuthorizationException : Exception
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public AuthorizationException()
{ }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
public AuthorizationException(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 AuthorizationException(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 AuthorizationException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
}
}
diff --git a/src/Umbraco.Core/Exceptions/BootFailedException.cs b/src/Umbraco.Core/Exceptions/BootFailedException.cs
index c3262d26c6..e8ffe1d2e9 100644
--- a/src/Umbraco.Core/Exceptions/BootFailedException.cs
+++ b/src/Umbraco.Core/Exceptions/BootFailedException.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.Serialization;
using System.Text;
namespace Umbraco.Core.Exceptions
@@ -6,6 +7,8 @@ namespace Umbraco.Core.Exceptions
///
/// An exception that is thrown if the Umbraco application cannot boot.
///
+ ///
+ [Serializable]
public class BootFailedException : Exception
{
///
@@ -14,27 +17,47 @@ namespace Umbraco.Core.Exceptions
public const string DefaultMessage = "Boot failed: Umbraco cannot run. See Umbraco's log file for more details.";
///
- /// Initializes a new instance of the class with a specified error message.
+ /// Initializes a new instance of the class.
///
- /// The message that describes the error.
+ public BootFailedException()
+ { }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ /// The message that describes the error.
public BootFailedException(string message)
: base(message)
{ }
///
- /// Initializes a new instance of the class with a specified error message
+ /// Initializes a new instance of the class with a specified error message
/// and a reference to the inner exception which is the cause of this exception.
///
- /// The message that describes the error.
- /// The inner exception, or null.
- public BootFailedException(string message, Exception inner)
- : base(message, inner)
+ /// The message that describes the error.
+ /// The inner exception, or null.
+ public BootFailedException(string message, Exception innerException)
+ : base(message, innerException)
{ }
///
- /// Rethrows a captured .
+ /// Initializes a new instance of the class.
///
- /// The exception can be null, in which case a default message is used.
+ /// The that holds the serialized object data about the exception being thrown.
+ /// The that contains contextual information about the source or destination.
+ protected BootFailedException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
+
+ ///
+ /// Rethrows a captured .
+ ///
+ /// The boot failed exception.
+ ///
+ ///
+ ///
+ /// The exception can be null, in which case a default message is used.
+ ///
public static void Rethrow(BootFailedException bootFailedException)
{
if (bootFailedException == null)
diff --git a/src/Umbraco.Core/Exceptions/ConnectionException.cs b/src/Umbraco.Core/Exceptions/ConnectionException.cs
deleted file mode 100644
index 64fdbeee52..0000000000
--- a/src/Umbraco.Core/Exceptions/ConnectionException.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-
-namespace Umbraco.Core.Exceptions
-{
- internal class ConnectionException : Exception
- {
- public ConnectionException(string message, Exception innerException) : base(message, innerException)
- {
-
- }
- }
-}
diff --git a/src/Umbraco.Core/Exceptions/DataOperationException.cs b/src/Umbraco.Core/Exceptions/DataOperationException.cs
index 14fefcf9d3..c004e391fe 100644
--- a/src/Umbraco.Core/Exceptions/DataOperationException.cs
+++ b/src/Umbraco.Core/Exceptions/DataOperationException.cs
@@ -1,21 +1,98 @@
using System;
+using System.Runtime.Serialization;
namespace Umbraco.Core.Exceptions
{
+ ///
+ ///
+ ///
+ ///
+ ///
+ [Serializable]
internal class DataOperationException : Exception
+ where T : Enum
{
+ ///
+ /// Gets the operation.
+ ///
+ ///
+ /// The operation.
+ ///
+ ///
+ /// This object should be serializable to prevent a to be thrown.
+ ///
public T Operation { get; private set; }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DataOperationException()
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public DataOperationException(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 DataOperationException(string message, Exception innerException)
+ : base(message, innerException)
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The operation.
+ public DataOperationException(T operation)
+ : this(operation, "Data operation exception: " + operation)
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The operation.
+ /// The message.
public DataOperationException(T operation, string message)
: base(message)
{
Operation = operation;
}
- public DataOperationException(T operation)
- : base("Data operation exception: " + operation)
+ ///
+ /// 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.
+ /// info
+ protected DataOperationException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
{
- Operation = operation;
+ Operation = (T)Enum.Parse(typeof(T), info.GetString(nameof(Operation)));
+ }
+
+ ///
+ /// When overridden in a derived class, sets the with information about the exception.
+ ///
+ /// The that holds the serialized object data about the exception being thrown.
+ /// The that contains contextual information about the source or destination.
+ /// info
+ public override void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ if (info == null)
+ {
+ throw new ArgumentNullException(nameof(info));
+ }
+
+ info.AddValue(nameof(Operation), Enum.GetName(typeof(T), Operation));
+
+ base.GetObjectData(info, context);
}
}
}
diff --git a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs
index 9d154c6a6f..684e23b020 100644
--- a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs
+++ b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs
@@ -1,44 +1,126 @@
using System;
+using System.Runtime.Serialization;
namespace Umbraco.Core.Exceptions
{
+ ///
+ /// The exception that is thrown when a composition is invalid.
+ ///
+ ///
+ [Serializable]
public class InvalidCompositionException : Exception
{
- public InvalidCompositionException(string contentTypeAlias, string addedCompositionAlias, string[] propertyTypeAliass)
- {
- ContentTypeAlias = contentTypeAlias;
- AddedCompositionAlias = addedCompositionAlias;
- PropertyTypeAliases = propertyTypeAliass;
- }
+ ///
+ /// Gets the content type alias.
+ ///
+ ///
+ /// The content type alias.
+ ///
+ public string ContentTypeAlias { get; }
- public InvalidCompositionException(string contentTypeAlias, string[] propertyTypeAliass)
- {
- ContentTypeAlias = contentTypeAlias;
- PropertyTypeAliases = propertyTypeAliass;
- }
+ ///
+ /// Gets the added composition alias.
+ ///
+ ///
+ /// The added composition alias.
+ ///
+ public string AddedCompositionAlias { get; }
- public string ContentTypeAlias { get; private set; }
+ ///
+ /// Gets the property type aliases.
+ ///
+ ///
+ /// The property type aliases.
+ ///
+ public string[] PropertyTypeAliases { get; }
- public string AddedCompositionAlias { get; private set; }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public InvalidCompositionException()
+ { }
- public string[] PropertyTypeAliases { get; private set; }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The content type alias.
+ /// The property type aliases.
+ public InvalidCompositionException(string contentTypeAlias, string[] propertyTypeAliases)
+ : this(contentTypeAlias, null, propertyTypeAliases)
+ { }
- public override string Message
- {
- get
- {
- return AddedCompositionAlias.IsNullOrWhiteSpace()
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The content type alias.
+ /// The added composition alias.
+ /// The property type aliases.
+ public InvalidCompositionException(string contentTypeAlias, string addedCompositionAlias, string[] propertyTypeAliases)
+ : this(addedCompositionAlias.IsNullOrWhiteSpace()
? string.Format(
"ContentType with alias '{0}' has an invalid composition " +
"and there was a conflict on the following PropertyTypes: '{1}'. " +
"PropertyTypes must have a unique alias across all Compositions in order to compose a valid ContentType Composition.",
- ContentTypeAlias, string.Join(", ", PropertyTypeAliases))
+ contentTypeAlias, string.Join(", ", propertyTypeAliases))
: string.Format(
"ContentType with alias '{0}' was added as a Composition to ContentType with alias '{1}', " +
"but there was a conflict on the following PropertyTypes: '{2}'. " +
"PropertyTypes must have a unique alias across all Compositions in order to compose a valid ContentType Composition.",
- AddedCompositionAlias, ContentTypeAlias, string.Join(", ", PropertyTypeAliases));
+ addedCompositionAlias, contentTypeAlias, string.Join(", ", propertyTypeAliases)))
+ {
+ ContentTypeAlias = contentTypeAlias;
+ AddedCompositionAlias = addedCompositionAlias;
+ PropertyTypeAliases = propertyTypeAliases;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public InvalidCompositionException(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 InvalidCompositionException(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 InvalidCompositionException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ ContentTypeAlias = info.GetString(nameof(ContentTypeAlias));
+ AddedCompositionAlias = info.GetString(nameof(AddedCompositionAlias));
+ PropertyTypeAliases = (string[])info.GetValue(nameof(PropertyTypeAliases), typeof(string[]));
+ }
+
+ ///
+ /// When overridden in a derived class, sets the with information about the exception.
+ ///
+ /// The that holds the serialized object data about the exception being thrown.
+ /// The that contains contextual information about the source or destination.
+ /// info
+ public override void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ if (info == null)
+ {
+ throw new ArgumentNullException(nameof(info));
}
+
+ info.AddValue(nameof(ContentTypeAlias), ContentTypeAlias);
+ info.AddValue(nameof(AddedCompositionAlias), AddedCompositionAlias);
+ info.AddValue(nameof(PropertyTypeAliases), PropertyTypeAliases);
+
+ base.GetObjectData(info, context);
}
}
}
diff --git a/src/Umbraco.Core/Exceptions/PanicException.cs b/src/Umbraco.Core/Exceptions/PanicException.cs
new file mode 100644
index 0000000000..75edf7fd73
--- /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]
+ public 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/Exceptions/WontImplementException.cs b/src/Umbraco.Core/Exceptions/WontImplementException.cs
index 7774bf53de..e354bc4c3d 100644
--- a/src/Umbraco.Core/Exceptions/WontImplementException.cs
+++ b/src/Umbraco.Core/Exceptions/WontImplementException.cs
@@ -1,27 +1,52 @@
using System;
+using System.Runtime.Serialization;
namespace Umbraco.Core.Exceptions
{
///
/// The exception that is thrown when a requested method or operation is not, and will not be, implemented.
///
- /// The is to be used when some code is not implemented,
+ ///
+ /// The is to be used when some code is not implemented,
/// but should eventually be implemented (i.e. work in progress) and is reported by tools such as ReSharper.
/// This exception is to be used when some code is not implemented, and is not meant to be, for whatever
- /// reason.
+ /// reason.
+ ///
+ ///
+ [Serializable]
+ [Obsolete("If a method or operation is not, and will not be, implemented, it is invalid or not supported, so we should throw either an InvalidOperationException or NotSupportedException instead.")]
public class WontImplementException : NotImplementedException
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
public WontImplementException()
{ }
///
- /// Initializes a new instance of the class with a specified reason message.
+ /// Initializes a new instance of the class with a specified reason message.
///
+ /// The error message that explains the reason for the exception.
public WontImplementException(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. If the parameter is not , the current exception is raised in a block that handles the inner exception.
+ public WontImplementException(string message, Exception inner)
+ : base(message, inner)
+ { }
+
+ ///
+ /// 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 WontImplementException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
}
}
diff --git a/src/Umbraco.Core/IO/FileSecurityException.cs b/src/Umbraco.Core/IO/FileSecurityException.cs
index 7b4f7d2625..8ce9ab34a5 100644
--- a/src/Umbraco.Core/IO/FileSecurityException.cs
+++ b/src/Umbraco.Core/IO/FileSecurityException.cs
@@ -1,20 +1,46 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+using System.Runtime.Serialization;
namespace Umbraco.Core.IO
{
+ ///
+ /// The exception that is thrown when the caller does not have the required permission to access a file.
+ ///
+ ///
+ [Obsolete("Throw an UnauthorizedAccessException instead.")]
+ [Serializable]
public class FileSecurityException : Exception
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public FileSecurityException()
- {
+ { }
- }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public FileSecurityException(string message)
+ : base(message)
+ { }
- public FileSecurityException(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 FileSecurityException(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 FileSecurityException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
}
}
diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs
index 2ce1230bcc..05c02171ba 100644
--- a/src/Umbraco.Core/IO/MediaFileSystem.cs
+++ b/src/Umbraco.Core/IO/MediaFileSystem.cs
@@ -1,15 +1,10 @@
using System;
using System.Collections.Generic;
-using System.Configuration;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
-using Umbraco.Core.Composing;
-using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
-using Umbraco.Core.Exceptions;
using Umbraco.Core.Logging;
-using Umbraco.Core.Media;
using Umbraco.Core.Models;
namespace Umbraco.Core.IO
@@ -92,7 +87,8 @@ namespace Umbraco.Core.IO
{
if (content == null) throw new ArgumentNullException(nameof(content));
if (propertyType == null) throw new ArgumentNullException(nameof(propertyType));
- if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullOrEmptyException(nameof(filename));
+ if (filename == null) throw new ArgumentNullException(nameof(filename));
+ if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(filename));
if (filestream == null) throw new ArgumentNullException(nameof(filestream));
// clear the old file, if any
@@ -111,7 +107,8 @@ namespace Umbraco.Core.IO
{
if (content == null) throw new ArgumentNullException(nameof(content));
if (propertyType == null) throw new ArgumentNullException(nameof(propertyType));
- if (string.IsNullOrWhiteSpace(sourcepath)) throw new ArgumentNullOrEmptyException(nameof(sourcepath));
+ if (sourcepath == null) throw new ArgumentNullException(nameof(sourcepath));
+ if (string.IsNullOrWhiteSpace(sourcepath)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(sourcepath));
// ensure we have a file to copy
if (FileExists(sourcepath) == false) return null;
diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs
index e4edb2b86b..96aaf7ca27 100644
--- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs
+++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Umbraco.Core.Composing;
-using Umbraco.Core.Exceptions;
using System.Threading;
using Umbraco.Core.Logging;
@@ -39,9 +38,11 @@ namespace Umbraco.Core.IO
public PhysicalFileSystem(string rootPath, string rootUrl)
{
- if (string.IsNullOrEmpty(rootPath)) throw new ArgumentNullOrEmptyException(nameof(rootPath));
- if (string.IsNullOrEmpty(rootUrl)) throw new ArgumentNullOrEmptyException(nameof(rootUrl));
- if (rootPath.StartsWith("~/")) throw new ArgumentException("The rootPath argument cannot be a virtual path and cannot start with '~/'");
+ if (rootPath == null) throw new ArgumentNullException(nameof(rootPath));
+ if (string.IsNullOrEmpty(rootPath)) throw new ArgumentException("Value can't be empty.", nameof(rootPath));
+ if (rootUrl == null) throw new ArgumentNullException(nameof(rootUrl));
+ if (string.IsNullOrEmpty(rootUrl)) throw new ArgumentException("Value can't be empty.", nameof(rootUrl));
+ if (rootPath.StartsWith("~/")) throw new ArgumentException("Value can't be a virtual path and start with '~/'.", nameof(rootPath));
// rootPath should be... rooted, as in, it's a root path!
if (Path.IsPathRooted(rootPath) == false)
@@ -314,7 +315,7 @@ namespace Umbraco.Core.IO
// nothing prevents us to reach the file, security-wise, yet it is outside
// this filesystem's root - throw
- throw new FileSecurityException("File '" + opath + "' is outside this filesystem's root.");
+ throw new UnauthorizedAccessException("File '" + opath + "' is outside this filesystem's root.");
}
///
diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs
index 2f33d82bdc..b4688d2e9f 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";
@@ -27,13 +29,13 @@ namespace Umbraco.Core.IO
public static string MacroPartials => MvcViews + "/MacroPartials/";
- public static string Media => IOHelper.ReturnPath("umbracoMediaPath", "~/media");
+ public static string Media => IOHelper.ReturnPath(Constants.AppSettings.MediaPath, "~/media");
- public static string Scripts => IOHelper.ReturnPath("umbracoScriptsPath", "~/scripts");
+ public static string Scripts => IOHelper.ReturnPath(Constants.AppSettings.ScriptsPath, "~/scripts");
- public static string Css => IOHelper.ReturnPath("umbracoCssPath", "~/css");
+ public static string Css => IOHelper.ReturnPath(Constants.AppSettings.CssPath, "~/css");
- public static string Umbraco => IOHelper.ReturnPath("umbracoPath", "~/umbraco");
+ public static string Umbraco => IOHelper.ReturnPath(Constants.AppSettings.Path, "~/umbraco");
public static string Packages => Data + "/packages";
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
deleted file mode 100644
index d1012fb669..0000000000
--- a/src/Umbraco.Core/MainDom.cs
+++ /dev/null
@@ -1,217 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Threading;
-using System.Web.Hosting;
-using Umbraco.Core.Logging;
-
-namespace Umbraco.Core
-{
- ///
- /// Provides the full implementation of .
- ///
- ///
- /// When an AppDomain starts, it tries to acquire the main domain status.
- /// When an AppDomain stops (eg the application is restarting) it should release the main domain status.
- ///
- internal class MainDom : IMainDom, IRegisteredObject
- {
- #region Vars
-
- private readonly ILogger _logger;
-
- // our own lock for local consistency
- private readonly object _locko = new object();
-
- // async lock representing the main domain lock
- private readonly AsyncLock _asyncLock;
- private IDisposable _asyncLocker;
-
- // event wait handle used to notify current main domain that it should
- // release the lock because a new domain wants to be the main domain
- private readonly EventWaitHandle _signal;
-
- // indicates whether...
- private volatile bool _isMainDom; // we are the main domain
- private volatile bool _signaled; // we have been signaled
-
- // actions to run before releasing the main domain
- private readonly List> _callbacks = new List>();
-
- private const int LockTimeoutMilliseconds = 90000; // (1.5 * 60 * 1000) == 1 min 30 seconds
-
- #endregion
-
- #region Ctor
-
- // initializes a new instance of MainDom
- public MainDom(ILogger logger)
- {
- _logger = logger;
-
- var appId = string.Empty;
- // HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail
- if (HostingEnvironment.ApplicationID != null)
- appId = HostingEnvironment.ApplicationID.ReplaceNonAlphanumericChars(string.Empty);
-
- // combining with the physical path because if running on eg IIS Express,
- // two sites could have the same appId even though they are different.
- //
- // now what could still collide is... two sites, running in two different processes
- // and having the same appId, and running on the same app physical path
- //
- // we *cannot* use the process ID here because when an AppPool restarts it is
- // a new process for the same application path
-
- var appPath = HostingEnvironment.ApplicationPhysicalPath;
- var hash = (appId + ":::" + appPath).ToSHA1();
-
- var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK";
- _asyncLock = new AsyncLock(lockName);
-
- var eventName = "UMBRACO-" + hash + "-MAINDOM-EVT";
- _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
- }
-
- #endregion
-
- ///
- /// Registers a resource that requires the current AppDomain to be the main domain to function.
- ///
- /// An action to execute before the AppDomain releases the main domain status.
- /// An optional weight (lower goes first).
- /// A value indicating whether it was possible to register.
- public bool Register(Action release, int weight = 100)
- => Register(null, release, weight);
-
- ///
- /// Registers a resource that requires the current AppDomain to be the main domain to function.
- ///
- /// An action to execute when registering.
- /// An action to execute before the AppDomain releases the main domain status.
- /// An optional weight (lower goes first).
- /// A value indicating whether it was possible to register.
- /// If registering is successful, then the action
- /// is guaranteed to execute before the AppDomain releases the main domain status.
- public bool Register(Action install, Action release, int weight = 100)
- {
- lock (_locko)
- {
- if (_signaled) return false;
- install?.Invoke();
- if (release != null)
- _callbacks.Add(new KeyValuePair(weight, release));
- return true;
- }
- }
-
- // handles the signal requesting that the main domain is released
- private void OnSignal(string source)
- {
- // once signaled, we stop waiting, but then there is the hosting environment
- // so we have to make sure that we only enter that method once
-
- lock (_locko)
- {
- _logger.Debug("Signaled ({Signaled}) ({SignalSource})", _signaled ? "again" : "first", source);
- if (_signaled) return;
- if (_isMainDom == false) return; // probably not needed
- _signaled = true;
- }
-
- try
- {
- _logger.Info("Stopping ({SignalSource})", source);
- foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value))
- {
- try
- {
- callback(); // no timeout on callbacks
- }
- catch (Exception e)
- {
- _logger.Error(e, "Error while running callback, remaining callbacks will not run.");
- throw;
- }
-
- }
- _logger.Debug("Stopped ({SignalSource})", source);
- }
- finally
- {
- // in any case...
- _isMainDom = false;
- _asyncLocker.Dispose();
- _logger.Info("Released ({SignalSource})", source);
- }
- }
-
- // acquires the main domain
- internal bool Acquire()
- {
- lock (_locko) // we don't want the hosting environment to interfere by signaling
- {
- // if signaled, too late to acquire, give up
- // the handler is not installed so that would be the hosting environment
- if (_signaled)
- {
- _logger.Info("Cannot acquire (signaled).");
- return false;
- }
-
- _logger.Info("Acquiring.");
-
- // signal other instances that we want the lock, then wait one the lock,
- // which may timeout, and this is accepted - see comments below
-
- // signal, then wait for the lock, then make sure the event is
- // reset (maybe there was noone listening..)
- _signal.Set();
-
- // 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;
-
- // we need to reset the event, because otherwise we would end up
- // signaling ourselves and committing suicide immediately.
- // only 1 instance can reach that point, but other instances may
- // have started and be trying to get the lock - they will timeout,
- // 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"));
-
- HostingEnvironment.RegisterObject(this);
-
- _logger.Info("Acquired.");
- return true;
- }
- }
-
- ///
- /// Gets a value indicating whether the current domain is the main domain.
- ///
- public bool IsMainDom => _isMainDom;
-
- // IRegisteredObject
- void IRegisteredObject.Stop(bool immediate)
- {
- try
- {
- OnSignal("environment"); // will run once
- }
- finally
- {
- HostingEnvironment.UnregisterObject(this);
- }
- }
- }
-}
diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs
index 1c50a4b895..788310d7a1 100644
--- a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs
+++ b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs
@@ -19,6 +19,8 @@ namespace Umbraco.Core.Manifest
// '-content/foo', // hide for content type 'foo'
// '+content/*', // show for all other content types
// '+media/*', // show for all media types
+ // '-member/foo' // hide for member type 'foo'
+ // '+member/*' // show for all member types
// '+role/admin' // show for admin users. Role based permissions will override others.
// ]
// },
@@ -56,6 +58,10 @@ namespace Umbraco.Core.Manifest
partA = "media";
partB = media.ContentType.Alias;
break;
+ case IMember member:
+ partA = "member";
+ partB = member.ContentType.Alias;
+ break;
default:
return null;
diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs
index efd9e92b1f..1ecc738b95 100644
--- a/src/Umbraco.Core/Manifest/ManifestParser.cs
+++ b/src/Umbraco.Core/Manifest/ManifestParser.cs
@@ -5,7 +5,6 @@ using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Umbraco.Core.Cache;
-using Umbraco.Core.Exceptions;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.PropertyEditors;
@@ -42,7 +41,8 @@ namespace Umbraco.Core.Manifest
_cache = appCaches.RuntimeCache;
_validators = validators ?? throw new ArgumentNullException(nameof(validators));
_filters = filters ?? throw new ArgumentNullException(nameof(filters));
- if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path));
+ if (path == null) throw new ArgumentNullException(nameof(path));
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(path));
Path = path;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -155,8 +155,8 @@ namespace Umbraco.Core.Manifest
///
internal PackageManifest ParseManifest(string text)
{
- if (string.IsNullOrWhiteSpace(text))
- throw new ArgumentNullOrEmptyException(nameof(text));
+ if (text == null) throw new ArgumentNullException(nameof(text));
+ if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text));
var manifest = JsonConvert.DeserializeObject(text,
new DataEditorConverter(_logger),
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/DataLossException.cs b/src/Umbraco.Core/Migrations/DataLossException.cs
deleted file mode 100644
index 6ff332f626..0000000000
--- a/src/Umbraco.Core/Migrations/DataLossException.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-
-namespace Umbraco.Core.Migrations
-{
- ///
- /// Used if a migration has executed but the whole process has failed and cannot be rolled back
- ///
- internal class DataLossException : Exception
- {
- public DataLossException(string msg)
- : base(msg)
- {
-
- }
-
- public DataLossException(string msg, Exception inner)
- : base(msg, inner)
- {
-
- }
- }
-}
diff --git a/src/Umbraco.Core/Migrations/Expressions/Delete/DeleteBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Delete/DeleteBuilder.cs
index 9a4f437f62..65c15456a5 100644
--- a/src/Umbraco.Core/Migrations/Expressions/Delete/DeleteBuilder.cs
+++ b/src/Umbraco.Core/Migrations/Expressions/Delete/DeleteBuilder.cs
@@ -1,4 +1,4 @@
-using Umbraco.Core.Exceptions;
+using System;
using Umbraco.Core.Migrations.Expressions.Common;
using Umbraco.Core.Migrations.Expressions.Delete.Column;
using Umbraco.Core.Migrations.Expressions.Delete.Constraint;
@@ -39,8 +39,9 @@ namespace Umbraco.Core.Migrations.Expressions.Delete
///
public IExecutableBuilder KeysAndIndexes(string tableName, bool local = true, bool foreign = true)
{
- if (tableName.IsNullOrWhiteSpace())
- throw new ArgumentNullOrEmptyException(nameof(tableName));
+ if (tableName == null) throw new ArgumentNullException(nameof(tableName));
+ if (string.IsNullOrWhiteSpace(tableName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(tableName));
+
return new DeleteKeysAndIndexesBuilder(_context) { TableName = tableName, DeleteLocal = local, DeleteForeign = foreign };
}
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/IncompleteMigrationExpressionException.cs b/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs
index 91d1838d6f..3c81e2f0e2 100644
--- a/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs
+++ b/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs
@@ -1,28 +1,49 @@
using System;
+using System.Runtime.Serialization;
namespace Umbraco.Core.Migrations
{
///
- /// Represents errors that occurs when a migration exception is not executed.
+ /// The exception that is thrown when a migration expression is not executed.
///
///
- /// Migration expression such as Alter.Table(...).Do() *must* end with Do() else they are
- /// not executed. When a non-executed expression is detected, an IncompleteMigrationExpressionException
- /// is thrown.
+ /// Migration expressions such as Alter.Table(...).Do() must end with Do(), else they are not executed.
+ /// When a non-executed expression is detected, an IncompleteMigrationExpressionException is thrown.
///
+ ///
+ [Serializable]
public class IncompleteMigrationExpressionException : Exception
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
public IncompleteMigrationExpressionException()
{ }
///
- /// Initializes a new instance of the class with a message.
+ /// Initializes a new instance of the class with a message.
///
+ /// The message that describes the error.
public IncompleteMigrationExpressionException(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 IncompleteMigrationExpressionException(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 IncompleteMigrationExpressionException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
}
}
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
index d86c682bd5..d5d8bbab6f 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
@@ -5,7 +5,6 @@ using System.IO;
using System.Linq;
using System.Xml.Linq;
using Umbraco.Core.Configuration;
-using Umbraco.Core.Exceptions;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations.Upgrade;
@@ -278,8 +277,10 @@ namespace Umbraco.Core.Migrations.Install
/// A logger.
private static void SaveConnectionString(string connectionString, string providerName, ILogger logger)
{
- if (string.IsNullOrWhiteSpace(connectionString)) throw new ArgumentNullOrEmptyException(nameof(connectionString));
- if (string.IsNullOrWhiteSpace(providerName)) throw new ArgumentNullOrEmptyException(nameof(providerName));
+ if (connectionString == null) throw new ArgumentNullException(nameof(connectionString));
+ if (string.IsNullOrWhiteSpace(connectionString)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(connectionString));
+ if (providerName == null) throw new ArgumentNullException(nameof(providerName));
+ if (string.IsNullOrWhiteSpace(providerName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(providerName));
var fileSource = "web.config";
var fileName = IOHelper.MapPath(SystemDirectories.Root +"/" + fileSource);
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
index dd5c17713a..76a8b3a2f4 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
@@ -151,6 +151,8 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Domains, Name = "Domains" });
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.KeyValues, Name = "KeyValues" });
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Languages, Name = "Languages" });
+
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" });
}
private void CreateContentTypeData()
@@ -190,6 +192,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 +229,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 });
}
@@ -309,14 +312,27 @@ namespace Umbraco.Core.Migrations.Install
private void CreateRelationTypeData()
{
var relationType = new RelationTypeDto { Id = 1, Alias = Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = Constants.ObjectTypes.Document, ParentObjectType = Constants.ObjectTypes.Document, Dual = true, Name = Constants.Conventions.RelationTypes.RelateDocumentOnCopyName };
- relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid();
+ relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
relationType = new RelationTypeDto { Id = 2, Alias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = Constants.ObjectTypes.Document, ParentObjectType = Constants.ObjectTypes.Document, Dual = false, Name = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName };
- relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid();
+ relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
relationType = new RelationTypeDto { Id = 3, Alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = Constants.ObjectTypes.Media, ParentObjectType = Constants.ObjectTypes.Media, Dual = false, Name = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName };
- relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid();
+ relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
+
+ relationType = new RelationTypeDto { Id = 4, Alias = Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedMediaName };
+ relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
+ _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
+
+ relationType = new RelationTypeDto { Id = 5, Alias = Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedDocumentName };
+ relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
+ _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
+ }
+
+ internal static Guid CreateUniqueRelationTypeId(string alias, string name)
+ {
+ return (alias + "____" + name).ToGuid();
}
private void CreateKeyValueData()
diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs
index 37d1a03a5a..89c3c809e8 100644
--- a/src/Umbraco.Core/Migrations/MigrationPlan.cs
+++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Umbraco.Core.Exceptions;
using Umbraco.Core.Logging;
using Umbraco.Core.Scoping;
using Type = System.Type;
@@ -25,7 +24,9 @@ namespace Umbraco.Core.Migrations
/// The name of the plan.
public MigrationPlan(string name)
{
- if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name));
+ if (name == null) throw new ArgumentNullException(nameof(name));
+ if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
+
Name = name;
}
@@ -43,7 +44,8 @@ namespace Umbraco.Core.Migrations
private MigrationPlan Add(string sourceState, string targetState, Type migration)
{
if (sourceState == null) throw new ArgumentNullException(nameof(sourceState));
- if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentNullOrEmptyException(nameof(targetState));
+ if (targetState == null) throw new ArgumentNullException(nameof(targetState));
+ if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState));
if (sourceState == targetState) throw new ArgumentException("Source and target state cannot be identical.");
if (migration == null) throw new ArgumentNullException(nameof(migration));
if (!migration.Implements()) throw new ArgumentException($"Type {migration.Name} does not implement IMigration.", nameof(migration));
@@ -135,10 +137,12 @@ namespace Umbraco.Core.Migrations
///
public MigrationPlan ToWithClone(string startState, string endState, string targetState)
{
- if (string.IsNullOrWhiteSpace(startState)) throw new ArgumentNullOrEmptyException(nameof(startState));
- if (string.IsNullOrWhiteSpace(endState)) throw new ArgumentNullOrEmptyException(nameof(endState));
- if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentNullOrEmptyException(nameof(targetState));
-
+ if (startState == null) throw new ArgumentNullException(nameof(startState));
+ if (string.IsNullOrWhiteSpace(startState)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(startState));
+ if (endState == null) throw new ArgumentNullException(nameof(endState));
+ if (string.IsNullOrWhiteSpace(endState)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(endState));
+ if (targetState == null) throw new ArgumentNullException(nameof(targetState));
+ if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState));
if (startState == endState) throw new ArgumentException("Start and end states cannot be identical.");
startState = startState.Trim();
@@ -317,7 +321,7 @@ namespace Umbraco.Core.Migrations
// throw a raw exception here: this should never happen as the plan has
// been validated - this is just a paranoid safety test
if (!_transitions.TryGetValue(origState, out transition))
- throw new Exception($"Unknown state \"{origState}\".");
+ throw new InvalidOperationException($"Unknown state \"{origState}\".");
}
// prepare and de-duplicate post-migrations, only keeping the 1st occurence
@@ -339,7 +343,7 @@ namespace Umbraco.Core.Migrations
// safety check - again, this should never happen as the plan has been validated,
// and this is just a paranoid safety test
if (origState != _finalState)
- throw new Exception($"Internal error, reached state {origState} which is not final state {_finalState}");
+ throw new InvalidOperationException($"Internal error, reached state {origState} which is not final state {_finalState}");
return origState;
}
@@ -358,7 +362,7 @@ namespace Umbraco.Core.Migrations
var states = new List { origState };
if (!_transitions.TryGetValue(origState, out var transition))
- throw new Exception($"Unknown state \"{origState}\".");
+ throw new InvalidOperationException($"Unknown state \"{origState}\".");
while (transition != null)
{
@@ -373,12 +377,12 @@ namespace Umbraco.Core.Migrations
}
if (!_transitions.TryGetValue(origState, out transition))
- throw new Exception($"Unknown state \"{origState}\".");
+ throw new InvalidOperationException($"Unknown state \"{origState}\".");
}
// safety check
if (origState != (toState ?? _finalState))
- throw new Exception($"Internal error, reached state {origState} which is not state {toState ?? _finalState}");
+ throw new InvalidOperationException($"Internal error, reached state {origState} which is not state {toState ?? _finalState}");
return states;
}
@@ -417,7 +421,7 @@ namespace Umbraco.Core.Migrations
public override string ToString()
{
return MigrationType == typeof(NoopMigration)
- ? $"{(SourceState == "" ? "" : SourceState)} --> {TargetState}"
+ ? $"{(SourceState == string.Empty ? "" : SourceState)} --> {TargetState}"
: $"{SourceState} -- ({MigrationType.FullName}) --> {TargetState}";
}
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
index e8fd3414ec..302bf21700 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
@@ -6,6 +6,7 @@ using Umbraco.Core.Migrations.Upgrade.Common;
using Umbraco.Core.Migrations.Upgrade.V_8_0_0;
using Umbraco.Core.Migrations.Upgrade.V_8_0_1;
using Umbraco.Core.Migrations.Upgrade.V_8_1_0;
+using Umbraco.Core.Migrations.Upgrade.V_8_6_0;
namespace Umbraco.Core.Migrations.Upgrade
{
@@ -182,6 +183,13 @@ namespace Umbraco.Core.Migrations.Upgrade
To("{0372A42B-DECF-498D-B4D1-6379E907EB94}");
To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}");
+ // to 8.6.0...
+ To("{4759A294-9860-46BC-99F9-B4C975CAE580}");
+ To("{0BC866BC-0665-487A-9913-0290BD0169AD}");
+ To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}");
+ To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}");
+ To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}");
+
//FINAL
}
}
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 4802750f4b..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
@@ -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 7b2daa99ef..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
@@ -74,9 +74,18 @@ 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
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/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/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 1b6597a660..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
@@ -50,10 +50,10 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0
var obj = JsonConvert.DeserializeObject(value);
var allControls = obj.SelectTokens("$.sections..rows..areas..controls");
- foreach (var control in allControls.SelectMany(c => c))
+ foreach (var control in allControls.SelectMany(c => c).OfType())
{
var controlValue = control["value"];
- if (controlValue.Type == JTokenType.String)
+ if (controlValue?.Type == JTokenType.String)
{
control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value());
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs
new file mode 100644
index 0000000000..6ca493ac7e
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs
@@ -0,0 +1,16 @@
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0
+{
+ public class AddMainDomLock : MigrationBase
+ {
+ public AddMainDomLock(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" });
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs
new file mode 100644
index 0000000000..2e2e00a9bc
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs
@@ -0,0 +1,33 @@
+using Umbraco.Core.Migrations.Install;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0
+{
+ ///
+ /// Ensures the new relation types are created
+ ///
+ public class AddNewRelationTypes : MigrationBase
+ {
+ public AddNewRelationTypes(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ CreateRelation(
+ Constants.Conventions.RelationTypes.RelatedMediaAlias,
+ Constants.Conventions.RelationTypes.RelatedMediaName);
+
+ CreateRelation(
+ Constants.Conventions.RelationTypes.RelatedDocumentAlias,
+ Constants.Conventions.RelationTypes.RelatedDocumentName);
+ }
+
+ private void CreateRelation(string alias, string name)
+ {
+ var uniqueId = DatabaseDataCreator.CreateUniqueRelationTypeId(alias ,name); //this is the same as how it installs so everything is consistent
+ Insert.IntoTable(Constants.DatabaseSchema.Tables.RelationType)
+ .Row(new { typeUniqueId = uniqueId, dual = 0, name, alias })
+ .Do();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs
new file mode 100644
index 0000000000..f44695da69
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs
@@ -0,0 +1,21 @@
+using System.Linq;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0
+{
+
+ public class AddPropertyTypeValidationMessageColumns : MigrationBase
+ {
+ public AddPropertyTypeValidationMessageColumns(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
+
+ AddColumnIfNotExists(columns, "mandatoryMessage");
+ AddColumnIfNotExists(columns, "validationRegExpMessage");
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs
new file mode 100644
index 0000000000..75de01dd7f
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs
@@ -0,0 +1,24 @@
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0
+{
+ public class MissingContentVersionsIndexes : MigrationBase
+ {
+ public MissingContentVersionsIndexes(IMigrationContext context) : base(context)
+ {
+ }
+
+ public override void Migrate()
+ {
+ Create
+ .Index("IX_" + ContentVersionDto.TableName + "_NodeId")
+ .OnTable(ContentVersionDto.TableName)
+ .OnColumn("nodeId")
+ .Ascending()
+ .OnColumn("current")
+ .Ascending()
+ .WithOptions().NonClustered()
+ .Do();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs
new file mode 100644
index 0000000000..c79f43d20f
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs
@@ -0,0 +1,38 @@
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0
+{
+
+ public class UpdateRelationTypeTable : MigrationBase
+ {
+ public UpdateRelationTypeTable(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+
+ Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("parentObjectType").AsGuid().Nullable().Do();
+ Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("childObjectType").AsGuid().Nullable().Do();
+
+ //TODO: We have to update this field to ensure it's not null, we can just copy across the name since that is not nullable
+
+ //drop index before we can alter the column
+ if (IndexExists("IX_umbracoRelationType_alias"))
+ Delete
+ .Index("IX_umbracoRelationType_alias")
+ .OnTable(Constants.DatabaseSchema.Tables.RelationType)
+ .Do();
+ //change the column to non nullable
+ Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("alias").AsString(100).NotNullable().Do();
+ //re-create the index
+ Create
+ .Index("IX_umbracoRelationType_alias")
+ .OnTable(Constants.DatabaseSchema.Tables.RelationType)
+ .OnColumn("alias")
+ .Ascending()
+ .WithOptions().Unique().WithOptions().NonClustered()
+ .Do();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs
index fbb68194b7..d02ea82012 100644
--- a/src/Umbraco.Core/Models/ContentBase.cs
+++ b/src/Umbraco.Core/Models/ContentBase.cs
@@ -4,8 +4,6 @@ using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
-using Umbraco.Core.Composing;
-using Umbraco.Core.Exceptions;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
@@ -229,8 +227,8 @@ namespace Umbraco.Core.Models
private void ClearCultureInfo(string culture)
{
- if (culture.IsNullOrWhiteSpace())
- throw new ArgumentNullOrEmptyException(nameof(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));
if (_cultureInfos == null) return;
_cultureInfos.Remove(culture);
diff --git a/src/Umbraco.Core/Models/ContentCultureInfos.cs b/src/Umbraco.Core/Models/ContentCultureInfos.cs
index c8c4bea56a..2f9c08b985 100644
--- a/src/Umbraco.Core/Models/ContentCultureInfos.cs
+++ b/src/Umbraco.Core/Models/ContentCultureInfos.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using Umbraco.Core.Exceptions;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
@@ -18,7 +17,9 @@ namespace Umbraco.Core.Models
///
public ContentCultureInfos(string culture)
{
- if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(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));
+
Culture = culture;
}
diff --git a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs
index 52f6f9adb6..9bc78fc56d 100644
--- a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs
+++ b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs
@@ -1,8 +1,6 @@
using System;
-using System.Collections.Generic;
using System.Collections.Specialized;
using Umbraco.Core.Collections;
-using Umbraco.Core.Exceptions;
namespace Umbraco.Core.Models
{
@@ -23,7 +21,9 @@ namespace Umbraco.Core.Models
///
public void AddOrUpdate(string culture, string name, DateTime date)
{
- if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(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));
+
culture = culture.ToLowerInvariant();
if (TryGetValue(culture, out var item))
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 bf7228ca47..af8c781072 100644
--- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
+++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Umbraco.Core.Exceptions;
namespace Umbraco.Core.Models
{
@@ -103,11 +102,11 @@ namespace Umbraco.Core.Models
public static void SetPublishInfo(this IContent content, string culture, string name, DateTime date)
{
- if (string.IsNullOrWhiteSpace(name))
- throw new ArgumentNullOrEmptyException(nameof(name));
+ if (name == null) throw new ArgumentNullException(nameof(name));
+ if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
- if (culture.IsNullOrWhiteSpace())
- throw new ArgumentNullOrEmptyException(nameof(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.AddOrUpdate(culture, name, date);
}
@@ -153,11 +152,11 @@ namespace Umbraco.Core.Models
public static void SetCultureInfo(this IContentBase content, string culture, string name, DateTime date)
{
- if (name.IsNullOrWhiteSpace())
- throw new ArgumentNullOrEmptyException(nameof(name));
+ if (name == null) throw new ArgumentNullException(nameof(name));
+ if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
- if (culture.IsNullOrWhiteSpace())
- throw new ArgumentNullOrEmptyException(nameof(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.CultureInfos.AddOrUpdate(culture, name, date);
}
@@ -222,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();
@@ -230,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)
@@ -247,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.IsNullOrWhiteSpace())
- throw new ArgumentNullOrEmptyException(nameof(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/Editors/ContentPropertyFile.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs
index ac236e1fdd..225e29a8a1 100644
--- a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs
+++ b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs
@@ -1,5 +1,6 @@
namespace Umbraco.Core.Models.Editors
{
+
///
/// Represents an uploaded file for a property.
///
diff --git a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs
new file mode 100644
index 0000000000..31d48e60cf
--- /dev/null
+++ b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Models.Editors
+{
+ ///
+ /// Used to track reference to other entities in a property value
+ ///
+ public struct UmbracoEntityReference : IEquatable
+ {
+ private static readonly UmbracoEntityReference _empty = new UmbracoEntityReference(Udi.UnknownTypeUdi.Instance, string.Empty);
+
+ public UmbracoEntityReference(Udi udi, string relationTypeAlias)
+ {
+ Udi = udi ?? throw new ArgumentNullException(nameof(udi));
+ RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias));
+ }
+
+ public UmbracoEntityReference(Udi udi)
+ {
+ Udi = udi ?? throw new ArgumentNullException(nameof(udi));
+
+ switch (udi.EntityType)
+ {
+ case Constants.UdiEntityType.Media:
+ RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias;
+ break;
+ default:
+ RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias;
+ break;
+ }
+ }
+
+ public static UmbracoEntityReference Empty() => _empty;
+
+ public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty();
+
+ public Udi Udi { get; }
+ public string RelationTypeAlias { get; }
+
+ public override bool Equals(object obj)
+ {
+ return obj is UmbracoEntityReference reference && Equals(reference);
+ }
+
+ public bool Equals(UmbracoEntityReference other)
+ {
+ return EqualityComparer.Default.Equals(Udi, other.Udi) &&
+ RelationTypeAlias == other.RelationTypeAlias;
+ }
+
+ public override int GetHashCode()
+ {
+ var hashCode = -487348478;
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Udi);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(RelationTypeAlias);
+ return hashCode;
+ }
+
+ public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right)
+ {
+ return !(left == right);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Entities/EntitySlim.cs b/src/Umbraco.Core/Models/Entities/EntitySlim.cs
index b095965056..b4b09b58c2 100644
--- a/src/Umbraco.Core/Models/Entities/EntitySlim.cs
+++ b/src/Umbraco.Core/Models/Entities/EntitySlim.cs
@@ -66,7 +66,7 @@ namespace Umbraco.Core.Models.Entities
public int ParentId { get; set; }
///
- public void SetParent(ITreeEntity parent) => throw new WontImplementException();
+ public void SetParent(ITreeEntity parent) => throw new InvalidOperationException("This property won't be implemented.");
///
[DataMember]
@@ -116,7 +116,7 @@ namespace Umbraco.Core.Models.Entities
///
public object DeepClone()
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
#endregion
@@ -128,47 +128,47 @@ namespace Umbraco.Core.Models.Entities
public bool IsDirty()
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
public bool IsPropertyDirty(string propName)
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
public IEnumerable GetDirtyProperties()
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
public void ResetDirtyProperties()
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
public bool WasDirty()
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
public bool WasPropertyDirty(string propertyName)
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
public void ResetWereDirtyProperties()
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
public void ResetDirtyProperties(bool rememberDirty)
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
public IEnumerable GetWereDirtyProperties()
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
#endregion
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/ITreeEntity.cs b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs
index afa3399202..ab63e1e1d8 100644
--- a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs
+++ b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs
@@ -24,7 +24,7 @@
/// Sets the parent entity.
///
/// Use this method to set the parent entity when the parent entity is known, but has not
- /// been persistent and does not yet have an identity. The parent identifier will we retrieved
+ /// been persistent and does not yet have an identity. The parent identifier will be retrieved
/// from the parent entity when needed. If the parent entity still does not have an entity by that
/// time, an exception will be thrown by getter.
void SetParent(ITreeEntity parent);
@@ -53,4 +53,4 @@
///
bool Trashed { get; }
}
-}
\ No newline at end of file
+}
diff --git a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs
new file mode 100644
index 0000000000..338f363856
--- /dev/null
+++ b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Core.Models.Entities
+{
+ public class MemberEntitySlim : ContentEntitySlim, IMemberEntitySlim
+ {
+ }
+}
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/IRelation.cs b/src/Umbraco.Core/Models/IRelation.cs
index 745216fba1..6bd348d72f 100644
--- a/src/Umbraco.Core/Models/IRelation.cs
+++ b/src/Umbraco.Core/Models/IRelation.cs
@@ -1,4 +1,5 @@
-using System.Runtime.Serialization;
+using System;
+using System.Runtime.Serialization;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
@@ -11,12 +12,18 @@ namespace Umbraco.Core.Models
[DataMember]
int ParentId { get; set; }
+ [DataMember]
+ Guid ParentObjectType { get; set; }
+
///
/// Gets or sets the Child Id of the Relation (Destination)
///
[DataMember]
int ChildId { get; set; }
+ [DataMember]
+ Guid ChildObjectType { get; set; }
+
///
/// Gets or sets the for the Relation
///
diff --git a/src/Umbraco.Core/Models/IRelationType.cs b/src/Umbraco.Core/Models/IRelationType.cs
index 8bbe657427..9253fae8ab 100644
--- a/src/Umbraco.Core/Models/IRelationType.cs
+++ b/src/Umbraco.Core/Models/IRelationType.cs
@@ -29,13 +29,13 @@ namespace Umbraco.Core.Models
///
/// Corresponds to the NodeObjectType in the umbracoNode table
[DataMember]
- Guid ParentObjectType { get; set; }
+ Guid? ParentObjectType { get; set; }
///
/// Gets or sets the Childs object type id
///
/// Corresponds to the NodeObjectType in the umbracoNode table
[DataMember]
- Guid ChildObjectType { get; set; }
+ Guid? ChildObjectType { get; set; }
}
}
diff --git a/src/Umbraco.Core/Models/MediaExtensions.cs b/src/Umbraco.Core/Models/MediaExtensions.cs
index 1166698adb..96f183b6e6 100644
--- a/src/Umbraco.Core/Models/MediaExtensions.cs
+++ b/src/Umbraco.Core/Models/MediaExtensions.cs
@@ -1,10 +1,8 @@
-using System;
-using System.Linq;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
+using System.Linq;
+using Umbraco.Core.Composing;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
-using Umbraco.Core.PropertyEditors.ValueConverters;
+using Umbraco.Core.PropertyEditors;
namespace Umbraco.Core.Models
{
@@ -18,29 +16,12 @@ namespace Umbraco.Core.Models
if (!media.Properties.TryGetValue(propertyAlias, out var property))
return string.Empty;
- // TODO: would need to be adjusted to variations, when media become variants
- if (!(property.GetValue() is string jsonString))
- return string.Empty;
-
- if (property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.UploadField)
- return jsonString;
-
- if (property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.ImageCropper)
+ if (Current.PropertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out var editor)
+ && editor is IDataEditorWithMediaPath dataEditor)
{
- if (jsonString.DetectIsJson() == false)
- return jsonString;
-
- try
- {
- var json = JsonConvert.DeserializeObject(jsonString);
- if (json["src"] != null)
- return json["src"].Value();
- }
- catch (Exception ex)
- {
- logger.Error(ex, "Could not parse the string '{JsonString}' to a json object", jsonString);
- return string.Empty;
- }
+ // TODO: would need to be adjusted to variations, when media become variants
+ var value = property.GetValue();
+ return dataEditor.GetMediaPath(value);
}
// Without knowing what it is, just adding a string here might not be very nice
diff --git a/src/Umbraco.Core/Models/MediaTypeExtensions.cs b/src/Umbraco.Core/Models/MediaTypeExtensions.cs
new file mode 100644
index 0000000000..4e2ae5822a
--- /dev/null
+++ b/src/Umbraco.Core/Models/MediaTypeExtensions.cs
@@ -0,0 +1,10 @@
+namespace Umbraco.Core.Models
+{
+ internal static class MediaTypeExtensions
+ {
+ internal static bool IsSystemMediaType(this IMediaType mediaType) =>
+ mediaType.Alias == Constants.Conventions.MediaTypes.File
+ || mediaType.Alias == Constants.Conventions.MediaTypes.Folder
+ || mediaType.Alias == Constants.Conventions.MediaTypes.Image;
+ }
+}
diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs
index b473a154f1..49e07a486d 100644
--- a/src/Umbraco.Core/Models/Member.cs
+++ b/src/Umbraco.Core/Models/Member.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Umbraco.Core.Composing;
-using Umbraco.Core.Exceptions;
using Umbraco.Core.Logging;
namespace Umbraco.Core.Models
@@ -43,7 +42,8 @@ namespace Umbraco.Core.Models
public Member(string name, IMemberType contentType)
: base(name, -1, contentType, new PropertyCollection())
{
- if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name));
+ if (name == null) throw new ArgumentNullException(nameof(name));
+ if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
IsApproved = true;
@@ -63,9 +63,12 @@ namespace Umbraco.Core.Models
public Member(string name, string email, string username, IMemberType contentType, bool isApproved = true)
: base(name, -1, contentType, new PropertyCollection())
{
- if (string.IsNullOrWhiteSpace(email)) throw new ArgumentNullOrEmptyException(nameof(email));
- if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name));
- if (string.IsNullOrWhiteSpace(username)) throw new ArgumentNullOrEmptyException(nameof(username));
+ if (name == null) throw new ArgumentNullException(nameof(name));
+ if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
+ if (email == null) throw new ArgumentNullException(nameof(email));
+ if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(email));
+ if (username == null) throw new ArgumentNullException(nameof(username));
+ if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username));
_email = email;
_username = username;
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/ObjectTypes.cs b/src/Umbraco.Core/Models/ObjectTypes.cs
index dd943ee02b..2ddbdca77a 100644
--- a/src/Umbraco.Core/Models/ObjectTypes.cs
+++ b/src/Umbraco.Core/Models/ObjectTypes.cs
@@ -41,7 +41,7 @@ namespace Umbraco.Core.Models
///
public static UmbracoObjectTypes GetUmbracoObjectType(string name)
{
- return (UmbracoObjectTypes) Enum.Parse(typeof (UmbracoObjectTypes), name, false);
+ return (UmbracoObjectTypes) Enum.Parse(typeof (UmbracoObjectTypes), name, true);
}
#region Guid object type utilities
diff --git a/src/Umbraco.Core/Models/PagedResult.cs b/src/Umbraco.Core/Models/PagedResult.cs
index ef4d4efdfd..4119751eb3 100644
--- a/src/Umbraco.Core/Models/PagedResult.cs
+++ b/src/Umbraco.Core/Models/PagedResult.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Runtime.Serialization;
namespace Umbraco.Core.Models
@@ -9,7 +8,7 @@ namespace Umbraco.Core.Models
///
///
[DataContract(Name = "pagedCollection", Namespace = "")]
- public class PagedResult
+ public abstract class PagedResult
{
public PagedResult(long totalItems, long pageNumber, long pageSize)
{
@@ -39,9 +38,6 @@ namespace Umbraco.Core.Models
[DataMember(Name = "totalItems")]
public long TotalItems { get; private set; }
- [DataMember(Name = "items")]
- public IEnumerable Items { get; set; }
-
///
/// Calculates the skip size based on the paged parameters specified
///
diff --git a/src/Umbraco.Core/Models/PagedResultOfT.cs b/src/Umbraco.Core/Models/PagedResultOfT.cs
new file mode 100644
index 0000000000..efb68863dd
--- /dev/null
+++ b/src/Umbraco.Core/Models/PagedResultOfT.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Core.Models
+{
+ ///
+ /// Represents a paged result for a model collection
+ ///
+ ///
+ [DataContract(Name = "pagedCollection", Namespace = "")]
+ public class PagedResult : PagedResult
+ {
+ public PagedResult(long totalItems, long pageNumber, long pageSize)
+ : base(totalItems, pageNumber, pageSize)
+ { }
+
+ [DataMember(Name = "items")]
+ public IEnumerable Items { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs
index 61736607c6..fd23756acb 100644
--- a/src/Umbraco.Core/Models/PropertyType.cs
+++ b/src/Umbraco.Core/Models/PropertyType.cs
@@ -26,8 +26,10 @@ namespace Umbraco.Core.Models
private string _propertyEditorAlias;
private ValueStorageType _valueStorageType;
private bool _mandatory;
+ private string _mandatoryMessage;
private int _sortOrder;
private string _validationRegExp;
+ private string _validationRegExpMessage;
private ContentVariation _variations;
///
@@ -183,7 +185,7 @@ namespace Umbraco.Core.Models
}
///
- /// Gets of sets a value indicating whether a value for this property type is required.
+ /// Gets or sets a value indicating whether a value for this property type is required.
///
[DataMember]
public bool Mandatory
@@ -192,6 +194,16 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(Mandatory));
}
+ ///
+ /// Gets or sets the custom validation message used when a value for this PropertyType is required
+ ///
+ [DataMember]
+ public string MandatoryMessage
+ {
+ get => _mandatoryMessage;
+ set => SetPropertyValueAndDetectChanges(value, ref _mandatoryMessage, nameof(MandatoryMessage));
+ }
+
///
/// Gets of sets the sort order of the property type.
///
@@ -212,6 +224,16 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _validationRegExp, nameof(ValidationRegExp));
}
+ ///
+ /// Gets or sets the custom validation message used when a pattern for this PropertyType must be matched
+ ///
+ [DataMember]
+ public string ValidationRegExpMessage
+ {
+ get => _validationRegExpMessage;
+ set => SetPropertyValueAndDetectChanges(value, ref _validationRegExpMessage, nameof(ValidationRegExpMessage));
+ }
+
///
/// Gets or sets the content variation of the property type.
///
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 540abda2c5..dd60eb9beb 100644
--- a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs
@@ -20,7 +20,9 @@ namespace Umbraco.Core.Models.PublishedContent
{
private ModelType(string contentTypeAlias)
{
- if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(contentTypeAlias));
+ if (contentTypeAlias == null) throw new ArgumentNullException(nameof(contentTypeAlias));
+ if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias));
+
ContentTypeAlias = contentTypeAlias;
Name = "{" + ContentTypeAlias + "}";
}
@@ -75,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);
@@ -114,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/PublishedContent/PublishedCultureInfos.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs
index d5096158a7..908b97fc36 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs
@@ -1,5 +1,4 @@
using System;
-using Umbraco.Core.Exceptions;
namespace Umbraco.Core.Models.PublishedContent
{
@@ -13,7 +12,8 @@ namespace Umbraco.Core.Models.PublishedContent
///
public PublishedCultureInfo(string culture, string name, string urlSegment, DateTime date)
{
- if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name));
+ if (name == null) throw new ArgumentNullException(nameof(name));
+ if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
Culture = culture ?? throw new ArgumentNullException(nameof(culture));
Name = name;
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs
index 16eb614ee7..3f2c63a78f 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs
@@ -1,5 +1,4 @@
using System;
-using Umbraco.Core.Exceptions;
namespace Umbraco.Core.Models.PublishedContent
{
@@ -19,7 +18,9 @@ namespace Umbraco.Core.Models.PublishedContent
/// The content type alias.
public PublishedModelAttribute(string contentTypeAlias)
{
- if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(contentTypeAlias));
+ if (contentTypeAlias == null) throw new ArgumentNullException(nameof(contentTypeAlias));
+ if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias));
+
ContentTypeAlias = contentTypeAlias;
}
diff --git a/src/Umbraco.Core/Models/Relation.cs b/src/Umbraco.Core/Models/Relation.cs
index f5d13c70c4..7afa476226 100644
--- a/src/Umbraco.Core/Models/Relation.cs
+++ b/src/Umbraco.Core/Models/Relation.cs
@@ -17,13 +17,36 @@ namespace Umbraco.Core.Models
private IRelationType _relationType;
private string _comment;
+ ///
+ /// Constructor for constructing the entity to be created
+ ///
+ ///
+ ///
+ ///
public Relation(int parentId, int childId, IRelationType relationType)
{
_parentId = parentId;
_childId = childId;
_relationType = relationType;
}
-
+
+ ///
+ /// Constructor for reconstructing the entity from the data source
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Relation(int parentId, int childId, Guid parentObjectType, Guid childObjectType, IRelationType relationType)
+ {
+ _parentId = parentId;
+ _childId = childId;
+ _relationType = relationType;
+ ParentObjectType = parentObjectType;
+ ChildObjectType = childObjectType;
+ }
+
///
/// Gets or sets the Parent Id of the Relation (Source)
@@ -35,6 +58,9 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId));
}
+ [DataMember]
+ public Guid ParentObjectType { get; set; }
+
///
/// Gets or sets the Child Id of the Relation (Destination)
///
@@ -45,6 +71,9 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _childId, nameof(ChildId));
}
+ [DataMember]
+ public Guid ChildObjectType { get; set; }
+
///
/// Gets or sets the for the Relation
///
diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs
index 259b7bc4ef..1085ecdcdd 100644
--- a/src/Umbraco.Core/Models/RelationType.cs
+++ b/src/Umbraco.Core/Models/RelationType.cs
@@ -1,6 +1,5 @@
using System;
using System.Runtime.Serialization;
-using Umbraco.Core.Exceptions;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
@@ -15,24 +14,26 @@ namespace Umbraco.Core.Models
private string _name;
private string _alias;
private bool _isBidrectional;
- private Guid _parentObjectType;
- private Guid _childObjectType;
+ private Guid? _parentObjectType;
+ private Guid? _childObjectType;
- public RelationType(Guid childObjectType, Guid parentObjectType, string alias)
+ //TODO: Should we put back the broken ctors with obsolete attributes?
+
+ public RelationType(string alias, string name)
+ : this(name, alias, false, null, null)
{
- if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentNullOrEmptyException(nameof(alias));
- _childObjectType = childObjectType;
- _parentObjectType = parentObjectType;
+ }
+
+ public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType)
+ {
+ _name = name;
_alias = alias;
- Name = _alias;
+ _isBidrectional = isBidrectional;
+ _parentObjectType = parentObjectType;
+ _childObjectType = childObjectType;
}
- public RelationType(Guid childObjectType, Guid parentObjectType, string alias, string name)
- : this(childObjectType, parentObjectType, alias)
- {
- if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name));
- Name = name;
- }
+
///
/// Gets or sets the Name of the RelationType
@@ -69,7 +70,7 @@ namespace Umbraco.Core.Models
///
/// Corresponds to the NodeObjectType in the umbracoNode table
[DataMember]
- public Guid ParentObjectType
+ public Guid? ParentObjectType
{
get => _parentObjectType;
set => SetPropertyValueAndDetectChanges(value, ref _parentObjectType, nameof(ParentObjectType));
@@ -80,7 +81,7 @@ namespace Umbraco.Core.Models
///
/// Corresponds to the NodeObjectType in the umbracoNode table
[DataMember]
- public Guid ChildObjectType
+ public Guid? ChildObjectType
{
get => _childObjectType;
set => SetPropertyValueAndDetectChanges(value, ref _childObjectType, nameof(ChildObjectType));
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..6a5acb0dc7 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,21 @@ 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,
+ MandatoryMessage = property.Element("MandatoryMessage") != null
+ ? (string)property.Element("MandatoryMessage")
+ : string.Empty,
+
ValidationRegExp = (string)property.Element("Validation"),
- SortOrder = sortOrder
+ ValidationRegExpMessage = property.Element("ValidationRegExpMessage") != null
+ ? (string)property.Element("ValidationRegExpMessage")
+ : string.Empty,
+ 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 +865,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 +994,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 +1077,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 +1106,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 +1196,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 +1264,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/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs
index 1dcd2408e7..e64f40ced7 100644
--- a/src/Umbraco.Core/Persistence/Constants-Locks.cs
+++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs
@@ -8,6 +8,11 @@ namespace Umbraco.Core
///
public static class Locks
{
+ ///
+ /// The lock
+ ///
+ public const int MainDom = -1000;
+
///
/// All servers.
///
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/Dtos/ContentVersionDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
index 3c2c3deda4..4b203c128f 100644
--- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
@@ -19,6 +19,7 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("nodeId")]
[ForeignKey(typeof(ContentDto))]
+ [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current")]
public int NodeId { get; set; }
[Column("versionDate")] // TODO: db rename to 'updateDate'
@@ -30,7 +31,6 @@ namespace Umbraco.Core.Persistence.Dtos
[NullSetting(NullSetting = NullSettings.Null)]
public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero
- // TODO: we need an index on this it is used almost always in querying and sorting
[Column("current")]
public bool Current { get; set; }
diff --git a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs
index 8c52aa1e15..3e8d6e7496 100644
--- a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs
@@ -43,10 +43,20 @@ namespace Umbraco.Core.Persistence.Dtos
[Constraint(Default = "0")]
public bool Mandatory { get; set; }
+ [Column("mandatoryMessage")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ [Length(500)]
+ public string MandatoryMessage { get; set; }
+
[Column("validationRegExp")]
[NullSetting(NullSetting = NullSettings.Null)]
public string ValidationRegExp { get; set; }
+ [Column("validationRegExpMessage")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ [Length(500)]
+ public string ValidationRegExpMessage { get; set; }
+
[Column("Description")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(2000)]
diff --git a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs
index c68dee42b5..4c352a0134 100644
--- a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs
@@ -32,9 +32,15 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("mandatory")]
public bool Mandatory { get; set; }
+ [Column("mandatoryMessage")]
+ public string MandatoryMessage { get; set; }
+
[Column("validationRegExp")]
public string ValidationRegExp { get; set; }
+ [Column("validationRegExpMessage")]
+ public string ValidationRegExpMessage { get; set; }
+
[Column("Description")]
public string Description { get; set; }
diff --git a/src/Umbraco.Core/Persistence/Dtos/RelationDto.cs b/src/Umbraco.Core/Persistence/Dtos/RelationDto.cs
index f1fd3007d7..b21866eb8b 100644
--- a/src/Umbraco.Core/Persistence/Dtos/RelationDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/RelationDto.cs
@@ -34,5 +34,13 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("comment")]
[Length(1000)]
public string Comment { get; set; }
+
+ [ResultColumn]
+ [Column("parentObjectType")]
+ public Guid ParentObjectType { get; set; }
+
+ [ResultColumn]
+ [Column("childObjectType")]
+ public Guid ChildObjectType { get; set; }
}
}
diff --git a/src/Umbraco.Core/Persistence/Dtos/RelationTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/RelationTypeDto.cs
index e972192844..d3e107d23f 100644
--- a/src/Umbraco.Core/Persistence/Dtos/RelationTypeDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/RelationTypeDto.cs
@@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Dtos
[ExplicitColumns]
internal class RelationTypeDto
{
- public const int NodeIdSeed = 4;
+ public const int NodeIdSeed = 10;
[Column("id")]
[PrimaryKeyColumn(IdentitySeed = NodeIdSeed)]
@@ -23,17 +23,20 @@ namespace Umbraco.Core.Persistence.Dtos
public bool Dual { get; set; }
[Column("parentObjectType")]
- public Guid ParentObjectType { get; set; }
+ [NullSetting(NullSetting = NullSettings.Null)]
+ public Guid? ParentObjectType { get; set; }
[Column("childObjectType")]
- public Guid ChildObjectType { get; set; }
+ [NullSetting(NullSetting = NullSettings.Null)]
+ public Guid? ChildObjectType { get; set; }
[Column("name")]
+ [NullSetting(NullSetting = NullSettings.NotNull)]
[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")]
public string Name { get; set; }
[Column("alias")]
- [NullSetting(NullSetting = NullSettings.Null)]
+ [NullSetting(NullSetting = NullSettings.NotNull)]
[Length(100)]
[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")]
public string Alias { get; set; }
diff --git a/src/Umbraco.Core/Persistence/EntityNotFoundException.cs b/src/Umbraco.Core/Persistence/EntityNotFoundException.cs
index e0fe778fa6..ea6d5142f0 100644
--- a/src/Umbraco.Core/Persistence/EntityNotFoundException.cs
+++ b/src/Umbraco.Core/Persistence/EntityNotFoundException.cs
@@ -1,38 +1,96 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+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
+ /// An exception used to indicate that an Umbraco entity could not be found.
///
+ ///
+ [Obsolete("Instead of throwing an exception, return null or an HTTP 404 status code instead.")]
+ [Serializable]
public class EntityNotFoundException : Exception
{
+ ///
+ /// Gets the identifier.
+ ///
+ ///
+ /// The identifier.
+ ///
+ ///
+ /// This object should be serializable to prevent a to be thrown.
+ ///
public object Id { get; private set; }
- private readonly string _msg;
- public EntityNotFoundException(object id, string msg)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public EntityNotFoundException()
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The identifier.
+ /// The message.
+ public EntityNotFoundException(object id, string message)
+ : base(message)
{
Id = id;
- _msg = msg;
}
- public EntityNotFoundException(string msg)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public EntityNotFoundException(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 EntityNotFoundException(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 EntityNotFoundException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
{
- _msg = msg;
+ Id = info.GetValue(nameof(Id), typeof(object));
}
- public override string Message
+ ///
+ /// When overridden in a derived class, sets the with information about the exception.
+ ///
+ /// The that holds the serialized object data about the exception being thrown.
+ /// The that contains contextual information about the source or destination.
+ /// info
+ public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
- get { return _msg; }
+ if (info == null)
+ {
+ throw new ArgumentNullException(nameof(info));
+ }
+
+ info.AddValue(nameof(Id), Id);
+
+ base.GetObjectData(info, context);
}
+ ///
+ /// Returns a that represents this instance.
+ ///
+ ///
+ /// A that represents this instance.
+ ///
public override string ToString()
{
var result = base.ToString();
diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs
index 434e0393cd..2b2bed1d9e 100644
--- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs
@@ -1,17 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text.RegularExpressions;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Repositories;
+using Umbraco.Core.PropertyEditors;
namespace Umbraco.Core.Persistence.Factories
{
internal class ContentBaseFactory
{
- private static readonly Regex MediaPathPattern = new Regex(@"(/media/.+?)(?:['""]|$)", RegexOptions.Compiled);
-
///
/// Builds an IContent item from a dto and content type.
///
@@ -189,7 +187,7 @@ namespace Umbraco.Core.Persistence.Factories
///
/// Builds a dto from an IMedia item.
///
- public static MediaDto BuildDto(IMedia entity)
+ public static MediaDto BuildDto(PropertyEditorCollection propertyEditors, IMedia entity)
{
var contentDto = BuildContentDto(entity, Constants.ObjectTypes.Media);
@@ -197,7 +195,7 @@ namespace Umbraco.Core.Persistence.Factories
{
NodeId = entity.Id,
ContentDto = contentDto,
- MediaVersionDto = BuildMediaVersionDto(entity, contentDto)
+ MediaVersionDto = BuildMediaVersionDto(propertyEditors, entity, contentDto)
};
return dto;
@@ -291,12 +289,20 @@ namespace Umbraco.Core.Persistence.Factories
return dto;
}
- private static MediaVersionDto BuildMediaVersionDto(IMedia entity, ContentDto contentDto)
+ private static MediaVersionDto BuildMediaVersionDto(PropertyEditorCollection propertyEditors, IMedia entity, ContentDto contentDto)
{
// try to get a path from the string being stored for media
// TODO: only considering umbracoFile
- TryMatch(entity.GetValue("umbracoFile"), out var path);
+ string path = null;
+
+ if (entity.Properties.TryGetValue(Constants.Conventions.Media.File, out var property)
+ && propertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out var editor)
+ && editor is IDataEditorWithMediaPath dataEditor)
+ {
+ var value = property.GetValue();
+ path = dataEditor.GetMediaPath(value);
+ }
var dto = new MediaVersionDto
{
@@ -308,22 +314,5 @@ namespace Umbraco.Core.Persistence.Factories
return dto;
}
-
- // TODO: this should NOT be here?!
- // more dark magic ;-(
- internal static bool TryMatch(string text, out string path)
- {
- // In v8 we should allow exposing this via the property editor in a much nicer way so that the property editor
- // can tell us directly what any URL is for a given property if it contains an asset
-
- path = null;
- if (string.IsNullOrWhiteSpace(text)) return false;
-
- var m = MediaPathPattern.Match(text);
- if (!m.Success || m.Groups.Count != 2) return false;
-
- path = m.Groups[1].Value;
- return true;
- }
}
}
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/Factories/PropertyGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs
index db8e2b20d9..dc1629e8f7 100644
--- a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs
@@ -60,8 +60,10 @@ namespace Umbraco.Core.Persistence.Factories
propertyType.Key = typeDto.UniqueId;
propertyType.Name = typeDto.Name;
propertyType.Mandatory = typeDto.Mandatory;
+ propertyType.MandatoryMessage = typeDto.MandatoryMessage;
propertyType.SortOrder = typeDto.SortOrder;
propertyType.ValidationRegExp = typeDto.ValidationRegExp;
+ propertyType.ValidationRegExpMessage = typeDto.ValidationRegExpMessage;
propertyType.PropertyGroupId = new Lazy(() => tempGroupDto.Id);
propertyType.CreateDate = createDate;
propertyType.UpdateDate = updateDate;
@@ -124,9 +126,11 @@ namespace Umbraco.Core.Persistence.Factories
DataTypeId = propertyType.DataTypeId,
Description = propertyType.Description,
Mandatory = propertyType.Mandatory,
+ MandatoryMessage = propertyType.MandatoryMessage,
Name = propertyType.Name,
SortOrder = propertyType.SortOrder,
ValidationRegExp = propertyType.ValidationRegExp,
+ ValidationRegExpMessage = propertyType.ValidationRegExpMessage,
UniqueId = propertyType.Key,
Variations = (byte)propertyType.Variations
};
diff --git a/src/Umbraco.Core/Persistence/Factories/RelationFactory.cs b/src/Umbraco.Core/Persistence/Factories/RelationFactory.cs
index 6abb858e94..d8f100cdbe 100644
--- a/src/Umbraco.Core/Persistence/Factories/RelationFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/RelationFactory.cs
@@ -3,20 +3,11 @@ using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Core.Persistence.Factories
{
- internal class RelationFactory
+ internal static class RelationFactory
{
- private readonly IRelationType _relationType;
-
- public RelationFactory(IRelationType relationType)
+ public static IRelation BuildEntity(RelationDto dto, IRelationType relationType)
{
- _relationType = relationType;
- }
-
- #region Implementation of IEntityFactory
-
- public IRelation BuildEntity(RelationDto dto)
- {
- var entity = new Relation(dto.ParentId, dto.ChildId, _relationType);
+ var entity = new Relation(dto.ParentId, dto.ChildId, dto.ParentObjectType, dto.ChildObjectType, relationType);
try
{
@@ -37,7 +28,7 @@ namespace Umbraco.Core.Persistence.Factories
}
}
- public RelationDto BuildDto(IRelation entity)
+ public static RelationDto BuildDto(IRelation entity)
{
var dto = new RelationDto
{
@@ -54,6 +45,5 @@ namespace Umbraco.Core.Persistence.Factories
return dto;
}
- #endregion
}
}
diff --git a/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs
index ca6928a0a1..edd87fec68 100644
--- a/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs
@@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Factories
public static IRelationType BuildEntity(RelationTypeDto dto)
{
- var entity = new RelationType(dto.ChildObjectType, dto.ParentObjectType, dto.Alias);
+ var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ChildObjectType, dto.ParentObjectType);
try
{
@@ -17,8 +17,6 @@ namespace Umbraco.Core.Persistence.Factories
entity.Id = dto.Id;
entity.Key = dto.UniqueId;
- entity.IsBidirectional = dto.Dual;
- entity.Name = dto.Name;
// reset dirty initial properties (U4-1946)
entity.ResetDirtyProperties(false);
diff --git a/src/Umbraco.Core/Persistence/FaultHandling/RetryLimitExceededException.cs b/src/Umbraco.Core/Persistence/FaultHandling/RetryLimitExceededException.cs
index c537281dc9..a1a0db2983 100644
--- a/src/Umbraco.Core/Persistence/FaultHandling/RetryLimitExceededException.cs
+++ b/src/Umbraco.Core/Persistence/FaultHandling/RetryLimitExceededException.cs
@@ -4,59 +4,51 @@ using System.Runtime.Serialization;
namespace Umbraco.Core.Persistence.FaultHandling
{
///
- /// The special type of exception that provides managed exit from a retry loop. The user code can use this
- /// exception to notify the retry policy that no further retry attempts are required.
+ /// The special type of exception that provides managed exit from a retry loop. The user code can use this exception to notify the retry policy that no further retry attempts are required.
///
+ ///
[Serializable]
public sealed class RetryLimitExceededException : Exception
{
///
- /// Initializes a new instance of the class with a default error message.
+ /// Initializes a new instance of the class with a default error message.
///
public RetryLimitExceededException()
- : this("RetryLimitExceeded")
- {
- }
+ : base()
+ { }
///
- /// Initializes a new instance of the class with a specified error message.
+ /// Initializes a new instance of the class with a specified error message.
///
/// The message that describes the error.
public RetryLimitExceededException(string message)
: base(message)
- {
- }
+ { }
///
- /// Initializes a new instance of the class with a reference to the inner exception
- /// that is the cause of this exception.
+ /// Initializes a new instance of the class with a reference to the inner exception that is the cause of this exception.
///
/// The exception that is the cause of the current exception.
public RetryLimitExceededException(Exception innerException)
- : base(innerException != null ? innerException.Message : "RetryLimitExceeded", innerException)
- {
- }
+ : base(null, innerException)
+ { }
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The message that describes the error.
/// The exception that is the cause of the current exception.
public RetryLimitExceededException(string message, Exception innerException)
: base(message, innerException)
- {
- }
+ { }
///
- /// Initializes a new instance of the class.
+ /// 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.
- /// The parameter is null.
- /// The class name is null or is zero (0).
+ /// The that holds the serialized object data about the exception being thrown.
+ /// The that contains contextual information about the source or destination.
private RetryLimitExceededException(SerializationInfo info, StreamingContext context)
: base(info, context)
- {
- }
+ { }
}
}
diff --git a/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs b/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs
index ab1869a7f5..6f22b61f9a 100644
--- a/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs
+++ b/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs
@@ -24,9 +24,11 @@ namespace Umbraco.Core.Persistence.Mappers
DefineMap(nameof(PropertyType.DataTypeId), nameof(PropertyTypeDto.DataTypeId));
DefineMap(nameof(PropertyType.Description), nameof(PropertyTypeDto.Description));
DefineMap(nameof(PropertyType.Mandatory), nameof(PropertyTypeDto.Mandatory));
+ DefineMap(nameof(PropertyType.MandatoryMessage), nameof(PropertyTypeDto.MandatoryMessage));
DefineMap(nameof(PropertyType.Name), nameof(PropertyTypeDto.Name));
DefineMap(nameof(PropertyType.SortOrder), nameof(PropertyTypeDto.SortOrder));
DefineMap(nameof(PropertyType.ValidationRegExp), nameof(PropertyTypeDto.ValidationRegExp));
+ DefineMap(nameof(PropertyType.ValidationRegExpMessage), nameof(PropertyTypeDto.ValidationRegExpMessage));
DefineMap(nameof(PropertyType.PropertyEditorAlias), nameof(DataTypeDto.EditorAlias));
DefineMap(nameof(PropertyType.ValueStorageType), nameof(DataTypeDto.DbType));
}
diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs
index 0574e37c4c..10db1ca18e 100644
--- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs
+++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs
@@ -14,7 +14,21 @@ namespace Umbraco.Core.Persistence
///
public static partial class NPocoDatabaseExtensions
{
- // TODO: review NPoco native InsertBulk to replace the code below
+ ///
+ /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the underlying RetryDbConnection and ProfiledDbTransaction
+ ///
+ ///
+ /// This is required to use NPoco's own method because we use wrapped DbConnection and DbTransaction instances.
+ /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for bulk inserting of records for
+ /// any other database type and in which case will just insert records one at a time.
+ /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own BulkInsertRecords methods
+ /// do not handle this scenario.
+ ///
+ public static void ConfigureNPocoBulkExtensions()
+ {
+ SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection(dbConn);
+ SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction(dbTran);
+ }
///
/// Bulk-inserts records within a transaction.
@@ -235,7 +249,7 @@ namespace Umbraco.Core.Persistence
//we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared
//to the order in which they are declared in the model then this will not work, so instead we will add column mappings by name so that this explicitly uses
//the names instead of their ordering.
- foreach(var col in bulkReader.ColumnMappings)
+ foreach (var col in bulkReader.ColumnMappings)
{
copy.ColumnMappings.Add(col.DestinationColumn, col.DestinationColumn);
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs
index 69b0698a96..254e04d2d5 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs
@@ -26,5 +26,10 @@ namespace Umbraco.Core.Persistence.Repositories
///
///
bool HasContainerInPath(string contentPath);
+
+ ///
+ /// Returns true or false depending on whether content nodes have been created based on the provided content type id.
+ ///
+ bool HasContentNodes(int id);
}
}
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/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs
index 69f6ef4c5f..a0ddcac8e6 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs
@@ -1,4 +1,5 @@
-using System;
+using NPoco;
+using System;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
@@ -15,10 +16,22 @@ namespace Umbraco.Core.Persistence.Repositories
IEntitySlim Get(int id, Guid objectTypeId);
IEntitySlim Get(Guid key, Guid objectTypeId);
- IEnumerable GetAll(Guid objectType, params int[] ids);
+ IEnumerable GetAll(Guid objectType, params int[] ids);
IEnumerable GetAll(Guid objectType, params Guid[] keys);
+ ///
+ /// Gets entities for a query
+ ///
+ ///
+ ///
IEnumerable GetByQuery(IQuery query);
+
+ ///
+ /// Gets entities for a query and a specific object type allowing the query to be slightly more optimized
+ ///
+ ///
+ ///
+ ///
IEnumerable GetByQuery(IQuery query, Guid objectType);
UmbracoObjectTypes GetObjectType(int id);
@@ -30,7 +43,41 @@ namespace Umbraco.Core.Persistence.Repositories
bool Exists(int id);
bool Exists(Guid key);
+ ///
+ /// Gets paged entities for a query and a subset of object types
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// A callback providing the ability to customize the generated SQL used to retrieve entities
+ ///
+ ///
+ /// A collection of mixed entity types which would be of type , , ,
+ ///
+ ///
+ IEnumerable GetPagedResultsByQuery(
+ IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords,
+ IQuery filter, Ordering ordering, Action> sqlCustomization = null);
+
+ ///
+ /// Gets paged entities for a query and a specific object type
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords,
IQuery filter, Ordering ordering);
+
+
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs
index 51d7656d8a..fc1be20e6f 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs
@@ -1,9 +1,33 @@
-using Umbraco.Core.Models;
+using System;
+using System.Collections.Generic;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.Entities;
+using Umbraco.Core.Persistence.Querying;
+using Umbraco.Core.Services;
namespace Umbraco.Core.Persistence.Repositories
{
public interface IRelationRepository : IReadWriteQueryRepository
{
+ IEnumerable GetPagedRelationsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Ordering ordering);
+ ///
+ /// Persist multiple at once
+ ///
+ ///
+ void Save(IEnumerable relations);
+
+ ///
+ /// Deletes all relations for a parent for any specified relation type alias
+ ///
+ ///
+ ///
+ /// A list of relation types to match for deletion, if none are specified then all relations for this parent id are deleted
+ ///
+ void DeleteByParent(int parentId, params string[] relationTypeAliases);
+
+ IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes);
+
+ IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes);
}
}
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..13b687eb4e 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
@@ -9,6 +9,7 @@ using Umbraco.Core.Composing;
using Umbraco.Core.Events;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
+using Umbraco.Core.Models.Editors;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Dtos;
@@ -24,26 +25,48 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
internal sealed class ContentRepositoryBase
{
///
+ ///
/// This is used for unit tests ONLY
///
public static bool ThrowOnWarning = false;
}
internal abstract class ContentRepositoryBase : NPocoRepositoryBase, IContentRepository
- where TEntity : class, IUmbracoEntity
+ where TEntity : class, IContentBase
where TRepository : class, IRepository
{
- protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILanguageRepository languageRepository, ILogger logger)
+ private readonly Lazy _propertyEditors;
+ private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors
+ ///
+ protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger,
+ ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository,
+ Lazy propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories)
: base(scopeAccessor, cache, logger)
{
LanguageRepository = languageRepository;
+ RelationRepository = relationRepository;
+ RelationTypeRepository = relationTypeRepository;
+ _propertyEditors = propertyEditors;
+ _dataValueReferenceFactories = dataValueReferenceFactories;
}
protected abstract TRepository This { get; }
protected ILanguageRepository LanguageRepository { get; }
+ protected IRelationRepository RelationRepository { get; }
+ protected IRelationTypeRepository RelationTypeRepository { get; }
- protected PropertyEditorCollection PropertyEditors => Current.PropertyEditors; // TODO: inject
+ protected PropertyEditorCollection PropertyEditors => _propertyEditors.Value;
#region Versions
@@ -512,31 +535,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();
@@ -812,5 +820,56 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
#endregion
+
+ protected void PersistRelations(TEntity entity)
+ {
+ // Get all references from our core built in DataEditors/Property Editors
+ // Along with seeing if deverlopers want to collect additional references from the DataValueReferenceFactories collection
+ var trackedRelations = new List();
+ trackedRelations.AddRange(_dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors));
+
+ //First delete all auto-relations for this entity
+ RelationRepository.DeleteByParent(entity.Id, Constants.Conventions.RelationTypes.AutomaticRelationTypes);
+
+ if (trackedRelations.Count == 0) return;
+
+ trackedRelations = trackedRelations.Distinct().ToList();
+ var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi)
+ .ToDictionary(x => (Udi)x, x => x.Guid);
+
+ //lookup in the DB all INT ids for the GUIDs and chuck into a dictionary
+ var keyToIds = Database.Fetch(Sql().Select(x => x.NodeId, x => x.UniqueId).From().WhereIn(x => x.UniqueId, udiToGuids.Values))
+ .ToDictionary(x => x.UniqueId, x => x.NodeId);
+
+ var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty())
+ .ToDictionary(x => x.Alias, x => x);
+
+ var toSave = trackedRelations.Select(rel =>
+ {
+ if (!allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType))
+ throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist");
+
+ if (!udiToGuids.TryGetValue(rel.Udi, out var guid))
+ return null; // This shouldn't happen!
+
+ if (!keyToIds.TryGetValue(guid, out var id))
+ return null; // This shouldn't happen!
+
+ return new Relation(entity.Id, id, relationType);
+ }).WhereNotNull();
+
+ // Save bulk relations
+ RelationRepository.Save(toSave);
+
+ }
+
+ private class NodeIdKey
+ {
+ [Column("id")]
+ public int NodeId { get; set; }
+
+ [Column("uniqueId")]
+ public Guid UniqueId { get; set; }
+ }
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
index 6b751eb8ff..7781e2e38a 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
@@ -296,10 +297,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
Id = dto.Id,
Key = dto.UniqueId,
Mandatory = dto.Mandatory,
+ MandatoryMessage = dto.MandatoryMessage,
Name = dto.Name,
PropertyGroupId = groupId.HasValue ? new Lazy(() => groupId.Value) : null,
SortOrder = dto.SortOrder,
ValidationRegExp = dto.ValidationRegExp,
+ ValidationRegExpMessage = dto.ValidationRegExpMessage,
Variations = (ContentVariation)dto.Variations
};
}
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..6f714ff187 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;
@@ -26,14 +27,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 +100,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 +167,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 +218,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 +377,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 +395,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 +411,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 +437,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 +490,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 +502,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 +545,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 +592,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 +656,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 +688,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 +979,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)
@@ -1108,6 +1323,17 @@ WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentT
return Database.ExecuteScalar(sql) > 0;
}
+ ///
+ /// Returns true or false depending on whether content nodes have been created based on the provided content type id.
+ ///
+ public bool HasContentNodes(int id)
+ {
+ var sql = new Sql(
+ $"SELECT CASE WHEN EXISTS (SELECT * FROM {Constants.DatabaseSchema.Tables.Content} WHERE contentTypeId = @id) THEN 1 ELSE 0 END",
+ new { id });
+ return Database.ExecuteScalar(sql) == 1;
+ }
+
protected override IEnumerable GetDeleteClauses()
{
// in theory, services should have ensured that content items of the given content type
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(pt => pt.Alias);
+
+ var dtos = Database.FetchOneToMany(ct => ct.PropertyTypes, sql);
+
+ return dtos.ToDictionary(
+ x => (Udi)new GuidUdi(ObjectTypes.GetUdiType(x.NodeDto.NodeObjectType.Value), x.NodeDto.UniqueId).EnsureClosed(),
+ x => (IEnumerable)x.PropertyTypes.Select(p => p.Alias).ToList());
+ }
+
private string EnsureUniqueNodeName(string nodeName, int id = 0)
{
var template = SqlContext.Templates.Get("Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName", tsql => tsql
@@ -291,5 +313,24 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return SimilarNodeName.GetUniqueName(names, id, nodeName);
}
+
+
+ [TableName(Constants.DatabaseSchema.Tables.ContentType)]
+ private class ContentTypeReferenceDto : ContentTypeDto
+ {
+ [ResultColumn]
+ [Reference(ReferenceType.Many)]
+ public List PropertyTypes { get; set; }
+ }
+
+ [TableName(Constants.DatabaseSchema.Tables.PropertyType)]
+ private class PropertyTypeReferenceDto
+ {
+ [Column("ptAlias")]
+ public string Alias { get; set; }
+
+ [Column("ptName")]
+ public string Name { get; set; }
+ }
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs
index d137d7ac76..e150b2e632 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs
@@ -2,6 +2,7 @@
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
+using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Scoping;
namespace Umbraco.Core.Persistence.Repositories.Implement
@@ -17,8 +18,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
///
internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository
{
- public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository)
- : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository)
+ public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger,
+ IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository,
+ Lazy propertyEditorCollection, DataValueReferenceFactoryCollection dataValueReferenceFactories)
+ : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories)
{
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
index 30a2927cc8..ccfa8209fb 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
@@ -3,7 +3,6 @@ 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;
@@ -12,6 +11,7 @@ using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Factories;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.SqlSyntax;
+using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
@@ -30,8 +30,23 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
private readonly ContentByGuidReadRepository _contentByGuidReadRepository;
private readonly IScopeAccessor _scopeAccessor;
- public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository)
- : base(scopeAccessor, appCaches, languageRepository, logger)
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors
+ ///
+ public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger,
+ IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository,
+ Lazy propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories)
+ : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories)
{
_contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository));
_templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository));
@@ -229,6 +244,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return MapDtosToContent(Database.Fetch(sql), true);
}
+ // TODO: This method needs to return a readonly version of IContent! The content returned
+ // from this method does not contain all of the data required to re-persist it and if that
+ // is attempted some odd things will occur.
+ // Either we create an IContentReadOnly (which ultimately we should for vNext so we can
+ // differentiate between methods that return entities that can be re-persisted or not), or
+ // in the meantime to not break API compatibility, we can add a property to IContentBase
+ // (or go further and have it on IUmbracoEntity): "IsReadOnly" and if that is true we throw
+ // an exception if that entity is passed to a Save method.
+ // Ideally we return "Slim" versions of content for all sorts of methods here and in ContentService.
+ // Perhaps another non-breaking alternative is to have new services like IContentServiceReadOnly
+ // which can return IContentReadOnly.
+ // We have the ability with `MapDtosToContent` to reduce the amount of data looked up for a
+ // content item. Ideally for paged data that populates list views, these would be ultra slim
+ // content items, there's no reason to populate those with really anything apart from property data,
+ // but until we do something like the above, we can't do that since it would be breaking and unclear.
public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take)
{
var sql = GetBaseQuery(QueryType.Many, false)
@@ -236,7 +266,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.OrderByDescending(x => x.Current)
.AndByDescending(x => x.VersionDate);
- return MapDtosToContent(Database.Fetch(sql), true, true).Skip(skip).Take(take);
+ return MapDtosToContent(Database.Fetch(sql), true,
+ // load bare minimum, need variants though since this is used to rollback with variants
+ false, false, false, true).Skip(skip).Take(take);
}
public override IContent GetVersion(int versionId)
@@ -248,14 +280,63 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return dto == null ? null : MapDtoToContent(dto);
}
+ // deletes a specific version
+ public override void DeleteVersion(int versionId)
+ {
+ // TODO: test object node type?
+
+ // get the version we want to delete
+ var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersion", tsql =>
+ tsql.Select()
+ .AndSelect()
+ .From()
+ .InnerJoin()
+ .On((c, d) => c.Id == d.Id)
+ .Where(x => x.Id == SqlTemplate.Arg("versionId"))
+ );
+ var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault();
+
+ // nothing to delete
+ if (versionDto == null)
+ return;
+
+ // don't delete the current or published version
+ if (versionDto.ContentVersionDto.Current)
+ throw new InvalidOperationException("Cannot delete the current version.");
+ else if (versionDto.Published)
+ throw new InvalidOperationException("Cannot delete the published version.");
+
+ PerformDeleteVersion(versionDto.ContentVersionDto.NodeId, versionId);
+ }
+
+ // deletes all versions of an entity, older than a date.
+ public override void DeleteVersions(int nodeId, DateTime versionDate)
+ {
+ // TODO: test object node type?
+
+ // get the versions we want to delete, excluding the current one
+ var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersions", tsql =>
+ tsql.Select()
+ .From()
+ .InnerJoin()
+ .On((c, d) => c.Id == d.Id)
+ .Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate"))
+ .Where( x => !x.Published)
+ );
+ var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate }));
+ foreach (var versionDto in versionDtos)
+ PerformDeleteVersion(versionDto.NodeId, versionDto.Id);
+ }
+
protected override void PerformDeleteVersion(int id, int versionId)
{
// raise event first else potential FK issues
OnUowRemovingVersion(new ScopedVersionEventArgs(AmbientScope, id, versionId));
Database.Delete("WHERE versionId = @versionId", new { versionId });
- Database.Delete("WHERE id = @versionId", new { versionId });
+ Database.Delete("WHERE versionId = @versionId", new { versionId });
Database.Delete("WHERE id = @versionId", new { versionId });
+ Database.Delete("WHERE id = @versionId", new { versionId });
}
#endregion
@@ -386,7 +467,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing));
// insert document variations
- Database.BulkInsertRecords(GetDocumentVariationDtos(entity, publishing, editedCultures));
+ Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures));
}
// refresh content
@@ -419,6 +500,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
ClearEntityTags(entity, _tagRepository);
}
+ PersistRelations(entity);
+
entity.ResetDirtyProperties();
// troubleshooting
@@ -511,7 +594,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id
documentVersionDto.Published = false; // non-published version
- Database.Insert(documentVersionDto);
+ Database.Insert(documentVersionDto);
}
// replace the property data (rather than updating)
@@ -571,7 +654,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing));
// insert document variations
- Database.BulkInsertRecords(GetDocumentVariationDtos(entity, publishing, editedCultures));
+ Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures));
}
// refresh content
@@ -622,6 +705,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
ClearEntityTags(entity, _tagRepository);
}
+ PersistRelations(entity);
+
// TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what?
entity.ResetDirtyProperties();
@@ -875,32 +960,32 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected override IEnumerable PerformGetByQuery(IQuery query)
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
protected override IEnumerable GetDeleteClauses()
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
protected override void PersistNewItem(IContent entity)
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
protected override void PersistUpdatedItem(IContent entity)
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
protected override Sql GetBaseQuery(bool isCount)
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
protected override string GetBaseWhereClause()
{
- throw new WontImplementException();
+ throw new InvalidOperationException("This method won't be implemented.");
}
}
@@ -1007,7 +1092,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return base.ApplySystemOrdering(ref sql, ordering);
}
- private IEnumerable MapDtosToContent(List dtos, bool withCache = false, bool slim = false)
+ private IEnumerable MapDtosToContent(List dtos,
+ bool withCache = false,
+ bool loadProperties = true,
+ bool loadTemplates = true,
+ bool loadSchedule = true,
+ bool loadVariants = true)
{
var temps = new List>();
var contentTypes = new Dictionary();
@@ -1040,7 +1130,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType);
- if (!slim)
+ if (loadTemplates)
{
// need templates
var templateId = dto.DocumentVersionDto.TemplateId;
@@ -1065,49 +1155,71 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
temps.Add(temp);
}
- if (!slim)
+ Dictionary templates = null;
+ if (loadTemplates)
{
// load all required templates in 1 query, and index
- var templates = _templateRepository.GetMany(templateIds.ToArray())
+ templates = _templateRepository.GetMany(templateIds.ToArray())
.ToDictionary(x => x.Id, x => x);
+ }
+ IDictionary properties = null;
+ if (loadProperties)
+ {
// load all properties for all documents from database in 1 query - indexed by version id
- var properties = GetPropertyCollections(temps);
- var schedule = GetContentSchedule(temps.Select(x => x.Content.Id).ToArray());
+ properties = GetPropertyCollections(temps);
+ }
- // assign templates and properties
- foreach (var temp in temps)
+ var schedule = GetContentSchedule(temps.Select(x => x.Content.Id).ToArray());
+
+ // assign templates and properties
+ foreach (var temp in temps)
+ {
+ if (loadTemplates)
{
// set the template ID if it matches an existing template
if (temp.Template1Id.HasValue && templates.ContainsKey(temp.Template1Id.Value))
temp.Content.TemplateId = temp.Template1Id;
if (temp.Template2Id.HasValue && templates.ContainsKey(temp.Template2Id.Value))
temp.Content.PublishTemplateId = temp.Template2Id;
+ }
+
- // set properties
+ // set properties
+ if (loadProperties)
+ {
if (properties.ContainsKey(temp.VersionId))
temp.Content.Properties = properties[temp.VersionId];
else
throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'.");
+ }
+ if (loadSchedule)
+ {
// load in the schedule
if (schedule.TryGetValue(temp.Content.Id, out var s))
temp.Content.ContentSchedule = s;
}
+
}
- // set variations, if varying
- temps = temps.Where(x => x.ContentType.VariesByCulture()).ToList();
- if (temps.Count > 0)
+ if (loadVariants)
{
- // load all variations for all documents from database, in one query
- var contentVariations = GetContentVariations(temps);
- var documentVariations = GetDocumentVariations(temps);
- foreach (var temp in temps)
- SetVariations(temp.Content, contentVariations, documentVariations);
+ // set variations, if varying
+ temps = temps.Where(x => x.ContentType.VariesByCulture()).ToList();
+ if (temps.Count > 0)
+ {
+ // load all variations for all documents from database, in one query
+ var contentVariations = GetContentVariations(temps);
+ var documentVariations = GetDocumentVariations(temps);
+ foreach (var temp in temps)
+ SetVariations(temp.Content, contentVariations, documentVariations);
+ }
}
+
- foreach(var c in content)
+
+ foreach (var c in content)
c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946)
return content;
@@ -1297,25 +1409,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
};
}
- private IEnumerable GetDocumentVariationDtos(IContent content, bool publishing, HashSet editedCultures)
+ private IEnumerable GetDocumentVariationDtos(IContent content, HashSet editedCultures)
{
var allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct
foreach (var culture in allCultures)
- yield return new DocumentCultureVariationDto
+ {
+ var dto = new DocumentCultureVariationDto
{
NodeId = content.Id,
LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."),
Culture = culture,
Name = content.GetCultureName(culture) ?? content.GetPublishName(culture),
-
- // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem
-
Available = content.IsCultureAvailable(culture),
Published = content.IsCulturePublished(culture),
+ // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem
Edited = content.IsCultureAvailable(culture) &&
(!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture)))
};
+
+ yield return dto;
+ }
+
}
private class ContentVariation
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityContainerRepository.cs
index 09fe949df1..505cbfc816 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityContainerRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityContainerRepository.cs
@@ -3,7 +3,6 @@ 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.Persistence.Dtos;
@@ -129,6 +128,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected override void PersistDeletedItem(EntityContainer entity)
{
+ if (entity == null) throw new ArgumentNullException(nameof(entity));
EnsureContainerType(entity);
var nodeDto = Database.FirstOrDefault(Sql().SelectAll()
@@ -162,9 +162,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected override void PersistNewItem(EntityContainer entity)
{
+ if (entity == null) throw new ArgumentNullException(nameof(entity));
EnsureContainerType(entity);
- if (string.IsNullOrWhiteSpace(entity.Name)) throw new ArgumentNullOrEmptyException("entity.Name");
+ if (entity.Name == null) throw new InvalidOperationException("Entity name can't be null.");
+ if (string.IsNullOrWhiteSpace(entity.Name)) throw new InvalidOperationException("Entity name can't be empty or consist only of white-space characters.");
entity.Name = entity.Name.Trim();
// guard against duplicates
@@ -184,7 +186,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.Where(dto => dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType));
if (parentDto == null)
- throw new NullReferenceException("Could not find parent container with id " + entity.ParentId);
+ throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId);
level = parentDto.Level;
path = parentDto.Path;
@@ -223,10 +225,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
//
protected override void PersistUpdatedItem(EntityContainer entity)
{
+ if (entity == null) throw new ArgumentNullException(nameof(entity));
EnsureContainerType(entity);
+ if (entity.Name == null) throw new InvalidOperationException("Entity name can't be null.");
+ if (string.IsNullOrWhiteSpace(entity.Name)) throw new InvalidOperationException("Entity name can't be empty or consist only of white-space characters.");
entity.Name = entity.Name.Trim();
- if (string.IsNullOrWhiteSpace(entity.Name)) throw new ArgumentNullOrEmptyException("entity.Name");
// find container to update
var nodeDto = Database.FirstOrDefault(Sql().SelectAll()
@@ -255,7 +259,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.Where(dto => dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType));
if (parent == null)
- throw new NullReferenceException("Could not find parent container with id " + entity.ParentId);
+ throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId);
nodeDto.Level = Convert.ToInt16(parent.Level + 1);
nodeDto.Path = parent.Path + "," + nodeDto.NodeId;
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs
index 4a32e373c1..0eafebbfde 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs
@@ -35,26 +35,39 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected ISqlSyntaxProvider SqlSyntax => _scopeAccessor.AmbientScope.SqlContext.SqlSyntax;
#region Repository
-
- // get a page of entities
+
public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords,
IQuery filter, Ordering ordering)
{
- var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint;
- var isMedia = objectType == Constants.ObjectTypes.Media;
+ return GetPagedResultsByQuery(query, new[] { objectType }, pageIndex, pageSize, out totalRecords, filter, ordering);
+ }
- var sql = GetBaseWhere(isContent, isMedia, false, x =>
+ // get a page of entities
+ public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords,
+ IQuery filter, Ordering ordering, Action> sqlCustomization = null)
+ {
+ var isContent = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint);
+ var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media);
+ var isMember = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Member);
+
+ var sql = GetBaseWhere(isContent, isMedia, isMember, false, s =>
{
- if (filter == null) return;
- foreach (var filterClause in filter.GetWhereClauses())
- x.Where(filterClause.Item1, filterClause.Item2);
- }, objectType);
+ sqlCustomization?.Invoke(s);
+
+ if (filter != null)
+ {
+ foreach (var filterClause in filter.GetWhereClauses())
+ s.Where(filterClause.Item1, filterClause.Item2);
+ }
+
+
+ }, objectTypes);
ordering = ordering ?? Ordering.ByDefault();
var translator = new SqlTranslator(sql, query);
sql = translator.Translate();
- sql = AddGroupBy(isContent, isMedia, sql, ordering.IsEmpty);
+ sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty);
if (!ordering.IsEmpty)
{
@@ -69,47 +82,31 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// for content we must query for ContentEntityDto entities to produce the correct culture variant entity names
var pageIndexToFetch = pageIndex + 1;
IEnumerable dtos;
- if(isContent)
- {
- var page = Database.Page(pageIndexToFetch, pageSize, sql);
- dtos = page.Items;
- totalRecords = page.TotalItems;
- }
- else if (isMedia)
- {
- var page = Database.Page(pageIndexToFetch, pageSize, sql);
- dtos = page.Items;
- totalRecords = page.TotalItems;
- }
- else
- {
- var page = Database.Page(pageIndexToFetch, pageSize, sql);
- dtos = page.Items;
- totalRecords = page.TotalItems;
- }
+ var page = Database.Page(pageIndexToFetch, pageSize, sql);
+ dtos = page.Items;
+ totalRecords = page.TotalItems;
- var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray();
+ var entities = dtos.Select(BuildEntity).ToArray();
- if (isContent)
- BuildVariants(entities.Cast());
+ BuildVariants(entities.OfType());
return entities;
}
public IEntitySlim Get(Guid key)
{
- var sql = GetBaseWhere(false, false, false, key);
+ var sql = GetBaseWhere(false, false, false, false, key);
var dto = Database.FirstOrDefault(sql);
- return dto == null ? null : BuildEntity(false, false, dto);
+ return dto == null ? null : BuildEntity(dto);
}
- private IEntitySlim GetEntity(Sql sql, bool isContent, bool isMedia)
+ private IEntitySlim GetEntity(Sql sql, bool isContent, bool isMedia, bool isMember)
{
//isContent is going to return a 1:M result now with the variants so we need to do different things
if (isContent)
{
- var cdtos = Database.Fetch(sql);
+ var cdtos = Database.Fetch(sql);
return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0]));
}
@@ -120,7 +117,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (dto == null) return null;
- var entity = BuildEntity(false, isMedia, dto);
+ var entity = BuildEntity(dto);
return entity;
}
@@ -129,25 +126,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint;
var isMedia = objectTypeId == Constants.ObjectTypes.Media;
+ var isMember = objectTypeId == Constants.ObjectTypes.Member;
- var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, key);
- return GetEntity(sql, isContent, isMedia);
+ var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, key);
+ return GetEntity(sql, isContent, isMedia, isMember);
}
public IEntitySlim Get(int id)
{
- var sql = GetBaseWhere(false, false, false, id);
+ var sql = GetBaseWhere(false, false, false, false, id);
var dto = Database.FirstOrDefault(sql);
- return dto == null ? null : BuildEntity(false, false, dto);
+ return dto == null ? null : BuildEntity(dto);
}
public IEntitySlim Get(int id, Guid objectTypeId)
{
var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint;
var isMedia = objectTypeId == Constants.ObjectTypes.Media;
+ var isMember = objectTypeId == Constants.ObjectTypes.Member;
- var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, id);
- return GetEntity(sql, isContent, isMedia);
+ var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, id);
+ return GetEntity(sql, isContent, isMedia, isMember);
}
public IEnumerable GetAll(Guid objectType, params int[] ids)
@@ -164,12 +163,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
: PerformGetAll(objectType);
}
- private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia)
+ private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool isMember)
{
//isContent is going to return a 1:M result now with the variants so we need to do different things
if (isContent)
{
- var cdtos = Database.Fetch(sql);
+ var cdtos = Database.Fetch(sql);
return cdtos.Count == 0
? Enumerable.Empty()
@@ -180,7 +179,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
? (IEnumerable)Database.Fetch(sql)
: Database.Fetch(sql);
- var entities = dtos.Select(x => BuildEntity(false, isMedia, x)).ToArray();
+ var entities = dtos.Select(BuildEntity).ToArray();
return entities;
}
@@ -189,9 +188,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint;
var isMedia = objectType == Constants.ObjectTypes.Media;
+ var isMember = objectType == Constants.ObjectTypes.Member;
- var sql = GetFullSqlForEntityType(isContent, isMedia, objectType, filter);
- return GetEntities(sql, isContent, isMedia);
+ var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, filter);
+ return GetEntities(sql, isContent, isMedia, isMember);
}
public IEnumerable GetAllPaths(Guid objectType, params int[] ids)
@@ -218,26 +218,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
public IEnumerable GetByQuery(IQuery query)
{
- var sqlClause = GetBase(false, false, null);
+ var sqlClause = GetBase(false, false, false, null);
var translator = new SqlTranslator(sqlClause, query);
var sql = translator.Translate();
- sql = AddGroupBy(false, false, sql, true);
+ sql = AddGroupBy(false, false, false, sql, true);
var dtos = Database.Fetch(sql);
- return dtos.Select(x => BuildEntity(false, false, x)).ToList();
+ return dtos.Select(BuildEntity).ToList();
}
public IEnumerable GetByQuery(IQuery query, Guid objectType)
{
var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint;
var isMedia = objectType == Constants.ObjectTypes.Media;
+ var isMember = objectType == Constants.ObjectTypes.Member;
- var sql = GetBaseWhere(isContent, isMedia, false, null, objectType);
+ var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] { objectType });
var translator = new SqlTranslator(sql, query);
sql = translator.Translate();
- sql = AddGroupBy(isContent, isMedia, sql, true);
+ sql = AddGroupBy(isContent, isMedia, isMember, sql, true);
- return GetEntities(sql, isContent, isMedia);
+ return GetEntities(sql, isContent, isMedia, isMember);
}
public UmbracoObjectTypes GetObjectType(int id)
@@ -329,30 +330,30 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
// gets the full sql for a given object type and a given unique id
- protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Guid uniqueId)
+ protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Guid uniqueId)
{
- var sql = GetBaseWhere(isContent, isMedia, false, objectType, uniqueId);
- return AddGroupBy(isContent, isMedia, sql, true);
+ var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, uniqueId);
+ return AddGroupBy(isContent, isMedia, isMember, sql, true);
}
// gets the full sql for a given object type and a given node id
- protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, int nodeId)
+ protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, int nodeId)
{
- var sql = GetBaseWhere(isContent, isMedia, false, objectType, nodeId);
- return AddGroupBy(isContent, isMedia, sql, true);
+ var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, nodeId);
+ return AddGroupBy(isContent, isMedia, isMember, sql, true);
}
// gets the full sql for a given object type, with a given filter
- protected Sql