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..ad33872423 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -1,4 +1,4 @@ -# Umbraco Cms Build +# Umbraco CMS Build ## Are you sure? @@ -39,8 +39,12 @@ 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). +If it runs without errors; Hooray! Now you can continue with [the next step](CONTRIBUTING.md#how-do-i-begin) and open the solution and build it. + ### Build Infrastructure The Umbraco Build infrastructure relies on a PowerShell object. The object can be retrieved with: @@ -66,7 +70,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 +213,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..5537a46ef8 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** @@ -22,13 +22,13 @@ This project and everyone participating in it is governed by the [our Code of Co [Reviews](#reviews) * [Styleguides](#styleguides) - * [The PR team](#the-pr-team) + * [The Core Contributors](#the-core-contributors-team) * [Questions?](#questions) [Working with the code](#working-with-the-code) * [Building Umbraco from source code](#building-umbraco-from-source-code) * [Working with the source code](#working-with-the-source-code) - * [Making changes after the PR was opened](#making-changes-after-the-pr-was-opened) + * [Making changes after the PR is open](#making-changes-after-the-pr-is-open) * [Which branch should I target for my contributions?](#which-branch-should-i-target-for-my-contributions) * [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository) @@ -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 ![Clone the fork](img/clonefork.png) - + + * **Switch to the correct branch** - switch to the `v8/contrib` branch * **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md) * **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions) - * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/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. ![Create a pull request](img/createpullrequest.png) ### Pull requests The most successful pull requests usually look a like this: - * Fill in the required template + * Fill in the required template (shown when starting a PR on GitHub), and link your pull request to an issue on the [issue tracker,](https://github.com/umbraco/Umbraco-CMS/issues) if applicable. * Include screenshots and animated GIFs in your pull request whenever possible. - * Unit tests, while optional are awesome, thank you! - * New code is commented with documentation from which [the reference documentation](https://our.umbraco.com/documentation/Reference/) is generated + * 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 @@ -97,23 +98,24 @@ To be honest, we don't like rules very much. We trust you have the best of inten That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc. -### The PR team +### The Core Contributors 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 Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members who have comitted to volunteering their free time: - [Anders Bjerner](https://github.com/abjerner) -- [Dave Woestenborghs](https://github.com/dawoe) - [Emma Burstow](https://github.com/emmaburstow) - [Poornima Nayar](https://github.com/poornimanayar) +- [Kenn Jacobsen](https://twitter.com/KennJacobsen_DK) -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 core contributors team](#the-core-contributors-team) in multiple ways; we love open conversations and we are a friendly bunch. No question you have is stupid. Any question you have usually helps out multiple people with the same question. Ask away: -- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward -- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.com/forum/contributing-to-umbraco-cms/) forum, the team monitors that one closely +- 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 @@ -121,23 +123,24 @@ You can get in touch with [the PR team](#the-pr-team) in multiple ways, we love In order to build the Umbraco source code locally, first make sure you have the following installed. - * Visual Studio 2017 v15.9.7+ - * Node v10+ - * npm v6.4.1+ + * [Visual Studio 2017 v15.9.7+](https://visualstudio.microsoft.com/vs/) + * [Node.js v10+](https://nodejs.org/en/download/) + * npm v6.4.1+ (installed with Node.js) + * [Git command line](https://git-scm.com/download/) -The easiest way to get started is to 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. ![Gulp build in Visual Studio](img/gulpbuild.png) -After this build completes, you should be able to hit `F5` in Visual Studio to build and run the project. A IISExpress webserver will start and the Umbraco installer will pop up in your browser, follow the directions there to get a working Umbraco install up and running. +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 +149,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. ![Which branch should I target?](img/defaultbranch.png) -### 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 +186,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 Core Contributors and HQ. So, if you see something on the issue tracker or pull requests you feel you can add to, please don't be shy. diff --git a/.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 a1e33e3854..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) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md) [![Build status](https://umbraco.visualstudio.com/Umbraco%20Cms/_apis/build/status/Cms%208%20Continuous?branchName=v8/dev)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![pullreminders](https://pullreminders.com/badge.svg)](https://pullreminders.com?ref=badge) +# [Umbraco CMS](https://umbraco.com) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md) [![Build status](https://umbraco.visualstudio.com/Umbraco%20Cms/_apis/build/status/Cms%208%20Continuous?branchName=v8/contrib)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![Twitter](https://img.shields.io/twitter/follow/umbraco.svg?style=social&label=Follow)](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..870d24c648 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/* @@ -99,6 +100,9 @@ src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/loader.dev.js src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/main.js src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/app.js src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/canvasdesigner.*.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/navigation.controller.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/main.controller.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/utilities.js src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/ src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/**/*.js @@ -132,7 +136,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/NuGet.Config b/NuGet.Config index 7d786702f4..31fd84e456 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -7,6 +7,5 @@ --> - diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index e502271115..fce15eb487 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -14,6 +14,7 @@ Contains the core assemblies needed to run Umbraco Cms en-US umbraco + @@ -39,10 +40,12 @@ + + - + @@ -50,9 +53,9 @@ - + - + diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 3c8fed78f5..72619db02e 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -14,6 +14,7 @@ Contains the core assemblies needed to run Umbraco Cms en-US umbraco + @@ -24,10 +25,10 @@ not want this to happen as the alpha of the next major is, really, the next major already. --> - - + + - + @@ -43,7 +44,7 @@ - + @@ -51,14 +52,17 @@ + - - - + + + + - - + + + diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 5cdacca419..a6b06d9964 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -14,6 +14,7 @@ Installs Umbraco Cms in your Visual Studio ASP.NET project en-US umbraco + @@ -25,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/Views.Web.config.install.xdt b/build/NuSpecs/tools/Views.Web.config.install.xdt index 4d660301a8..7dd2640d09 100644 --- a/build/NuSpecs/tools/Views.Web.config.install.xdt +++ b/build/NuSpecs/tools/Views.Web.config.install.xdt @@ -8,23 +8,23 @@ - + - + - + 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/NuSpecs/tools/install.ps1 b/build/NuSpecs/tools/install.ps1 index 1411cb5c97..0be28f9467 100644 --- a/build/NuSpecs/tools/install.ps1 +++ b/build/NuSpecs/tools/install.ps1 @@ -18,9 +18,20 @@ if ($project) { # Copy umbraco and umbraco_files from package to project folder $umbracoFolder = Join-Path $projectPath "Umbraco" - New-Item -ItemType Directory -Force -Path $umbracoFolder + New-Item -ItemType Directory -Force -Path $umbracoFolder $umbracoFolderSource = Join-Path $installPath "UmbracoFiles\Umbraco" - robocopy $umbracoFolderSource $umbracoFolder /is /it /e /xf UI.xml /LOG:$copyLogsPath\UmbracoCopy.log + + Write-Host "copying files to $umbracoFolder ..." + # see https://support.microsoft.com/en-us/help/954404/return-codes-that-are-used-by-the-robocopy-utility-in-windows-server-2 + robocopy $umbracoFolderSource $umbracoFolder /is /it /e + if (($lastexitcode -eq 1) -or ($lastexitcode -eq 3) -or ($lastexitcode -eq 5) -or ($lastexitcode -eq 7)) + { + write-host "Copy succeeded!" + } + else + { + write-host "Copy failed with exit code:" $lastexitcode + } $copyWebconfig = $true $destinationWebConfig = Join-Path $projectPath "Web.config" @@ -40,7 +51,11 @@ if ($project) { } } } - Catch { } + Catch + { + Write-Host "An error occurred:" + Write-Host $_ + } } if($copyWebconfig -eq $true) @@ -74,7 +89,9 @@ if ($project) { } Catch { - # Not a big problem if this fails, let it go + # Not a big problem if this fails, let it go + # Write-Host "An error occurred:" + # Write-Host $_ } } diff --git a/build/build-bootstrap.ps1 b/build/build-bootstrap.ps1 index 71a25bfd7e..82c789ff22 100644 --- a/build/build-bootstrap.ps1 +++ b/build/build-bootstrap.ps1 @@ -22,6 +22,8 @@ # get NuGet $cache = 4 $nuget = "$scriptTemp\nuget.exe" + # ensure the correct NuGet-source is used. This one is used by Umbraco + $nugetsourceUmbraco = "https://www.myget.org/F/umbracocore/api/v3/index.json" if (-not $local) { $source = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" @@ -61,7 +63,7 @@ # get the build system if (-not $local) { - $params = "-OutputDirectory", $scriptTemp, "-Verbosity", "quiet", "-PreRelease" + $params = "-OutputDirectory", $scriptTemp, "-Verbosity", "quiet", "-PreRelease", "-Source", $nugetsourceUmbraco &$nuget install Umbraco.Build @params if (-not $?) { throw "Failed to download Umbraco.Build." } } diff --git a/build/build.ps1 b/build/build.ps1 index 978d6b2c26..b69f02442b 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -15,7 +15,7 @@ [Parameter(Mandatory=$false)] [Alias("doc")] [switch] $docfx = $false, - + # keep the build directories, don't clear them [Parameter(Mandatory=$false)] [Alias("c")] @@ -375,11 +375,14 @@ }) + $nugetsourceUmbraco = "https://api.nuget.org/v3/index.json" + $ubuild.DefineMethod("RestoreNuGet", { Write-Host "Restore NuGet" Write-Host "Logging to $($this.BuildTemp)\nuget.restore.log" - &$this.BuildEnv.NuGet restore "$($this.SolutionRoot)\src\Umbraco.sln" > "$($this.BuildTemp)\nuget.restore.log" + $params = "-Source", $nugetsourceUmbraco + &$this.BuildEnv.NuGet restore "$($this.SolutionRoot)\src\Umbraco.sln" > "$($this.BuildTemp)\nuget.restore.log" @params if (-not $?) { throw "Failed to restore NuGet packages." } }) @@ -392,13 +395,13 @@ &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.Core.nuspec" ` -Properties BuildTmp="$($this.BuildTemp)" ` -Version "$($this.Version.Semver.ToString())" ` - -Symbols -SymbolPackageFormat snupkg -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cmscore.log" + -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cmscore.log" if (-not $?) { throw "Failed to pack NuGet UmbracoCms.Core." } &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.Web.nuspec" ` -Properties BuildTmp="$($this.BuildTemp)" ` -Version "$($this.Version.Semver.ToString())" ` - -Symbols -SymbolPackageFormat snupkg -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cmsweb.log" + -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cmsweb.log" if (-not $?) { throw "Failed to pack NuGet UmbracoCms.Web." } &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.nuspec" ` @@ -429,51 +432,50 @@ Write-Host "Prepare Azure Gallery" $this.CopyFile("$($this.SolutionRoot)\build\Azure\azuregalleryrelease.ps1", $this.BuildOutput) }) - + $ubuild.DefineMethod("BuildCSharpDocs", { Write-Host "Building C# Documentation" - + $src = "$($this.SolutionRoot)\src" $tmp = $this.BuildTemp $out = $this.BuildOutput $DocFxJson = Join-Path -Path $src "\ApiDocs\docfx.json" $DocFxSiteOutput = Join-Path -Path $tmp "\_site\*.*" - + # run DocFx $DocFx = $this.BuildEnv.DocFx - + & $DocFx metadata $DocFxJson & $DocFx build $DocFxJson # zip it & $this.BuildEnv.Zip a -tzip -r "$out\csharp-docs.zip" $DocFxSiteOutput }) - + $ubuild.DefineMethod("BuildAngularDocs", { Write-Host "Building Angular Documentation" - + $src = "$($this.SolutionRoot)\src" $out = $this.BuildOutput - - # Check if the solution has been built - if (!(Test-Path "$src\Umbraco.Web.UI.Client\node_modules")) {throw "Umbraco needs to be built before generating the Angular Docs"} - Push-Location "$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 Pop-Location # change baseUrl $BaseUrl = "https://our.umbraco.com/apidocs/v8/ui/" - $IndexPath = "$src\Umbraco.Web.UI.Client\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 9ed398d52f..5587979a6c 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -2,7 +2,7 @@ using System.Resources; [assembly: AssemblyCompany("Umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2019")] +[assembly: AssemblyCopyright("Copyright © Umbraco 2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.1.0")] -[assembly: AssemblyInformationalVersion("8.1.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/Cache/WebCachingAppCache.cs b/src/Umbraco.Core/Cache/WebCachingAppCache.cs index c6e104221a..bec596b129 100644 --- a/src/Umbraco.Core/Cache/WebCachingAppCache.cs +++ b/src/Umbraco.Core/Cache/WebCachingAppCache.cs @@ -37,23 +37,13 @@ namespace Umbraco.Core.Cache /// public object Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { - CacheDependency dependency = null; - if (dependentFiles != null && dependentFiles.Any()) - { - dependency = new CacheDependency(dependentFiles); - } - return Get(key, factory, timeout, isSliding, priority, removedCallback, dependency); + return GetInternal(key, factory, timeout, isSliding, priority, removedCallback, dependentFiles); } /// public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { - CacheDependency dependency = null; - if (dependentFiles != null && dependentFiles.Any()) - { - dependency = new CacheDependency(dependentFiles); - } - Insert(key, factory, timeout, isSliding, priority, removedCallback, dependency); + InsertInternal(key, factory, timeout, isSliding, priority, removedCallback, dependentFiles); } #region Dictionary @@ -86,7 +76,7 @@ namespace Umbraco.Core.Cache protected override void EnterWriteLock() { - _locker.EnterWriteLock();; + _locker.EnterWriteLock(); } protected override void ExitReadLock() @@ -103,7 +93,7 @@ namespace Umbraco.Core.Cache #endregion - private object Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, CacheDependency dependency = null) + private object GetInternal(string key, Func factory, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { key = GetCacheKey(key); @@ -163,6 +153,10 @@ namespace Umbraco.Core.Cache var sliding = isSliding == false ? System.Web.Caching.Cache.NoSlidingExpiration : (timeout ?? System.Web.Caching.Cache.NoSlidingExpiration); lck.UpgradeToWriteLock(); + + // create a cache dependency if one is needed. + var dependency = dependentFiles != null && dependentFiles.Length > 0 ? new CacheDependency(dependentFiles) : null; + //NOTE: 'Insert' on System.Web.Caching.Cache actually does an add or update! _cache.Insert(key, result, dependency, absolute, sliding, priority, removedCallback); } @@ -180,7 +174,7 @@ namespace Umbraco.Core.Cache return value; } - private void Insert(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, CacheDependency dependency = null) + private void InsertInternal(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { // NOTE - here also we must insert a Lazy but we can evaluate it right now // and make sure we don't store a null value. @@ -197,6 +191,10 @@ namespace Umbraco.Core.Cache try { _locker.EnterWriteLock(); + + // create a cache dependency if one is needed. + var dependency = dependentFiles != null && dependentFiles.Length > 0 ? new CacheDependency(dependentFiles) : null; + //NOTE: 'Insert' on System.Web.Caching.Cache actually does an add or update! _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); } 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/Collections/TopoGraph.cs b/src/Umbraco.Core/Collections/TopoGraph.cs index b8ded4a458..955a210465 100644 --- a/src/Umbraco.Core/Collections/TopoGraph.cs +++ b/src/Umbraco.Core/Collections/TopoGraph.cs @@ -126,7 +126,7 @@ namespace Umbraco.Core.Collections if (_items.TryGetValue(key, out value)) yield return value; else if (throwOnMissing) - throw new Exception(MissingDependencyError); + throw new Exception($"{MissingDependencyError} Error in type {typeof(TItem).Name}, with key {key}"); } } } diff --git a/src/Umbraco.Core/Compose/AuditEventsComponent.cs b/src/Umbraco.Core/Compose/AuditEventsComponent.cs index 15fdfeacff..453fd6314a 100644 --- a/src/Umbraco.Core/Compose/AuditEventsComponent.cs +++ b/src/Umbraco.Core/Compose/AuditEventsComponent.cs @@ -44,21 +44,22 @@ namespace Umbraco.Core.Compose public void Terminate() { } + internal static IUser UnknownUser => new User { Id = Constants.Security.UnknownUserId, Name = Constants.Security.UnknownUserName, Email = "" }; + private IUser CurrentPerformingUser { get { var identity = Thread.CurrentPrincipal?.GetUmbracoIdentity(); - return identity == null - ? new User { Id = 0, Name = "SYSTEM", Email = "" } - : _userService.GetUserById(Convert.ToInt32(identity.Id)); + var user = identity == null ? null : _userService.GetUserById(Convert.ToInt32(identity.Id)); + return user ?? UnknownUser; } } private IUser GetPerformingUser(int userId) { var found = userId >= 0 ? _userService.GetUserById(userId) : null; - return found ?? new User {Id = 0, Name = "SYSTEM", Email = ""}; + return found ?? UnknownUser; } private string PerformingIp 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/CollectionBuilderBase.cs b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs index 41038ea4e9..0d398be83b 100644 --- a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs @@ -79,9 +79,12 @@ namespace Umbraco.Core.Composing foreach (var type in types) EnsureType(type, "register"); - // register them + // register them - ensuring that each item is registered with the same lifetime as the collection. + // NOTE: Previously each one was not registered with the same lifetime which would mean that if there + // was a dependency on an individual item, it would resolve a brand new transient instance which isn't what + // we would expect to happen. The same item should be resolved from the container as the collection. foreach (var type in types) - register.Register(type); + register.Register(type, CollectionLifetime); _registeredTypes = types; } 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/Repositories.cs b/src/Umbraco.Core/Composing/CompositionExtensions/Repositories.cs index 23dc9e67c6..00b29dd97f 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions/Repositories.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions/Repositories.cs @@ -47,6 +47,8 @@ namespace Umbraco.Core.Composing.CompositionExtensions composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); 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..a06f09baf6 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -5,6 +5,7 @@ using Umbraco.Core.Dictionary; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PackageActions; using Umbraco.Core.Packaging; @@ -154,6 +155,9 @@ namespace Umbraco.Core.Composing public static DataEditorCollection DataEditors => Factory.GetInstance(); + public static DataValueReferenceFactoryCollection DataValueReferenceFactories + => Factory.GetInstance(); + public static PropertyEditorCollection PropertyEditors => Factory.GetInstance(); @@ -205,6 +209,8 @@ namespace Umbraco.Core.Composing public static IVariationContextAccessor VariationContextAccessor => Factory.GetInstance(); + public static IImageUrlGenerator ImageUrlGenerator + => Factory.GetInstance(); #endregion } } 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 af9277fce9..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; @@ -42,8 +43,8 @@ namespace Umbraco.Core.Composing private string _currentAssembliesHash; private IEnumerable _assemblies; private bool _reportedChange; - private static string _localTempPath; - private static string _fileBasePath; + private readonly string _localTempPath; + private string _fileBasePath; /// /// Initializes a new instance of the class. @@ -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 828a577c34..ced9a9386a 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Dictionary; using Umbraco.Core.IO; using Umbraco.Core.Logging.Viewer; +using Umbraco.Core.Manifest; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PackageActions; using Umbraco.Core.Persistence.Mappers; @@ -48,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. /// @@ -66,9 +74,16 @@ namespace Umbraco.Core /// Gets the validators collection builder. /// /// The composition. - internal static ManifestValueValidatorCollectionBuilder Validators(this Composition composition) + internal static ManifestValueValidatorCollectionBuilder ManifestValueValidators(this Composition composition) => composition.WithCollectionBuilder(); + /// + /// Gets the manifest filter collection builder. + /// + /// The composition. + public static ManifestFilterCollectionBuilder ManifestFilters(this Composition composition) + => composition.WithCollectionBuilder(); + /// /// Gets the components collection builder. /// diff --git a/src/Umbraco.Core/ConfigsExtensions.cs b/src/Umbraco.Core/ConfigsExtensions.cs index 6fdf7ea3b9..d1672c6c7f 100644 --- a/src/Umbraco.Core/ConfigsExtensions.cs +++ b/src/Umbraco.Core/ConfigsExtensions.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Configuration.HealthChecks; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Manifest; namespace Umbraco.Core { @@ -41,7 +42,12 @@ namespace Umbraco.Core configs.Add(() => new CoreDebug()); // GridConfig depends on runtime caches, manifest parsers... and cannot be available during composition - configs.Add(factory => new GridConfig(factory.GetInstance(), factory.GetInstance(), configDir, factory.GetInstance().Debug)); + configs.Add(factory => new GridConfig( + factory.GetInstance(), + factory.GetInstance(), + configDir, + factory.GetInstance(), + factory.GetInstance().Debug)); } } } 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/GridConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs index b2dae09fc9..9aead74886 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs @@ -1,14 +1,15 @@ using System.IO; using Umbraco.Core.Cache; using Umbraco.Core.Logging; +using Umbraco.Core.Manifest; namespace Umbraco.Core.Configuration.Grid { class GridConfig : IGridConfig { - public GridConfig(ILogger logger, AppCaches appCaches, DirectoryInfo configFolder, bool isDebug) + public GridConfig(ILogger logger, AppCaches appCaches, DirectoryInfo configFolder, ManifestParser manifestParser, bool isDebug) { - EditorsConfig = new GridEditorsConfig(logger, appCaches, configFolder, isDebug); + EditorsConfig = new GridEditorsConfig(logger, appCaches, configFolder, manifestParser, isDebug); } public IGridEditorsConfig EditorsConfig { get; } diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index 0e7ef62c58..d434da8c70 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using Newtonsoft.Json.Linq; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.PropertyEditors; @@ -15,13 +13,15 @@ namespace Umbraco.Core.Configuration.Grid private readonly ILogger _logger; private readonly AppCaches _appCaches; private readonly DirectoryInfo _configFolder; + private readonly ManifestParser _manifestParser; private readonly bool _isDebug; - public GridEditorsConfig(ILogger logger, AppCaches appCaches, DirectoryInfo configFolder, bool isDebug) + public GridEditorsConfig(ILogger logger, AppCaches appCaches, DirectoryInfo configFolder, ManifestParser manifestParser, bool isDebug) { _logger = logger; _appCaches = appCaches; _configFolder = configFolder; + _manifestParser = manifestParser; _isDebug = isDebug; } @@ -31,9 +31,6 @@ namespace Umbraco.Core.Configuration.Grid { List GetResult() { - // TODO: should use the common one somehow! + ignoring _appPlugins here! - var parser = new ManifestParser(_appCaches, Current.ManifestValidators, _logger); - var editors = new List(); var gridConfig = Path.Combine(_configFolder.FullName, "grid.editors.config.js"); if (File.Exists(gridConfig)) @@ -42,7 +39,7 @@ namespace Umbraco.Core.Configuration.Grid try { - editors.AddRange(parser.ParseGridEditors(sourceString)); + editors.AddRange(_manifestParser.ParseGridEditors(sourceString)); } catch (Exception ex) { @@ -51,7 +48,7 @@ namespace Umbraco.Core.Configuration.Grid } // add manifest editors, skip duplicates - foreach (var gridEditor in parser.Manifest.GridEditors) + foreach (var gridEditor in _manifestParser.Manifest.GridEditors) { if (editors.Contains(gridEditor) == false) editors.Add(gridEditor); } 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 = @"
Preview modeClick to end
"; [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-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs index f2b31be28f..673da8f9a3 100644 --- a/src/Umbraco.Core/Constants-DataTypes.cs +++ b/src/Umbraco.Core/Constants-DataTypes.cs @@ -1,27 +1,375 @@ -namespace Umbraco.Core +using System; + +namespace Umbraco.Core { public static partial class Constants { public static class DataTypes { - public const int LabelString = -92; + //NOTE: unfortunately due to backwards compat we can't move/rename these, with the addition of the GUID + //constants, it would make more sense to have these suffixed with "ID" or in a Subclass called "INT", for + //now all we can do is make a subclass called Guids to put the GUID IDs. + + public const int LabelString = System.DefaultLabelDataTypeId; public const int LabelInt = -91; public const int LabelBigint = -93; public const int LabelDateTime = -94; public const int LabelTime = -98; public const int LabelDecimal = -99; + public const int Textarea = -89; public const int Textbox = -88; + public const int RichtextEditor = -87; public const int Boolean = -49; public const int DateTime = -36; public const int DropDownSingle = -39; public const int DropDownMultiple = -42; + public const int Upload = -90; public const int DefaultContentListView = -95; public const int DefaultMediaListView = -96; public const int DefaultMembersListView = -97; + public const int ImageCropper = 1043; public const int Tags = 1041; + + public static class ReservedPreValueKeys + { + public const string IgnoreUserStartNodes = "ignoreUserStartNodes"; + } + + /// + /// Defines the identifiers for Umbraco data types as constants for easy centralized access/management. + /// + public static class Guids + { + + /// + /// Guid for Content Picker as string + /// + public const string ContentPicker = "FD1E0DA5-5606-4862-B679-5D0CF3A52A59"; + + /// + /// Guid for Content Picker + /// + public static readonly Guid ContentPickerGuid = new Guid(ContentPicker); + + + /// + /// Guid for Member Picker as string + /// + public const string MemberPicker = "1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"; + + /// + /// Guid for Member Picker + /// + public static readonly Guid MemberPickerGuid = new Guid(MemberPicker); + + + /// + /// Guid for Media Picker as string + /// + public const string MediaPicker = "135D60E0-64D9-49ED-AB08-893C9BA44AE5"; + + /// + /// Guid for Media Picker + /// + public static readonly Guid MediaPickerGuid = new Guid(MediaPicker); + + + /// + /// Guid for Multiple Media Picker as string + /// + public const string MultipleMediaPicker = "9DBBCBBB-2327-434A-B355-AF1B84E5010A"; + + /// + /// Guid for Multiple Media Picker + /// + public static readonly Guid MultipleMediaPickerGuid = new Guid(MultipleMediaPicker); + + + /// + /// Guid for Related Links as string + /// + public const string RelatedLinks = "B4E3535A-1753-47E2-8568-602CF8CFEE6F"; + + /// + /// Guid for Related Links + /// + public static readonly Guid RelatedLinksGuid = new Guid(RelatedLinks); + + + /// + /// Guid for Member as string + /// + public const string Member = "d59be02f-1df9-4228-aa1e-01917d806cda"; + + /// + /// Guid for Member + /// + public static readonly Guid MemberGuid = new Guid(Member); + + + /// + /// Guid for Image Cropper as string + /// + public const string ImageCropper = "1df9f033-e6d4-451f-b8d2-e0cbc50a836f"; + + /// + /// Guid for Image Cropper + /// + public static readonly Guid ImageCropperGuid = new Guid(ImageCropper); + + + /// + /// Guid for Tags as string + /// + public const string Tags = "b6b73142-b9c1-4bf8-a16d-e1c23320b549"; + + /// + /// Guid for Tags + /// + public static readonly Guid TagsGuid = new Guid(Tags); + + + /// + /// Guid for List View - Content as string + /// + public const string ListViewContent = "C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"; + + /// + /// Guid for List View - Content + /// + public static readonly Guid ListViewContentGuid = new Guid(ListViewContent); + + + /// + /// Guid for List View - Media as string + /// + public const string ListViewMedia = "3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"; + + /// + /// Guid for List View - Media + /// + public static readonly Guid ListViewMediaGuid = new Guid(ListViewMedia); + + + /// + /// Guid for List View - Members as string + /// + public const string ListViewMembers = "AA2C52A0-CE87-4E65-A47C-7DF09358585D"; + + /// + /// Guid for List View - Members + /// + public static readonly Guid ListViewMembersGuid = new Guid(ListViewMembers); + + + /// + /// Guid for Date Picker with time as string + /// + public const string DatePickerWithTime = "e4d66c0f-b935-4200-81f0-025f7256b89a"; + + /// + /// Guid for Date Picker with time + /// + public static readonly Guid DatePickerWithTimeGuid = new Guid(DatePickerWithTime); + + + /// + /// Guid for Approved Color as string + /// + public const string ApprovedColor = "0225af17-b302-49cb-9176-b9f35cab9c17"; + + /// + /// Guid for Approved Color + /// + public static readonly Guid ApprovedColorGuid = new Guid(ApprovedColor); + + + /// + /// Guid for Dropdown multiple as string + /// + public const string DropdownMultiple = "f38f0ac7-1d27-439c-9f3f-089cd8825a53"; + + /// + /// Guid for Dropdown multiple + /// + public static readonly Guid DropdownMultipleGuid = new Guid(DropdownMultiple); + + + /// + /// Guid for Radiobox as string + /// + public const string Radiobox = "bb5f57c9-ce2b-4bb9-b697-4caca783a805"; + + /// + /// Guid for Radiobox + /// + public static readonly Guid RadioboxGuid = new Guid(Radiobox); + + + /// + /// Guid for Date Picker as string + /// + public const string DatePicker = "5046194e-4237-453c-a547-15db3a07c4e1"; + + /// + /// Guid for Date Picker + /// + public static readonly Guid DatePickerGuid = new Guid(DatePicker); + + + /// + /// Guid for Dropdown as string + /// + public const string Dropdown = "0b6a45e7-44ba-430d-9da5-4e46060b9e03"; + + /// + /// Guid for Dropdown + /// + public static readonly Guid DropdownGuid = new Guid(Dropdown); + + + /// + /// Guid for Checkbox list as string + /// + public const string CheckboxList = "fbaf13a8-4036-41f2-93a3-974f678c312a"; + + /// + /// Guid for Checkbox list + /// + public static readonly Guid CheckboxListGuid = new Guid(CheckboxList); + + + /// + /// Guid for Checkbox as string + /// + public const string Checkbox = "92897bc6-a5f3-4ffe-ae27-f2e7e33dda49"; + + /// + /// Guid for Checkbox + /// + public static readonly Guid CheckboxGuid = new Guid(Checkbox); + + + /// + /// Guid for Numeric as string + /// + public const string Numeric = "2e6d3631-066e-44b8-aec4-96f09099b2b5"; + + /// + /// Guid for Dropdown + /// + public static readonly Guid NumericGuid = new Guid(Numeric); + + + /// + /// Guid for Richtext editor as string + /// + public const string RichtextEditor = "ca90c950-0aff-4e72-b976-a30b1ac57dad"; + + /// + /// Guid for Richtext editor + /// + public static readonly Guid RichtextEditorGuid = new Guid(RichtextEditor); + + + /// + /// Guid for Textstring as string + /// + public const string Textstring = "0cc0eba1-9960-42c9-bf9b-60e150b429ae"; + + /// + /// Guid for Textstring + /// + public static readonly Guid TextstringGuid = new Guid(Textstring); + + + /// + /// Guid for Textarea as string + /// + public const string Textarea = "c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3"; + + /// + /// Guid for Dropdown + /// + public static readonly Guid TextareaGuid = new Guid(Textarea); + + + /// + /// Guid for Upload as string + /// + public const string Upload = "84c6b441-31df-4ffe-b67e-67d5bc3ae65a"; + + /// + /// Guid for Upload + /// + public static readonly Guid UploadGuid = new Guid(Upload); + + + /// + /// Guid for Label as string + /// + public const string LabelString = "f0bc4bfb-b499-40d6-ba86-058885a5178c"; + + /// + /// Guid for Label string + /// + public static readonly Guid LabelStringGuid = new Guid(LabelString); + + /// + /// Guid for Label as int + /// + public const string LabelInt = "8e7f995c-bd81-4627-9932-c40e568ec788"; + + /// + /// Guid for Label int + /// + public static readonly Guid LabelIntGuid = new Guid(LabelInt); + + /// + /// Guid for Label as big int + /// + public const string LabelBigInt = "930861bf-e262-4ead-a704-f99453565708"; + + /// + /// Guid for Label big int + /// + public static readonly Guid LabelBigIntGuid = new Guid(LabelBigInt); + + /// + /// Guid for Label as date time + /// + public const string LabelDateTime = "0e9794eb-f9b5-4f20-a788-93acd233a7e4"; + + /// + /// Guid for Label date time + /// + public static readonly Guid LabelDateTimeGuid = new Guid(LabelDateTime); + + /// + /// Guid for Label as time + /// + public const string LabelTime = "a97cec69-9b71-4c30-8b12-ec398860d7e8"; + + /// + /// Guid for Label time + /// + public static readonly Guid LabelTimeGuid = new Guid(LabelTime); + + /// + /// Guid for Label as decimal + /// + public const string LabelDecimal = "8f1ef1e1-9de4-40d3-a072-6673f631ca64"; + + /// + /// Guid for Label decimal + /// + public static readonly Guid LabelDecimalGuid = new Guid(LabelDecimal); + + + } } } } diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index d3e8b4ad3b..05213ed1c4 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -5,39 +5,89 @@ public static class Icons { /// - /// System contenttype icon + /// System default icon /// - public const string ContentType = "icon-arrangement"; + public const string DefaultIcon = Content; /// - /// System datatype icon + /// System content icon + /// + public const string Content = "icon-document"; + + /// + /// System content type icon + /// + public const string ContentType = "icon-item-arrangement"; + + /// + /// System data type icon /// public const string DataType = "icon-autofill"; /// - /// System property editor icon + /// System list view icon /// - public const string PropertyEditor = "icon-autofill"; + public const string ListView = "icon-thumbnail-list"; /// /// System macro icon /// public const string Macro = "icon-settings-alt"; + /// + /// System media file icon + /// + public const string MediaFile = "icon-document"; + + /// + /// System media folder icon + /// + public const string MediaFolder = "icon-folder"; + + /// + /// System media image icon + /// + public const string MediaImage = "icon-picture"; + + /// + /// System media type icon + /// + public const string MediaType = "icon-thumbnails"; + /// /// System member icon /// public const string Member = "icon-user"; /// - /// System member icon + /// System member group icon + /// + public const string MemberGroup = "icon-users-alt"; + + /// + /// System member type icon /// public const string MemberType = "icon-users"; + /// + /// System property editor icon + /// + public const string PropertyEditor = "icon-autofill"; + /// /// System member icon /// public const string Template = "icon-layout"; + + /// + /// System user icon + /// + public const string User = "icon-user"; + + /// + /// System user group icon + /// + public const string UserGroup = "icon-users"; } } } diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 0c2e246721..eb2b3525a7 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -14,6 +14,23 @@ namespace Umbraco.Core ///
public const string InternalGenericPropertiesPrefix = "_umb_"; + public static class Legacy + { + public static class Aliases + { + public const string Textbox = "Umbraco.Textbox"; + public const string Date = "Umbraco.Date"; + public const string ContentPicker2 = "Umbraco.ContentPicker2"; + public const string MediaPicker2 = "Umbraco.MediaPicker2"; + public const string MemberPicker2 = "Umbraco.MemberPicker2"; + public const string MultiNodeTreePicker2 = "Umbraco.MultiNodeTreePicker2"; + public const string TextboxMultiple = "Umbraco.TextboxMultiple"; + public const string RelatedLinks2 = "Umbraco.RelatedLinks2"; + public const string RelatedLinks = "Umbraco.RelatedLinks"; + + } + } + /// /// Defines Umbraco built-in property editor aliases. /// @@ -68,17 +85,17 @@ namespace Umbraco.Core /// ListView. /// public const string ListView = "Umbraco.ListView"; - - /// - /// Macro Container. - /// - public const string MacroContainer = "Umbraco.MacroContainer"; - + /// /// Media Picker. /// public const string MediaPicker = "Umbraco.MediaPicker"; + /// + /// Multiple Media Picker. + /// + public const string MultipleMediaPicker = "Umbraco.MultipleMediaPicker"; + /// /// Member Picker. /// @@ -186,6 +203,24 @@ namespace Umbraco.Core /// Must be a valid value. public const string DataValueType = "umbracoDataValueType"; } + + /// + /// Defines Umbraco's built-in property editor groups. + /// + public static class Groups + { + public const string Common = "Common"; + + public const string Lists = "Lists"; + + public const string Media = "Media"; + + public const string People = "People"; + + public const string Pickers = "Pickers"; + + public const string RichContent = "Rich Content"; + } } } } diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 80f18eb2e0..43c989805b 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -15,16 +15,23 @@ namespace Umbraco.Core public const int SuperUserId = -1; /// - /// The id for the 'unknown' user + /// The id for the 'unknown' user. /// /// /// This is a user row that exists in the DB only for referential integrity but the user is never returned from any of the services /// public const int UnknownUserId = 0; + /// + /// The name of the 'unknown' user. + /// + public const string UnknownUserName = "SYTEM"; + public const string AdminGroupAlias = "admin"; + public const string EditorGroupAlias = "editor"; public const string SensitiveDataGroupAlias = "sensitiveData"; public const string TranslatorGroupAlias = "translator"; + public const string WriterGroupAlias = "writer"; public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs new file mode 100644 index 0000000000..984bc495b0 --- /dev/null +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -0,0 +1,20 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + public static class SqlTemplates + { + public static class VersionableRepository + { + public const string GetVersionIds = "Umbraco.Core.VersionableRepository.GetVersionIds"; + public const string GetVersion = "Umbraco.Core.VersionableRepository.GetVersion"; + public const string GetVersions = "Umbraco.Core.VersionableRepository.GetVersions"; + public const string EnsureUniqueNodeName = "Umbraco.Core.VersionableRepository.EnsureUniqueNodeName"; + public const string GetSortOrder = "Umbraco.Core.VersionableRepository.GetSortOrder"; + public const string GetParentNode = "Umbraco.Core.VersionableRepository.GetParentNode"; + public const string GetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId"; + } + + } + } +} 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..7bce23e98e 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 @@ -57,6 +60,19 @@ namespace Umbraco.Core #endregion + internal static bool IsMoving(this IContentBase entity) + { + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below + // operations which will make this whole operation go much faster. When moving we don't need to create + // new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsPropertyDirty(nameof(entity.Path)) + && entity.IsPropertyDirty(nameof(entity.Level)) + && entity.IsPropertyDirty(nameof(entity.UpdateDate)); + + return isMoving; + } + /// /// Removes characters that are not valid XML characters from all entity properties /// of type string. See: http://stackoverflow.com/a/961504/5018 @@ -162,14 +178,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 d25997b5f0..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. + /// Determines whether the content type is invariant. /// - /// 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(); + /// 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 property type is invariant. /// + /// 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 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. - /// - public static bool VariesByNothing(this PublishedContentType 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 PublishedContentType 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 PublishedContentType contentType) => contentType.Variations.VariesBySegment(); - - /// - /// Determines whether the content type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this PublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); - /// /// Determines whether the property type is invariant. /// - public static bool VariesByNothing(this PublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing(); - - /// - /// Determines whether the property type varies by culture. - /// - public static bool VariesByCulture(this PublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture(); - - /// - /// Determines whether the property type varies by segment. - /// - public static bool VariesBySegment(this PublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment(); - - /// - /// Determines whether the property type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this PublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); + /// The property type. + /// + /// A value indicating whether the property type is invariant. + /// + public static bool VariesByNothing(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing(); /// /// 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/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index 3719bb0750..59f5937874 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -10,6 +10,8 @@ namespace Umbraco.Core /// public static class EnumerableExtensions { + internal static bool IsCollectionEmpty(this IReadOnlyCollection list) => list == null || list.Count == 0; + internal static bool HasDuplicates(this IEnumerable items, bool includeNull) { var hs = new HashSet(); @@ -112,7 +114,6 @@ namespace Umbraco.Core } } - /// /// Returns true if all items in the other collection exist in this collection /// diff --git a/src/Umbraco.Core/Events/UserGroupWithUsers.cs b/src/Umbraco.Core/Events/UserGroupWithUsers.cs index b69650d33f..7d456a22ea 100644 --- a/src/Umbraco.Core/Events/UserGroupWithUsers.cs +++ b/src/Umbraco.Core/Events/UserGroupWithUsers.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Events { - internal class UserGroupWithUsers + public class UserGroupWithUsers { public UserGroupWithUsers(IUserGroup userGroup, IUser[] addedUsers, IUser[] removedUsers) { 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/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs index 80cc3c3de4..58034f05d7 100644 --- a/src/Umbraco.Core/HashGenerator.cs +++ b/src/Umbraco.Core/HashGenerator.cs @@ -41,7 +41,7 @@ namespace Umbraco.Core internal void AddDateTime(DateTime d) { - _writer.Write(d.Ticks);; + _writer.Write(d.Ticks); } internal void AddString(string s) 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 4aaf6d25f2..dbdd7842ba 100644 --- a/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs @@ -26,26 +26,32 @@ namespace Umbraco.Core.Logging.Viewer /// A count of number of errors /// By counting Warnings with Exceptions, Errors & Fatal messages /// - int GetNumberOfErrors(DateTimeOffset startDate, DateTimeOffset endDate); + int GetNumberOfErrors(LogTimePeriod logTimePeriod); /// /// Returns a number of the different log level entries /// - LogLevelCounts GetLogLevelCounts(DateTimeOffset startDate, DateTimeOffset endDate); + LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod); /// /// Returns a list of all unique message templates and their counts /// - IEnumerable GetMessageTemplates(DateTimeOffset startDate, DateTimeOffset endDate); + IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod); bool CanHandleLargeLogs { get; } - bool CheckCanOpenLogs(DateTimeOffset startDate, DateTimeOffset endDate); + bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); + + /// + /// Gets the current Serilog minimum log level + /// + /// + string GetLogLevel(); /// /// Returns the collection of logs /// - PagedResult GetLogs(DateTimeOffset startDate, DateTimeOffset endDate, + PagedResult GetLogs(LogTimePeriod logTimePeriod, int pageNumber = 1, int pageSize = 100, Direction orderDirection = Direction.Descending, diff --git a/src/Umbraco.Core/Logging/Viewer/JsonLogViewer.cs b/src/Umbraco.Core/Logging/Viewer/JsonLogViewer.cs index 9e6d911489..bbe2f3704d 100644 --- a/src/Umbraco.Core/Logging/Viewer/JsonLogViewer.cs +++ b/src/Umbraco.Core/Logging/Viewer/JsonLogViewer.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.Logging.Viewer public override bool CanHandleLargeLogs => false; - public override bool CheckCanOpenLogs(DateTimeOffset startDate, DateTimeOffset endDate) + public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod) { //Log Directory var logDirectory = _logsPath; @@ -36,7 +36,7 @@ namespace Umbraco.Core.Logging.Viewer //foreach full day in the range - see if we can find one or more filenames that end with //yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing - for (var day = startDate.Date; day.Date <= endDate.Date; day = day.AddDays(1)) + for (var day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) { //Filename ending to search for (As could be multiple) var filesToFind = GetSearchPattern(day); @@ -57,7 +57,7 @@ namespace Umbraco.Core.Logging.Viewer return $"*{day:yyyyMMdd}*.json"; } - protected override IReadOnlyList GetLogs(DateTimeOffset startDate, DateTimeOffset endDate, ILogFilter filter, int skip, int take) + protected override IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take) { var logs = new List(); @@ -68,7 +68,7 @@ namespace Umbraco.Core.Logging.Viewer //foreach full day in the range - see if we can find one or more filenames that end with //yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing - for (var day = startDate.Date; day.Date <= endDate.Date; day = day.AddDays(1)) + for (var day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) { //Filename ending to search for (As could be multiple) var filesToFind = GetSearchPattern(day); diff --git a/src/Umbraco.Core/Logging/Viewer/LogTimePeriod.cs b/src/Umbraco.Core/Logging/Viewer/LogTimePeriod.cs new file mode 100644 index 0000000000..0f41faef0a --- /dev/null +++ b/src/Umbraco.Core/Logging/Viewer/LogTimePeriod.cs @@ -0,0 +1,16 @@ +using System; + +namespace Umbraco.Core.Logging.Viewer +{ + public class LogTimePeriod + { + public LogTimePeriod(DateTime startTime, DateTime endTime) + { + StartTime = startTime; + EndTime = endTime; + } + + public DateTime StartTime { get; } + public DateTime EndTime { get; } + } +} diff --git a/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs b/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs index 3578a56e74..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 { @@ -27,9 +30,9 @@ namespace Umbraco.Core.Logging.Viewer /// /// Get all logs from your chosen data source back as Serilog LogEvents /// - protected abstract IReadOnlyList GetLogs(DateTimeOffset startDate, DateTimeOffset endDate, ILogFilter filter, int skip, int take); + protected abstract IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take); - public abstract bool CheckCanOpenLogs(DateTimeOffset startDate, DateTimeOffset endDate); + public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); public virtual IReadOnlyList GetSavedSearches() { @@ -82,24 +85,34 @@ namespace Umbraco.Core.Logging.Viewer return searches; } - public int GetNumberOfErrors(DateTimeOffset startDate, DateTimeOffset endDate) + public int GetNumberOfErrors(LogTimePeriod logTimePeriod) { var errorCounter = new ErrorCounterFilter(); - GetLogs(startDate, endDate, errorCounter, 0, int.MaxValue); + GetLogs(logTimePeriod, errorCounter, 0, int.MaxValue); return errorCounter.Count; } - public LogLevelCounts GetLogLevelCounts(DateTimeOffset startDate, DateTimeOffset endDate) + /// + /// 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(); - GetLogs(startDate, endDate, counter, 0, int.MaxValue); + GetLogs(logTimePeriod, counter, 0, int.MaxValue); return counter.Counts; } - public IEnumerable GetMessageTemplates(DateTimeOffset startDate, DateTimeOffset endDate) + public IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod) { var messageTemplates = new MessageTemplateFilter(); - GetLogs(startDate, endDate, messageTemplates, 0, int.MaxValue); + GetLogs(logTimePeriod, messageTemplates, 0, int.MaxValue); var templates = messageTemplates.Counts. Select(x => new LogTemplate { MessageTemplate = x.Key, Count = x.Value }) @@ -108,14 +121,14 @@ namespace Umbraco.Core.Logging.Viewer return templates; } - public PagedResult GetLogs(DateTimeOffset startDate, DateTimeOffset endDate, + public PagedResult GetLogs(LogTimePeriod logTimePeriod, int pageNumber = 1, int pageSize = 100, Direction orderDirection = Direction.Descending, string filterExpression = null, string[] logLevels = null) { var expression = new ExpressionFilter(filterExpression); - var filteredLogs = GetLogs(startDate, endDate, expression, 0, int.MaxValue); + var filteredLogs = GetLogs(logTimePeriod, expression, 0, int.MaxValue); //This is user used the checkbox UI to toggle which log levels they wish to see //If an empty array or null - its implied all levels to be viewed diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs deleted file mode 100644 index ca875c2167..0000000000 --- a/src/Umbraco.Core/MainDom.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System; -using System.Collections.Generic; -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)" : string.Empty, 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 - - _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(); - _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/IManifestFilter.cs b/src/Umbraco.Core/Manifest/IManifestFilter.cs new file mode 100644 index 0000000000..505f13d385 --- /dev/null +++ b/src/Umbraco.Core/Manifest/IManifestFilter.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Manifest +{ + /// + /// Provides filtering for package manifests. + /// + public interface IManifestFilter + { + /// + /// Filters package manifests. + /// + /// The package manifests. + /// + /// It is possible to remove, change, or add manifests. + /// + void Filter(List manifests); + } +} 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/ManifestFilterCollection.cs b/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs new file mode 100644 index 0000000000..febdb7e356 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Manifest +{ + /// + /// Contains the manifest filters. + /// + public class ManifestFilterCollection : BuilderCollectionBase + { + /// + /// Initializes a new instance of the class. + /// + public ManifestFilterCollection(IEnumerable items) + : base(items) + { } + + /// + /// Filters package manifests. + /// + /// The package manifests. + public void Filter(List manifests) + { + foreach (var filter in this) + filter.Filter(manifests); + } + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs b/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs new file mode 100644 index 0000000000..4d98700f93 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs @@ -0,0 +1,12 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Manifest +{ + public class ManifestFilterCollectionBuilder : OrderedCollectionBuilderBase + { + protected override ManifestFilterCollectionBuilder This => this; + + // do NOT cache this, it's only used once + protected override Lifetime CollectionLifetime => Lifetime.Transient; + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs index dc40cd90a2..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; @@ -22,25 +21,28 @@ namespace Umbraco.Core.Manifest private readonly IAppPolicyCache _cache; private readonly ILogger _logger; private readonly ManifestValueValidatorCollection _validators; + private readonly ManifestFilterCollection _filters; private string _path; /// /// Initializes a new instance of the class. /// - public ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, ILogger logger) - : this(appCaches, validators, "~/App_Plugins", logger) + public ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, ManifestFilterCollection filters, ILogger logger) + : this(appCaches, validators, filters, "~/App_Plugins", logger) { } /// /// Initializes a new instance of the class. /// - private ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, string path, ILogger logger) + private ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, ManifestFilterCollection filters, string path, ILogger logger) { if (appCaches == null) throw new ArgumentNullException(nameof(appCaches)); _cache = appCaches.RuntimeCache; _validators = validators ?? throw new ArgumentNullException(nameof(validators)); - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path)); + _filters = filters ?? throw new ArgumentNullException(nameof(filters)); + if (path == null) throw new ArgumentNullException(nameof(path)); + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(path)); Path = path; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -78,6 +80,7 @@ namespace Umbraco.Core.Manifest if (string.IsNullOrWhiteSpace(text)) continue; var manifest = ParseManifest(text); + manifest.Source = path; manifests.Add(manifest); } catch (Exception e) @@ -86,6 +89,8 @@ namespace Umbraco.Core.Manifest } } + _filters.Filter(manifests); + return manifests; } @@ -150,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/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 475ee8a7f8..e50eb69467 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -9,6 +9,16 @@ namespace Umbraco.Core.Manifest /// public class PackageManifest { + /// + /// Gets the source path of the manifest. + /// + /// + /// Gets the full absolute file path of the manifest, + /// using system directory separators. + /// + [JsonIgnore] + public string Source { get; set; } + /// /// Gets or sets the scripts listed in the manifest. /// diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index 2d495b38b5..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 { @@ -191,7 +192,7 @@ namespace Umbraco.Core.Mapping private TTarget Map(object source, Type sourceType, MapperContext context) { if (source == null) - throw new ArgumentNullException(nameof(source)); + return default; var targetType = typeof(TTarget); @@ -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/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs index b11ef7e2c8..6bf450a9b8 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs @@ -25,11 +25,25 @@ namespace Umbraco.Core.Migrations.Expressions.Create.KeysAndIndexes var syntax = _context.SqlContext.SqlSyntax; var tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); + // note: of course we are creating the keys and indexes as per the DTO, so + // changing the DTO may break old migrations - or, better, these migrations + // should capture a copy of the DTO class that will not change + ExecuteSql(syntax.FormatPrimaryKey(tableDefinition)); foreach (var sql in syntax.Format(tableDefinition.Indexes)) ExecuteSql(sql); foreach (var sql in syntax.Format(tableDefinition.ForeignKeys)) ExecuteSql(sql); + + // note: we do *not* create the DF_ default constraints + /* + foreach (var column in tableDefinition.Columns) + { + var sql = syntax.FormatDefaultConstraint(column); + if (!sql.IsNullOrWhiteSpace()) + ExecuteSql(sql); + } + */ } private void ExecuteSql(string sql) diff --git a/src/Umbraco.Core/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs index 373b375fa8..92bc11b04d 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs @@ -10,9 +10,13 @@ namespace Umbraco.Core.Migrations.Expressions.Delete.DefaultConstraint IDeleteDefaultConstraintOnTableBuilder, IDeleteDefaultConstraintOnColumnBuilder { - public DeleteDefaultConstraintBuilder(DeleteDefaultConstraintExpression expression) + private readonly IMigrationContext _context; + + public DeleteDefaultConstraintBuilder(IMigrationContext context, DeleteDefaultConstraintExpression expression) : base(expression) - { } + { + _context = context; + } /// public IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName) @@ -25,6 +29,9 @@ namespace Umbraco.Core.Migrations.Expressions.Delete.DefaultConstraint public IExecutableBuilder OnColumn(string columnName) { Expression.ColumnName = columnName; + Expression.HasDefaultConstraint = _context.SqlContext.SqlSyntax.TryGetDefaultConstraint(_context.Database, Expression.TableName, columnName, out var constraintName); + Expression.ConstraintName = constraintName ?? string.Empty; + return new ExecutableBuilder(Expression); } } diff --git a/src/Umbraco.Core/Migrations/Expressions/Delete/DeleteBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Delete/DeleteBuilder.cs index d081305cde..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 NPoco; +using System; using Umbraco.Core.Migrations.Expressions.Common; using Umbraco.Core.Migrations.Expressions.Delete.Column; using Umbraco.Core.Migrations.Expressions.Delete.Constraint; @@ -29,9 +29,20 @@ namespace Umbraco.Core.Migrations.Expressions.Delete } /// - public IExecutableBuilder KeysAndIndexes(string tableName = null) + public IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true) { - return new DeleteKeysAndIndexesBuilder(_context) { TableName = tableName }; + var syntax = _context.SqlContext.SqlSyntax; + var tableDefinition = DefinitionFactory.GetTableDefinition(typeof(TDto), syntax); + return KeysAndIndexes(tableDefinition.Name, local, foreign); + } + + /// + public IExecutableBuilder KeysAndIndexes(string tableName, bool local = true, bool foreign = true) + { + 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 }; } /// @@ -100,7 +111,7 @@ namespace Umbraco.Core.Migrations.Expressions.Delete public IDeleteDefaultConstraintOnTableBuilder DefaultConstraint() { var expression = new DeleteDefaultConstraintExpression(_context); - return new DeleteDefaultConstraintBuilder(expression); + return new DeleteDefaultConstraintBuilder(_context, expression); } } } diff --git a/src/Umbraco.Core/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs b/src/Umbraco.Core/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs index 8b0b20c0e2..b73d3f0d13 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs @@ -10,12 +10,17 @@ namespace Umbraco.Core.Migrations.Expressions.Delete.Expressions public virtual string TableName { get; set; } public virtual string ColumnName { get; set; } + public virtual string ConstraintName { get; set; } + public virtual bool HasDefaultConstraint { get; set; } protected override string GetSql() { - return string.Format(SqlSyntax.DeleteDefaultConstraint, - SqlSyntax.GetQuotedTableName(TableName), - SqlSyntax.GetQuotedColumnName(ColumnName)); + return HasDefaultConstraint + ? string.Format(SqlSyntax.DeleteDefaultConstraint, + SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.GetQuotedColumnName(ColumnName), + SqlSyntax.GetQuotedName(ConstraintName)) + : string.Empty; } } } diff --git a/src/Umbraco.Core/Migrations/Expressions/Delete/IDeleteBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Delete/IDeleteBuilder.cs index 07faf5028e..84e44d0d93 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Delete/IDeleteBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Delete/IDeleteBuilder.cs @@ -19,9 +19,14 @@ namespace Umbraco.Core.Migrations.Expressions.Delete IExecutableBuilder Table(string tableName); /// - /// Specifies the table to delete keys and indexes for. + /// Builds a Delete Keys and Indexes expression, and executes. /// - IExecutableBuilder KeysAndIndexes(string tableName = null); + IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true); + + /// + /// Builds a Delete Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(string tableName, bool local = true, bool foreign = true); /// /// Specifies the column to delete. diff --git a/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs index 1c595d9103..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; @@ -18,36 +20,66 @@ namespace Umbraco.Core.Migrations.Expressions.Delete.KeysAndIndexes public string TableName { get; set; } + public bool DeleteLocal { get; set; } + + public bool DeleteForeign { get; set; } + /// public void Do() { - if (TableName == null) - { - // drop keys - var keys = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToArray(); - foreach (var key in keys.Where(x => x.Item2.StartsWith("FK_"))) - Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do(); - foreach (var key in keys.Where(x => x.Item2.StartsWith("PK_"))) - Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do(); + _context.BuildingExpression = false; - // drop indexes - var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToArray(); - foreach (var index in indexes) - Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + //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 + + if (DeleteForeign) + { + //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 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 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 + } } - else - { - // drop keys - var keys = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToArray(); - foreach (var key in keys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("FK_"))) - Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do(); - foreach (var key in keys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("PK_"))) - Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do(); - // drop indexes - var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToArray(); + // drop indexes + if (DeleteLocal) + { 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/IMigrationContext.cs b/src/Umbraco.Core/Migrations/IMigrationContext.cs index 2baadc79eb..c76de8dfb3 100644 --- a/src/Umbraco.Core/Migrations/IMigrationContext.cs +++ b/src/Umbraco.Core/Migrations/IMigrationContext.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Migrations bool BuildingExpression { get; set; } /// - /// Adds a post-migrations. + /// Adds a post-migration. /// void AddPostMigration() where TMigration : IMigration; 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 5c4defab0c..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); @@ -389,7 +390,7 @@ namespace Umbraco.Core.Migrations.Install private DatabaseSchemaResult ValidateSchema(IScope scope) { - if (_databaseFactory.Configured == false) + if (_databaseFactory.Initialized == false) return new DatabaseSchemaResult(_databaseFactory.SqlContext.SqlSyntax); if (_databaseSchemaValidationResult != null) @@ -513,7 +514,7 @@ namespace Umbraco.Core.Migrations.Install private Attempt CheckReadyForInstall() { - if (_databaseFactory.Configured == false) + if (_databaseFactory.CanConnect == false) { return Attempt.Fail(new Result { @@ -539,7 +540,7 @@ namespace Umbraco.Core.Migrations.Install { Message = "The database configuration failed with the following message: " + ex.Message + - "\n Please check log file for additional information (can be found in '/App_Data/Logs/UmbracoTraceLog.txt')", + "\n Please check log file for additional information (can be found in '/App_Data/Logs/')", Success = false, Percentage = "90" }; diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 1de983636b..9bd26749ad 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -101,33 +101,33 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -1, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1", SortOrder = 0, UniqueId = new Guid("916724a5-173d-4619-b97e-b9de133dd6f5"), Text = "SYSTEM DATA: umbraco master root", NodeObjectType = Constants.ObjectTypes.SystemRoot, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -20, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1,-20", SortOrder = 0, UniqueId = new Guid("0F582A79-1E41-4CF0-BFA0-76340651891A"), Text = "Recycle Bin", NodeObjectType = Constants.ObjectTypes.ContentRecycleBin, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -21, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1,-21", SortOrder = 0, UniqueId = new Guid("BF7C7CBC-952F-4518-97A2-69E9C7B33842"), Text = "Recycle Bin", NodeObjectType = Constants.ObjectTypes.MediaRecycleBin, CreateDate = DateTime.Now }); - InsertDataTypeNodeDto(Constants.DataTypes.LabelString, 35, "f0bc4bfb-b499-40d6-ba86-058885a5178c", "Label"); - InsertDataTypeNodeDto(Constants.DataTypes.LabelInt, 36, "8e7f995c-bd81-4627-9932-c40e568ec788", "Label (integer)"); - InsertDataTypeNodeDto(Constants.DataTypes.LabelBigint, 36, "930861bf-e262-4ead-a704-f99453565708", "Label (bigint)"); - InsertDataTypeNodeDto(Constants.DataTypes.LabelDateTime, 37, "0e9794eb-f9b5-4f20-a788-93acd233a7e4", "Label (datetime)"); - InsertDataTypeNodeDto(Constants.DataTypes.LabelTime, 38, "a97cec69-9b71-4c30-8b12-ec398860d7e8", "Label (time)"); - InsertDataTypeNodeDto(Constants.DataTypes.LabelDecimal, 39, "8f1ef1e1-9de4-40d3-a072-6673f631ca64", "Label (decimal)"); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -90, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-90", SortOrder = 34, UniqueId = new Guid("84c6b441-31df-4ffe-b67e-67d5bc3ae65a"), Text = "Upload", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -89, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-89", SortOrder = 33, UniqueId = new Guid("c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3"), Text = "Textarea", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -88, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-88", SortOrder = 32, UniqueId = new Guid("0cc0eba1-9960-42c9-bf9b-60e150b429ae"), Text = "Textstring", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -87, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-87", SortOrder = 4, UniqueId = new Guid("ca90c950-0aff-4e72-b976-a30b1ac57dad"), Text = "Richtext editor", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -51, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-51", SortOrder = 2, UniqueId = new Guid("2e6d3631-066e-44b8-aec4-96f09099b2b5"), Text = "Numeric", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -49, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-49", SortOrder = 2, UniqueId = new Guid("92897bc6-a5f3-4ffe-ae27-f2e7e33dda49"), Text = "True/false", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -43, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-43", SortOrder = 2, UniqueId = new Guid("fbaf13a8-4036-41f2-93a3-974f678c312a"), Text = "Checkbox list", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DropDownSingle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DropDownSingle}", SortOrder = 2, UniqueId = new Guid("0b6a45e7-44ba-430d-9da5-4e46060b9e03"), Text = "Dropdown", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -41, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-41", SortOrder = 2, UniqueId = new Guid("5046194e-4237-453c-a547-15db3a07c4e1"), Text = "Date Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -40, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-40", SortOrder = 2, UniqueId = new Guid("bb5f57c9-ce2b-4bb9-b697-4caca783a805"), Text = "Radiobox", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DropDownMultiple, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DropDownMultiple}", SortOrder = 2, UniqueId = new Guid("f38f0ac7-1d27-439c-9f3f-089cd8825a53"), Text = "Dropdown multiple", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -37, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-37", SortOrder = 2, UniqueId = new Guid("0225af17-b302-49cb-9176-b9f35cab9c17"), Text = "Approved Color", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -36, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-36", SortOrder = 2, UniqueId = new Guid("e4d66c0f-b935-4200-81f0-025f7256b89a"), Text = "Date Picker with time", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultContentListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultContentListView}", SortOrder = 2, UniqueId = new Guid("C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMediaListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultMediaListView}", SortOrder = 2, UniqueId = new Guid("3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Media", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultMembersListView}", SortOrder = 2, UniqueId = new Guid("AA2C52A0-CE87-4E65-A47C-7DF09358585D"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + InsertDataTypeNodeDto(Constants.DataTypes.LabelString, 35, Constants.DataTypes.Guids.LabelString, "Label (string)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelInt, 36, Constants.DataTypes.Guids.LabelInt, "Label (integer)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelBigint, 36, Constants.DataTypes.Guids.LabelBigInt, "Label (bigint)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelDateTime, 37, Constants.DataTypes.Guids.LabelDateTime, "Label (datetime)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelTime, 38, Constants.DataTypes.Guids.LabelTime, "Label (time)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelDecimal, 39, Constants.DataTypes.Guids.LabelDecimal, "Label (decimal)"); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Upload, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Upload}", SortOrder = 34, UniqueId = Constants.DataTypes.Guids.UploadGuid, Text = "Upload", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Textarea, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Textarea}", SortOrder = 33, UniqueId = Constants.DataTypes.Guids.TextareaGuid, Text = "Textarea", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Textbox, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Textbox}", SortOrder = 32, UniqueId = Constants.DataTypes.Guids.TextstringGuid, Text = "Textstring", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.RichtextEditor, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.RichtextEditor}", SortOrder = 4, UniqueId = Constants.DataTypes.Guids.RichtextEditorGuid, Text = "Richtext editor", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -51, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-51", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.NumericGuid, Text = "Numeric", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Boolean, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Boolean}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.CheckboxGuid, Text = "True/false", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -43, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-43", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.CheckboxListGuid, Text = "Checkbox list", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DropDownSingle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DropDownSingle}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.DropdownGuid, Text = "Dropdown", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -41, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-41", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.DatePickerGuid, Text = "Date Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -40, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-40", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.RadioboxGuid, Text = "Radiobox", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DropDownMultiple, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DropDownMultiple}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.DropdownMultipleGuid, Text = "Dropdown multiple", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -37, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-37", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.ApprovedColorGuid, Text = "Approved Color", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DateTime, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DateTime}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.DatePickerWithTimeGuid, Text = "Date Picker with time", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultContentListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultContentListView}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.ListViewContentGuid, Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMediaListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultMediaListView}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.ListViewMediaGuid, Text = Constants.Conventions.DataTypes.ListViewPrefix + "Media", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultMembersListView}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.ListViewMembersGuid, Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"), Text = Constants.Conventions.MediaTypes.Folder, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"), Text = Constants.Conventions.MediaTypes.Image, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1041, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1041", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1043, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1043", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Tags, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Tags}", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.ImageCropper, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.ImageCropper}", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = Constants.ObjectTypes.MemberType, CreateDate = DateTime.Now }); //New UDI pickers with newer Ids @@ -151,14 +151,16 @@ 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() { - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "icon-folder", IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "icon-picture", AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "icon-document", AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = "icon-user", Thumbnail = "icon-user", Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = Constants.Icons.MediaFolder, Thumbnail = Constants.Icons.MediaFolder, IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = Constants.Icons.MediaImage, Thumbnail = Constants.Icons.MediaImage, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = Constants.Icons.MediaFile, Thumbnail = Constants.Icons.MediaFile, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = Constants.Icons.Member, Thumbnail = Constants.Icons.Member, Variations = (byte) ContentVariation.Nothing }); } private void CreateUserData() @@ -169,8 +171,8 @@ namespace Umbraco.Core.Migrations.Install private void CreateUserGroupData() { _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 1, StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.AdminGroupAlias, Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7ï", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-medal" }); - _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 2, StartMediaId = -1, StartContentId = -1, Alias = "writer", Name = "Writers", DefaultPermissions = "CAH:F", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-edit" }); - _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 3, StartMediaId = -1, StartContentId = -1, Alias = "editor", Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5Fï", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-tools" }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 2, StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.WriterGroupAlias, Name = "Writers", DefaultPermissions = "CAH:F", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-edit" }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 3, StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.EditorGroupAlias, Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5Fï", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-tools" }); _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 4, StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.TranslatorGroupAlias, Name = "Translators", DefaultPermissions = "AF", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-globe" }); _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 5, StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.SensitiveDataGroupAlias, Name = "Sensitive data", DefaultPermissions = "", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-lock" }); } @@ -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 }); @@ -210,24 +213,24 @@ namespace Umbraco.Core.Migrations.Install private void CreatePropertyTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = 1043, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); //membership property types - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = -89, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = Constants.DataTypes.Textarea, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 30, UniqueId = 30.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsApproved, Name = Constants.Conventions.Member.IsApprovedLabel, SortOrder = 2, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 31, UniqueId = 31.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsLockedOut, Name = Constants.Conventions.Member.IsLockedOutLabel, SortOrder = 3, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 30, UniqueId = 30.ToGuid(), DataTypeId = Constants.DataTypes.Boolean, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsApproved, Name = Constants.Conventions.Member.IsApprovedLabel, SortOrder = 2, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 31, UniqueId = 31.ToGuid(), DataTypeId = Constants.DataTypes.Boolean, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsLockedOut, Name = Constants.Conventions.Member.IsLockedOutLabel, SortOrder = 3, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _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 }); } @@ -266,30 +269,30 @@ namespace Umbraco.Core.Migrations.Install const string layouts = "[" + cardLayout + "," + listLayout + "]"; // TODO: Check which of the DataTypeIds below doesn't exist in umbracoNode, which results in a foreign key constraint errors. - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -49, EditorAlias = Constants.PropertyEditors.Aliases.Boolean, DbType = "Integer" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.Boolean, EditorAlias = Constants.PropertyEditors.Aliases.Boolean, DbType = "Integer" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -51, EditorAlias = Constants.PropertyEditors.Aliases.Integer, DbType = "Integer" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -87, EditorAlias = Constants.PropertyEditors.Aliases.TinyMce, DbType = "Ntext", Configuration = "{\"value\":\",code,undo,redo,cut,copy,mcepasteword,stylepicker,bold,italic,bullist,numlist,outdent,indent,mcelink,unlink,mceinsertanchor,mceimage,umbracomacro,mceinserttable,umbracoembed,mcecharmap,|1|1,2,3,|0|500,400|1049,|true|\"}" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -88, EditorAlias = Constants.PropertyEditors.Aliases.TextBox, DbType = "Nvarchar" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -89, EditorAlias = Constants.PropertyEditors.Aliases.TextArea, DbType = "Ntext" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -90, EditorAlias = Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.Textbox, EditorAlias = Constants.PropertyEditors.Aliases.TextBox, DbType = "Nvarchar" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.Textarea, EditorAlias = Constants.PropertyEditors.Aliases.TextArea, DbType = "Ntext" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.Upload, EditorAlias = Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar" }); InsertDataTypeDto(Constants.DataTypes.LabelString, Constants.PropertyEditors.Aliases.Label, "Nvarchar", "{\"umbracoDataValueType\":\"STRING\"}"); InsertDataTypeDto(Constants.DataTypes.LabelInt, Constants.PropertyEditors.Aliases.Label, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); InsertDataTypeDto(Constants.DataTypes.LabelBigint, Constants.PropertyEditors.Aliases.Label, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); InsertDataTypeDto(Constants.DataTypes.LabelDateTime, Constants.PropertyEditors.Aliases.Label, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); InsertDataTypeDto(Constants.DataTypes.LabelDecimal, Constants.PropertyEditors.Aliases.Label, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); InsertDataTypeDto(Constants.DataTypes.LabelTime, Constants.PropertyEditors.Aliases.Label, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -36, EditorAlias = Constants.PropertyEditors.Aliases.DateTime, DbType = "Date" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DateTime, EditorAlias = Constants.PropertyEditors.Aliases.DateTime, DbType = "Date" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -37, EditorAlias = Constants.PropertyEditors.Aliases.ColorPicker, DbType = "Nvarchar" }); InsertDataTypeDto(Constants.DataTypes.DropDownSingle, Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":false}"); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -40, EditorAlias = Constants.PropertyEditors.Aliases.RadioButtonList, DbType = "Nvarchar" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -41, EditorAlias = "Umbraco.DateTime", DbType = "Date", Configuration = "{\"format\":\"YYYY-MM-DD\"}" }); InsertDataTypeDto(Constants.DataTypes.DropDownMultiple, Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":true}"); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -43, EditorAlias = Constants.PropertyEditors.Aliases.CheckBoxList, DbType = "Nvarchar" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1041, EditorAlias = Constants.PropertyEditors.Aliases.Tags, DbType = "Ntext", + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.Tags, EditorAlias = Constants.PropertyEditors.Aliases.Tags, DbType = "Ntext", Configuration = "{\"group\":\"default\", \"storageType\":\"Json\"}" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1043, EditorAlias = Constants.PropertyEditors.Aliases.ImageCropper, DbType = "Ntext" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.ImageCropper, EditorAlias = Constants.PropertyEditors.Aliases.ImageCropper, DbType = "Ntext" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DefaultContentListView, EditorAlias = Constants.PropertyEditors.Aliases.ListView, DbType = "Nvarchar", Configuration = "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + layouts + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DefaultMediaListView, EditorAlias = Constants.PropertyEditors.Aliases.ListView, DbType = "Nvarchar", @@ -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/MigrationBase_Extra.cs b/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs index bf07e4d08f..f4c6150073 100644 --- a/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs +++ b/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.SqlSyntax; @@ -84,10 +85,18 @@ namespace Umbraco.Core.Migrations protected void ReplaceColumn(string tableName, string currentName, string newName) { - AddColumn(tableName, newName, out var sqls); - Execute.Sql($"UPDATE {SqlSyntax.GetQuotedTableName(tableName)} SET {SqlSyntax.GetQuotedColumnName(newName)}={SqlSyntax.GetQuotedColumnName(currentName)}").Do(); - foreach (var sql in sqls) Execute.Sql(sql).Do(); - Delete.Column(currentName).FromTable(tableName).Do(); + if (DatabaseType.IsSqlCe()) + { + AddColumn(tableName, newName, out var sqls); + Execute.Sql($"UPDATE {SqlSyntax.GetQuotedTableName(tableName)} SET {SqlSyntax.GetQuotedColumnName(newName)}={SqlSyntax.GetQuotedColumnName(currentName)}").Do(); + foreach (var sql in sqls) Execute.Sql(sql).Do(); + Delete.Column(currentName).FromTable(tableName).Do(); + } + else + { + Execute.Sql(SqlSyntax.FormatColumnRename(tableName, currentName, newName)).Do(); + AlterColumn(tableName, newName); + } } protected bool TableExists(string tableName) diff --git a/src/Umbraco.Core/Migrations/MigrationContext.cs b/src/Umbraco.Core/Migrations/MigrationContext.cs index a8d052036c..0bb2bc11ae 100644 --- a/src/Umbraco.Core/Migrations/MigrationContext.cs +++ b/src/Umbraco.Core/Migrations/MigrationContext.cs @@ -41,6 +41,7 @@ namespace Umbraco.Core.Migrations public void AddPostMigration() where TMigration : IMigration { + // just adding - will be de-duplicated when executing PostMigrations.Add(typeof(TMigration)); } } diff --git a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs index 8b5d9cc78c..be06a32ec7 100644 --- a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs +++ b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs @@ -92,6 +92,7 @@ namespace Umbraco.Core.Migrations if (_executed) throw new InvalidOperationException("This expression has already been executed."); _executed = true; + Context.BuildingExpression = false; if (sql == null) { diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs index 4be4ae7d30..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(); @@ -240,7 +244,8 @@ namespace Umbraco.Core.Migrations if (finalState == null) finalState = kvp.Key; else - throw new Exception("Multiple final states have been detected."); + throw new InvalidOperationException($"Multiple final states have been detected in the plan (\"{finalState}\", \"{kvp.Key}\")." + + " Make sure the plan contains only one final state."); } // now check for loops @@ -254,7 +259,8 @@ namespace Umbraco.Core.Migrations while (nextTransition != null && !verified.Contains(nextTransition.SourceState)) { if (visited.Contains(nextTransition.SourceState)) - throw new Exception("A loop has been detected."); + throw new InvalidOperationException($"A loop has been detected in the plan around state \"{nextTransition.SourceState}\"." + + " Make sure the plan does not contain circular transition paths."); visited.Add(nextTransition.SourceState); nextTransition = _transitions[nextTransition.TargetState]; } @@ -264,6 +270,14 @@ namespace Umbraco.Core.Migrations _finalState = finalState; } + /// + /// Throws an exception when the initial state is unknown. + /// + protected virtual void ThrowOnUnknownInitialState(string state) + { + throw new InvalidOperationException($"The migration plan does not support migrating from state \"{state}\"."); + } + /// /// Executes the plan. /// @@ -287,13 +301,15 @@ namespace Umbraco.Core.Migrations logger.Info("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState); if (!_transitions.TryGetValue(origState, out var transition)) - throw new Exception($"Unknown state \"{origState}\"."); + ThrowOnUnknownInitialState(origState); var context = new MigrationContext(scope.Database, logger); context.PostMigrations.AddRange(_postMigrationTypes); while (transition != null) { + logger.Info("Execute {MigrationType}", transition.MigrationType.Name); + var migration = migrationBuilder.Build(transition.MigrationType, context); migration.Migrate(); @@ -302,8 +318,10 @@ namespace Umbraco.Core.Migrations logger.Info("At {OrigState}", origState); + // 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 @@ -322,9 +340,10 @@ namespace Umbraco.Core.Migrations logger.Info("Done (pending scope completion)."); - // safety check + // 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; } @@ -343,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) { @@ -358,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; } @@ -402,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/Common/CreateKeysAndIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs new file mode 100644 index 0000000000..4872e0019d --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs @@ -0,0 +1,22 @@ +using Umbraco.Core.Migrations.Install; + +namespace Umbraco.Core.Migrations.Upgrade.Common +{ + public class CreateKeysAndIndexes : MigrationBase + { + public CreateKeysAndIndexes(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + // remove those that may already have keys + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.KeyValue).Do(); + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.PropertyData).Do(); + + // re-create *all* keys and indexes + foreach (var x in DatabaseSchemaCreator.OrderedTables) + Create.KeysAndIndexes(x).Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs new file mode 100644 index 0000000000..9e4af17c09 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs @@ -0,0 +1,75 @@ +namespace Umbraco.Core.Migrations.Upgrade.Common +{ + public class DeleteKeysAndIndexes : MigrationBase + { + public DeleteKeysAndIndexes(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + // all v7.14 tables + var tables = new[] + { + "cmsContent", + "cmsContentType", + "cmsContentType2ContentType", + "cmsContentTypeAllowedContentType", + "cmsContentVersion", + "cmsContentXml", + "cmsDataType", + "cmsDataTypePreValues", + "cmsDictionary", + "cmsDocument", + "cmsDocumentType", + "cmsLanguageText", + "cmsMacro", + "cmsMacroProperty", + "cmsMedia", + "cmsMember", + "cmsMember2MemberGroup", + "cmsMemberType", + "cmsPreviewXml", + "cmsPropertyData", + "cmsPropertyType", + "cmsPropertyTypeGroup", + "cmsTagRelationship", + "cmsTags", + "cmsTask", + "cmsTaskType", + "cmsTemplate", + "umbracoAccess", + "umbracoAccessRule", + "umbracoAudit", + "umbracoCacheInstruction", + "umbracoConsent", + "umbracoDomains", + "umbracoExternalLogin", + "umbracoLanguage", + "umbracoLock", + "umbracoLog", + "umbracoMigration", + "umbracoNode", + "umbracoRedirectUrl", + "umbracoRelation", + "umbracoRelationType", + "umbracoServer", + "umbracoUser", + "umbracoUser2NodeNotify", + "umbracoUser2UserGroup", + "umbracoUserGroup", + "umbracoUserGroup2App", + "umbracoUserGroup2NodePermission", + "umbracoUserLogin", + "umbracoUserStartNode", + }; + + // delete *all* keys and indexes - because of FKs + // on known v7 tables only + foreach (var table in tables) + Delete.KeysAndIndexes(table, false, true).Do(); + foreach (var table in tables) + Delete.KeysAndIndexes(table, true, false).Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index cf06a16d31..f0cfc08281 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -2,11 +2,11 @@ using System.Configuration; using Semver; using Umbraco.Core.Configuration; -using Umbraco.Core.Migrations.Upgrade.V_7_12_0; -using Umbraco.Core.Migrations.Upgrade.V_7_14_0; +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 { @@ -15,6 +15,9 @@ namespace Umbraco.Core.Migrations.Upgrade /// public class UmbracoPlan : MigrationPlan { + private const string InitPrefix = "{init-"; + private const string InitSuffix = "}"; + /// /// Initializes a new instance of the class. /// @@ -24,6 +27,27 @@ namespace Umbraco.Core.Migrations.Upgrade DefinePlan(); } + /// + /// Gets the initial state corresponding to a version. + /// + private static string GetInitState(SemVersion version) + => InitPrefix + version + InitSuffix; + + /// + /// Tries to extract a version from an initial state. + /// + private static bool TryGetInitStateVersion(string state, out string version) + { + if (state.StartsWith(InitPrefix) && state.EndsWith(InitSuffix)) + { + version = state.TrimStart(InitPrefix).TrimEnd(InitSuffix); + return true; + } + + version = null; + return false; + } + /// /// /// The default initial state in plans is string.Empty. @@ -41,23 +65,37 @@ namespace Umbraco.Core.Migrations.Upgrade if (!SemVersion.TryParse(ConfigurationManager.AppSettings[Constants.AppSettings.ConfigurationStatus], out var currentVersion)) throw new InvalidOperationException($"Could not get current version from web.config {Constants.AppSettings.ConfigurationStatus} appSetting."); - // we currently support upgrading from 7.10.0 and later - if (currentVersion < new SemVersion(7, 10)) - throw new InvalidOperationException($"Version {currentVersion} cannot be migrated to {UmbracoVersion.SemanticVersion}."); - // cannot go back in time if (currentVersion > UmbracoVersion.SemanticVersion) throw new InvalidOperationException($"Version {currentVersion} cannot be downgraded to {UmbracoVersion.SemanticVersion}."); - // upgrading from version 7 => initial state is eg "{init-7.10.0}" - // anything else is not supported - ie if 8 and above, we should have an initial state already - if (currentVersion.Major != 7) - throw new InvalidOperationException($"Version {currentVersion} is not supported by the migration plan."); + // only from 7.14.0 and above + var minVersion = new SemVersion(7, 14); + if (currentVersion < minVersion) + throw new InvalidOperationException($"Version {currentVersion} cannot be migrated to {UmbracoVersion.SemanticVersion}." + + $" Please upgrade first to at least {minVersion}."); - return "{init-" + currentVersion + "}"; + // Force versions between 7.14.*-7.15.* into into 7.14 initial state. Because there is no db-changes, + // and we don't want users to workaround my putting in version 7.14.0 them self. + if (minVersion <= currentVersion && currentVersion < new SemVersion(7, 16)) + return GetInitState(minVersion); + + // initial state is eg "{init-7.14.0}" + return GetInitState(currentVersion); } } + protected override void ThrowOnUnknownInitialState(string state) + { + if (TryGetInitStateVersion(state, out var initVersion)) + { + throw new InvalidOperationException($"Version {UmbracoVersion.SemanticVersion} does not support migrating from {initVersion}." + + $" Please verify which versions support migrating from {initVersion}."); + } + + base.ThrowOnUnknownInitialState(state); + } + // define the plan protected void DefinePlan() { @@ -70,21 +108,23 @@ namespace Umbraco.Core.Migrations.Upgrade // // If the new migration causes a merge conflict, because someone else also added another // new migration, you NEED to fix the conflict by providing one default path, and paths - // out of the conflict states (see example below). + // out of the conflict states (see examples below). // // * Porting from version 7: // Append the ported migration to the main chain, using a new guid (same as above). - // Create a new special chain from the {init-...} state to the main chain (see example - // below). + // Create a new special chain from the {init-...} state to the main chain. - // plan starts at 7.10.0 (anything before 7.10.0 is not supported) - // upgrades from 7 to 8, and then takes care of all eventual upgrades + // plan starts at 7.14.0 (anything before 7.14.0 is not supported) // - From("{init-7.10.0}"); + From(GetInitState(new SemVersion(7, 14, 0))); + + // begin migrating from v7 - remove all keys and indexes + To("{B36B9ABD-374E-465B-9C5F-26AB0D39326F}"); + To("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); To("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); - To("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}"); + To("{5CB66059-45F4-48BA-BCBD-C5035D79206B}"); To("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); To("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); To("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); @@ -95,91 +135,66 @@ namespace Umbraco.Core.Migrations.Upgrade ToWithReplace("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1 To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); - Merge().To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") // shannon added that one - .With().To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") // stephan added that one + Merge() + .To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") + .With() + .To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") .As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); To("{1350617A-4930-4D61-852F-E3AA9E692173}"); - To("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0 - - Merge() - .To("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}") // from 7.12.0 - .To("{EB34B5DC-BB87-4005-985E-D983EA496C38}") // from 7.12.0 - .To("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}") // from 7.12.0 - .To("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}") // from 7.12.0 - .With() - .To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // andy added that one - .As("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); - - ToWithReplace("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}", "{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // merge - ToWithReplace("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}", "{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); // merge + To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); + To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); + To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); To("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); - To("{77874C77-93E5-4488-A404-A630907CEEF0}"); To("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); To("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); To("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); To("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); To("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); To("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); - To("{648A2D5F-7467-48F8-B309-E99CEEE00E2A}"); // fixed version To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); To("{64EBCE53-E1F0-463A-B40B-E98EFCCA8AE2}"); To("{0009109C-A0B8-4F3F-8FEB-C137BBDDA268}"); - To("{8A027815-D5CD-4872-8B88-9A51AB5986A6}"); // from 7.14.0 To("{ED28B66A-E248-4D94-8CDB-9BDF574023F0}"); To("{38C809D5-6C34-426B-9BEA-EFD39162595C}"); To("{6017F044-8E70-4E10-B2A3-336949692ADD}"); - To("{98339BEF-E4B2-48A8-B9D1-D173DC842BBE}"); Merge() .To("{CDBEDEE4-9496-4903-9CF2-4104E00FF960}") .With() - .To("{940FD19A-00A8-4D5C-B8FF-939143585726}") + .To("{940FD19A-00A8-4D5C-B8FF-939143585726}") .As("{0576E786-5C30-4000-B969-302B61E90CA3}"); + To("{48AD6CCD-C7A4-4305-A8AB-38728AD23FC5}"); + To("{DF470D86-E5CA-42AC-9780-9D28070E25F9}"); + + // finish migrating from v7 - recreate all keys and indexes + To("{3F9764F5-73D0-4D45-8804-1240A66E43A2}"); + To("{E0CBE54D-A84F-4A8F-9B13-900945FD7ED9}"); To("{78BAF571-90D0-4D28-8175-EF96316DA789}"); + // release-8.0.0 + + // to 8.0.1... To("{80C0A0CB-0DD5-4573-B000-C4B7C313C70D}"); + // release-8.0.1 + + // to 8.1.0... To("{B69B6E8C-A769-4044-A27E-4A4E18D1645A}"); + 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}"); + + + To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); //FINAL - - - - - // and then, need to support upgrading from more recent 7.x - // - From("{init-7.10.1}").To("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.10.2}").To("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.10.3}").To("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.10.4}").To("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.10.5}").To("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.11.0}").To("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.11.1}").To("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.11.2}").To("{init-7.10.0}"); // same as 7.10.0 - - // 7.12.0 has migrations, define a custom chain which copies the chain - // going from {init-7.10.0} to former final (1350617A) , and then goes straight to - // main chain, skipping the migrations - // - From("{init-7.12.0}"); - // clone start / clone stop / target - ToWithClone("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); - - From("{init-7.12.1}").To("{init-7.10.0}"); // same as 7.12.0 - From("{init-7.12.2}").To("{init-7.10.0}"); // same as 7.12.0 - From("{init-7.12.3}").To("{init-7.10.0}"); // same as 7.12.0 - From("{init-7.12.4}").To("{init-7.10.0}"); // same as 7.12.0 - From("{init-7.13.0}").To("{init-7.10.0}"); // same as 7.12.0 - From("{init-7.13.1}").To("{init-7.10.0}"); // same as 7.12.0 - - // 7.14.0 has migrations, handle it... - // clone going from 7.10 to 1350617A (the last one before we started to merge 7.12 migrations), then - // clone going from CF51B39B (after 7.12 migrations) to 0009109C (the last one before we started to merge 7.12 migrations), - // ending in 8A027815 (after 7.14 migrations) - From("{init-7.14.0}") - .ToWithClone("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{9109B8AF-6B34-46EE-9484-7434196D0C79}") - .ToWithClone("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}", "{0009109C-A0B8-4F3F-8FEB-C137BBDDA268}", "{8A027815-D5CD-4872-8B88-9A51AB5986A6}"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_10_0/RenamePreviewFolder.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_10_0/RenamePreviewFolder.cs deleted file mode 100644 index 48e6d0085d..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_10_0/RenamePreviewFolder.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.IO; -using Umbraco.Core.IO; -using File = System.IO.File; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_10_0 -{ - /// - /// Renames the preview folder containing static HTML files to ensure it does not interfere with the MVC route - /// that is now supposed to render these views dynamically. We don't want to delete as people may have made - /// customizations to these files that would need to be migrated to the new .cshtml view files. - /// - public class RenamePreviewFolder : MigrationBase - { - public RenamePreviewFolder(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var previewFolderPath = IOHelper.MapPath(SystemDirectories.Umbraco + "/preview"); - if (Directory.Exists(previewFolderPath)) - { - var newPath = previewFolderPath.Replace("preview", "preview.old"); - if (Directory.Exists(newPath) == false) - { - Directory.Move(previewFolderPath, newPath); - var readmeText = - $"Static HTML files used for preview and canvas editing functionality no longer live in this directory.\r\n" + - $"Instead they have been recreated as MVC views and can now be found in '~/Umbraco/Views/Preview'.\r\n" + - $"See issue: http://issues.umbraco.org/issue/U4-11090"; - File.WriteAllText(Path.Combine(newPath, "readme.txt"), readmeText); - } - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/AddRelationTypeForMediaFolderOnDelete.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/AddRelationTypeForMediaFolderOnDelete.cs deleted file mode 100644 index f34ed9fb68..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/AddRelationTypeForMediaFolderOnDelete.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 -{ - public class AddRelationTypeForMediaFolderOnDelete : MigrationBase - { - - public AddRelationTypeForMediaFolderOnDelete(IMigrationContext context) : base(context) - { - } - - public override void Migrate() - { - var relationTypeCount = Context.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoRelationType WHERE alias=@alias", - new { alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias }); - - if (relationTypeCount > 0) - return; - - var uniqueId = (Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias + "____" + Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName).ToGuid(); - Insert.IntoTable("umbracoRelationType").Row(new - { - typeUniqueId = uniqueId, - alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, - name = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, - childObjectType = Constants.ObjectTypes.MediaType, - parentObjectType = Constants.ObjectTypes.MediaType, - dual = false - }).Do(); - } - - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/IncreaseLanguageIsoCodeColumnLength.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/IncreaseLanguageIsoCodeColumnLength.cs deleted file mode 100644 index fc7a21f4fa..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/IncreaseLanguageIsoCodeColumnLength.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 -{ - public class IncreaseLanguageIsoCodeColumnLength : MigrationBase - { - public IncreaseLanguageIsoCodeColumnLength(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - // Some people seem to have a constraint in their DB instead of an index, we'd need to drop that one - // See: https://our.umbraco.com/forum/using-umbraco-and-getting-started/93282-upgrade-from-711-to-712-fails - var constraints = SqlSyntax.GetConstraintsPerTable(Context.Database).Distinct().ToArray(); - if (constraints.Any(x => x.Item2.InvariantEquals("IX_umbracoLanguage_languageISOCode"))) - { - Delete.UniqueConstraint("IX_umbracoLanguage_languageISOCode").FromTable("umbracoLanguage").Do(); - } - - //Now check for indexes of that name and drop that if it exists - var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); - if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoLanguage_languageISOCode"))) - { - Delete.Index("IX_umbracoLanguage_languageISOCode").OnTable("umbracoLanguage").Do(); - } - - Alter.Table("umbracoLanguage") - .AlterColumn("languageISOCode") - .AsString(14) - .Nullable() - .Do(); - - Create.Index("IX_umbracoLanguage_languageISOCode") - .OnTable("umbracoLanguage") - .OnColumn("languageISOCode") - .Ascending() - .WithOptions() - .Unique() - .Do(); - } - - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/RenameTrueFalseField.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/RenameTrueFalseField.cs deleted file mode 100644 index 4022739ece..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/RenameTrueFalseField.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 -{ - public class RenameTrueFalseField : MigrationBase - { - public RenameTrueFalseField(IMigrationContext context) : base(context) - { - } - - public override void Migrate() - { - //rename the existing true/false field - Update.Table(NodeDto.TableName).Set(new { text = "Checkbox" }).Where(new { id = Constants.DataTypes.Boolean }).Do(); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs deleted file mode 100644 index c8d65961f4..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Umbraco.Core.Models; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 -{ - /// - /// Set the default storageType for the tags datatype to "CSV" to ensure backwards compatibility since the default is going to be JSON in new versions. - /// - public class SetDefaultTagsStorageType : MigrationBase - { - public SetDefaultTagsStorageType(IMigrationContext context) - : base(context) - { } - - // dummy editor for deserialization - private class TagConfigurationEditor : ConfigurationEditor - { } - - public override void Migrate() - { - // get all Umbraco.Tags datatypes - var dataTypeDtos = Database.Fetch(Context.SqlContext.Sql() - .Select() - .From() - .Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.Tags)); - - // get a dummy editor for deserialization - var editor = new TagConfigurationEditor(); - - foreach (var dataTypeDto in dataTypeDtos) - { - // need to check storageType on raw dictionary, as TagConfiguration would have a default value - var dictionary = JsonConvert.DeserializeObject(dataTypeDto.Configuration); - - // if missing, use TagConfiguration to properly update the configuration - // due to ... reasons ... the key can start with a lower or upper 'S' - if (!dictionary.ContainsKey("storageType") && !dictionary.ContainsKey("StorageType")) - { - var configuration = (TagConfiguration)editor.FromDatabase(dataTypeDto.Configuration); - configuration.StorageType = TagsStorageType.Csv; - dataTypeDto.Configuration = ConfigurationEditor.ToDatabase(configuration); - Database.Update(dataTypeDto); - } - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs deleted file mode 100644 index 7596e3d7dd..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 -{ public class UpdateUmbracoConsent : MigrationBase { - public UpdateUmbracoConsent(IMigrationContext context) - : base(context) - { } - - public override void Migrate() { Alter.Table(Constants.DatabaseSchema.Tables.Consent).AlterColumn("comment").AsString().Nullable().Do(); } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_14_0/UpdateMemberGroupPickerData.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_14_0/UpdateMemberGroupPickerData.cs deleted file mode 100644 index c70f42076f..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_14_0/UpdateMemberGroupPickerData.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Umbraco.Core.Migrations.Upgrade.V_7_14_0 -{ - /// - /// Migrates member group picker properties from NVarchar to NText. See https://github.com/umbraco/Umbraco-CMS/issues/3268. - /// - public class UpdateMemberGroupPickerData : MigrationBase - { - /// - /// Migrates member group picker properties from NVarchar to NText. See https://github.com/umbraco/Umbraco-CMS/issues/3268. - /// - public UpdateMemberGroupPickerData(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - Database.Execute($@"UPDATE umbracoPropertyData SET textValue = varcharValue, varcharValue = NULL - WHERE textValue IS NULL AND id IN ( - SELECT id FROM umbracoPropertyData WHERE propertyTypeId in ( - SELECT id from cmsPropertyType where dataTypeId IN ( - SELECT nodeId FROM umbracoDataType WHERE propertyEditorAlias = '{Constants.PropertyEditors.Aliases.MemberGroupPicker}' - ) - ) - )"); - - Database.Execute($"UPDATE umbracoDataType SET dbType = 'Ntext' WHERE propertyEditorAlias = '{Constants.PropertyEditors.Aliases.MemberGroupPicker}'"); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_5_0/AddRedirectUrlTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_5_0/AddRedirectUrlTable.cs deleted file mode 100644 index 7040874a74..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_5_0/AddRedirectUrlTable.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Linq; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_5_0 -{ - public class AddRedirectUrlTable : MigrationBase - { - public AddRedirectUrlTable(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var database = Database; - var umbracoRedirectUrlTableName = "umbracoRedirectUrl"; - - var tables = SqlSyntax.GetTablesInSchema(database).ToArray(); - - if (tables.InvariantContains(umbracoRedirectUrlTableName)) - { - var columns = SqlSyntax.GetColumnsInSchema(database).ToArray(); - if (columns.Any(x => x.TableName.InvariantEquals(umbracoRedirectUrlTableName) && x.ColumnName.InvariantEquals("id") && x.DataType == "uniqueidentifier")) - return; - Delete.Table(umbracoRedirectUrlTableName).Do(); - } - - Create.Table(umbracoRedirectUrlTableName) - .WithColumn("id").AsGuid().NotNullable().PrimaryKey("PK_" + umbracoRedirectUrlTableName) - .WithColumn("createDateUtc").AsDateTime().NotNullable() - .WithColumn("url").AsString(2048).NotNullable() - .WithColumn("contentKey").AsGuid().NotNullable() - .WithColumn("urlHash").AsString(40).NotNullable() - .Do(); - - Create.Index("IX_" + umbracoRedirectUrlTableName).OnTable(umbracoRedirectUrlTableName) - .OnColumn("urlHash") - .Ascending() - .OnColumn("contentKey") - .Ascending() - .OnColumn("createDateUtc") - .Descending() - .WithOptions().NonClustered() - .Do(); - - Create.ForeignKey("FK_" + umbracoRedirectUrlTableName) - .FromTable(umbracoRedirectUrlTableName).ForeignColumn("contentKey") - .ToTable("umbracoNode").PrimaryColumn("uniqueID") - .Do(); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_5_0/RemoveStylesheetDataAndTablesAgain.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_5_0/RemoveStylesheetDataAndTablesAgain.cs deleted file mode 100644 index d27ed11a21..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_5_0/RemoveStylesheetDataAndTablesAgain.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Linq; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_5_0 -{ - /// - /// This is here to re-remove these tables, we dropped them in 7.3 but new installs created them again so we're going to re-drop them - /// - public class RemoveStylesheetDataAndTablesAgain : MigrationBase - { - public RemoveStylesheetDataAndTablesAgain(IMigrationContext context) - : base(context) - { - } - - public override void Migrate() - { - // these have been obsoleted, need to copy the values here - var stylesheetPropertyObjectType = new Guid("5555da4f-a123-42b2-4488-dcdfb25e4111"); - var stylesheetObjectType = new Guid("9F68DA4F-A3A8-44C2-8226-DCBD125E4840"); - - //Clear all stylesheet data if the tables exist - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); - if (tables.InvariantContains("cmsStylesheetProperty")) - { - Delete.FromTable("cmsStylesheetProperty").AllRows().Do(); - Delete.FromTable("umbracoNode").Row(new { nodeObjectType = stylesheetPropertyObjectType }).Do(); - - Delete.Table("cmsStylesheetProperty").Do(); - } - if (tables.InvariantContains("cmsStylesheet")) - { - Delete.FromTable("cmsStylesheet").AllRows().Do(); - Delete.FromTable("umbracoNode").Row(new { nodeObjectType = stylesheetObjectType }).Do(); - - Delete.Table("cmsStylesheet").Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_5_0/UpdateUniqueIndexOnPropertyData.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_5_0/UpdateUniqueIndexOnPropertyData.cs deleted file mode 100644 index 8c688cfd28..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_5_0/UpdateUniqueIndexOnPropertyData.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_5_0 -{ - /// - /// See: http://issues.umbraco.org/issue/U4-8522 - /// - public class UpdateUniqueIndexOnPropertyData : MigrationBase - { - public UpdateUniqueIndexOnPropertyData(IMigrationContext context) - : base(context) - { - } - - public override void Migrate() - { - //Clear all stylesheet data if the tables exist - //tuple = tablename, indexname, columnname, unique - var indexes = SqlSyntax.GetDefinedIndexes(Context.Database).ToArray(); - var found = indexes.FirstOrDefault( - x => x.Item1.InvariantEquals("cmsPropertyData") - && x.Item2.InvariantEquals("IX_cmsPropertyData_1") - //we're searching for the old index which is not unique - && x.Item4 == false); - - if (found != null) - { - Database.Execute("DELETE FROM cmsPropertyData WHERE id NOT IN (SELECT MIN(id) FROM cmsPropertyData GROUP BY nodeId, versionId, propertytypeid HAVING MIN(id) IS NOT NULL)"); - - //we need to re create this index - Delete.Index("IX_cmsPropertyData_1").OnTable("cmsPropertyData").Do(); - Create.Index("IX_cmsPropertyData_1").OnTable("cmsPropertyData") - .OnColumn("nodeId").Ascending() - .OnColumn("versionId").Ascending() - .OnColumn("propertytypeid").Ascending() - .WithOptions().NonClustered() - .WithOptions().Unique() - .Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_5_5/UpdateAllowedMediaTypesAtRoot.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_5_5/UpdateAllowedMediaTypesAtRoot.cs deleted file mode 100644 index 362b8e251c..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_5_5/UpdateAllowedMediaTypesAtRoot.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Umbraco.Core.Migrations.Upgrade.V_7_5_5 -{ - /// - /// See: http://issues.umbraco.org/issue/U4-4196 - /// - public class UpdateAllowedMediaTypesAtRoot : MigrationBase - { - public UpdateAllowedMediaTypesAtRoot(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - Database.Execute("UPDATE cmsContentType SET allowAtRoot = 1 WHERE nodeId = 1032 OR nodeId = 1033"); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexToCmsMemberLoginName.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexToCmsMemberLoginName.cs deleted file mode 100644 index a223d76a07..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexToCmsMemberLoginName.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - public class AddIndexToCmsMemberLoginName : MigrationBase - { - public AddIndexToCmsMemberLoginName(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var database = Database; - - //Now we need to check if we can actually d6 this because we won't be able to if there's data in there that is too long - //http://issues.umbraco.org/issue/U4-9758 - - var colLen = database.ExecuteScalar("select max(datalength(LoginName)) from cmsMember"); - - if (colLen < 900 == false && colLen != null) - { - return; - } - - var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); - - //make sure it doesn't already exist - if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_cmsMember_LoginName")) == false) - { - //we can apply the index - Create.Index("IX_cmsMember_LoginName").OnTable("cmsMember") - .OnColumn("LoginName") - .Ascending() - .WithOptions() - .NonClustered() - .Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexToUmbracoNodePath.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexToUmbracoNodePath.cs deleted file mode 100644 index 191f410b47..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexToUmbracoNodePath.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - public class AddIndexToUmbracoNodePath : MigrationBase - { - public AddIndexToUmbracoNodePath(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); - - //make sure it doesn't already exist - if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoNodePath")) == false) - { - Create.Index("IX_umbracoNodePath").OnTable("umbracoNode") - .OnColumn("path") - .Ascending() - .WithOptions() - .NonClustered() - .Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexToUser2NodePermission.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexToUser2NodePermission.cs deleted file mode 100644 index 87b392b09c..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexToUser2NodePermission.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - public class AddIndexToUser2NodePermission : MigrationBase - { - public AddIndexToUser2NodePermission(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); - - //make sure it doesn't already exist - if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoUser2NodePermission_nodeId")) == false) - { - Create.Index("IX_umbracoUser2NodePermission_nodeId").OnTable("umbracoUser2NodePermission") - .OnColumn("nodeId") - .Ascending() - .WithOptions() - .NonClustered() - .Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexesToUmbracoRelationTables.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexesToUmbracoRelationTables.cs deleted file mode 100644 index 9042ae105e..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddIndexesToUmbracoRelationTables.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Linq; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - public class AddIndexesToUmbracoRelationTables : MigrationBase - { - public AddIndexesToUmbracoRelationTables(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database).ToArray(); - - //make sure it doesn't already exist - if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelation_parentChildType")) == false) - { - //This will remove any corrupt/duplicate data in the relation table before the index is applied - //Ensure this executes in a deferred block which will be done inside of the migration transaction - var database = Database; - - //We need to check if this index has corrupted data and then clear that data - var duplicates = database.Fetch("SELECT parentId,childId,relType FROM umbracoRelation GROUP BY parentId,childId,relType HAVING COUNT(*) > 1"); - if (duplicates.Count > 0) - { - //need to fix this there cannot be duplicates so we'll take the latest entries, it's really not going to matter though - foreach (var duplicate in duplicates) - { - var ids = database.Fetch("SELECT id FROM umbracoRelation WHERE parentId=@parentId AND childId=@childId AND relType=@relType ORDER BY datetime DESC", - new { parentId = duplicate.parentId, childId = duplicate.childId, relType = duplicate.relType }); - - if (ids.Count == 1) - { - //this is just a safety check, this should absolutely never happen - throw new InvalidOperationException("Duplicates were detected but could not be discovered"); - } - - //delete the others - ids = ids.Skip(0).ToList(); - - //iterate in groups of 2000 to avoid the max sql parameter limit - foreach (var idGroup in ids.InGroupsOf(2000)) - { - database.Execute("DELETE FROM umbracoRelation WHERE id IN (@ids)", new { ids = idGroup }); - } - } - } - - //unique index to prevent duplicates - and for better perf - Create.Index("IX_umbracoRelation_parentChildType").OnTable("umbracoRelation") - .OnColumn("parentId").Ascending() - .OnColumn("childId").Ascending() - .OnColumn("relType").Ascending() - .WithOptions() - .Unique() - .Do(); - } - - //need indexes on alias and name for relation type since these are queried against - - //make sure it doesn't already exist - if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelationType_alias")) == false) - { - Create.Index("IX_umbracoRelationType_alias").OnTable("umbracoRelationType") - .OnColumn("alias") - .Ascending() - .WithOptions() - .NonClustered() - .Do(); - } - if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelationType_name")) == false) - { - Create.Index("IX_umbracoRelationType_name").OnTable("umbracoRelationType") - .OnColumn("name") - .Ascending() - .WithOptions() - .NonClustered() - .Do(); - } - - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddLockObjects.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddLockObjects.cs deleted file mode 100644 index 2cc62cd2f9..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddLockObjects.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - public class AddLockObjects : MigrationBase - { - public AddLockObjects(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - EnsureLockObject(Constants.Locks.Servers, "Servers"); - } - - private void EnsureLockObject(int id, string name) - { - var db = Database; - var exists = db.Exists(id); - if (exists) return; - // be safe: delete old umbracoNode lock objects if any - db.Execute("DELETE FROM umbracoNode WHERE id=@id;", new { id }); - // then create umbracoLock object - db.Execute("INSERT umbracoLock (id, name, value) VALUES (@id, @name, 1);", new { id, name }); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddLockTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddLockTable.cs deleted file mode 100644 index 369bda0758..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddLockTable.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Linq; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - public class AddLockTable : MigrationBase - { - public AddLockTable(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); - if (tables.InvariantContains("umbracoLock") == false) - { - Create.Table("umbracoLock") - .WithColumn("id").AsInt32().PrimaryKey("PK_umbracoLock") - .WithColumn("value").AsInt32().NotNullable() - .WithColumn("name").AsString(64).NotNullable() - .Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddMacroUniqueIdColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddMacroUniqueIdColumn.cs deleted file mode 100644 index 82888ab970..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddMacroUniqueIdColumn.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Linq; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - public class AddMacroUniqueIdColumn : MigrationBase - { - public AddMacroUniqueIdColumn(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); - - if (columns.Any(x => x.TableName.InvariantEquals("cmsMacro") && x.ColumnName.InvariantEquals("uniqueId")) == false) - { - Create.Column("uniqueId").OnTable("cmsMacro").AsGuid().Nullable().Do(); - UpdateMacroGuids(); - Alter.Table("cmsMacro").AlterColumn("uniqueId").AsGuid().NotNullable().Do(); - Create.Index("IX_cmsMacro_UniqueId").OnTable("cmsMacro").OnColumn("uniqueId") - .Ascending() - .WithOptions().NonClustered() - .WithOptions().Unique() - .Do(); - - } - - if (columns.Any(x => x.TableName.InvariantEquals("cmsMacroProperty") && x.ColumnName.InvariantEquals("uniquePropertyId")) == false) - { - Create.Column("uniquePropertyId").OnTable("cmsMacroProperty").AsGuid().Nullable().Do(); - UpdateMacroPropertyGuids(); - Alter.Table("cmsMacroProperty").AlterColumn("uniquePropertyId").AsGuid().NotNullable().Do(); - Create.Index("IX_cmsMacroProperty_UniquePropertyId").OnTable("cmsMacroProperty").OnColumn("uniquePropertyId") - .Ascending() - .WithOptions().NonClustered() - .WithOptions().Unique() - .Do(); - } - } - - private void UpdateMacroGuids() - { - var database = Database; - - var updates = database.Query("SELECT id, macroAlias FROM cmsMacro") - .Select(macro => Tuple.Create((int) macro.id, ("macro____" + (string) macro.macroAlias).ToGuid())) - .ToList(); - - foreach (var update in updates) - database.Execute("UPDATE cmsMacro set uniqueId=@guid WHERE id=@id", new { guid = update.Item2, id = update.Item1 }); - } - - private void UpdateMacroPropertyGuids() - { - var database = Database; - - var updates = database.Query(@"SELECT cmsMacroProperty.id id, macroPropertyAlias propertyAlias, cmsMacro.macroAlias macroAlias -FROM cmsMacroProperty -JOIN cmsMacro ON cmsMacroProperty.macro=cmsMacro.id") - .Select(prop => Tuple.Create((int) prop.id, ("macro____" + (string) prop.macroAlias + "____" + (string) prop.propertyAlias).ToGuid())) - .ToList(); - - foreach (var update in updates) - database.Execute("UPDATE cmsMacroProperty set uniquePropertyId=@guid WHERE id=@id", new { guid = update.Item2, id = update.Item1 }); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddRelationTypeUniqueIdColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddRelationTypeUniqueIdColumn.cs deleted file mode 100644 index e166a4582d..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/AddRelationTypeUniqueIdColumn.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Linq; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - public class AddRelationTypeUniqueIdColumn : MigrationBase - { - public AddRelationTypeUniqueIdColumn(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); - - if (columns.Any(x => x.TableName.InvariantEquals("umbracoRelationType") && x.ColumnName.InvariantEquals("typeUniqueId")) == false) - { - Create.Column("typeUniqueId").OnTable("umbracoRelationType").AsGuid().Nullable().Do(); - UpdateRelationTypeGuids(); - Alter.Table("umbracoRelationType").AlterColumn("typeUniqueId").AsGuid().NotNullable().Do(); - Create.Index("IX_umbracoRelationType_UniqueId").OnTable("umbracoRelationType").OnColumn("typeUniqueId") - .Ascending() - .WithOptions().NonClustered() - .WithOptions().Unique() - .Do(); - } - } - - private void UpdateRelationTypeGuids() - { - var database = Database; - var updates = database.Query("SELECT id, alias, name FROM umbracoRelationType") - .Select(relationType => Tuple.Create((int) relationType.id, ("relationType____" + (string) relationType.alias + "____" + (string) relationType.name).ToGuid())) - .ToList(); - - foreach (var update in updates) - database.Execute("UPDATE umbracoRelationType set typeUniqueId=@guid WHERE id=@id", new { guid = update.Item2, id = update.Item1 }); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/NormalizeTemplateGuids.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/NormalizeTemplateGuids.cs deleted file mode 100644 index 9e8c739104..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/NormalizeTemplateGuids.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Linq; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - public class NormalizeTemplateGuids : MigrationBase - { - public NormalizeTemplateGuids(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var database = Database; - - // we need this migration because ppl running pre-7.6 on Cloud and Courier have templates in different - // environments having different GUIDs (Courier does not sync template GUIDs) and we need to normalize - // these GUIDs so templates with the same alias on different environments have the same GUID. - // however, if already running a prerelease version of 7.6, we do NOT want to normalize the GUIDs as quite - // probably, we are already running Deploy and the GUIDs are OK. assuming noone is running a prerelease - // of 7.6 on Courier. - // so... testing if we already have a 7.6.0 version installed. not pretty but...? - // - var version = database.FirstOrDefault("SELECT version FROM umbracoMigration WHERE name=@name ORDER BY version DESC", new { name = Constants.System.UmbracoUpgradePlanName }); - if (version != null && version.StartsWith("7.6.0")) return; - - var updates = database.Query(@"SELECT umbracoNode.id, cmsTemplate.alias FROM umbracoNode -JOIN cmsTemplate ON umbracoNode.id=cmsTemplate.nodeId -WHERE nodeObjectType = @guid", new { guid = Constants.ObjectTypes.TemplateType }) - .Select(template => Tuple.Create((int) template.id, ("template____" + (string) template.alias).ToGuid())) - .ToList(); - - foreach (var update in updates) - database.Execute("UPDATE umbracoNode set uniqueId=@guid WHERE id=@id", new { guid = update.Item2, id = update.Item1 }); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/ReduceLoginNameColumnsSize.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/ReduceLoginNameColumnsSize.cs deleted file mode 100644 index 1af21617f3..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/ReduceLoginNameColumnsSize.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - public class ReduceLoginNameColumnsSize : MigrationBase - { - public ReduceLoginNameColumnsSize(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - //Now we need to check if we can actually d6 this because we won't be able to if there's data in there that is too long - //http://issues.umbraco.org/issue/U4-9758 - - var database = Database; - var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(database); - - var colLen = database.ExecuteScalar("select max(datalength(LoginName)) from cmsMember"); - - if (colLen < 900 == false) return; - - //if an index exists on this table we need to drop it. Normally we'd check via index name but in some odd cases (i.e. Our) - //the index name is something odd (starts with "mi_"). In any case, the index cannot exist if we want to alter the column - //so we'll drop whatever index is there and add one with the correct name after. - var loginNameIndex = dbIndexes.FirstOrDefault(x => x.TableName.InvariantEquals("cmsMember") && x.ColumnName.InvariantEquals("LoginName")); - if (loginNameIndex != null) - { - Delete.Index(loginNameIndex.IndexName).OnTable("cmsMember").Do(); - } - - //we can apply the col length change - Alter.Table("cmsMember") - .AlterColumn("LoginName") - .AsString(225) - .NotNullable() - .Do(); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/RemovePropertyDataIdIndex.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/RemovePropertyDataIdIndex.cs deleted file mode 100644 index 804714d1ff..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/RemovePropertyDataIdIndex.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Linq; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - /// - /// See: http://issues.umbraco.org/issue/U4-9188 - /// - public class UpdateUniqueIndexOnPropertyData : MigrationBase - { - public UpdateUniqueIndexOnPropertyData(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - //tuple = tablename, indexname, columnname, unique - var indexes = SqlSyntax.GetDefinedIndexes(Context.Database).ToArray(); - var found = indexes.FirstOrDefault( - x => x.Item1.InvariantEquals("cmsPropertyData") - && x.Item2.InvariantEquals("IX_cmsPropertyData")); - - if (found != null) - { - //drop the index - Delete.Index("IX_cmsPropertyData").OnTable("cmsPropertyData").Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/RemoveUmbracoDeployTables.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/RemoveUmbracoDeployTables.cs deleted file mode 100644 index 744b441b32..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_6_0/RemoveUmbracoDeployTables.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Linq; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_6_0 -{ - public class RemoveUmbracoDeployTables : MigrationBase - { - public RemoveUmbracoDeployTables(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); - - // there are two versions of umbracoDeployDependency, - // 1. one created by 7.4 and never used, we need to remove it (has a sourceId column) - // 2. one created by Deploy itself, we need to keep it (has a sourceUdi column) - if (tables.InvariantContains("umbracoDeployDependency")) - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); - if (columns.Any(x => x.TableName.InvariantEquals("umbracoDeployDependency") && x.ColumnName.InvariantEquals("sourceId"))) - Delete.Table("umbracoDeployDependency").Do(); - } - - // always remove umbracoDeployChecksum - if (tables.InvariantContains("umbracoDeployChecksum")) - Delete.Table("umbracoDeployChecksum").Do(); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddIndexToDictionaryKeyColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddIndexToDictionaryKeyColumn.cs deleted file mode 100644 index b366c7dab9..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddIndexToDictionaryKeyColumn.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_7_0 -{ - public class AddIndexToDictionaryKeyColumn : MigrationBase - { - public AddIndexToDictionaryKeyColumn(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var database = Database; - //Now we need to check if we can actually do this because we won't be able to if there's data in there that is too long - var colLen = database.ExecuteScalar(string.Format("select max(datalength({0})) from cmsDictionary", SqlSyntax.GetQuotedColumnName("key"))); - - if (colLen < 900 == false && colLen != null) - { - return; - } - - var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); - - //make sure it doesn't already exist - if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_cmsDictionary_key")) == false) - { - //we can apply the index - Create.Index("IX_cmsDictionary_key").OnTable("cmsDictionary") - .OnColumn("key") - .Ascending() - .WithOptions() - .NonClustered() - .Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddUserGroupTables.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddUserGroupTables.cs deleted file mode 100644 index edd78e6c84..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddUserGroupTables.cs +++ /dev/null @@ -1,357 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; -using ColumnInfo = Umbraco.Core.Persistence.SqlSyntax.ColumnInfo; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_7_0 -{ - public class AddUserGroupTables : MigrationBase - { - private readonly string _collateSyntax; - - public AddUserGroupTables(IMigrationContext context) - : base(context) - { - //For some of the migration data inserts we require to use a special MSSQL collate expression since - //some databases may have a custom collation specified and if that is the case, when we compare strings - //in dynamic SQL it will try to compare strings in different collations and this will yield errors. - _collateSyntax = "COLLATE DATABASE_DEFAULT"; - } - - public override void Migrate() - { - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToList(); - var constraints = SqlSyntax.GetConstraintsPerColumn(Context.Database).Distinct().ToArray(); - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); - - //In some very rare cases, there might already be user group tables that we'll need to remove first - //but of course we don't want to remove the tables we will be creating below if they already exist so - //need to do some checks first since these old rare tables have a different schema - RemoveOldTablesIfExist(tables, columns); - - if (AddNewTables(tables)) - { - MigrateUserPermissions(); - MigrateUserTypesToGroups(); - DeleteOldTables(tables, constraints); - SetDefaultIcons(); - } - else - { - //if we aren't adding the tables, make sure that the umbracoUserGroup table has the correct FKs - these - //were added after the beta release so we need to do some cleanup - //if the FK doesn't exist - if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUserGroup") - && x.Item2.InvariantEquals("startContentId") - && x.Item3.InvariantEquals("FK_startContentId_umbracoNode_id")) == false) - { - //before we add any foreign key we need to make sure there's no stale data in there which would have happened in the beta - //release if a start node was assigned and then that start node was deleted. - Database.Execute(@"UPDATE umbracoUserGroup SET startContentId = NULL WHERE startContentId NOT IN (SELECT id FROM umbracoNode)"); - - Create.ForeignKey("FK_startContentId_umbracoNode_id") - .FromTable("umbracoUserGroup") - .ForeignColumn("startContentId") - .ToTable("umbracoNode") - .PrimaryColumn("id") - .OnDelete(Rule.None) - .OnUpdate(Rule.None) - .Do(); - } - - if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUserGroup") - && x.Item2.InvariantEquals("startMediaId") - && x.Item3.InvariantEquals("FK_startMediaId_umbracoNode_id")) == false) - { - //before we add any foreign key we need to make sure there's no stale data in there which would have happened in the beta - //release if a start node was assigned and then that start node was deleted. - Database.Execute(@"UPDATE umbracoUserGroup SET startMediaId = NULL WHERE startMediaId NOT IN (SELECT id FROM umbracoNode)"); - - Create.ForeignKey("FK_startMediaId_umbracoNode_id") - .FromTable("umbracoUserGroup") - .ForeignColumn("startMediaId") - .ToTable("umbracoNode") - .PrimaryColumn("id") - .OnDelete(Rule.None) - .OnUpdate(Rule.None) - .Do(); - } - } - } - - /// - /// In some very rare cases, there might already be user group tables that we'll need to remove first - /// but of course we don't want to remove the tables we will be creating below if they already exist so - /// need to do some checks first since these old rare tables have a different schema - /// - /// - /// - private void RemoveOldTablesIfExist(List tables, ColumnInfo[] columns) - { - if (tables.Contains("umbracoUser2userGroup", StringComparer.InvariantCultureIgnoreCase)) - { - //this column doesn't exist in the 7.7 schema, so if it's there, then this is a super old table - var foundOldColumn = columns - .FirstOrDefault(x => - x.ColumnName.Equals("user", StringComparison.InvariantCultureIgnoreCase) - && x.TableName.Equals("umbracoUser2userGroup", StringComparison.InvariantCultureIgnoreCase)); - if (foundOldColumn != null) - { - Delete.Table("umbracoUser2userGroup").Do(); - //remove from the tables list since this will be re-checked in further logic - tables.Remove("umbracoUser2userGroup"); - } - } - - if (tables.Contains("umbracoUserGroup", StringComparer.InvariantCultureIgnoreCase)) - { - //The new schema has several columns, the super old one for this table only had 2 so if it's 2 get rid of it - var countOfCols = columns - .Count(x => x.TableName.Equals("umbracoUserGroup", StringComparison.InvariantCultureIgnoreCase)); - if (countOfCols == 2) - { - Delete.Table("umbracoUserGroup").Do(); - //remove from the tables list since this will be re-checked in further logic - tables.Remove("umbracoUserGroup"); - } - } - } - - private void SetDefaultIcons() - { - Database.Execute($"UPDATE umbracoUserGroup SET icon = \'\' WHERE userGroupAlias = \'{Constants.Security.AdminGroupAlias}\'"); - Database.Execute("UPDATE umbracoUserGroup SET icon = \'icon-edit\' WHERE userGroupAlias = \'writer\'"); - Database.Execute("UPDATE umbracoUserGroup SET icon = \'icon-tools\' WHERE userGroupAlias = \'editor\'"); - Database.Execute("UPDATE umbracoUserGroup SET icon = \'icon-globe\' WHERE userGroupAlias = \'translator\'"); - } - - private bool AddNewTables(List tables) - { - var updated = false; - if (tables.InvariantContains("umbracoUserGroup") == false) - { - Create.Table().Do(); - updated = true; - } - - if (tables.InvariantContains("umbracoUser2UserGroup") == false) - { - Create.Table().Do(); - updated = true; - } - - if (tables.InvariantContains("umbracoUserGroup2App") == false) - { - Create.Table().Do(); - updated = true; - } - - if (tables.InvariantContains("umbracoUserGroup2NodePermission") == false) - { - Create.Table().Do(); - updated = true; - } - - return updated; - } - - private void MigrateUserTypesToGroups() - { - // Create a user group for each user type - Database.Execute(@"INSERT INTO umbracoUserGroup (userGroupAlias, userGroupName, userGroupDefaultPermissions) - SELECT userTypeAlias, userTypeName, userTypeDefaultPermissions - FROM umbracoUserType"); - - // Add each user to the group created from their type - Database.Execute(string.Format(@"INSERT INTO umbracoUser2UserGroup (userId, userGroupId) - SELECT u.id, ug.id - FROM umbracoUser u - INNER JOIN umbracoUserType ut ON ut.id = u.userType - INNER JOIN umbracoUserGroup ug ON ug.userGroupAlias {0} = ut.userTypeAlias {0}", _collateSyntax)); - - // Add the built-in administrator account to all apps - // this will lookup all of the apps that the admin currently has access to in order to assign the sections - // instead of use statically assigning since there could be extra sections we don't know about. - Database.Execute(@"INSERT INTO umbracoUserGroup2app (userGroupId,app) - SELECT ug.id, app - FROM umbracoUserGroup ug - INNER JOIN umbracoUser2UserGroup u2ug ON u2ug.userGroupId = ug.id - INNER JOIN umbracoUser u ON u.id = u2ug.userId - INNER JOIN umbracoUser2app u2a ON u2a." + SqlSyntax.GetQuotedColumnName("user") + @" = u.id - WHERE u.id = 0"); - - // Add the default section access to the other built-in accounts - // writer: - Database.Execute(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId, app) - SELECT ug.id, 'content' as app - FROM umbracoUserGroup ug - WHERE ug.userGroupAlias {0} = 'writer' {0}", _collateSyntax)); - // editor - Database.Execute(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId, app) - SELECT ug.id, 'content' as app - FROM umbracoUserGroup ug - WHERE ug.userGroupAlias {0} = 'editor' {0}", _collateSyntax)); - Database.Execute(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId, app) - SELECT ug.id, 'media' as app - FROM umbracoUserGroup ug - WHERE ug.userGroupAlias {0} = 'editor' {0}", _collateSyntax)); - // translator - Database.Execute(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId, app) - SELECT ug.id, 'translation' as app - FROM umbracoUserGroup ug - WHERE ug.userGroupAlias {0} = 'translator' {0}", _collateSyntax)); - - //We need to lookup all distinct combinations of section access and create a group for each distinct collection - //and assign groups accordingly. We'll perform the lookup 'now' to then create the queued SQL migrations. - var userAppsData = Context.Database.Query(@"SELECT u.id, u2a.app FROM umbracoUser u - INNER JOIN umbracoUser2app u2a ON u2a." + SqlSyntax.GetQuotedColumnName("user") + @" = u.id - ORDER BY u.id, u2a.app"); - var usersWithApps = new Dictionary>(); - foreach (var userApps in userAppsData) - { - if (usersWithApps.TryGetValue(userApps.id, out List apps) == false) - { - apps = new List {userApps.app}; - usersWithApps.Add(userApps.id, apps); - } - else - { - apps.Add(userApps.app); - } - } - //At this stage we have a dictionary of users with a collection of their apps which are sorted - //and we need to determine the unique/distinct app collections for each user to create groups with. - //We can do this by creating a hash value of all of the app values and since they are already sorted we can get a distinct - //collection by this hash. - var distinctApps = usersWithApps - .Select(x => new {appCollection = x.Value, appsHash = string.Join("", x.Value).GenerateHash()}) - .DistinctBy(x => x.appsHash) - .ToArray(); - //Now we need to create user groups for each of these distinct app collections, and then assign the corresponding users to those groups - for (var i = 0; i < distinctApps.Length; i++) - { - //create the group - var alias = "MigratedSectionAccessGroup_" + (i + 1); - Insert.IntoTable("umbracoUserGroup").Row(new - { - userGroupAlias = "MigratedSectionAccessGroup_" + (i + 1), - userGroupName = "Migrated Section Access Group " + (i + 1) - }).Do(); - //now assign the apps - var distinctApp = distinctApps[i]; - foreach (var app in distinctApp.appCollection) - { - Database.Execute(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId, app) - SELECT ug.id, '" + app + @"' as app - FROM umbracoUserGroup ug - WHERE ug.userGroupAlias {0} = '" + alias + "' {0}", _collateSyntax)); - } - //now assign the corresponding users to this group - foreach (var userWithApps in usersWithApps) - { - //check if this user's groups hash matches the current groups hash - var hash = string.Join("", userWithApps.Value).GenerateHash(); - if (hash == distinctApp.appsHash) - { - //it matches so assign the user to this group - Database.Execute(string.Format(@"INSERT INTO umbracoUser2UserGroup (userId, userGroupId) - SELECT " + userWithApps.Key + @", ug.id - FROM umbracoUserGroup ug - WHERE ug.userGroupAlias {0} = '" + alias + "' {0}", _collateSyntax)); - } - } - } - - // Rename some groups for consistency (plural form) - Database.Execute("UPDATE umbracoUserGroup SET userGroupName = 'Writers' WHERE userGroupAlias = 'writer'"); - Database.Execute("UPDATE umbracoUserGroup SET userGroupName = 'Translators' WHERE userGroupAlias = 'translator'"); - - //Ensure all built in groups have a start node of -1 - Database.Execute("UPDATE umbracoUserGroup SET startContentId = -1 WHERE userGroupAlias = 'editor'"); - Database.Execute("UPDATE umbracoUserGroup SET startMediaId = -1 WHERE userGroupAlias = 'editor'"); - Database.Execute("UPDATE umbracoUserGroup SET startContentId = -1 WHERE userGroupAlias = 'writer'"); - Database.Execute("UPDATE umbracoUserGroup SET startMediaId = -1 WHERE userGroupAlias = 'writer'"); - Database.Execute("UPDATE umbracoUserGroup SET startContentId = -1 WHERE userGroupAlias = 'translator'"); - Database.Execute("UPDATE umbracoUserGroup SET startMediaId = -1 WHERE userGroupAlias = 'translator'"); - Database.Execute("UPDATE umbracoUserGroup SET startContentId = -1 WHERE userGroupAlias = 'admin'"); - Database.Execute("UPDATE umbracoUserGroup SET startMediaId = -1 WHERE userGroupAlias = 'admin'"); - } - - private void MigrateUserPermissions() - { - // Create user group records for all non-admin users that have specific permissions set - Database.Execute(@"INSERT INTO umbracoUserGroup(userGroupAlias, userGroupName) - SELECT 'permissionGroupFor' + userLogin, 'Migrated Permission Group for ' + userLogin - FROM umbracoUser - WHERE (id IN ( - SELECT userid - FROM umbracoUser2NodePermission - )) - AND id > 0"); - - // Associate those groups with the users - Database.Execute(string.Format(@"INSERT INTO umbracoUser2UserGroup (userId, userGroupId) - SELECT u.id, ug.id - FROM umbracoUser u - INNER JOIN umbracoUserGroup ug ON ug.userGroupAlias {0} = 'permissionGroupFor' + userLogin {0}", _collateSyntax)); - - // Create node permissions on the groups - Database.Execute(string.Format(@"INSERT INTO umbracoUserGroup2NodePermission (userGroupId,nodeId,permission) - SELECT ug.id, nodeId, permission - FROM umbracoUserGroup ug - INNER JOIN umbracoUser2UserGroup u2ug ON u2ug.userGroupId = ug.id - INNER JOIN umbracoUser u ON u.id = u2ug.userId - INNER JOIN umbracoUser2NodePermission u2np ON u2np.userId = u.id - WHERE ug.userGroupAlias {0} NOT IN ( - SELECT userTypeAlias {0} - FROM umbracoUserType - )", _collateSyntax)); - - // Create app permissions on the groups - Database.Execute(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId,app) - SELECT ug.id, app - FROM umbracoUserGroup ug - INNER JOIN umbracoUser2UserGroup u2ug ON u2ug.userGroupId = ug.id - INNER JOIN umbracoUser u ON u.id = u2ug.userId - INNER JOIN umbracoUser2app u2a ON u2a." + SqlSyntax.GetQuotedColumnName("user") + @" = u.id - WHERE ug.userGroupAlias {0} NOT IN ( - SELECT userTypeAlias {0} - FROM umbracoUserType - )", _collateSyntax)); - } - - private void DeleteOldTables(List tables, Tuple[] constraints) - { - if (tables.InvariantContains("umbracoUser2App")) - { - Delete.Table("umbracoUser2App").Do(); - } - - if (tables.InvariantContains("umbracoUser2NodePermission")) - { - Delete.Table("umbracoUser2NodePermission").Do(); - } - - if (tables.InvariantContains("umbracoUserType") && tables.InvariantContains("umbracoUser")) - { - //Delete the FK if it exists before dropping the column - if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUser") && x.Item3.InvariantEquals("FK_umbracoUser_umbracoUserType_id"))) - { - Delete.ForeignKey("FK_umbracoUser_umbracoUserType_id").OnTable("umbracoUser").Do(); - } - - //This is the super old constraint name of the FK for user type so check this one too - if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUser") && x.Item3.InvariantEquals("FK_user_userType"))) - { - Delete.ForeignKey("FK_user_userType").OnTable("umbracoUser").Do(); - } - - Delete.Column("userType").FromTable("umbracoUser").Do(); - Delete.Table("umbracoUserType").Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddUserStartNodeTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddUserStartNodeTable.cs deleted file mode 100644 index 54545e06d3..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddUserStartNodeTable.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_7_0 -{ - public class AddUserStartNodeTable : MigrationBase - { - public AddUserStartNodeTable(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); - - if (tables.InvariantContains("umbracoUserStartNode")) return; - - Create.Table().Do(); - - MigrateUserStartNodes(); - - //now remove the old columns - - Delete.Column("startStructureID").FromTable("umbracoUser").Do(); - Delete.Column("startMediaID").FromTable("umbracoUser").Do(); - } - - private void MigrateUserStartNodes() - { - Database.Execute(@"INSERT INTO umbracoUserStartNode (userId, startNode, startNodeType) - SELECT id, startStructureID, 1 - FROM umbracoUser - WHERE startStructureID IS NOT NULL AND startStructureID > 0 AND startStructureID IN (SELECT id FROM umbracoNode WHERE nodeObjectType='" + Constants.ObjectTypes.Document + "')"); - - Database.Execute(@"INSERT INTO umbracoUserStartNode (userId, startNode, startNodeType) - SELECT id, startMediaID, 2 - FROM umbracoUser - WHERE startMediaID IS NOT NULL AND startMediaID > 0 AND startMediaID IN (SELECT id FROM umbracoNode WHERE nodeObjectType='" + Constants.ObjectTypes.Media + "')"); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/EnsureContentTemplatePermissions.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/EnsureContentTemplatePermissions.cs deleted file mode 100644 index 1f7e2faf33..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/EnsureContentTemplatePermissions.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_7_0 -{ - /// - /// Ensures the built-in user groups have the blueprint permission by default on upgrade - /// - public class EnsureContentTemplatePermissions : MigrationBase - { - public EnsureContentTemplatePermissions(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var database = Database; - var userGroups = database.Fetch( - Context.SqlContext.Sql().Select("*") - .From() - .Where(x => x.Alias == "admin" || x.Alias == "editor")); - - foreach (var userGroup in userGroups) - { - if (userGroup.DefaultPermissions.Contains('�') == false) - { - userGroup.DefaultPermissions += "�"; - Update.Table("umbracoUserGroup") - .Set(new { userGroupDefaultPermissions = userGroup.DefaultPermissions }) - .Where(new { id = userGroup.Id }) - .Do(); - } - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/ReduceDictionaryKeyColumnsSize.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/ReduceDictionaryKeyColumnsSize.cs deleted file mode 100644 index 17dd3064eb..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/ReduceDictionaryKeyColumnsSize.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_7_0 -{ - public class ReduceDictionaryKeyColumnsSize : MigrationBase - { - public ReduceDictionaryKeyColumnsSize(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - //Now we need to check if we can actually do this because we won't be able to if there's data in there that is too long - - var database = Database; - var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(database); - - var colLen = database.ExecuteScalar(string.Format("select max(datalength({0})) from cmsDictionary", SqlSyntax.GetQuotedColumnName("key"))); - - if (colLen < 900 == false) return; - - //if it exists we need to drop it first - if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_cmsDictionary_key"))) - { - Delete.Index("IX_cmsDictionary_key").OnTable("cmsDictionary").Do(); - } - - //we can apply the col length change - Alter.Table("cmsDictionary") - .AlterColumn("key") - .AsString(450) - .NotNullable() - .Do(); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/UpdateUserTables.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/UpdateUserTables.cs deleted file mode 100644 index 509b3f91dd..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/UpdateUserTables.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Linq; -using System.Web.Security; -using Newtonsoft.Json; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Security; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_7_0 -{ - public class UpdateUserTables : MigrationBase - { - public UpdateUserTables(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - //Don't execute if the column is already there - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); - - if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("createDate")) == false) - Create.Column("createDate").OnTable("umbracoUser").AsDateTime().NotNullable().WithDefault(SystemMethods.CurrentDateTime).Do(); - - if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("updateDate")) == false) - Create.Column("updateDate").OnTable("umbracoUser").AsDateTime().NotNullable().WithDefault(SystemMethods.CurrentDateTime).Do(); - - if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("emailConfirmedDate")) == false) - Create.Column("emailConfirmedDate").OnTable("umbracoUser").AsDateTime().Nullable().Do(); - - if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("invitedDate")) == false) - Create.Column("invitedDate").OnTable("umbracoUser").AsDateTime().Nullable().Do(); - - if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("avatar")) == false) - Create.Column("avatar").OnTable("umbracoUser").AsString(500).Nullable().Do(); - - if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("passwordConfig")) == false) - { - Create.Column("passwordConfig").OnTable("umbracoUser").AsString(500).Nullable().Do(); - //Check if we have a known config, we only want to store config for hashing - var membershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider(); - if (membershipProvider.PasswordFormat == MembershipPasswordFormat.Hashed) - { - var json = JsonConvert.SerializeObject(new { hashAlgorithm = Membership.HashAlgorithmType }); - Database.Execute("UPDATE umbracoUser SET passwordConfig = '" + json + "'"); - } - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddIndexToPropertyTypeAliasColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddIndexToPropertyTypeAliasColumn.cs deleted file mode 100644 index ddb084a609..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddIndexToPropertyTypeAliasColumn.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_8_0 -{ - internal class AddIndexToPropertyTypeAliasColumn : MigrationBase - { - public AddIndexToPropertyTypeAliasColumn(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); - - //make sure it doesn't already exist - if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_cmsPropertyTypeAlias")) == false) - { - //we can apply the index - Create.Index("IX_cmsPropertyTypeAlias").OnTable(Constants.DatabaseSchema.Tables.PropertyType) - .OnColumn("Alias") - .Ascending().WithOptions().NonClustered() - .Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddInstructionCountColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddInstructionCountColumn.cs deleted file mode 100644 index 0ce2c91f2e..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddInstructionCountColumn.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_8_0 -{ - internal class AddInstructionCountColumn : MigrationBase - { - public AddInstructionCountColumn(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); - - if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.CacheInstruction) && x.ColumnName.InvariantEquals("instructionCount")) == false) - AddColumn("instructionCount"); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddMediaVersionTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddMediaVersionTable.cs deleted file mode 100644 index b4c0062770..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddMediaVersionTable.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Persistence.Factories; -using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_8_0 -{ - internal class AddMediaVersionTable : MigrationBase - { - public AddMediaVersionTable(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); - - if (tables.InvariantContains(Constants.DatabaseSchema.Tables.MediaVersion)) return; - - Create.Table().Do(); - MigrateMediaPaths(); - } - - private void MigrateMediaPaths() - { - // this may not be the most efficient way to do it, compared to how it's done in v7, but this - // migration should only run for v8 sites that are being developed, before v8 is released, so - // no big sites and performances don't matter here - keep it simple - - var sql = Sql() - .Select(x => x.VarcharValue, x => x.TextValue) - .AndSelect(x => Alias(x.Id, "versionId")) - .From() - .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin().On((left, right) => left.VersionId == right.Id) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .Where(x => x.Alias == "umbracoFile") - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media); - - var paths = new List(); - - //using QUERY = a db cursor, we won't load this all into memory first, just row by row - foreach (var row in Database.Query(sql)) - { - // if there's values then ensure there's a media path match and extract it - string mediaPath = null; - if ( - (row.varcharValue != null && ContentBaseFactory.TryMatch((string) row.varcharValue, out mediaPath)) - || (row.textValue != null && ContentBaseFactory.TryMatch((string) row.textValue, out mediaPath))) - { - paths.Add(new MediaVersionDto - { - Id = (int) row.versionId, - Path = mediaPath - }); - } - } - - // bulk insert - Database.BulkInsertRecords(paths); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddTourDataUserColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddTourDataUserColumn.cs deleted file mode 100644 index cd2678205f..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddTourDataUserColumn.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.DatabaseAnnotations; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_8_0 -{ - internal class AddTourDataUserColumn : MigrationBase - { - public AddTourDataUserColumn(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); - - if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.User) && x.ColumnName.InvariantEquals("tourData")) == false) - AddColumn("tourData"); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddUserLoginTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddUserLoginTable.cs deleted file mode 100644 index 7a55362072..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddUserLoginTable.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_8_0 -{ - internal class AddUserLoginTable : MigrationBase - { - public AddUserLoginTable(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); - - if (tables.InvariantContains(Constants.DatabaseSchema.Tables.UserLogin) == false) - { - Create.Table().Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddIsSensitiveMemberTypeColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddIsSensitiveMemberTypeColumn.cs deleted file mode 100644 index 2b5f481769..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddIsSensitiveMemberTypeColumn.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_9_0 -{ - internal class AddIsSensitiveMemberTypeColumn : MigrationBase - { - public AddIsSensitiveMemberTypeColumn(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); - - if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.MemberPropertyType) && x.ColumnName.InvariantEquals("isSensitive")) == false) - AddColumn("isSensitive"); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoAuditTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoAuditTable.cs deleted file mode 100644 index e7880dfc73..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoAuditTable.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_9_0 -{ - internal class AddUmbracoAuditTable : MigrationBase - { - public AddUmbracoAuditTable(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); - - if (tables.InvariantContains(Constants.DatabaseSchema.Tables.AuditEntry)) - return; - - Create.Table().Do(); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoConsentTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoConsentTable.cs deleted file mode 100644 index e3656f69ac..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoConsentTable.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Linq; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_9_0 -{ - internal class AddUmbracoConsentTable : MigrationBase - { - public AddUmbracoConsentTable(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); - - if (tables.InvariantContains(Constants.DatabaseSchema.Tables.Consent)) - return; - - Create.Table().Do(); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/CreateSensitiveDataUserGroup.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/CreateSensitiveDataUserGroup.cs deleted file mode 100644 index ce656aa0c1..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/CreateSensitiveDataUserGroup.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_7_9_0 -{ - internal class CreateSensitiveDataUserGroup : MigrationBase - { - public CreateSensitiveDataUserGroup(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var sql = Sql() - .SelectCount() - .From() - .Where(x => x.Alias == Constants.Security.SensitiveDataGroupAlias); - - var exists = Database.ExecuteScalar(sql) > 0; - if (exists) return; - - var groupId = Database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", new UserGroupDto { StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.SensitiveDataGroupAlias, Name = "Sensitive data", DefaultPermissions = "", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-lock" }); - Database.Insert(new User2UserGroupDto { UserGroupId = Convert.ToInt32(groupId), UserId = Constants.Security.SuperUserId }); // add super to sensitive data - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs index efa54a0f59..5342755ebf 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs @@ -1,6 +1,7 @@ using System.Data; using System.Linq; using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { @@ -12,31 +13,10 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + var tables = SqlSyntax.GetTablesInSchema(Context.Database); if (tables.InvariantContains("cmsContentNu")) return; - var textType = SqlSyntax.GetSpecialDbType(SpecialDbTypes.NTEXT); - - Create.Table("cmsContentNu") - .WithColumn("nodeId").AsInt32().NotNullable() - .WithColumn("published").AsBoolean().NotNullable() - .WithColumn("data").AsCustom(textType).NotNullable() - .WithColumn("rv").AsInt64().NotNullable().WithDefaultValue(0) - .Do(); - - Create.PrimaryKey("PK_cmsContentNu") - .OnTable("cmsContentNu") - .Columns(new[] { "nodeId", "published" }) - .Do(); - - Create.ForeignKey("FK_cmsContentNu_umbracoNode_id") - .FromTable("cmsContentNu") - .ForeignColumn("nodeId") - .ToTable("umbracoNode") - .PrimaryColumn("id") - .OnDelete(Rule.Cascade) - .OnUpdate(Rule.None) - .Do(); + Create.Table(true).Do(); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddLockTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddLockTable.cs deleted file mode 100644 index aa14bc81c8..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddLockTable.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Linq; - -namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 -{ - public class AddLockTable : MigrationBase - { - public AddLockTable(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); - if (tables.InvariantContains("umbracoLock") == false) - { - Create.Table("umbracoLock") - .WithColumn("id").AsInt32().PrimaryKey("PK_umbracoLock") - .WithColumn("value").AsInt32().NotNullable() - .WithColumn("name").AsString(64).NotNullable() - .Do(); - } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs new file mode 100644 index 0000000000..d44e637a2c --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class AddPackagesSectionAccess : MigrationBase + { + public AddPackagesSectionAccess(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + // Any user group which had access to the Developer section should have access to Packages + Database.Execute($@" + insert into {Constants.DatabaseSchema.Tables.UserGroup2App} + select userGroupId, '{Constants.Applications.Packages}' + from {Constants.DatabaseSchema.Tables.UserGroup2App} + where app='developer'"); + } + } +} 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..66f9114370 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,20 @@ 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)); @@ -89,16 +100,18 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 foreach (var value in values) Database.Execute(Sql() .Update(u => u - .Set(x => x.IntegerValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (int?) null : int.Parse(value.VarcharValue, NumberStyles.Any, CultureInfo.InvariantCulture)) - .Set(x => x.TextValue, null)) + .Set(x => x.IntegerValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (int?)null : int.Parse(value.VarcharValue, NumberStyles.Any, CultureInfo.InvariantCulture)) + .Set(x => x.TextValue, null) + .Set(x => x.VarcharValue, null)) .Where(x => x.Id == value.Id)); values = Database.Fetch(Sql().Select(x => x.Id, x => x.VarcharValue).From().WhereIn(x => x.PropertyTypeId, dtPropertyTypes)); foreach (var value in values) Database.Execute(Sql() .Update(u => u - .Set(x => x.DateValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (DateTime?) null : DateTime.Parse(value.VarcharValue, CultureInfo.InvariantCulture, DateTimeStyles.None)) - .Set(x => x.TextValue, null)) + .Set(x => x.DateValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (DateTime?)null : DateTime.Parse(value.VarcharValue, CultureInfo.InvariantCulture, DateTimeStyles.None)) + .Set(x => x.TextValue, null) + .Set(x => x.VarcharValue, null)) .Where(x => x.Id == value.Id)); // anything that's custom... ppl will have to figure it out manually, there isn't much we can do about it diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddUserLoginDtoDateIndex.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddUserLoginDtoDateIndex.cs deleted file mode 100644 index 0ccc2d93ff..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddUserLoginDtoDateIndex.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 -{ - public class AddUserLoginDtoDateIndex : MigrationBase - { - public AddUserLoginDtoDateIndex(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - if (!IndexExists("IX_umbracoUserLogin_lastValidatedUtc")) - Create.Index("IX_umbracoUserLogin_lastValidatedUtc") - .OnTable(UserLoginDto.TableName) - .OnColumn("lastValidatedUtc") - .Ascending() - .WithOptions().NonClustered() - .Do(); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs index c6cad2eac1..4044b5a173 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs @@ -11,8 +11,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - Create.Table().Do(); - Create.Table().Do(); + Create.Table(true).Do(); + Create.Table(true).Do(); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs index 84dd393b0d..eabbd34b08 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using NPoco; +using Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 @@ -47,14 +49,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } } - var propertyTypes = Database.Fetch(Sql().Select().From()); + var propertyTypes = Database.Fetch(Sql().Select().From()); foreach (var dto in propertyTypes) { dto.Variations = GetNewValue(dto.Variations); Database.Update(dto); } - var contentTypes = Database.Fetch(Sql().Select().From()); + var contentTypes = Database.Fetch(Sql().Select().From()); foreach (var dto in contentTypes) { dto.Variations = GetNewValue(dto.Variations); @@ -62,57 +64,11 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } } - // we *need* to use this private DTO here, which does *not* have extra properties, which would kill the migration + // we *need* to use these private DTOs here, which does *not* have extra properties, which would kill the migration - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - private class ContentTypeDto - { - public const string TableName = Constants.DatabaseSchema.Tables.ContentType; + - [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 535)] - public int PrimaryKey { get; set; } - - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] - public int NodeId { get; set; } - - [Column("alias")] - [NullSetting(NullSetting = NullSettings.Null)] - public string Alias { get; set; } - - [Column("icon")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public string Icon { get; set; } - - [Column("thumbnail")] - [Constraint(Default = "folder.png")] - public string Thumbnail { get; set; } - - [Column("description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(1500)] - public string Description { get; set; } - - [Column("isContainer")] - [Constraint(Default = "0")] - public bool IsContainer { get; set; } - - [Column("allowAtRoot")] - [Constraint(Default = "0")] - public bool AllowAtRoot { get; set; } - - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } - - [ResultColumn] - public NodeDto NodeDto { get; set; } - } + } } 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 ed1c08f0f8..1e327346cf 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 @@ -2,8 +2,7 @@ using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; -using Umbraco.Core.Logging; -using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -19,8 +18,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var sqlDataTypes = Sql() .Select() .From() - .Where(x => x.EditorAlias == "Umbraco.RelatedLinks" - || x.EditorAlias == "Umbraco.RelatedLinks2"); + .Where(x => x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks + || x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2); var dataTypes = Database.Fetch(sqlDataTypes); var dataTypeIds = dataTypes.Select(x => x.NodeId).ToList(); @@ -34,11 +33,11 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } var sqlPropertyTpes = Sql() - .Select() - .From() - .Where(x => dataTypeIds.Contains(x.DataTypeId)); + .Select() + .From() + .Where(x => dataTypeIds.Contains(x.DataTypeId)); - var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); + var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); if (propertyTypeIds.Count == 0) return; @@ -50,10 +49,10 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var properties = Database.Fetch(sqlPropertyData); // Create a Multi URL Picker datatype for the converted RelatedLinks data - + foreach (var property in properties) { - var value = property.Value.ToString(); + var value = property.Value?.ToString(); if (string.IsNullOrWhiteSpace(value)) continue; @@ -72,6 +71,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(); @@ -89,7 +89,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 Name = relatedLink.Caption, Target = relatedLink.NewWindow ? "_blank" : null, Udi = udi, - // Should only have a URL if it's an external link otherwise it wil be a UDI + // Should only have a URL if it's an external link otherwise it wil be a UDI Url = relatedLink.IsInternal == false ? relatedLink.Link : null }; @@ -103,7 +103,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 Database.Update(property); } - + } } 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 d7180385f0..95b272dcb4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs @@ -1,28 +1,46 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using Newtonsoft.Json; -using NPoco; -using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { public class DataTypeMigration : MigrationBase { - public DataTypeMigration(IMigrationContext context) + private readonly PreValueMigratorCollection _preValueMigrators; + private readonly PropertyEditorCollection _propertyEditors; + private readonly ILogger _logger; + + private static readonly ISet LegacyAliases = new HashSet() + { + Constants.PropertyEditors.Legacy.Aliases.Date, + Constants.PropertyEditors.Legacy.Aliases.Textbox, + Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, + Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2, + Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, + Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, + }; + + public DataTypeMigration(IMigrationContext context, PreValueMigratorCollection preValueMigrators, PropertyEditorCollection propertyEditors, ILogger logger) : base(context) - { } + { + _preValueMigrators = preValueMigrators; + _propertyEditors = propertyEditors; + _logger = logger; + } public override void Migrate() { - // delete *all* keys and indexes - because of FKs - Delete.KeysAndIndexes().Do(); - // drop and create columns Delete.Column("pk").FromTable("cmsDataType").Do(); @@ -31,21 +49,17 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 // create column AddColumn(Constants.DatabaseSchema.Tables.DataType, "config"); - Execute.Sql(Sql().Update(u => u.Set(x => x.Configuration, string.Empty)).SQL).Do(); - - // re-create *all* keys and indexes - foreach (var x in DatabaseSchemaCreator.OrderedTables) - Create.KeysAndIndexes(x).Do(); + Execute.Sql(Sql().Update(u => u.Set(x => x.Configuration, string.Empty))).Do(); // renames Execute.Sql(Sql() .Update(u => u.Set(x => x.EditorAlias, "Umbraco.ColorPicker")) - .Where(x => x.EditorAlias == "Umbraco.ColorPickerAlias").SQL).Do(); + .Where(x => x.EditorAlias == "Umbraco.ColorPickerAlias")).Do(); // from preValues to configuration... var sql = Sql() .Select() - .AndSelect(x => x.Alias, x => x.SortOrder, x => x.Value) + .AndSelect(x => x.Id, x => x.Alias, x => x.SortOrder, x => x.Value) .From() .InnerJoin().On((left, right) => left.NodeId == right.NodeId) .OrderBy(x => x.NodeId) @@ -60,43 +74,61 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 .From() .Where(x => x.NodeId == group.Key)).First(); - var aliases = group.Select(x => x.Alias).Distinct().ToArray(); - if (aliases.Length == 1 && string.IsNullOrWhiteSpace(aliases[0])) + // 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, dictionary); + var json = JsonConvert.SerializeObject(config); + + // validate - and kill the migration if it fails + var newAlias = migrator.GetNewAlias(dataType.EditorAlias); + if (newAlias == null) { - // array-based prevalues - var values = new Dictionary { ["values"] = group.OrderBy(x => x.SortOrder).Select(x => x.Value).ToArray() }; - dataType.Configuration = JsonConvert.SerializeObject(values); + if (!LegacyAliases.Contains(dataType.EditorAlias)) + { + _logger.Warn( + "Skipping validation of configuration for data type {NodeId} : {EditorAlias}." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, dataType.EditorAlias); + } + } + else if (!_propertyEditors.TryGet(newAlias, out var propertyEditor)) + { + if (!LegacyAliases.Contains(newAlias)) + { + _logger.Warn("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" + + " because no property editor with that alias was found." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, newAlias, dataType.EditorAlias); + } } else { - // assuming we don't want to fall back to array - if (aliases.Length != group.Count() || aliases.Any(string.IsNullOrWhiteSpace)) - throw new InvalidOperationException($"Cannot migrate datatype w/ id={dataType.NodeId} preValues: duplicate or null/empty alias."); - - // dictionary-base prevalues - var values = group.ToDictionary(x => x.Alias, x => x.Value); - dataType.Configuration = JsonConvert.SerializeObject(values); + var configEditor = propertyEditor.GetConfigurationEditor(); + try + { + var _ = configEditor.FromDatabase(json); + } + catch (Exception e) + { + _logger.Warn(e, "Failed to validate configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, newAlias, dataType.EditorAlias); + } } + // update + dataType.Configuration = JsonConvert.SerializeObject(config); Database.Update(dataType); } } - - [TableName("cmsDataTypePreValues")] - [ExplicitColumns] - public class PreValueDto - { - [Column("datatypeNodeId")] - public int NodeId { get; set; } - - [Column("alias")] - public string Alias { get; set; } - - [Column("sortorder")] - public int SortOrder { get; set; } - - [Column("value")] - public string Value { get; set; } - } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs new file mode 100644 index 0000000000..f445742aa9 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs @@ -0,0 +1,20 @@ +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class ContentPickerPreValueMigrator : DefaultPreValueMigrator + { + public override bool CanMigrate(string editorAlias) + => editorAlias == Constants.PropertyEditors.Legacy.Aliases.ContentPicker2; + + public override string GetNewAlias(string editorAlias) + => null; + + protected override object GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "showOpenButton" || + preValue.Alias == "ignoreUserStartNodes") + return preValue.Value == "1"; + + return base.GetPreValueValue(preValue); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs new file mode 100644 index 0000000000..8dc0f1dcd5 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class DecimalPreValueMigrator : DefaultPreValueMigrator + { + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.Decimal"; + + protected override object GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "min" || + preValue.Alias == "step" || + preValue.Alias == "max") + return decimal.TryParse(preValue.Value, out var d) ? (decimal?) d : null; + + return preValue.Value.DetectIsJson() ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + } + } +} 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 new file mode 100644 index 0000000000..0c8161c9ef --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class DefaultPreValueMigrator : IPreValueMigrator + { + public virtual bool CanMigrate(string editorAlias) + => true; + + public virtual string GetNewAlias(string editorAlias) + => editorAlias; + + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + var preValuesA = preValues.Values.ToList(); + var aliases = preValuesA.Select(x => x.Alias).Distinct().ToArray(); + if (aliases.Length == 1 && string.IsNullOrWhiteSpace(aliases[0])) + { + // array-based prevalues + return new Dictionary { ["values"] = preValuesA.OrderBy(x => x.SortOrder).Select(x => x.Value).ToArray() }; + } + + // assuming we don't want to fall back to array + 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); + } + + protected virtual IEnumerable GetPreValues(IEnumerable preValues) + => preValues; + + protected virtual object GetPreValueValue(PreValueDto preValue) + { + return preValue.Value.DetectIsJson() ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs new file mode 100644 index 0000000000..35ca574bab --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class DropDownFlexiblePreValueMigrator : IPreValueMigrator + { + public bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.DropDown.Flexible"; + + public virtual string GetNewAlias(string editorAlias) + => null; + + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + var config = new DropDownFlexibleConfiguration(); + foreach (var preValue in preValues.Values) + { + if (preValue.Alias == "multiple") + { + config.Multiple = (preValue.Value == "1"); + } + else + { + config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); + } + } + return config; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs new file mode 100644 index 0000000000..01e0ed3875 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + /// + /// Defines a service migrating preValues. + /// + public interface IPreValueMigrator + { + /// + /// Determines whether this migrator can migrate a data type. + /// + /// The data type editor alias. + bool CanMigrate(string editorAlias); + + /// + /// Gets the v8 codebase data type editor alias. + /// + /// The original v7 codebase editor alias. + /// + /// This is used to validate that the migrated configuration can be parsed + /// by the new property editor. Return null to bypass this validation, + /// when for instance we know it will fail, and another, later migration will + /// deal with it. + /// + string GetNewAlias(string editorAlias); + + /// + /// Gets the configuration object corresponding to preValue. + /// + /// The data type identifier. + /// The data type editor alias. + /// PreValues. + object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues); + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs new file mode 100644 index 0000000000..d8daf9b5e6 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class ListViewPreValueMigrator : DefaultPreValueMigrator + { + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.ListView"; + + protected override IEnumerable GetPreValues(IEnumerable preValues) + { + return preValues.Where(preValue => preValue.Alias != "displayAtTabNumber"); + } + + protected override object GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "pageSize") + return int.TryParse(preValue.Value, out var i) ? (int?)i : null; + + return preValue.Value.DetectIsJson() ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs new file mode 100644 index 0000000000..87a1fd6504 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class MarkdownEditorPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + { + public override bool CanMigrate(string editorAlias) + => editorAlias == Constants.PropertyEditors.Aliases.MarkdownEditor; + + protected override object GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "preview") + return preValue.Value == "1"; + + return base.GetPreValueValue(preValue); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs new file mode 100644 index 0000000000..922d886595 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs @@ -0,0 +1,37 @@ +using System.Linq; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class MediaPickerPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + { + private readonly string[] _editors = + { + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Aliases.MediaPicker + }; + + public override bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); + + public override string GetNewAlias(string editorAlias) + => Constants.PropertyEditors.Aliases.MediaPicker; + + // you wish - but MediaPickerConfiguration lives in Umbraco.Web + /* + public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + return new MediaPickerConfiguration { ... }; + } + */ + + protected override object GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "multiPicker" || + preValue.Alias == "onlyImages" || + preValue.Alias == "disableFolderSelect") + return preValue.Value == "1"; + + return base.GetPreValueValue(preValue); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs new file mode 100644 index 0000000000..22c87f8503 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class NestedContentPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + { + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.NestedContent"; + + // you wish - but NestedContentConfiguration lives in Umbraco.Web + /* + public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + return new NestedContentConfiguration { ... }; + } + */ + + protected override object GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "confirmDeletes" || + preValue.Alias == "showIcons" || + preValue.Alias == "hideLabel") + return preValue.Value == "1"; + + if (preValue.Alias == "minItems" || + preValue.Alias == "maxItems") + return int.TryParse(preValue.Value, out var i) ? (int?)i : null; + + return preValue.Value.DetectIsJson() ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs new file mode 100644 index 0000000000..b6ab622510 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs @@ -0,0 +1,24 @@ +using NPoco; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + [TableName("cmsDataTypePreValues")] + [ExplicitColumns] + public class PreValueDto + { + [Column("id")] + public int Id { get; set; } + + [Column("datatypeNodeId")] + public int NodeId { get; set; } + + [Column("alias")] + public string Alias { get; set; } + + [Column("sortorder")] + public int SortOrder { get; set; } + + [Column("value")] + public string Value { get; set; } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs new file mode 100644 index 0000000000..62e2b2347b --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + public abstract class PreValueMigratorBase : IPreValueMigrator + { + public abstract bool CanMigrate(string editorAlias); + + public virtual string GetNewAlias(string editorAlias) + => editorAlias; + + public abstract object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues); + + protected bool GetBoolValue(Dictionary preValues, string alias, bool defaultValue = false) + => preValues.TryGetValue(alias, out var preValue) ? preValue.Value == "1" : defaultValue; + + protected decimal GetDecimalValue(Dictionary preValues, string alias, decimal defaultValue = 0) + => preValues.TryGetValue(alias, out var preValue) && decimal.TryParse(preValue.Value, out var value) ? value : defaultValue; + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs new file mode 100644 index 0000000000..06f5d45deb --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + public class PreValueMigratorCollection : BuilderCollectionBase + { + private readonly ILogger _logger; + + public PreValueMigratorCollection(IEnumerable items, ILogger logger) + : base(items) + { + _logger = logger; + _logger.Debug(GetType(), "Migrators: " + string.Join(", ", items.Select(x => x.GetType().Name))); + } + + public IPreValueMigrator GetMigrator(string editorAlias) + { + var migrator = this.FirstOrDefault(x => x.CanMigrate(editorAlias)); + _logger.Debug(GetType(), "Getting migrator for \"{EditorAlias}\" = {MigratorType}", editorAlias, migrator == null ? "" : migrator.GetType().Name); + return migrator; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs new file mode 100644 index 0000000000..3859e63767 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + public class PreValueMigratorCollectionBuilder : OrderedCollectionBuilderBase + { + protected override PreValueMigratorCollectionBuilder This => this; + } +} 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 new file mode 100644 index 0000000000..db9021d653 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs @@ -0,0 +1,27 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ +[RuntimeLevel(MinLevel = RuntimeLevel.Upgrade, MaxLevel = RuntimeLevel.Upgrade)] // only on upgrades +public class PreValueMigratorComposer : ICoreComposer +{ + public void Compose(Composition composition) + { + // do NOT add DefaultPreValueMigrator to this list! + // it will be automatically used if nothing matches + + composition.WithCollectionBuilder() + .Append() + .Append() + .Append() + .Append() + .Append() + .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 new file mode 100644 index 0000000000..89a71fdaf4 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using Umbraco.Core.Exceptions; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class RenamingPreValueMigrator : DefaultPreValueMigrator + { + private readonly string[] _editors = + { + "Umbraco.NoEdit" + }; + + public override bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); + + public override string GetNewAlias(string editorAlias) + { + switch (editorAlias) + { + case "Umbraco.NoEdit": + return Constants.PropertyEditors.Aliases.Label; + default: + throw new PanicException($"The alias {editorAlias} is not supported"); + } + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs new file mode 100644 index 0000000000..3670c52c64 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class RichTextPreValueMigrator : DefaultPreValueMigrator + { + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.TinyMCEv3"; + + public override string GetNewAlias(string editorAlias) + => Constants.PropertyEditors.Aliases.TinyMce; + + protected override object GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "hideLabel") + return preValue.Value == "1"; + + return preValue.Value.DetectIsJson() ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs new file mode 100644 index 0000000000..dc6de5e493 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class UmbracoSliderPreValueMigrator : PreValueMigratorBase + { + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.Slider"; + + public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + return new SliderConfiguration + { + EnableRange = GetBoolValue(preValues, "enableRange"), + InitialValue = GetDecimalValue(preValues, "initVal1"), + InitialValue2 = GetDecimalValue(preValues, "initVal2"), + MaximumValue = GetDecimalValue(preValues, "maxVal"), + MinimumValue = GetDecimalValue(preValues, "minVal"), + StepIncrements = GetDecimalValue(preValues, "step") + }; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs new file mode 100644 index 0000000000..07fefc8e85 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class ValueListPreValueMigrator : IPreValueMigrator + { + private readonly string[] _editors = + { + "Umbraco.RadioButtonList", + "Umbraco.CheckBoxList", + "Umbraco.DropDown", + "Umbraco.DropdownlistPublishingKeys", + "Umbraco.DropDownMultiple", + "Umbraco.DropdownlistMultiplePublishKeys" + }; + + public bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); + + public virtual string GetNewAlias(string editorAlias) + => null; + + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + var config = new ValueListConfiguration(); + foreach (var preValue in preValues.Values) + config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); + return config; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs index ed2990aff7..d30719231a 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs @@ -1,39 +1,35 @@ using System; using System.Collections.Generic; using System.Linq; -using Umbraco.Core.Cache; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Logging; +using Umbraco.Core.Migrations.PostMigrations; using Umbraco.Core.Models; -using Umbraco.Core.Sync; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { - public class DropDownPropertyEditorsMigration : MigrationBase + public class DropDownPropertyEditorsMigration : PropertyEditorsMigrationBase { - private readonly CacheRefresherCollection _cacheRefreshers; - private readonly IServerMessenger _serverMessenger; - - public DropDownPropertyEditorsMigration(IMigrationContext context, CacheRefresherCollection cacheRefreshers, IServerMessenger serverMessenger) + public DropDownPropertyEditorsMigration(IMigrationContext context) : base(context) - { - _cacheRefreshers = cacheRefreshers; - _serverMessenger = serverMessenger; - } - - // dummy editor for deserialization - private class ValueListConfigurationEditor : ConfigurationEditor { } public override void Migrate() { - //need to convert the old drop down data types to use the new one - var dataTypes = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.EditorAlias.Contains(".DropDown"))); + var refreshCache = Migrate(GetDataTypes(".DropDown", false)); + + // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), + // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table + if (refreshCache) + Context.AddPostMigration(); + } + + private bool Migrate(IEnumerable dataTypes) + { + var refreshCache = false; + ConfigurationEditor configurationEditor = null; foreach (var dataType in dataTypes) { @@ -42,14 +38,16 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 if (!dataType.Configuration.IsNullOrWhiteSpace()) { // parse configuration, and update everything accordingly + if (configurationEditor == null) + configurationEditor = new ValueListConfigurationEditor(); try { - config = (ValueListConfiguration) new ValueListConfigurationEditor().FromDatabase(dataType.Configuration); + config = (ValueListConfiguration) configurationEditor.FromDatabase(dataType.Configuration); } catch (Exception ex) { Logger.Error( - ex, "Invalid drop down configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", + ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", dataType.Configuration); // reset @@ -65,7 +63,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 .Where(x => x.DataTypeId == dataType.NodeId)); // update dtos - var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config)); + var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, true)); // persist changes foreach (var propertyDataDto in updatedDtos) @@ -77,7 +75,6 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 config = new ValueListConfiguration(); } - var requiresCacheRebuild = false; switch (dataType.EditorAlias) { case string ea when ea.InvariantEquals("Umbraco.DropDown"): @@ -85,29 +82,25 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 break; case string ea when ea.InvariantEquals("Umbraco.DropdownlistPublishingKeys"): UpdateDataType(dataType, config, false); - requiresCacheRebuild = true; break; case string ea when ea.InvariantEquals("Umbraco.DropDownMultiple"): UpdateDataType(dataType, config, true); break; case string ea when ea.InvariantEquals("Umbraco.DropdownlistMultiplePublishKeys"): UpdateDataType(dataType, config, true); - requiresCacheRebuild = true; break; } - if (requiresCacheRebuild) - { - var dataTypeCacheRefresher = _cacheRefreshers[Guid.Parse("35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2")]; - _serverMessenger.PerformRefreshAll(dataTypeCacheRefresher); - } + refreshCache = true; } + + return refreshCache; } private void UpdateDataType(DataTypeDto dataType, ValueListConfiguration config, bool isMultiple) { - dataType.EditorAlias = Constants.PropertyEditors.Aliases.DropDownListFlexible; dataType.DbType = ValueStorageType.Nvarchar.ToString(); + dataType.EditorAlias = Constants.PropertyEditors.Aliases.DropDownListFlexible; var flexConfig = new DropDownFlexibleConfiguration { @@ -118,68 +111,5 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 Database.Update(dataType); } - - private bool UpdatePropertyDataDto(PropertyDataDto propData, ValueListConfiguration config) - { - //Get the INT ids stored for this property/drop down - int[] ids = null; - if (!propData.VarcharValue.IsNullOrWhiteSpace()) - { - ids = ConvertStringValues(propData.VarcharValue); - } - else if (!propData.TextValue.IsNullOrWhiteSpace()) - { - ids = ConvertStringValues(propData.TextValue); - } - else if (propData.IntegerValue.HasValue) - { - ids = new[] { propData.IntegerValue.Value }; - } - - //if there are INT ids, convert them to values based on the configured pre-values - if (ids != null && ids.Length > 0) - { - //map the ids to values - var vals = new List(); - var canConvert = true; - foreach (var id in ids) - { - var val = config.Items.FirstOrDefault(x => x.Id == id); - if (val != null) - vals.Add(val.Value); - else - { - Logger.Warn( - "Could not find associated data type configuration for stored Id {DataTypeId}", id); - canConvert = false; - } - } - if (canConvert) - { - propData.VarcharValue = string.Join(",", vals); - propData.TextValue = null; - propData.IntegerValue = null; - return true; - } - } - - return false; - } - - private int[] ConvertStringValues(string val) - { - var splitVals = val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - var intVals = splitVals - .Select(x => int.TryParse(x, out var i) ? i : int.MinValue) - .Where(x => x != int.MinValue) - .ToArray(); - - //only return if the number of values are the same (i.e. All INTs) - if (splitVals.Length == intVals.Length) - return intVals; - - return null; - } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs index 26668361bd..def6a93400 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs @@ -8,7 +8,8 @@ public override void Migrate() { - Delete.Table("umbracoMigration").Do(); + if (TableExists("umbracoMigration")) + Delete.Table("umbracoMigration").Do(); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs index fa6e47fac7..918510d13c 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs @@ -3,8 +3,7 @@ public class DropPreValueTable : MigrationBase { public DropPreValueTable(IMigrationContext context) : base(context) - { - } + { } public override void Migrate() { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs new file mode 100644 index 0000000000..8de06c9a6f --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs @@ -0,0 +1,21 @@ +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class FixLanguageIsoCodeLength : MigrationBase + { + public FixLanguageIsoCodeLength(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + // there is some confusion here when upgrading from v7 + // it should be 14 already but that's not always the case + + Alter.Table("umbracoLanguage") + .AlterColumn("languageISOCode") + .AsString(14) + .Nullable() + .Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FixLockTablePrimaryKey.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FixLockTablePrimaryKey.cs deleted file mode 100644 index fbb233927b..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FixLockTablePrimaryKey.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Linq; - -namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 -{ - public class FixLockTablePrimaryKey : MigrationBase - { - public FixLockTablePrimaryKey(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - // at some point, the KeyValueService dropped the PK and failed to re-create it, - // so the PK is gone - make sure we have one, and create if needed - - var constraints = SqlSyntax.GetConstraintsPerTable(Database); - var exists = constraints.Any(x => x.Item2 == "PK_umbracoLock"); - - if (!exists) - Create.PrimaryKey("PK_umbracoLock").OnTable(Constants.DatabaseSchema.Tables.Lock).Column("id").Do(); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs index 2e366c7c14..651c95e6bd 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs @@ -11,19 +11,6 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { AddColumn("culture"); - - Delete.Index("IX_umbracoRedirectUrl").OnTable(Constants.DatabaseSchema.Tables.RedirectUrl).Do(); - Create.Index("IX_umbracoRedirectUrl").OnTable(Constants.DatabaseSchema.Tables.RedirectUrl) - .OnColumn("urlHash") - .Ascending() - .OnColumn("contentKey") - .Ascending() - .OnColumn("culture") - .Ascending() - .OnColumn("createDateUtc") - .Ascending() - .WithOptions().Unique() - .Do(); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs index 9ccd6d5e76..c898187884 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs @@ -11,17 +11,6 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { AddColumn("languageId"); - - Delete.Index($"IX_{Constants.DatabaseSchema.Tables.Tag}").OnTable(Constants.DatabaseSchema.Tables.Tag).Do(); - Create.Index($"IX_{Constants.DatabaseSchema.Tables.Tag}").OnTable(Constants.DatabaseSchema.Tables.Tag) - .OnColumn("group") - .Ascending() - .OnColumn("tag") - .Ascending() - .OnColumn("languageId") - .Ascending() - .WithOptions().Unique() - .Do(); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs index a434b9f1c1..37a83bdd67 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - var dataTypes = GetDataTypes("Umbraco.Date"); + var dataTypes = GetDataTypes(Constants.PropertyEditors.Legacy.Aliases.Date); foreach (var dataType in dataTypes) { @@ -25,6 +25,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { config = (DateTimeConfiguration) new CustomDateTimeConfigurationEditor().FromDatabase( dataType.Configuration); + + // If the Umbraco.Date type is the default from V7 and it has never been updated, then the + // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that + // after the migration, so we force the format to the default from V7. + if (string.IsNullOrEmpty(dataType.Configuration)) + { + config.Format = "YYYY-MM-DD"; + } } catch (Exception ex) { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs new file mode 100644 index 0000000000..bc41e5e32a --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs @@ -0,0 +1,63 @@ +using NPoco; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models +{ + + /// + /// Snapshot of the as it was at version 8.0 + /// + /// + /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors + /// + [TableName(TableName)] + [PrimaryKey("pk")] + [ExplicitColumns] + internal class ContentTypeDto80 + { + public const string TableName = Constants.DatabaseSchema.Tables.ContentType; + + [Column("pk")] + [PrimaryKeyColumn(IdentitySeed = 535)] + public int PrimaryKey { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] + public int NodeId { get; set; } + + [Column("alias")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Alias { get; set; } + + [Column("icon")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public string Icon { get; set; } + + [Column("thumbnail")] + [Constraint(Default = "folder.png")] + public string Thumbnail { get; set; } + + [Column("description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(1500)] + public string Description { get; set; } + + [Column("isContainer")] + [Constraint(Default = "0")] + public bool IsContainer { get; set; } + + [Column("allowAtRoot")] + [Constraint(Default = "0")] + public bool AllowAtRoot { get; set; } + + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } + + [ResultColumn] + public NodeDto NodeDto { get; set; } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs new file mode 100644 index 0000000000..f0a36b223d --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs @@ -0,0 +1,142 @@ +using NPoco; +using System; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models +{ + /// + /// Snapshot of the as it was at version 8.0 + /// + /// + /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors + /// + [TableName(TableName)] + [PrimaryKey("id")] + [ExplicitColumns] + internal class PropertyDataDto80 + { + public const string TableName = Constants.DatabaseSchema.Tables.PropertyData; + public const int VarcharLength = 512; + public const int SegmentLength = 256; + + private decimal? _decimalValue; + + // pk, not used at the moment (never updating) + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] + public int VersionId { get; set; } + + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto80))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] + public int PropertyTypeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } + + [Column("segment")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(SegmentLength)] + public string Segment { get; set; } + + [Column("intValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? IntegerValue { get; set; } + + [Column("decimalValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public decimal? DecimalValue + { + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } + + [Column("dateValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? DateValue { get; set; } + + [Column("varcharValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(VarcharLength)] + public string VarcharValue { get; set; } + + [Column("textValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string TextValue { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] + public PropertyTypeDto80 PropertyTypeDto { get; set; } + + [Ignore] + public object Value + { + get + { + if (IntegerValue.HasValue) + return IntegerValue.Value; + + if (DecimalValue.HasValue) + return DecimalValue.Value; + + if (DateValue.HasValue) + return DateValue.Value; + + if (!string.IsNullOrEmpty(VarcharValue)) + return VarcharValue; + + if (!string.IsNullOrEmpty(TextValue)) + return TextValue; + + return null; + } + } + + public PropertyDataDto80 Clone(int versionId) + { + return new PropertyDataDto80 + { + VersionId = versionId, + PropertyTypeId = PropertyTypeId, + LanguageId = LanguageId, + Segment = Segment, + IntegerValue = IntegerValue, + DecimalValue = DecimalValue, + DateValue = DateValue, + VarcharValue = VarcharValue, + TextValue = TextValue, + PropertyTypeDto = PropertyTypeDto + }; + } + + protected bool Equals(PropertyDataDto other) + { + return Id == other.Id; + } + + public override bool Equals(object other) + { + return + !ReferenceEquals(null, other) // other is not null + && (ReferenceEquals(this, other) // and either ref-equals, or same id + || other is PropertyDataDto pdata && pdata.Id == Id); + } + + public override int GetHashCode() + { + // ReSharper disable once NonReadonlyMemberInGetHashCode + return Id; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs new file mode 100644 index 0000000000..6e2bd7371a --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs @@ -0,0 +1,80 @@ +using NPoco; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models +{ + /// + /// Snapshot of the as it was at version 8.0 + /// + /// + /// This is required during migrations before 8.6 since the schema has changed and running SQL against the new table would result in errors + /// + [TableName(Constants.DatabaseSchema.Tables.PropertyType)] + [PrimaryKey("id")] + [ExplicitColumns] + internal class PropertyTypeDto80 + { + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 50)] + public int Id { get; set; } + + [Column("dataTypeId")] + [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] + public int DataTypeId { get; set; } + + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeId { get; set; } + + [Column("propertyTypeGroupId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(PropertyTypeGroupDto))] + public int? PropertyTypeGroupId { get; set; } + + [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] + [Column("Alias")] + public string Alias { get; set; } + + [Column("Name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Name { get; set; } + + [Column("sortOrder")] + [Constraint(Default = "0")] + public int SortOrder { get; set; } + + [Column("mandatory")] + [Constraint(Default = "0")] + public bool Mandatory { get; set; } + + [Column("validationRegExp")] + [NullSetting(NullSetting = NullSettings.Null)] + public string ValidationRegExp { get; set; } + + [Column("Description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(2000)] + public string Description { get; set; } + + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] + public DataTypeDto DataTypeDto { get; set; } + + [Column("UniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] + public Guid UniqueId { get; set; } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs index 064ffc7228..89a8f010ec 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs @@ -12,12 +12,12 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - RenameDataType(Constants.PropertyEditors.Aliases.ContentPicker + "2", Constants.PropertyEditors.Aliases.ContentPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MediaPicker + "2", Constants.PropertyEditors.Aliases.MediaPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MemberPicker + "2", Constants.PropertyEditors.Aliases.MemberPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MultiNodeTreePicker + "2", Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - RenameDataType("Umbraco.TextboxMultiple", Constants.PropertyEditors.Aliases.TextArea, false); - RenameDataType("Umbraco.Textbox", Constants.PropertyEditors.Aliases.TextBox, false); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, Constants.PropertyEditors.Aliases.ContentPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, Constants.PropertyEditors.Aliases.MediaPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, Constants.PropertyEditors.Aliases.MemberPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, Constants.PropertyEditors.Aliases.TextArea, false); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.Textbox, Constants.PropertyEditors.Aliases.TextBox, false); } private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true) @@ -30,7 +30,20 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 .Where(x => x.EditorAlias == toAlias)); if (oldCount > 0) - throw new InvalidOperationException($"Cannot rename datatype alias \"{fromAlias}\" to \"{toAlias}\" because the target alias is already used."); + { + // If we throw it means that the upgrade will exit and cannot continue. + // This will occur if a v7 site has the old "Obsolete" property editors that are already named with the `toAlias` name. + // TODO: We should have an additional upgrade step when going from 7 -> 8 like we did with 6 -> 7 that shows a compatibility report, + // this would include this check and then we can provide users with information on what they should do (i.e. before upgrading to v8 they will + // need to migrate these old obsolete editors to non-obsolete editors) + + throw new InvalidOperationException( + $"Cannot rename datatype alias \"{fromAlias}\" to \"{toAlias}\" because the target alias is already used." + + $"This is generally because when upgrading from a v7 to v8 site, the v7 site contains Data Types that reference old and already Obsolete " + + $"Property Editors. Before upgrading to v8, any Data Types using property editors that are named with the prefix '(Obsolete)' must be migrated " + + $"to the non-obsolete v7 property editors of the same type."); + } + } Database.Execute(Sql() diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs new file mode 100644 index 0000000000..605f8a9eed --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public abstract class PropertyEditorsMigrationBase : MigrationBase + { + protected PropertyEditorsMigrationBase(IMigrationContext context) + : base(context) + { } + + internal List GetDataTypes(string editorAlias, bool strict = true) + { + var sql = Sql() + .Select() + .From(); + + sql = strict + ? sql.Where(x => x.EditorAlias == editorAlias) + : sql.Where(x => x.EditorAlias.Contains(editorAlias)); + + return Database.Fetch(sql); + } + + protected int[] ConvertStringValues(string val) + { + var splitVals = val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + var intVals = splitVals + .Select(x => int.TryParse(x, out var i) ? i : int.MinValue) + .Where(x => x != int.MinValue) + .ToArray(); + + //only return if the number of values are the same (i.e. All INTs) + if (splitVals.Length == intVals.Length) + return intVals; + + return null; + } + + internal bool UpdatePropertyDataDto(PropertyDataDto propData, ValueListConfiguration config, bool isMultiple) + { + //Get the INT ids stored for this property/drop down + int[] ids = null; + if (!propData.VarcharValue.IsNullOrWhiteSpace()) + { + ids = ConvertStringValues(propData.VarcharValue); + } + else if (!propData.TextValue.IsNullOrWhiteSpace()) + { + ids = ConvertStringValues(propData.TextValue); + } + else if (propData.IntegerValue.HasValue) + { + ids = new[] { propData.IntegerValue.Value }; + } + + // if there are INT ids, convert them to values based on the configuration + if (ids == null || ids.Length <= 0) return false; + + // map ids to values + var values = new List(); + var canConvert = true; + + foreach (var id in ids) + { + var val = config.Items.FirstOrDefault(x => x.Id == id); + if (val != null) + { + values.Add(val.Value); + continue; + } + + Logger.Warn(GetType(), "Could not find PropertyData {PropertyDataId} value '{PropertyValue}' in the datatype configuration: {Values}.", + propData.Id, id, string.Join(", ", config.Items.Select(x => x.Id + ":" + x.Value))); + canConvert = false; + } + + if (!canConvert) return false; + + propData.VarcharValue = isMultiple ? JsonConvert.SerializeObject(values) : values[0]; + propData.TextValue = null; + propData.IntegerValue = null; + return true; + } + + // dummy editor for deserialization + protected class ValueListConfigurationEditor : ConfigurationEditor + { } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxAndDropdownPropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxAndDropdownPropertyEditorsMigration.cs deleted file mode 100644 index 1944c8079f..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxAndDropdownPropertyEditorsMigration.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Umbraco.Core.Logging; -using Umbraco.Core.Migrations.PostMigrations; -using Umbraco.Core.Models; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 -{ - public class RadioAndCheckboxAndDropdownPropertyEditorsMigration : MigrationBase - { - public RadioAndCheckboxAndDropdownPropertyEditorsMigration(IMigrationContext context) - : base(context) - { - } - - public override void Migrate() - { - var refreshCache = false; - - refreshCache |= Migrate(Constants.PropertyEditors.Aliases.RadioButtonList, (dto, configuration) => UpdateRadioOrCheckboxPropertyDataDto(dto, configuration, true)); - refreshCache |= Migrate(Constants.PropertyEditors.Aliases.CheckBoxList, (dto, configuration) => UpdateRadioOrCheckboxPropertyDataDto(dto, configuration, false)); - refreshCache |= Migrate(Constants.PropertyEditors.Aliases.DropDownListFlexible, UpdateDropDownPropertyDataDto); - - if (refreshCache) - { - Context.AddPostMigration(); - } - } - - private bool Migrate(string editorAlias, Func updateRadioPropertyDataFunc) - { - var refreshCache = false; - var dataTypes = GetDataTypes(editorAlias); - - foreach (var dataType in dataTypes) - { - ValueListConfiguration config; - - if (dataType.Configuration.IsNullOrWhiteSpace()) - continue; - - // parse configuration, and update everything accordingly - try - { - config = (ValueListConfiguration) new ValueListConfigurationEditor().FromDatabase( - dataType.Configuration); - } - catch (Exception ex) - { - Logger.Error( - ex, - "Invalid property editor configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", - dataType.Configuration); - - continue; - } - - // get property data dtos - var propertyDataDtos = Database.Fetch(Sql() - .Select() - .From() - .InnerJoin() - .On((pt, pd) => pt.Id == pd.PropertyTypeId) - .InnerJoin() - .On((dt, pt) => dt.NodeId == pt.DataTypeId) - .Where(x => x.DataTypeId == dataType.NodeId)); - - // update dtos - var updatedDtos = propertyDataDtos.Where(x => updateRadioPropertyDataFunc(x, config)); - - // persist changes - foreach (var propertyDataDto in updatedDtos) Database.Update(propertyDataDto); - - UpdateDataType(dataType); - refreshCache = true; - } - - return refreshCache; - } - - private List GetDataTypes(string editorAlias) - { - //need to convert the old drop down data types to use the new one - var dataTypes = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.EditorAlias == editorAlias)); - return dataTypes; - } - - private void UpdateDataType(DataTypeDto dataType) - { - dataType.DbType = ValueStorageType.Nvarchar.ToString(); - Database.Update(dataType); - } - - private bool UpdateRadioOrCheckboxPropertyDataDto(PropertyDataDto propData, ValueListConfiguration config, bool singleValue) - { - //Get the INT ids stored for this property/drop down - int[] ids = null; - if (!propData.VarcharValue.IsNullOrWhiteSpace()) - { - ids = ConvertStringValues(propData.VarcharValue); - } - else if (!propData.TextValue.IsNullOrWhiteSpace()) - { - ids = ConvertStringValues(propData.TextValue); - } - else if (propData.IntegerValue.HasValue) - { - ids = new[] {propData.IntegerValue.Value}; - } - - //if there are INT ids, convert them to values based on the configuration - if (ids == null || ids.Length <= 0) return false; - - //map the ids to values - var values = new List(); - var canConvert = true; - - foreach (var id in ids) - { - var val = config.Items.FirstOrDefault(x => x.Id == id); - if (val != null) - values.Add(val.Value); - else - { - Logger.Warn( - "Could not find associated data type configuration for stored Id {DataTypeId}", id); - canConvert = false; - } - } - - if (!canConvert) return false; - - //The radio button only supports selecting a single value, so if there are multiple for some insane reason we can only use the first - propData.VarcharValue = singleValue ? values[0] : JsonConvert.SerializeObject(values); - propData.TextValue = null; - propData.IntegerValue = null; - return true; - } - - private bool UpdateDropDownPropertyDataDto(PropertyDataDto propData, ValueListConfiguration config) - { - //Get the INT ids stored for this property/drop down - var values = propData.VarcharValue.Split(new []{","}, StringSplitOptions.RemoveEmptyEntries); - - //if there are INT ids, convert them to values based on the configuration - if (values == null || values.Length <= 0) return false; - - //The radio button only supports selecting a single value, so if there are multiple for some insane reason we can only use the first - propData.VarcharValue = JsonConvert.SerializeObject(values); - propData.TextValue = null; - propData.IntegerValue = null; - return true; - } - - private int[] ConvertStringValues(string val) - { - var splitVals = val.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); - - var intVals = splitVals - .Select(x => int.TryParse(x, out var i) ? i : int.MinValue) - .Where(x => x != int.MinValue) - .ToArray(); - - //only return if the number of values are the same (i.e. All INTs) - if (splitVals.Length == intVals.Length) - return intVals; - - return null; - } - - private class ValueListConfigurationEditor : ConfigurationEditor - { - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs new file mode 100644 index 0000000000..e96fa1f7e9 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Migrations.PostMigrations; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class RadioAndCheckboxPropertyEditorsMigration : PropertyEditorsMigrationBase + { + public RadioAndCheckboxPropertyEditorsMigration(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + var refreshCache = false; + + refreshCache |= Migrate(GetDataTypes(Constants.PropertyEditors.Aliases.RadioButtonList), false); + refreshCache |= Migrate(GetDataTypes(Constants.PropertyEditors.Aliases.CheckBoxList), true); + + // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), + // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table + if (refreshCache) + Context.AddPostMigration(); + } + + private bool Migrate(IEnumerable dataTypes, bool isMultiple) + { + var refreshCache = false; + ConfigurationEditor configurationEditor = null; + + foreach (var dataType in dataTypes) + { + ValueListConfiguration config; + + if (dataType.Configuration.IsNullOrWhiteSpace()) + continue; + + // parse configuration, and update everything accordingly + if (configurationEditor == null) + configurationEditor = new ValueListConfigurationEditor(); + try + { + config = (ValueListConfiguration) configurationEditor.FromDatabase(dataType.Configuration); + } + catch (Exception ex) + { + Logger.Error( + ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", + dataType.Configuration); + + continue; + } + + // get property data dtos + var propertyDataDtos = Database.Fetch(Sql() + .Select() + .From() + .InnerJoin().On((pt, pd) => pt.Id == pd.PropertyTypeId) + .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) + .Where(x => x.DataTypeId == dataType.NodeId)); + + // update dtos + var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, isMultiple)); + + // persist changes + foreach (var propertyDataDto in updatedDtos) + Database.Update(propertyDataDto); + + UpdateDataType(dataType); + refreshCache = true; + } + + return refreshCache; + } + + private void UpdateDataType(DataTypeDto dataType) + { + dataType.DbType = ValueStorageType.Nvarchar.ToString(); + Database.Update(dataType); + } + } +} 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/RefactorXmlColumns.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorXmlColumns.cs deleted file mode 100644 index c683940f60..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorXmlColumns.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; - -namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 -{ - public class RefactorXmlColumns : MigrationBase - { - public RefactorXmlColumns(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - if (ColumnExists("cmsContentXml", "Rv") == false) - Alter.Table("cmsContentXml").AddColumn("Rv").AsInt64().NotNullable().WithDefaultValue(0).Do(); - - if (ColumnExists("cmsPreviewXml", "Rv") == false) - Alter.Table("cmsPreviewXml").AddColumn("Rv").AsInt64().NotNullable().WithDefaultValue(0).Do(); - - // remove the any PK_ and the FK_ to cmsContentVersion.VersionId - var constraints = SqlSyntax.GetConstraintsPerColumn(Context.Database).Distinct().ToArray(); - var dups = new List(); - foreach (var c in constraints.Where(x => x.Item1.InvariantEquals("cmsPreviewXml") && x.Item3.InvariantStartsWith("PK_"))) - { - var keyName = c.Item3.ToLowerInvariant(); - if (dups.Contains(keyName)) - { - Logger.Warn("Duplicate constraint '{Constraint}'", c.Item3); - continue; - } - dups.Add(keyName); - Delete.PrimaryKey(c.Item3).FromTable(c.Item1).Do(); - } - foreach (var c in constraints.Where(x => x.Item1.InvariantEquals("cmsPreviewXml") && x.Item3.InvariantStartsWith("FK_cmsPreviewXml_cmsContentVersion"))) - { - Delete.ForeignKey().FromTable("cmsPreviewXml").ForeignColumn("VersionId") - .ToTable("cmsContentVersion").PrimaryColumn("VersionId") - .Do(); - } - - if (ColumnExists("cmsPreviewXml", "Timestamp")) - Delete.Column("Timestamp").FromTable("cmsPreviewXml").Do(); - - if (ColumnExists("cmsPreviewXml", "VersionId")) - { - RemoveDuplicates(); - Delete.Column("VersionId").FromTable("cmsPreviewXml").Do(); - } - - // re-create the primary key - Create.PrimaryKey("PK_cmsPreviewXml") - .OnTable("cmsPreviewXml") - .Columns(new[] { "nodeId" }) - .Do(); - } - - private void RemoveDuplicates() - { - const string sql = @"delete from cmsPreviewXml where versionId in ( -select cmsPreviewXml.versionId from cmsPreviewXml -join cmsDocument on cmsPreviewXml.versionId=cmsDocument.versionId -where cmsDocument.newest <> 1)"; - - Context.Database.Execute(sql); - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs new file mode 100644 index 0000000000..2b27bdafe8 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs @@ -0,0 +1,57 @@ +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class RenameMediaVersionTable : MigrationBase + { + public RenameMediaVersionTable(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Rename.Table("cmsMedia").To(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + + // that is not supported on SqlCE + //Rename.Column("versionId").OnTable(Constants.DatabaseSchema.Tables.MediaVersion).To("id").Do(); + + AddColumn("id", out var sqls); + + if (Database.DatabaseType.IsSqlCe()) + { + // SQLCE does not support UPDATE...FROM + var versions = Database.Fetch($@"SELECT v.versionId, v.id +FROM cmsContentVersion v +JOIN umbracoNode n on v.contentId=n.id +WHERE n.nodeObjectType='{Constants.ObjectTypes.Media}'"); + foreach (var t in versions) + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET id={t.id} WHERE versionId='{t.versionId}'").Do(); + } + else + { + Database.Execute($@"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id +FROM {Constants.DatabaseSchema.Tables.MediaVersion} m +JOIN cmsContentVersion v on m.versionId = v.versionId +JOIN umbracoNode n on v.contentId=n.id +WHERE n.nodeObjectType='{Constants.ObjectTypes.Media}'"); + } + foreach (var sql in sqls) + Execute.Sql(sql).Do(); + + AddColumn("path", out sqls); + + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET path=mediaPath").Do(); + + foreach (var sql in sqls) + Execute.Sql(sql).Do(); + + // we had to run sqls to get the NULL constraints, but we need to get rid of most + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + + Delete.Column("mediaPath").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + Delete.Column("versionId").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + Delete.Column("nodeId").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs index 9bbccf368c..0543b57fcc 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs @@ -2,7 +2,9 @@ { public class RenameUmbracoDomainsTable : MigrationBase { - public RenameUmbracoDomainsTable(IMigrationContext context) : base(context) { } + public RenameUmbracoDomainsTable(IMigrationContext context) + : base(context) + { } public override void Migrate() { 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 ba29880e79..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 @@ -11,20 +11,28 @@ var exists = Database.Fetch("select id from umbracoUser where id=-1;").Count > 0; if (exists) return; - Database.Execute("update umbracoUser set userLogin = userLogin + '__' where userId=0"); + Database.Execute("update umbracoUser set userLogin = userLogin + '__' where id=0"); Database.Execute("set identity_insert umbracoUser on;"); Database.Execute(@" - insert into umbracoUser select -1, - userDisabled, userNoConsole, userName, substring(userLogin, 1, len(userLogin) - 2), userPassword, passwordConfig, + insert into umbracoUser (id, + userDisabled, userNoConsole, userName, userLogin, userPassword, passwordConfig, + userEmail, userLanguage, securityStampToken, failedLoginAttempts, lastLockoutDate, + lastPasswordChangeDate, lastLoginDate, emailConfirmedDate, invitedDate, + createDate, updateDate, avatar, tourData) + select + -1 id, + userDisabled, userNoConsole, userName, substring(userLogin, 1, len(userLogin) - 2) userLogin, userPassword, passwordConfig, userEmail, userLanguage, securityStampToken, failedLoginAttempts, lastLockoutDate, lastPasswordChangeDate, lastLoginDate, emailConfirmedDate, invitedDate, - createDate, updateDate, avatar + createDate, updateDate, avatar, tourData from umbracoUser where id=0;"); 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;"); Database.Execute("delete from umbracoUser where id=0;"); } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs index cd4de179bd..2f7ffe8679 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs @@ -14,33 +14,45 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { //Get anything currently scheduled - var scheduleSql = new Sql() - .Select("nodeId", "releaseDate", "expireDate") + var releaseSql = new Sql() + .Select("nodeId", "releaseDate") .From("umbracoDocument") - .Where("releaseDate IS NOT NULL OR expireDate IS NOT NULL"); - var schedules = Database.Dictionary (scheduleSql); + .Where("releaseDate IS NOT NULL"); + var releases = Database.Dictionary (releaseSql); + + var expireSql = new Sql() + .Select("nodeId", "expireDate") + .From("umbracoDocument") + .Where("expireDate IS NOT NULL"); + var expires = Database.Dictionary(expireSql); + //drop old cols Delete.Column("releaseDate").FromTable("umbracoDocument").Do(); Delete.Column("expireDate").FromTable("umbracoDocument").Do(); //add new table - Create.Table().Do(); + Create.Table(true).Do(); //migrate the schedule - foreach(var s in schedules) + foreach(var s in releases) { - var date = s.Value.releaseDate; + var date = s.Value; var action = ContentScheduleAction.Release.ToString(); - if (!date.HasValue) - { - date = s.Value.expireDate; - action = ContentScheduleAction.Expire.ToString(); - } Insert.IntoTable(ContentScheduleDto.TableName) - .Row(new { nodeId = s.Key, date = date.Value, action = action }) + .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) .Do(); } + + foreach (var s in expires) + { + var date = s.Value; + var action = ContentScheduleAction.Expire.ToString(); + + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) + .Do(); + } } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs index 5dc5e0b6fe..d6a5380e31 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Persistence.Dtos; +using System.Linq; +using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { @@ -14,13 +15,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 AlterColumn(Constants.DatabaseSchema.Tables.Tag, "group"); AlterColumn(Constants.DatabaseSchema.Tables.Tag, "tag"); - //AddColumn(Constants.DatabaseSchema.Tables.Tag, "key"); - // kill unused parentId column - Delete.ForeignKey("FK_cmsTags_cmsTags").OnTable(Constants.DatabaseSchema.Tables.Tag).Do(); Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do(); } } - - // fixes TagsMigration that... originally failed to properly drop the ParentId column } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs index 2e37c79632..7f7cf8474c 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs @@ -2,6 +2,7 @@ using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Umbraco.Core.Composing; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs index 68a68ec53e..f16c9cd761 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs @@ -1,6 +1,4 @@ -using NPoco; -using Umbraco.Core.Persistence.DatabaseAnnotations; -using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { @@ -15,24 +13,19 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - //first allow NULL-able + // first allow NULL-able Alter.Table(ContentVersionCultureVariationDto.TableName).AlterColumn("availableUserId").AsInt32().Nullable().Do(); Alter.Table(ContentVersionDto.TableName).AlterColumn("userId").AsInt32().Nullable().Do(); Alter.Table(Constants.DatabaseSchema.Tables.Log).AlterColumn("userId").AsInt32().Nullable().Do(); Alter.Table(NodeDto.TableName).AlterColumn("nodeUser").AsInt32().Nullable().Do(); - //then we can update any non existing users to NULL + // then we can update any non existing users to NULL Execute.Sql($"UPDATE {ContentVersionCultureVariationDto.TableName} SET availableUserId = NULL WHERE availableUserId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); Execute.Sql($"UPDATE {ContentVersionDto.TableName} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Log} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); Execute.Sql($"UPDATE {NodeDto.TableName} SET nodeUser = NULL WHERE nodeUser NOT IN (SELECT id FROM {UserDto.TableName})").Do(); - //now NULL-able with FKs - Alter.Table(ContentVersionCultureVariationDto.TableName).AlterColumn("availableUserId").AsInt32().Nullable().ForeignKey(UserDto.TableName, "id").Do(); - Alter.Table(ContentVersionDto.TableName).AlterColumn("userId").AsInt32().Nullable().ForeignKey(UserDto.TableName, "id").Do(); - Alter.Table(Constants.DatabaseSchema.Tables.Log).AlterColumn("userId").AsInt32().Nullable().ForeignKey(UserDto.TableName, "id").Do(); - Alter.Table(NodeDto.TableName).AlterColumn("nodeUser").AsInt32().Nullable().ForeignKey(UserDto.TableName, "id").Do(); + // FKs will be created after migrations } - } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index c193fdeb1f..adc451ef6a 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Umbraco.Core.Migrations.Install; @@ -18,10 +19,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - // delete *all* keys and indexes - because of FKs - Delete.KeysAndIndexes().Do(); - MigratePropertyData(); + CreatePropertyDataIndexes(); MigrateContentAndPropertyTypes(); MigrateContent(); MigrateVersions(); @@ -46,10 +45,6 @@ HAVING COUNT(v2.id) <> 1").Any()) Debugger.Break(); throw new Exception("Migration failed: missing or duplicate 'current' content versions."); } - - // re-create *all* keys and indexes - foreach (var x in DatabaseSchemaCreator.OrderedTables) - Create.KeysAndIndexes(x).Do(); } private void MigratePropertyData() @@ -81,10 +76,20 @@ HAVING COUNT(v2.id) <> 1").Any()) { Alter.Table(PreTables.PropertyData).AddColumn("versionId2").AsInt32().Nullable().Do(); - // SQLCE does not support UPDATE...FROM - var temp = Database.Fetch($"SELECT id, versionId FROM {PreTables.ContentVersion}"); - foreach (var t in temp) - Database.Execute($"UPDATE {PreTables.PropertyData} SET versionId2=@v2 WHERE versionId=@v1", new { v1 = t.versionId, v2 = t.id }); + if (Database.DatabaseType.IsSqlCe()) + { + // SQLCE does not support UPDATE...FROM + var versions = Database.Fetch($"SELECT id, versionId FROM {PreTables.ContentVersion}"); + foreach (var t in versions) + Database.Execute($"UPDATE {PreTables.PropertyData} SET versionId2=@v2 WHERE versionId=@v1", new { v1 = t.versionId, v2 = t.id }); + } + else + { + Database.Execute($@"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id +FROM {PreTables.ContentVersion} +INNER JOIN {PreTables.PropertyData} ON {PreTables.ContentVersion}.versionId = {PreTables.PropertyData}.versionId"); + } + Delete.Column("versionId").FromTable(PreTables.PropertyData).Do(); ReplaceColumn(PreTables.PropertyData, "versionId2", "versionId"); } @@ -97,6 +102,22 @@ HAVING COUNT(v2.id) <> 1").Any()) Rename.Table(PreTables.PropertyData).To(Constants.DatabaseSchema.Tables.PropertyData).Do(); } + private void CreatePropertyDataIndexes() + { + // Creates a temporary index on umbracoPropertyData to speed up other migrations which update property values. + // It will be removed in CreateKeysAndIndexes before the normal indexes for the table are created + var tableDefinition = Persistence.DatabaseModelDefinitions.DefinitionFactory.GetTableDefinition(typeof(PropertyDataDto), SqlSyntax); + Execute.Sql(SqlSyntax.FormatPrimaryKey(tableDefinition)).Do(); + Create.Index("IX_umbracoPropertyData_Temp").OnTable(PropertyDataDto.TableName) + .WithOptions().Unique() + .WithOptions().NonClustered() + .OnColumn("versionId").Ascending() + .OnColumn("propertyTypeId").Ascending() + .OnColumn("languageId").Ascending() + .OnColumn("segment").Ascending() + .Do(); + } + private void MigrateContentAndPropertyTypes() { if (!ColumnExists(PreTables.ContentType, "variations")) @@ -160,22 +181,40 @@ HAVING COUNT(v2.id) <> 1").Any()) ReplaceColumn(PreTables.ContentVersion, "ContentId", "nodeId"); // populate contentVersion text, current and userId columns for documents - // SQLCE does not support UPDATE...FROM - var temp1 = Database.Fetch($"SELECT versionId, text, newest, documentUser FROM {PreTables.Document}"); - foreach (var t in temp1) - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=@current, userId=@userId WHERE versionId=@versionId", - new { text = t.text, current = t.newest, userId=t.documentUser, versionId=t.versionId }); + if (Database.DatabaseType.IsSqlCe()) + { + // SQLCE does not support UPDATE...FROM + var documents = Database.Fetch($"SELECT versionId, text, published, newest, documentUser FROM {PreTables.Document}"); + foreach (var t in documents) + Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=@current, userId=@userId WHERE versionId=@versionId", + new { text = t.text, current = t.newest && !t.published, userId = t.documentUser, versionId = t.versionId }); + } + else + { + Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=d.text, {SqlSyntax.GetQuotedColumnName("current")}=(d.newest & ~d.published), userId=d.documentUser +FROM {PreTables.ContentVersion} v INNER JOIN {PreTables.Document} d ON d.versionId = v.versionId"); + } // populate contentVersion text and current columns for non-documents, userId is default - // SQLCE does not support UPDATE...FROM - var temp2 = Database.Fetch($@"SELECT cver.versionId, n.text + if (Database.DatabaseType.IsSqlCe()) + { + // SQLCE does not support UPDATE...FROM + var otherContent = Database.Fetch($@"SELECT cver.versionId, n.text FROM {PreTables.ContentVersion} cver JOIN {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); - foreach (var t in temp2) - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 WHERE versionId=@versionId", - new { text = t.text, versionId=t.versionId }); + foreach (var t in otherContent) + Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 WHERE versionId=@versionId", + new { text = t.text, versionId = t.versionId }); + } + else + { + Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=n.text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 +FROM {PreTables.ContentVersion} cver +JOIN {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id +WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); + } // create table Create.Table(withoutKeysAndIndexes: true).Do(); @@ -186,49 +225,57 @@ SELECT cver.id, doc.templateId, doc.published FROM {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId"); + // need to add extra rows for where published=newest // 'cos INSERT above has inserted the 'published' document version // and v8 always has a 'edited' document version too - var temp3 = Database.Fetch($@"SELECT doc.nodeId, doc.updateDate, doc.documentUser, doc.text, doc.templateId, cver.id versionId + Database.Execute($@" +INSERT INTO {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} (nodeId, versionId, versionDate, userId, {SqlSyntax.GetQuotedColumnName("current")}, text) +SELECT doc.nodeId, NEWID(), doc.updateDate, doc.documentUser, 1, doc.text FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId WHERE doc.newest=1 AND doc.published=1"); - var getIdentity = "@@@@IDENTITY"; - foreach (var t in temp3) - { - Database.Execute($@"INSERT INTO {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} (nodeId, versionId, versionDate, userId, {SqlSyntax.GetQuotedColumnName("current")}, text) -VALUES (@nodeId, @versionId, @versionDate, @userId, 1, @text)", new { nodeId=t.nodeId, versionId=Guid.NewGuid(), versionDate=t.updateDate, userId=t.documentUser, text=t.text }); - var id = Database.ExecuteScalar("SELECT " + getIdentity); - Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET {SqlSyntax.GetQuotedColumnName("current")}=0 WHERE nodeId=@0 AND id<>@1", (int) t.nodeId, id); - Database.Execute($@"INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) -VALUES (@id, @templateId, 0)", new { id=id, templateId=t.templateId }); - var versionId = (int) t.versionId; - var pdatas = Database.Fetch(Sql().Select().From().Where(x => x.VersionId == versionId)); - foreach (var pdata in pdatas) - { - pdata.VersionId = id; - Database.Insert(pdata); - } - } + Database.Execute($@" +INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) +SELECT cverNew.id, doc.templateId, 0 +FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc +JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cverNew ON doc.nodeId = cverNew.nodeId +WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); + + Database.Execute($@" +INSERT INTO {SqlSyntax.GetQuotedTableName(PropertyDataDto.TableName)} (propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,versionId) +SELECT propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,cverNew.id +FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc +JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId +JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cverNew ON doc.nodeId = cverNew.nodeId +JOIN {SqlSyntax.GetQuotedTableName(PropertyDataDto.TableName)} pd ON pd.versionId=cver.id +WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); + // reduce document to 1 row per content Database.Execute($@"DELETE FROM {PreTables.Document} WHERE versionId NOT IN (SELECT (versionId) FROM {PreTables.ContentVersion} WHERE {SqlSyntax.GetQuotedColumnName("current")} = 1) AND (published<>1 OR newest<>1)"); + // ensure that documents with a published version are marked as published + Database.Execute($@"UPDATE {PreTables.Document} SET published=1 WHERE nodeId IN ( +SELECT nodeId FROM {PreTables.ContentVersion} cv INNER JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} dv ON dv.id = cv.id WHERE dv.published=1)"); + // drop some document columns Delete.Column("text").FromTable(PreTables.Document).Do(); Delete.Column("templateId").FromTable(PreTables.Document).Do(); Delete.Column("documentUser").FromTable(PreTables.Document).Do(); + Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("updateDate").Do(); Delete.Column("updateDate").FromTable(PreTables.Document).Do(); Delete.Column("versionId").FromTable(PreTables.Document).Do(); + Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("newest").Do(); Delete.Column("newest").FromTable(PreTables.Document).Do(); // add and populate edited column if (!ColumnExists(PreTables.Document, "edited")) { AddColumn(PreTables.Document, "edited", out var sqls); - Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=0"); + Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=~published"); foreach (var sql in sqls) Database.Execute(sql); } @@ -245,11 +292,15 @@ JOIN {Constants.DatabaseSchema.Tables.PropertyData} v1 ON cv1.id=v1.versionId JOIN {PreTables.ContentVersion} cv2 ON n.id=cv2.nodeId JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} dv ON cv2.id=dv.id AND dv.published=1 JOIN {Constants.DatabaseSchema.Tables.PropertyData} v2 ON cv2.id=v2.versionId -WHERE v1.propertyTypeId=v2.propertyTypeId AND v1.languageId=v2.languageId AND v1.segment=v2.segment"); +WHERE v1.propertyTypeId=v2.propertyTypeId +AND (v1.languageId=v2.languageId OR (v1.languageId IS NULL AND v2.languageId IS NULL)) +AND (v1.segment=v2.segment OR (v1.segment IS NULL AND v2.segment IS NULL))"); + var updatedIds = new HashSet(); foreach (var t in temp) if (t.intValue1 != t.intValue2 || t.decimalValue1 != t.decimalValue2 || t.dateValue1 != t.dateValue2 || t.varcharValue1 != t.varcharValue2 || t.textValue1 != t.textValue2) - Database.Execute("UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=1 WHERE nodeId=@nodeIdd", new { t.id }); + if (updatedIds.Add((int)t.id)) + Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=1 WHERE nodeId=@nodeId", new { nodeId = t.id }); // drop more columns Delete.Column("versionId").FromTable(PreTables.ContentVersion).Do(); @@ -320,7 +371,7 @@ WHERE v1.propertyTypeId=v2.propertyTypeId AND v1.languageId=v2.languageId AND v1 public const string Tag = "cmsTags"; public const string TagRelationship = "cmsTagRelationship"; - + // ReSharper restore UnusedMember.Local } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ChangeNuCacheJsonFormat.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ChangeNuCacheJsonFormat.cs deleted file mode 100644 index 0ceb366e1c..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ChangeNuCacheJsonFormat.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Umbraco.Core.Migrations.PostMigrations; - -namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 -{ - public class ChangeNuCacheJsonFormat : MigrationBase - { - public ChangeNuCacheJsonFormat(IMigrationContext context) : base(context) - { } - - public override void Migrate() - { - // nothing - just adding the post-migration - Context.AddPostMigration(); - } - } -} 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 50c7daf65d..052b72ca26 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -1,9 +1,11 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Migrations.PostMigrations; +using Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Services; @@ -26,53 +28,82 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); var sqlPropertyData = Sql() - .Select(x => x.Id, x => x.TextValue) - .AndSelect(x => x.EditorAlias) - .From() - .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) + .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x.DataTypeDto))) + .From() + .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) + .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) .Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce || x.EditorAlias == Constants.PropertyEditors.Aliases.Grid); - var properties = Database.Fetch(sqlPropertyData); + var properties = Database.Fetch(sqlPropertyData); + var exceptions = new List(); foreach (var property in properties) { var value = property.TextValue; if (string.IsNullOrWhiteSpace(value)) continue; + + bool propertyChanged = false; if (property.PropertyTypeDto.DataTypeDto.EditorAlias == Constants.PropertyEditors.Aliases.Grid) { - var obj = JsonConvert.DeserializeObject(value); - var allControls = obj.SelectTokens("$.sections..rows..areas..controls"); - - foreach (var control in allControls.SelectMany(c => c)) + try { - var controlValue = control["value"]; - if (controlValue.Type == JTokenType.String) + var obj = JsonConvert.DeserializeObject(value); + var allControls = obj.SelectTokens("$.sections..rows..areas..controls"); + + foreach (var control in allControls.SelectMany(c => c).OfType()) { - control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()); + var controlValue = control["value"]; + if (controlValue?.Type == JTokenType.String) + { + control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value(), out var controlChanged); + propertyChanged |= controlChanged; + } } + + property.TextValue = JsonConvert.SerializeObject(obj); + } + catch (JsonException e) + { + exceptions.Add(new InvalidOperationException( + "Cannot deserialize the value as json. This can be because the property editor " + + "type is changed from another type into a grid. Old versions of the value in this " + + "property can have the structure from the old property editor type. This needs to be " + + "changed manually before updating the database.\n" + + $"Property info: Id = {property.Id}, LanguageId = {property.LanguageId}, VersionId = {property.VersionId}, Value = {property.Value}" + , e)); + continue; } - property.TextValue = JsonConvert.SerializeObject(obj); } else { - property.TextValue = UpdateMediaUrls(mediaLinkPattern, value); + property.TextValue = UpdateMediaUrls(mediaLinkPattern, value, out propertyChanged); } - Database.Update(property, x => x.TextValue); + if (propertyChanged) + Database.Update(property); + } + + + if (exceptions.Any()) + { + throw new AggregateException("One or more errors related to unexpected data in grid values occurred.", exceptions); } Context.AddPostMigration(); } - private string UpdateMediaUrls(Regex mediaLinkPattern, string value) + private string UpdateMediaUrls(Regex mediaLinkPattern, string value, out bool changed) { - return mediaLinkPattern.Replace(value, match => + bool matched = false; + + var result = mediaLinkPattern.Replace(value, match => { + matched = true; + // match groups: // - 1 = from the beginning of the a tag until href attribute value begins // - 2 = the href attribute value excluding the querystring (if present) @@ -84,6 +115,10 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 ? match.Value : $"{match.Groups[1].Value}/{{localLink:{media.GetUdi()}}}{match.Groups[3].Value}"; }); + + changed = matched; + + return result; } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs new file mode 100644 index 0000000000..09ea941742 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs @@ -0,0 +1,17 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 +{ + public class FixContentNuCascade : MigrationBase + { + public FixContentNuCascade(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Delete.KeysAndIndexes().Do(); + Create.KeysAndIndexes().Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs new file mode 100644 index 0000000000..c0b9c8f2db --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs @@ -0,0 +1,36 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 +{ + public class RenameUserLoginDtoDateIndex : MigrationBase + { + public RenameUserLoginDtoDateIndex(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + // there has been some confusion with an index name, resulting in + // different names depending on which migration path was followed, + // and discrepancies between an upgraded or an installed database. + // better normalize + + if (IndexExists("IX_umbracoUserLogin_lastValidatedUtc")) + return; + + if (IndexExists("IX_userLoginDto_lastValidatedUtc")) + Delete + .Index("IX_userLoginDto_lastValidatedUtc") + .OnTable(UserLoginDto.TableName) + .Do(); + + Create + .Index("IX_umbracoUserLogin_lastValidatedUtc") + .OnTable(UserLoginDto.TableName) + .OnColumn("lastValidatedUtc") + .Ascending() + .WithOptions().NonClustered() + .Do(); + } + } +} 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..c744409c2f --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs @@ -0,0 +1,33 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 +{ + public class MissingContentVersionsIndexes : MigrationBase + { + private const string IndexName = "IX_" + ContentVersionDto.TableName + "_NodeId"; + + public MissingContentVersionsIndexes(IMigrationContext context) : base(context) + { + } + + public override void Migrate() + { + // We must check before we create an index because if we are upgrading from v7 we force re-create all + // indexes in the whole DB and then this would throw + + if (!IndexExists(IndexName)) + { + Create + .Index(IndexName) + .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/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs new file mode 100644 index 0000000000..fa5116c990 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs @@ -0,0 +1,33 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 +{ + public class MissingDictionaryIndex : MigrationBase + { + public MissingDictionaryIndex(IMigrationContext context) + : base(context) + { + + } + + /// + /// Adds an index to the foreign key column parent on DictionaryDto's table + /// if it doesn't already exist + /// + public override void Migrate() + { + var indexName = "IX_" + DictionaryDto.TableName + "_Parent"; + + if (!IndexExists(indexName)) + { + Create + .Index(indexName) + .OnTable(DictionaryDto.TableName) + .OnColumn("parent") + .Ascending() + .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/ContentDataIntegrityReport.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs new file mode 100644 index 0000000000..9f0f147083 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models +{ + public class ContentDataIntegrityReport + { + public ContentDataIntegrityReport(IReadOnlyDictionary detectedIssues) + { + DetectedIssues = detectedIssues; + } + + public bool Ok => DetectedIssues.Count == 0 || DetectedIssues.Count == DetectedIssues.Values.Count(x => x.Fixed); + + public IReadOnlyDictionary DetectedIssues { get; } + + public IReadOnlyDictionary FixedIssues + => DetectedIssues.Where(x => x.Value.Fixed).ToDictionary(x => x.Key, x => x.Value); + + public enum IssueType + { + /// + /// The item's level and path are inconsistent with it's parent's path and level + /// + InvalidPathAndLevelByParentId, + + /// + /// The item's path doesn't contain all required parts + /// + InvalidPathEmpty, + + /// + /// The item's path parts are inconsistent with it's level value + /// + InvalidPathLevelMismatch, + + /// + /// The item's path does not end with it's own ID + /// + InvalidPathById, + + /// + /// The item's path does not have it's parent Id as the 2nd last entry + /// + InvalidPathByParentId, + } + } +} diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs new file mode 100644 index 0000000000..517b9e80dc --- /dev/null +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Models +{ + public class ContentDataIntegrityReportEntry + { + public ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType issueType) + { + IssueType = issueType; + } + + public ContentDataIntegrityReport.IssueType IssueType { get; } + public bool Fixed { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs new file mode 100644 index 0000000000..c4689467c1 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Models +{ + public class ContentDataIntegrityReportOptions + { + /// + /// Set to true to try to automatically resolve data integrity issues + /// + public bool FixIssues { get; set; } + + // TODO: We could define all sorts of options for the data integrity check like what to check for, what to fix, etc... + // things like Tag data consistency, etc... + } +} 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/DataType.cs b/src/Umbraco.Core/Models/DataType.cs index 8745e6dbec..c237f6381c 100644 --- a/src/Umbraco.Core/Models/DataType.cs +++ b/src/Umbraco.Core/Models/DataType.cs @@ -48,7 +48,16 @@ namespace Umbraco.Core.Models var configuration = Configuration; var json = JsonConvert.SerializeObject(configuration); _editor = value; - Configuration = _editor.GetConfigurationEditor().FromDatabase(json); + + try + { + Configuration = _editor.GetConfigurationEditor().FromDatabase(json); + } + catch (Exception e) + { + throw new InvalidOperationException($"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", e); + } } } @@ -76,7 +85,16 @@ namespace Umbraco.Core.Models if (_hasConfiguration) return _configuration; - _configuration = _editor.GetConfigurationEditor().FromDatabase(_configurationJson); + try + { + _configuration = _editor.GetConfigurationEditor().FromDatabase(_configurationJson); + } + catch (Exception e) + { + throw new InvalidOperationException($"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", e); + } + _hasConfiguration = true; _configurationJson = null; @@ -158,7 +176,18 @@ namespace Umbraco.Core.Models // else, create a Lazy de-serializer var capturedConfiguration = _configurationJson; var capturedEditor = _editor; - return new Lazy(() => capturedEditor.GetConfigurationEditor().FromDatabase(capturedConfiguration)); + return new Lazy(() => + { + try + { + return capturedEditor.GetConfigurationEditor().FromDatabase(capturedConfiguration); + } + catch (Exception e) + { + throw new InvalidOperationException($"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", e); + } + }); } } } diff --git a/src/Umbraco.Core/Models/DataTypeExtensions.cs b/src/Umbraco.Core/Models/DataTypeExtensions.cs index df8a3caea8..f460edbde7 100644 --- a/src/Umbraco.Core/Models/DataTypeExtensions.cs +++ b/src/Umbraco.Core/Models/DataTypeExtensions.cs @@ -35,5 +35,58 @@ namespace Umbraco.Core.Models throw new InvalidCastException($"Cannot cast dataType configuration, of type {configuration.GetType().Name}, to {typeof(T).Name}."); } + + private static readonly ISet IdsOfBuildInDataTypes = new HashSet() + { + Constants.DataTypes.Guids.ContentPickerGuid, + Constants.DataTypes.Guids.MemberPickerGuid, + Constants.DataTypes.Guids.MediaPickerGuid, + Constants.DataTypes.Guids.MultipleMediaPickerGuid, + Constants.DataTypes.Guids.RelatedLinksGuid, + Constants.DataTypes.Guids.MemberGuid, + Constants.DataTypes.Guids.ImageCropperGuid, + Constants.DataTypes.Guids.TagsGuid, + Constants.DataTypes.Guids.ListViewContentGuid, + Constants.DataTypes.Guids.ListViewMediaGuid, + Constants.DataTypes.Guids.ListViewMembersGuid, + Constants.DataTypes.Guids.DatePickerWithTimeGuid, + Constants.DataTypes.Guids.ApprovedColorGuid, + Constants.DataTypes.Guids.DropdownMultipleGuid, + Constants.DataTypes.Guids.RadioboxGuid, + Constants.DataTypes.Guids.DatePickerGuid, + Constants.DataTypes.Guids.DropdownGuid, + Constants.DataTypes.Guids.CheckboxListGuid, + Constants.DataTypes.Guids.CheckboxGuid, + Constants.DataTypes.Guids.NumericGuid, + Constants.DataTypes.Guids.RichtextEditorGuid, + Constants.DataTypes.Guids.TextstringGuid, + Constants.DataTypes.Guids.TextareaGuid, + Constants.DataTypes.Guids.UploadGuid, + Constants.DataTypes.Guids.LabelStringGuid, + Constants.DataTypes.Guids.LabelDecimalGuid, + Constants.DataTypes.Guids.LabelDateTimeGuid, + Constants.DataTypes.Guids.LabelBigIntGuid, + Constants.DataTypes.Guids.LabelTimeGuid, + Constants.DataTypes.Guids.LabelDateTimeGuid, + }; + + /// + /// Returns true if this date type is build-in/default. + /// + /// The data type definition. + /// + internal static bool IsBuildInDataType(this IDataType dataType) + { + return IsBuildInDataType(dataType.Key); + } + + /// + /// Returns true if this date type is build-in/default. + /// + internal static bool IsBuildInDataType(Guid key) + { + return IdsOfBuildInDataTypes.Contains(key); + } + } } diff --git a/src/Umbraco.Core/Models/DeepCloneHelper.cs b/src/Umbraco.Core/Models/DeepCloneHelper.cs index 6470de912b..453b455d4b 100644 --- a/src/Umbraco.Core/Models/DeepCloneHelper.cs +++ b/src/Umbraco.Core/Models/DeepCloneHelper.cs @@ -81,9 +81,10 @@ namespace Umbraco.Core.Models if (propertyInfo.PropertyType.IsGenericType && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>))) { - //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> + //if it is a IEnumerable<>, IReadOnlyCollection, IList or ICollection<> we'll use a List<> since it implements them all var genericType = typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); return new ClonePropertyInfo(propertyInfo) { GenericListType = genericType }; } 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/DocumentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs index 39ece5fa10..8536b1ded3 100644 --- a/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs @@ -3,6 +3,7 @@ using System.Linq; namespace Umbraco.Core.Models.Entities { + /// /// Implements . /// diff --git a/src/Umbraco.Core/Models/Entities/EntityBase.cs b/src/Umbraco.Core/Models/Entities/EntityBase.cs index 43837fa776..cdb3ecebc5 100644 --- a/src/Umbraco.Core/Models/Entities/EntityBase.cs +++ b/src/Umbraco.Core/Models/Entities/EntityBase.cs @@ -81,37 +81,7 @@ namespace Umbraco.Core.Models.Entities _key = Guid.Empty; _hasIdentity = false; } - - /// - /// Updates the entity when it is being saved for the first time. - /// - internal virtual void AddingEntity() - { - var now = DateTime.Now; - - // set the create and update dates, if not already set - if (IsPropertyDirty("CreateDate") == false || _createDate == default) - CreateDate = now; - if (IsPropertyDirty("UpdateDate") == false || _updateDate == default) - UpdateDate = now; - } - - /// - /// Updates the entity when it is being saved. - /// - internal virtual void UpdatingEntity() - { - var now = DateTime.Now; - - // just in case - if (_createDate == default) - CreateDate = now; - - // set the update date if not already set - if (IsPropertyDirty("UpdateDate") == false || _updateDate == default) - UpdateDate = now; - } - + public virtual bool Equals(EntityBase other) { return other != null && (ReferenceEquals(this, other) || SameIdentityAs(other)); diff --git a/src/Umbraco.Core/Models/Entities/EntityExtensions.cs b/src/Umbraco.Core/Models/Entities/EntityExtensions.cs new file mode 100644 index 0000000000..2ee6a2d5ed --- /dev/null +++ b/src/Umbraco.Core/Models/Entities/EntityExtensions.cs @@ -0,0 +1,46 @@ +using System; + +namespace Umbraco.Core.Models.Entities +{ + internal static class EntityExtensions + { + /// + /// Updates the entity when it is being saved. + /// + internal static void UpdatingEntity(this IEntity entity) + { + var now = DateTime.Now; + + // just in case + if (entity.CreateDate == default) + { + entity.CreateDate = now; + } + + // set the update date if not already set + if (entity.UpdateDate == default || (entity is ICanBeDirty canBeDirty && canBeDirty.IsPropertyDirty("UpdateDate") == false)) + { + entity.UpdateDate = now; + } + } + + /// + /// Updates the entity when it is being saved for the first time. + /// + internal static void AddingEntity(this IEntity entity) + { + var now = DateTime.Now; + var canBeDirty = entity as ICanBeDirty; + + // set the create and update dates, if not already set + if (entity.CreateDate == default || canBeDirty?.IsPropertyDirty("CreateDate") == false) + { + entity.CreateDate = now; + } + if (entity.UpdateDate == default || canBeDirty?.IsPropertyDirty("UpdateDate") == false) + { + entity.UpdateDate = now; + } + } + } +} diff --git a/src/Umbraco.Core/Models/Entities/EntitySlim.cs b/src/Umbraco.Core/Models/Entities/EntitySlim.cs index 3b8f997602..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] @@ -111,58 +111,12 @@ namespace Umbraco.Core.Models.Entities public virtual bool IsContainer { get; set; } - /// - /// Represents a lightweight property. - /// - public class PropertySlim - { - /// - /// Initializes a new instance of the class. - /// - public PropertySlim(string editorAlias, object value) - { - PropertyEditorAlias = editorAlias; - Value = value; - } - - /// - /// Gets the property editor alias. - /// - public string PropertyEditorAlias { get; } - - /// - /// Gets the property value. - /// - public object Value { get; } - - protected bool Equals(PropertySlim other) - { - return PropertyEditorAlias.Equals(other.PropertyEditorAlias) && Equals(Value, other.Value); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((PropertySlim) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (PropertyEditorAlias.GetHashCode() * 397) ^ (Value != null ? Value.GetHashCode() : 0); - } - } - } - #region IDeepCloneable /// public object DeepClone() { - throw new WontImplementException(); + throw new InvalidOperationException("This method won't be implemented."); } #endregion @@ -174,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/IDocumentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs index 38fd9a02f1..0258d49114 100644 --- a/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs @@ -2,6 +2,7 @@ namespace Umbraco.Core.Models.Entities { + /// /// Represents a lightweight document entity, managed by the entity service. /// diff --git a/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs new file mode 100644 index 0000000000..9440000146 --- /dev/null +++ b/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Core.Models.Entities +{ + /// + /// Represents a lightweight media entity, managed by the entity service. + /// + public interface IMediaEntitySlim : IContentEntitySlim + { + + /// + /// The media file's path/url + /// + string MediaPath { get; } + } +} 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/MediaEntitySlim.cs b/src/Umbraco.Core/Models/Entities/MediaEntitySlim.cs new file mode 100644 index 0000000000..6292a5a35a --- /dev/null +++ b/src/Umbraco.Core/Models/Entities/MediaEntitySlim.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Models.Entities +{ + /// + /// Implements . + /// + public class MediaEntitySlim : ContentEntitySlim, IMediaEntitySlim + { + public string MediaPath { get; set; } + } +} 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/IImageUrlGenerator.cs b/src/Umbraco.Core/Models/IImageUrlGenerator.cs new file mode 100644 index 0000000000..0dc2933fd9 --- /dev/null +++ b/src/Umbraco.Core/Models/IImageUrlGenerator.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Core.Models +{ + public interface IImageUrlGenerator + { + string GetImageUrl(ImageUrlGenerationOptions options); + } +} 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/ImageUrlGenerationOptions.cs b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs new file mode 100644 index 0000000000..f87657c33d --- /dev/null +++ b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs @@ -0,0 +1,66 @@ +namespace Umbraco.Core.Models +{ + /// + /// These are options that are passed to the IImageUrlGenerator implementation to determine + /// the propery URL that is needed + /// + public class ImageUrlGenerationOptions + { + public ImageUrlGenerationOptions (string imageUrl) + { + ImageUrl = imageUrl; + } + + public string ImageUrl { get; } + public int? Width { get; set; } + public int? Height { get; set; } + public decimal? WidthRatio { get; set; } + public decimal? HeightRatio { get; set; } + public int? Quality { get; set; } + public string ImageCropMode { get; set; } + public string ImageCropAnchor { get; set; } + public bool DefaultCrop { get; set; } + public FocalPointPosition FocalPoint { get; set; } + public CropCoordinates Crop { get; set; } + public string CacheBusterValue { get; set; } + public string FurtherOptions { get; set; } + public bool UpScale { get; set; } = true; + public string AnimationProcessMode { get; set; } + + /// + /// The focal point position, in whatever units the registered IImageUrlGenerator uses, + /// typically a percentage of the total image from 0.0 to 1.0. + /// + public class FocalPointPosition + { + public FocalPointPosition (decimal top, decimal left) + { + Left = left; + Top = top; + } + + public decimal Left { get; } + public decimal Top { get; } + } + + /// + /// The bounds of the crop within the original image, in whatever units the registered + /// IImageUrlGenerator uses, typically a percentage between 0 and 100. + /// + public class CropCoordinates + { + public CropCoordinates (decimal x1, decimal y1, decimal x2, decimal y2) + { + X1 = x1; + Y1 = y1; + X2 = x2; + Y2 = y2; + } + + public decimal X1 { get; } + public decimal Y1 { get; } + public decimal X2 { get; } + public decimal Y2 { get; } + } + } +} diff --git a/src/Umbraco.Core/Models/InstallLog.cs b/src/Umbraco.Core/Models/InstallLog.cs new file mode 100644 index 0000000000..cb14ebd650 --- /dev/null +++ b/src/Umbraco.Core/Models/InstallLog.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Models +{ + public class InstallLog + { + public Guid InstallId { get; } + public bool IsUpgrade { get; set; } + public bool InstallCompleted { get; set; } + public DateTime Timestamp { get; set; } + public int VersionMajor { get; } + public int VersionMinor { get; } + public int VersionPatch { get; } + public string VersionComment { get; } + public string Error { get; } + public string UserAgent { get; } + public string DbProvider { get; set; } + + public InstallLog(Guid installId, bool isUpgrade, bool installCompleted, DateTime timestamp, int versionMajor, int versionMinor, int versionPatch, string versionComment, string error, string userAgent, string dbProvider) + { + InstallId = installId; + IsUpgrade = isUpgrade; + InstallCompleted = installCompleted; + Timestamp = timestamp; + VersionMajor = versionMajor; + VersionMinor = versionMinor; + VersionPatch = versionPatch; + VersionComment = versionComment; + Error = error; + UserAgent = userAgent; + DbProvider = dbProvider; + } + } +} 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 0e91065d56..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; @@ -478,19 +481,6 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _providerUserKey, nameof(ProviderUserKey)); } - - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (ProviderUserKey == null) - ProviderUserKey = Key; - } - /* Internal experiment - only used for mapping queries. * Adding these to have first level properties instead of the Properties collection. */ diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 2fb293c349..3d071b0a18 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -54,7 +54,6 @@ namespace Umbraco.Core.Models.Membership _isLockedOut = false; _startContentIds = new int[] { }; _startMediaIds = new int[] { }; - } /// 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/Property.cs b/src/Umbraco.Core/Models/Property.cs index 76349823ac..1d044aa20d 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -50,7 +50,7 @@ namespace Umbraco.Core.Models /// /// Represents a property value. /// - public class PropertyValue + public class PropertyValue : IDeepCloneable, IEquatable { // TODO: Either we allow change tracking at this class level, or we add some special change tracking collections to the Property // class to deal with change tracking which variants have changed @@ -95,6 +95,32 @@ namespace Umbraco.Core.Models /// public PropertyValue Clone() => new PropertyValue { _culture = _culture, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; + + public object DeepClone() => Clone(); + + public override bool Equals(object obj) + { + return Equals(obj as PropertyValue); + } + + public bool Equals(PropertyValue other) + { + return other != null && + _culture == other._culture && + _segment == other._segment && + EqualityComparer.Default.Equals(EditedValue, other.EditedValue) && + EqualityComparer.Default.Equals(PublishedValue, other.PublishedValue); + } + + public override int GetHashCode() + { + var hashCode = 1885328050; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(_culture); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(_segment); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(EditedValue); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(PublishedValue); + return hashCode; + } } private static readonly DelegateEqualityComparer PropertyValueComparer = new DelegateEqualityComparer( diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 1e950a876c..75c2a7cc00 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -21,12 +21,15 @@ namespace Umbraco.Core.Models private string _alias; private string _description; private int _dataTypeId; + private Guid _dataTypeKey; private Lazy _propertyGroupId; private string _propertyEditorAlias; private ValueStorageType _valueStorageType; private bool _mandatory; + private string _mandatoryMessage; private int _sortOrder; private string _validationRegExp; + private string _validationRegExpMessage; private ContentVariation _variations; /// @@ -113,7 +116,7 @@ namespace Umbraco.Core.Models /// Gets of sets the alias of the property type. /// [DataMember] - public string Alias + public virtual string Alias { get => _alias; set => SetPropertyValueAndDetectChanges(SanitizeAlias(value), ref _alias, nameof(Alias)); @@ -139,6 +142,13 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _dataTypeId, nameof(DataTypeId)); } + [DataMember] + public Guid DataTypeKey + { + get => _dataTypeKey; + set => SetPropertyValueAndDetectChanges(value, ref _dataTypeKey, nameof(DataTypeKey)); + } + /// /// Gets or sets the alias of the property editor for this property type. /// @@ -168,14 +178,15 @@ namespace Umbraco.Core.Models /// /// For generic properties, the value is null. [DataMember] - internal Lazy PropertyGroupId + [DoNotClone] + public Lazy PropertyGroupId { get => _propertyGroupId; set => SetPropertyValueAndDetectChanges(value, ref _propertyGroupId, nameof(PropertyGroupId)); } /// - /// 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 @@ -184,6 +195,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. /// @@ -204,6 +225,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/ILivePublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs index 4027184f3c..39b9f038be 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs @@ -1,5 +1,21 @@ namespace Umbraco.Core.Models.PublishedContent { + /// + /// Provides a live published model creation service. + /// + public interface ILivePublishedModelFactory2 : ILivePublishedModelFactory + { + /// + /// Tells the factory that it should build a new generation of models + /// + void Reset(); + + /// + /// If the live model factory + /// + bool Enabled { get; } + } + /// /// Provides a live published model creation service. /// @@ -13,6 +29,10 @@ /// /// Refreshes the factory. /// + /// + /// This will typically re-compiled models/classes into a new DLL that are used to populate the cache. + /// This is called prior to refreshing the cache. + /// void Refresh(); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index d38c8eb721..4e154bf514 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; namespace Umbraco.Core.Models.PublishedContent { - /// /// /// Represents a published content item. @@ -27,21 +26,13 @@ namespace Umbraco.Core.Models.PublishedContent int Id { get; } /// - /// Gets the name of the content item. + /// Gets the name of the content item for the current culture. /// - /// - /// The value of this property is contextual. When the content type is multi-lingual, - /// this is the name for the 'current' culture. Otherwise, it is the invariant name. - /// string Name { get; } /// - /// Gets the url segment of the content item. + /// Gets the url segment of the content item for the current culture. /// - /// - /// The value of this property is contextual. When the content type is multi-lingual, - /// this is the name for the 'current' culture. Otherwise, it is the invariant url segment. - /// string UrlSegment { get; } /// @@ -72,6 +63,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the name of the user who created the content item. /// + [Obsolete("Use CreatorName(IUserService) extension instead")] string CreatorName { get; } /// @@ -87,6 +79,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the name of the user who last updated the content item. /// + [Obsolete("Use WriterName(IUserService) extension instead")] string WriterName { get; } /// @@ -94,43 +87,29 @@ namespace Umbraco.Core.Models.PublishedContent /// /// /// For published content items, this is also the date the item was published. - /// This date is always global to the content item, see GetCulture().Date for the + /// This date is always global to the content item, see CultureDate() for the /// date each culture was published. /// DateTime UpdateDate { get; } /// - /// Gets the url of the content item. + /// Gets the url of the content item for the current culture. /// /// /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// 'current' culture. Otherwise, it is the invariant url. + /// if any. /// + [Obsolete("Use the Url() extension instead")] string Url { get; } /// - /// Gets the url of the content item. - /// - /// - /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// specified culture. Otherwise, it is the invariant url. - /// - string GetUrl(string culture = null); - - /// - /// Gets culture infos for a culture. - /// - PublishedCultureInfo GetCulture(string culture = null); - - /// - /// Gets culture infos. + /// Gets available culture infos. /// /// /// Contains only those culture that are available. For a published content, these are /// the cultures that are published. For a draft content, those that are 'available' ie /// have a non-empty content name. + /// Does not contain the invariant culture. // fixme? /// IReadOnlyDictionary Cultures { get; } @@ -178,11 +157,15 @@ namespace Umbraco.Core.Models.PublishedContent IPublishedContent Parent { get; } /// - /// Gets the children of the content item. + /// Gets the children of the content item that are available for the current culture. /// - /// Children are sorted by their sortOrder. IEnumerable Children { get; } + /// + /// Gets all the children of the content item, regardless of whether they are available for the current culture. + /// + IEnumerable ChildrenForAllCultures { get; } + #endregion } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs new file mode 100644 index 0000000000..ab6920377c --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Represents an type. + /// + /// Instances implementing the interface should be + /// immutable, ie if the content type changes, then a new instance needs to be created. + public interface IPublishedContentType + { + /// + /// Gets the content type identifier. + /// + int Id { get; } + + /// + /// Gets the content type alias. + /// + string Alias { get; } + + /// + /// Gets the content item type. + /// + PublishedItemType ItemType { get; } + + /// + /// Gets the aliases of the content types participating in the composition. + /// + HashSet CompositionAliases { get; } + + /// + /// Gets the content variations of the content type. + /// + ContentVariation Variations { get; } + + /// + /// Gets a value indicating whether this content type is for an element. + /// + bool IsElement { get; } + + /// + /// Gets the content type properties. + /// + IEnumerable PropertyTypes { get; } + + /// + /// Gets a property type index. + /// + /// The alias is case-insensitive. This is the only place where alias strings are compared. + int GetPropertyIndex(string alias); + + /// + /// Gets a property type. + /// + IPublishedPropertyType GetPropertyType(string alias); + + /// + /// Gets a property type. + /// + IPublishedPropertyType GetPropertyType(int index); + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs index e75e8a4eb9..89009ac7b8 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs @@ -10,7 +10,7 @@ /// /// An content type. /// A published content type corresponding to the item type and content type. - PublishedContentType CreateContentType(IContentTypeComposition contentType); + IPublishedContentType CreateContentType(IContentTypeComposition contentType); /// /// Creates a published property type. @@ -18,7 +18,7 @@ /// The published content type owning the property. /// A property type. /// Is used by constructor to create property types. - PublishedPropertyType CreatePropertyType(PublishedContentType contentType, PropertyType propertyType); + IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, PropertyType propertyType); /// /// Creates a published property type. @@ -28,7 +28,17 @@ /// The datatype identifier. /// The variations. /// Is used by constructor to create special property types. - PublishedPropertyType CreatePropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations); + 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/IPublishedElement.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs index 4b579d824b..4c72dc914a 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the content type. /// - PublishedContentType ContentType { get; } + IPublishedContentType ContentType { get; } #endregion diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs index 9a00e94d3e..2ee7dcb28f 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs @@ -5,7 +5,7 @@ /// public interface IPublishedProperty { - PublishedPropertyType PropertyType { get; } + IPublishedPropertyType PropertyType { get; } /// /// Gets the alias of the property. diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs new file mode 100644 index 0000000000..40f2bf3df2 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs @@ -0,0 +1,108 @@ +using System; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Represents a published property type. + /// + /// Instances implementing the interface should be + /// immutable, ie if the property type changes, then a new instance needs to be created. + public interface IPublishedPropertyType + { + /// + /// Gets the published content type containing the property type. + /// + IPublishedContentType ContentType { get; } + + /// + /// Gets the data type. + /// + PublishedDataType DataType { get; } + + /// + /// Gets property type alias. + /// + string Alias { get; } + + /// + /// Gets the property editor alias. + /// + string EditorAlias { get; } + + /// + /// Gets a value indicating whether the property is a user content property. + /// + /// A non-user content property is a property that has been added to a + /// published content type by Umbraco but does not corresponds to a user-defined + /// published property. + bool IsUserProperty { get; } + + /// + /// Gets the content variations of the property type. + /// + ContentVariation Variations { get; } + + /// + /// Determines whether a value is an actual value, or not a value. + /// + /// Used by property.HasValue and, for instance, in fallback scenarios. + bool? IsValue(object value, PropertyValueLevel level); + + /// + /// Gets the property cache level. + /// + PropertyCacheLevel CacheLevel { get; } + + /// + /// Converts the source value into the intermediate value. + /// + /// The published element owning the property. + /// The source value. + /// A value indicating whether content should be considered draft. + /// The intermediate value. + object ConvertSourceToInter(IPublishedElement owner, object source, bool preview); + + /// + /// Converts the intermediate value into the object value. + /// + /// The published element owning the property. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether content should be considered draft. + /// The object value. + object ConvertInterToObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); + + /// + /// Converts the intermediate value into the XPath value. + /// + /// The published element owning the property. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether content should be considered draft. + /// The XPath value. + /// + /// The XPath value can be either a string or an XPathNavigator. + /// + object ConvertInterToXPath(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); + + /// + /// Gets the property model CLR type. + /// + /// + /// The model CLR type may be a type, or may contain types. + /// For the actual CLR type, see . + /// + Type ModelClrType { get; } + + /// + /// Gets the property CLR type. + /// + /// + /// Returns the actual CLR type which does not contain types. + /// Mapping from may throw if some instances + /// could not be mapped to actual CLR types. + /// + Type ClrType { get; } + } +} \ No newline at end of file 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 0798e9a4e0..458b63ade3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -5,13 +5,13 @@ using System.Linq; namespace Umbraco.Core.Models.PublishedContent { /// - /// Represents an type. + /// Represents an type. /// /// Instances of the class are immutable, ie /// if the content type changes, then a new class needs to be created. - public class PublishedContentType + public class PublishedContentType : IPublishedContentType { - private readonly PublishedPropertyType[] _propertyTypes; + private readonly IPublishedPropertyType[] _propertyTypes; // fast alias-to-index xref containing both the raw alias and its lowercase version private readonly Dictionary _indexes = new Dictionary(); @@ -35,11 +35,10 @@ namespace Umbraco.Core.Models.PublishedContent } /// - /// Initializes a new instance of the with specific values. + /// This constructor is for tests and is not intended to be used directly from application code. /// /// - /// This constructor is for tests and is not intended to be used directly from application code. - /// Values are assumed to be consisted and are not checked. + /// Values are assumed to be consistent and are not checked. /// public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations, bool isElement = false) : this (id, alias, itemType, compositionAliases, variations, isElement) @@ -52,6 +51,20 @@ namespace Umbraco.Core.Models.PublishedContent InitializeIndexes(); } + /// + /// This constructor is for tests and is not intended to be used directly from application code. + /// + /// + /// Values are assumed to be consistent and are not checked. + /// + public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations, bool isElement = false) + : this(id, alias, itemType, compositionAliases, variations, isElement) + { + _propertyTypes = propertyTypes(this).ToArray(); + + InitializeIndexes(); + } + private PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, ContentVariation variations, bool isElement) { Id = id; @@ -75,72 +88,57 @@ namespace Umbraco.Core.Models.PublishedContent // Members have properties such as IMember LastLoginDate which are plain C# properties and not content // properties; they are exposed as pseudo content properties, as long as a content property with the // same alias does not exist already. - private void EnsureMemberProperties(List propertyTypes, IPublishedContentTypeFactory factory) + private void EnsureMemberProperties(List propertyTypes, IPublishedContentTypeFactory factory) { 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.Textarea }, + { "IsApproved", Constants.DataTypes.Boolean }, + { "IsLockedOut", Constants.DataTypes.Boolean }, + { "LastLockoutDate", Constants.DataTypes.LabelDateTime }, + { "CreateDate", Constants.DataTypes.LabelDateTime }, + { "LastLoginDate", Constants.DataTypes.LabelDateTime }, + { "LastPasswordChangeDate", Constants.DataTypes.LabelDateTime } }; #region Content type - /// - /// Gets the content type identifier. - /// + /// public int Id { get; } - /// - /// Gets the content type alias. - /// + /// public string Alias { get; } - /// - /// Gets the content item type. - /// + /// public PublishedItemType ItemType { get; } - /// - /// Gets the aliases of the content types participating in the composition. - /// + /// public HashSet CompositionAliases { get; } - /// - /// Gets the content variations of the content type. - /// + /// public ContentVariation Variations { get; } #endregion #region Properties - /// - /// Gets the content type properties. - /// - public IEnumerable PropertyTypes => _propertyTypes; + /// + public IEnumerable PropertyTypes => _propertyTypes; - /// - /// Gets a property type index. - /// - /// The alias is case-insensitive. This is the only place where alias strings are compared. + /// public int GetPropertyIndex(string alias) { if (_indexes.TryGetValue(alias, out var index)) return index; // fastest @@ -150,10 +148,8 @@ namespace Umbraco.Core.Models.PublishedContent // virtual for unit tests // TODO: explain why - /// - /// Gets a property type. - /// - public virtual PublishedPropertyType GetPropertyType(string alias) + /// + public virtual IPublishedPropertyType GetPropertyType(string alias) { var index = GetPropertyIndex(alias); return GetPropertyType(index); @@ -161,17 +157,13 @@ namespace Umbraco.Core.Models.PublishedContent // virtual for unit tests // TODO: explain why - /// - /// Gets a property type. - /// - public virtual PublishedPropertyType GetPropertyType(int index) + /// + public virtual IPublishedPropertyType GetPropertyType(int index) { return index >= 0 && index < _propertyTypes.Length ? _propertyTypes[index] : null; } - /// - /// Gets a value indicating whether this content type is for an element. - /// + /// public bool IsElement { get; } #endregion diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 2ca3593b55..34094508c3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -26,37 +26,52 @@ namespace Umbraco.Core.Models.PublishedContent } /// - public PublishedContentType CreateContentType(IContentTypeComposition contentType) + public IPublishedContentType CreateContentType(IContentTypeComposition contentType) { return new PublishedContentType(contentType, this); } - // for tests - internal PublishedContentType CreateContentType(int id, string alias, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) + /// + /// This method is for tests and is not intended to be used directly from application code. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedContentType CreateContentType(int id, string alias, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) { return new PublishedContentType(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations, isElement); } - // for tests - internal PublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) + /// + /// This method is for tests and is not intended to be used directly from application code. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) { return new PublishedContentType(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement); } /// - public PublishedPropertyType CreatePropertyType(PublishedContentType contentType, PropertyType propertyType) + public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, PropertyType propertyType) { return new PublishedPropertyType(contentType, propertyType, _propertyValueConverters, _publishedModelFactory, this); } /// - public PublishedPropertyType CreatePropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) + public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) { return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); } - // for tests - internal PublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.Nothing) + /// + 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. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.Nothing) { return new PublishedPropertyType(propertyTypeAlias, dataTypeId, umbraco, variations, _propertyValueConverters, _publishedModelFactory, this); } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index 8bf8cec244..b83dc7a013 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -41,7 +41,7 @@ namespace Umbraco.Core.Models.PublishedContent #region ContentType /// - public virtual PublishedContentType ContentType => _content.ContentType; + public virtual IPublishedContentType ContentType => _content.ContentType; #endregion @@ -94,14 +94,9 @@ namespace Umbraco.Core.Models.PublishedContent public virtual DateTime UpdateDate => _content.UpdateDate; /// + [Obsolete("Use the Url() extension instead")] public virtual string Url => _content.Url; - /// - public virtual string GetUrl(string culture = null) => _content.GetUrl(culture); - - /// - public PublishedCultureInfo GetCulture(string culture = null) => _content.GetCulture(culture); - /// public IReadOnlyDictionary Cultures => _content.Cultures; @@ -124,6 +119,9 @@ namespace Umbraco.Core.Models.PublishedContent /// public virtual IEnumerable Children => _content.Children; + /// + public virtual IEnumerable ChildrenForAllCultures => _content.ChildrenForAllCultures; + #endregion #region Properties diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs index 749b37a41a..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,10 +12,10 @@ namespace Umbraco.Core.Models.PublishedContent /// public PublishedCultureInfo(string culture, string name, string urlSegment, DateTime date) { - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentNullOrEmptyException(nameof(culture)); - 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; + Culture = culture ?? throw new ArgumentNullException(nameof(culture)); Name = name; UrlSegment = urlSegment; Date = date; @@ -30,12 +29,12 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the name of the item. /// - public string Name { get; } + internal string Name { get; } /// /// Gets the url segment of the item. /// - public string UrlSegment { get; } + internal string UrlSegment { get; } /// /// Gets the date associated with the culture. diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs index 1989ac2caf..481b9bd5d2 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs @@ -28,7 +28,7 @@ namespace Umbraco.Core.Models.PublishedContent public IPublishedElement Unwrap() => _content; /// - public PublishedContentType ContentType => _content.ContentType; + public IPublishedContentType ContentType => _content.ContentType; /// public Guid Key => _content.Key; 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/PublishedContent/PublishedPropertyBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs index 5f374f8bc8..e11d2391ec 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Initializes a new instance of the class. /// - protected PublishedPropertyBase(PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel) + protected PublishedPropertyBase(IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel) { PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); ReferenceCacheLevel = referenceCacheLevel; @@ -42,7 +42,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the property type. /// - public PublishedPropertyType PropertyType { get; } + public IPublishedPropertyType PropertyType { get; } /// /// Gets the property reference cache level. diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index 68892fd79a..0c2e62770e 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -10,10 +10,8 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Instances of the class are immutable, ie /// if the property type changes, then a new class needs to be created. - public class PublishedPropertyType + public class PublishedPropertyType : IPublishedPropertyType { - // TODO: API design review, should this be an interface? - private readonly IPublishedModelFactory _publishedModelFactory; private readonly PropertyValueConverterCollection _propertyValueConverters; private readonly object _locker = new object(); @@ -32,7 +30,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// The new published property type belongs to the published content type. /// - public PublishedPropertyType(PublishedContentType contentType, PropertyType propertyType, PropertyValueConverterCollection propertyValueConverters, IPublishedModelFactory publishedModelFactory, IPublishedContentTypeFactory factory) + public PublishedPropertyType(IPublishedContentType contentType, PropertyType propertyType, PropertyValueConverterCollection propertyValueConverters, IPublishedModelFactory publishedModelFactory, IPublishedContentTypeFactory factory) : this(propertyType.Alias, propertyType.DataTypeId, true, propertyType.Variations, propertyValueConverters, publishedModelFactory, factory) { ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); @@ -45,7 +43,7 @@ namespace Umbraco.Core.Models.PublishedContent /// Values are assumed to be consisted and are not checked. /// The new published property type belongs to the published content type. /// - public PublishedPropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, bool isUserProperty, ContentVariation variations, PropertyValueConverterCollection propertyValueConverters, IPublishedModelFactory publishedModelFactory, IPublishedContentTypeFactory factory) + public PublishedPropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, bool isUserProperty, ContentVariation variations, PropertyValueConverterCollection propertyValueConverters, IPublishedModelFactory publishedModelFactory, IPublishedContentTypeFactory factory) : this(propertyTypeAlias, dataTypeId, isUserProperty, variations, propertyValueConverters, publishedModelFactory, factory) { ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); @@ -75,37 +73,22 @@ namespace Umbraco.Core.Models.PublishedContent #region Property type - /// - /// Gets the published content type containing the property type. - /// - public PublishedContentType ContentType { get; internal set; } // internally set by PublishedContentType constructor + /// + public IPublishedContentType ContentType { get; internal set; } // internally set by PublishedContentType constructor - /// - /// Gets the data type. - /// + /// public PublishedDataType DataType { get; } - /// - /// Gets property type alias. - /// + /// public string Alias { get; } - /// - /// Gets the property editor alias. - /// + /// public string EditorAlias => DataType.EditorAlias; - /// - /// Gets a value indicating whether the property is a user content property. - /// - /// A non-user content property is a property that has been added to a - /// published content type by Umbraco but does not corresponds to a user-defined - /// published property. + /// public bool IsUserProperty { get; } - /// - /// Gets the content variations of the property type. - /// + /// public ContentVariation Variations { get; } #endregion @@ -193,10 +176,7 @@ namespace Umbraco.Core.Models.PublishedContent _modelClrType = _converter == null ? typeof (object) : _converter.GetPropertyValueType(this); } - /// - /// Determines whether a value is an actual value, or not a value. - /// - /// Used by property.HasValue and, for instance, in fallback scenarios. + /// public bool? IsValue(object value, PropertyValueLevel level) { if (!_initialized) Initialize(); @@ -209,9 +189,7 @@ namespace Umbraco.Core.Models.PublishedContent return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); } - /// - /// Gets the property cache level. - /// + /// public PropertyCacheLevel CacheLevel { get @@ -221,13 +199,7 @@ namespace Umbraco.Core.Models.PublishedContent } } - /// - /// Converts the source value into the intermediate value. - /// - /// The published element owning the property. - /// The source value. - /// A value indicating whether content should be considered draft. - /// The intermediate value. + /// public object ConvertSourceToInter(IPublishedElement owner, object source, bool preview) { if (!_initialized) Initialize(); @@ -238,14 +210,7 @@ namespace Umbraco.Core.Models.PublishedContent : source; } - /// - /// Converts the intermediate value into the object value. - /// - /// The published element owning the property. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether content should be considered draft. - /// The object value. + /// public object ConvertInterToObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { if (!_initialized) Initialize(); @@ -256,17 +221,7 @@ namespace Umbraco.Core.Models.PublishedContent : inter; } - /// - /// Converts the intermediate value into the XPath value. - /// - /// The published element owning the property. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether content should be considered draft. - /// The XPath value. - /// - /// The XPath value can be either a string or an XPathNavigator. - /// + /// public object ConvertInterToXPath(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { if (!_initialized) Initialize(); @@ -282,13 +237,7 @@ namespace Umbraco.Core.Models.PublishedContent return inter.ToString().Trim(); } - /// - /// Gets the property model CLR type. - /// - /// - /// The model CLR type may be a type, or may contain types. - /// For the actual CLR type, see . - /// + /// public Type ModelClrType { get @@ -298,14 +247,7 @@ namespace Umbraco.Core.Models.PublishedContent } } - /// - /// Gets the property CLR type. - /// - /// - /// Returns the actual CLR type which does not contain types. - /// Mapping from may throw if some instances - /// could not be mapped to actual CLR types. - /// + /// public Type ClrType { get diff --git a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs index 7469222ab0..10f999532f 100644 --- a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs @@ -38,7 +38,7 @@ namespace Umbraco.Core.Models.PublishedContent public override object GetXPathValue(string culture = null, string segment = null) => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _xpathValue.Value : null; - public RawValueProperty(PublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) + public RawValueProperty(IPublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { if (propertyType.Variations != ContentVariation.Nothing) diff --git a/src/Umbraco.Web/Routing/UrlProviderMode.cs b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs similarity index 63% rename from src/Umbraco.Web/Routing/UrlProviderMode.cs rename to src/Umbraco.Core/Models/PublishedContent/UrlMode.cs index ac29aed3fb..4cd6a680f4 100644 --- a/src/Umbraco.Web/Routing/UrlProviderMode.cs +++ b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs @@ -1,14 +1,14 @@ -namespace Umbraco.Web.Routing +namespace Umbraco.Core.Models.PublishedContent { /// - /// Specifies the type of urls that the url provider should produce, Auto is the default + /// Specifies the type of urls that the url provider should produce, Auto is the default. /// - /// - /// The Relative option can lead to invalid results when combined with hostnames, but it is the only way to reproduce - /// the true, pre-4.10, always-relative behavior of Umbraco. - /// - public enum UrlProviderMode + public enum UrlMode { + /// + /// Indicates that the url provider should do what it has been configured to do. + /// + Default = 0, /// /// Indicates that the url provider should produce relative urls exclusively. diff --git a/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs b/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs index b83002fdce..424fc7c4a8 100644 --- a/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs +++ b/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs @@ -23,5 +23,12 @@ /// Gets the segment. /// public string Segment { get; } + + /// + /// Gets the segment for the content item + /// + /// + /// + public virtual string GetSegment(int contentId) => Segment; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs b/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs index 6710d79cc6..a387ea87b8 100644 --- a/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs +++ b/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs @@ -3,13 +3,32 @@ public static class VariationContextAccessorExtensions { public static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, ref string culture, ref string segment) + => variationContextAccessor.ContextualizeVariation(variations, null, ref culture, ref segment); + + public static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, int contentId, ref string culture, ref string segment) + => variationContextAccessor.ContextualizeVariation(variations, (int?)contentId, ref culture, ref segment); + + private static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, int? contentId, ref string culture, ref string segment) { if (culture != null && segment != null) return; // use context values var publishedVariationContext = variationContextAccessor?.VariationContext; if (culture == null) culture = variations.VariesByCulture() ? publishedVariationContext?.Culture : ""; - if (segment == null) segment = variations.VariesBySegment() ? publishedVariationContext?.Segment : ""; + + if (segment == null) + { + if (variations.VariesBySegment()) + { + segment = contentId == null + ? publishedVariationContext?.Segment + : publishedVariationContext?.GetSegment(contentId.Value); + } + else + { + segment = ""; + } + } } } } 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..25156cdb8a 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -14,24 +14,40 @@ namespace Umbraco.Core.Models { private string _name; private string _alias; - private bool _isBidrectional; - private Guid _parentObjectType; - private Guid _childObjectType; + private bool _isBidirectional; + private Guid? _parentObjectType; + private Guid? _childObjectType; - public RelationType(Guid childObjectType, Guid parentObjectType, string alias) + [Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")] + public RelationType(string alias, string name) + : this(name: name, alias: alias, false, null, null) { - if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentNullOrEmptyException(nameof(alias)); - _childObjectType = childObjectType; - _parentObjectType = parentObjectType; - _alias = alias; - Name = _alias; } - public RelationType(Guid childObjectType, Guid parentObjectType, string alias, string name) - : this(childObjectType, parentObjectType, alias) + public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType) + { + 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 (alias == null) throw new ArgumentNullException(nameof(alias)); + if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias)); + + _name = name; + _alias = alias; + _isBidirectional = isBidrectional; + _parentObjectType = parentObjectType; + _childObjectType = childObjectType; + } + + [Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")] + public RelationType(Guid childObjectType, Guid parentObjectType, string alias) + : this(name: alias, alias: alias, isBidrectional: false, parentObjectType: parentObjectType, childObjectType: childObjectType) + { + } + + [Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")] + public RelationType(Guid childObjectType, Guid parentObjectType, string alias, string name) + : this(name: name, alias: alias, isBidrectional: false, parentObjectType: parentObjectType, childObjectType: childObjectType) { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); - Name = name; } /// @@ -60,8 +76,8 @@ namespace Umbraco.Core.Models [DataMember] public bool IsBidirectional { - get => _isBidrectional; - set => SetPropertyValueAndDetectChanges(value, ref _isBidrectional, nameof(IsBidirectional)); + get => _isBidirectional; + set => SetPropertyValueAndDetectChanges(value, ref _isBidirectional, nameof(IsBidirectional)); } /// @@ -69,7 +85,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 +96,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/UpgradeResult.cs b/src/Umbraco.Core/Models/UpgradeResult.cs new file mode 100644 index 0000000000..a27f6bb6a3 --- /dev/null +++ b/src/Umbraco.Core/Models/UpgradeResult.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Models +{ + public class UpgradeResult + { + public string UpgradeType { get; } + public string Comment { get; } + public string UpgradeUrl { get; } + + public UpgradeResult(string upgradeType, string comment, string upgradeUrl) + { + UpgradeType = upgradeType; + Comment = comment; + UpgradeUrl = upgradeUrl; + } + } +} diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 0f83cf78a4..5be66bac47 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 @@ -106,13 +106,14 @@ namespace Umbraco.Core.Models //use the custom avatar var avatarUrl = Current.MediaFileSystem.GetUrl(user.Avatar); + var urlGenerator = Current.ImageUrlGenerator; return new[] { - avatarUrl + "?width=30&height=30&mode=crop", - avatarUrl + "?width=60&height=60&mode=crop", - avatarUrl + "?width=90&height=90&mode=crop", - avatarUrl + "?width=150&height=150&mode=crop", - avatarUrl + "?width=300&height=300&mode=crop" + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 30, Height = 30 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 60, Height = 60 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 90, Height = 90 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 150, Height = 150 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 300, Height = 300 }) }; } @@ -193,19 +194,6 @@ namespace Umbraco.Core.Models return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } - internal static bool IsInBranchOfStartNode(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId, out bool hasPathAccess) - { - switch (recycleBinId) - { - case Constants.System.RecycleBinMedia: - return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess); - case Constants.System.RecycleBinContent: - return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess); - default: - throw new NotSupportedException("Path access is only determined on content or media"); - } - } - /// /// Determines whether this user has access to view sensitive data /// 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/ConflictingPackageData.cs b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs index 82693677fb..33cb9c40d3 100644 --- a/src/Umbraco.Core/Packaging/ConflictingPackageData.cs +++ b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Packaging { - internal class ConflictingPackageData + internal class ConflictingPackageData { private readonly IMacroService _macroService; private readonly IFileService _fileService; @@ -23,7 +23,7 @@ namespace Umbraco.Core.Packaging return stylesheetNodes .Select(n => { - var xElement = n.Element("Name") ?? n.Element("name"); ; + var xElement = n.Element("Name") ?? n.Element("name"); if (xElement == null) throw new FormatException("Missing \"Name\" element"); 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/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index 72954b238d..5549b502fa 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -82,7 +82,7 @@ namespace Umbraco.Core.Packaging { var packagesXml = EnsureStorage(out _); if (packagesXml?.Root == null) - yield break;; + yield break; foreach (var packageXml in packagesXml.Root.Elements("package")) yield return _parser.ToPackageDefinition(packageXml); @@ -139,7 +139,7 @@ namespace Umbraco.Core.Packaging var updatedXml = _parser.ToXml(definition); packageXml.ReplaceWith(updatedXml); } - + packagesXml.Save(packagesFile); return true; @@ -212,7 +212,7 @@ namespace Umbraco.Core.Packaging compiledPackageXml.Save(packageXmlFileName); // check if there's a packages directory below media - + if (Directory.Exists(IOHelper.MapPath(_mediaFolderPath)) == false) Directory.CreateDirectory(IOHelper.MapPath(_mediaFolderPath)); @@ -510,7 +510,6 @@ namespace Umbraco.Core.Packaging private XElement GetStylesheetXml(string name, bool includeProperties) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); -; var sts = _fileService.GetStylesheetByName(name); if (sts == null) return null; var stylesheetXml = new XElement("Stylesheet"); @@ -562,7 +561,7 @@ namespace Umbraco.Core.Packaging package.Add(new XElement("url", definition.Url)); var requirements = new XElement("requirements"); - + requirements.Add(new XElement("major", definition.UmbracoVersion == null ? UmbracoVersion.SemanticVersion.Major.ToInvariantString() : definition.UmbracoVersion.Major.ToInvariantString())); requirements.Add(new XElement("minor", definition.UmbracoVersion == null ? UmbracoVersion.SemanticVersion.Minor.ToInvariantString() : definition.UmbracoVersion.Minor.ToInvariantString())); requirements.Add(new XElement("patch", definition.UmbracoVersion == null ? UmbracoVersion.SemanticVersion.Patch.ToInvariantString() : definition.UmbracoVersion.Build.ToInvariantString())); @@ -589,7 +588,7 @@ namespace Umbraco.Core.Packaging contributors.Add(new XElement("contributor", contributor)); } } - + info.Add(contributors); info.Add(new XElement("readme", new XCData(definition.Readme))); 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/DatabaseAnnotations/ForeignKeyAttribute.cs b/src/Umbraco.Core/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs index fceb32d609..2137b2ff44 100644 --- a/src/Umbraco.Core/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs +++ b/src/Umbraco.Core/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Data; namespace Umbraco.Core.Persistence.DatabaseAnnotations { @@ -9,11 +10,17 @@ namespace Umbraco.Core.Persistence.DatabaseAnnotations public class ForeignKeyAttribute : ReferencesAttribute { public ForeignKeyAttribute(Type type) : base(type) - { - } + { } - internal string OnDelete { get; set; } - internal string OnUpdate { get; set; } + /// + /// Gets or sets the cascade rule for deletions. + /// + public Rule OnDelete { get; set; } = Rule.None; + + /// + /// Gets or sets the cascade rule for updates. + /// + public Rule OnUpdate { get; set; } = Rule.None; /// /// Gets or sets the name of the foreign key reference diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index e2d3ac97ae..5925e58afc 100644 --- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -134,7 +134,9 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions { Name = foreignKeyName, ForeignTable = tableName, - PrimaryTable = referencedTable.Value + PrimaryTable = referencedTable.Value, + OnDelete = attribute.OnDelete, + OnUpdate = attribute.OnUpdate }; definition.ForeignColumns.Add(columnName); definition.PrimaryColumns.Add(referencedColumn); 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/ContentNuDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs index eea3d5d537..c6269d5317 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs @@ -1,4 +1,5 @@ -using NPoco; +using System.Data; +using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos @@ -10,7 +11,7 @@ namespace Umbraco.Core.Persistence.Dtos { [Column("nodeId")] [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsContentNu", OnColumns = "nodeId, published")] - [ForeignKey(typeof(ContentDto), Column = "nodeId")] + [ForeignKey(typeof(ContentDto), Column = "nodeId", OnDelete = Rule.Cascade)] public int NodeId { get; set; } [Column("published")] 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/DictionaryDto.cs b/src/Umbraco.Core/Persistence/Dtos/DictionaryDto.cs index 655474b217..d357e9adbc 100644 --- a/src/Umbraco.Core/Persistence/Dtos/DictionaryDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/DictionaryDto.cs @@ -5,11 +5,12 @@ using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos { - [TableName(Constants.DatabaseSchema.Tables.DictionaryEntry)] + [TableName(TableName)] [PrimaryKey("pk")] [ExplicitColumns] internal class DictionaryDto { + public const string TableName = Constants.DatabaseSchema.Tables.DictionaryEntry; [Column("pk")] [PrimaryKeyColumn] public int PrimaryKey { get; set; } @@ -21,6 +22,7 @@ namespace Umbraco.Core.Persistence.Dtos [Column("parent")] [NullSetting(NullSetting = NullSettings.Null)] [ForeignKey(typeof(DictionaryDto), Column = "id")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Parent")] public Guid? Parent { get; set; } [Column("key")] 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/Dtos/UserLoginDto.cs b/src/Umbraco.Core/Persistence/Dtos/UserLoginDto.cs index d7d02631b7..8bf254ea31 100644 --- a/src/Umbraco.Core/Persistence/Dtos/UserLoginDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/UserLoginDto.cs @@ -37,7 +37,7 @@ namespace Umbraco.Core.Persistence.Dtos /// [Column("lastValidatedUtc")] [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.NonClustered, Name = "IX_userLoginDto_lastValidatedUtc")] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUserLogin_lastValidatedUtc")] public DateTime LastValidatedUtc { 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/DataTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs index f5144e34eb..f189d38d05 100644 --- a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs @@ -15,7 +15,8 @@ namespace Umbraco.Core.Persistence.Factories { if (!editors.TryGet(dto.EditorAlias, out var editor)) { - logger.Warn(typeof(DataTypeFactory), "Could not find an editor with alias {EditorAlias}, converting to label", dto.EditorAlias); + logger.Warn(typeof(DataType), "Could not find an editor with alias {EditorAlias}, treating as Label." + +" The site may fail to boot and / or load data types and run.", dto.EditorAlias); //convert to label editor = new LabelPropertyEditor(logger); } diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index f1473b5888..92d397520e 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,34 +134,48 @@ namespace Umbraco.Core.Persistence.Factories // publishing = deal with edit and published values foreach (var propertyValue in property.Values) { + var isInvariantValue = propertyValue.Culture == null && propertyValue.Segment == null; + var isCultureValue = propertyValue.Culture != null; + var isSegmentValue = propertyValue.Segment != null; + // deal with published value - if (propertyValue.PublishedValue != null && publishedVersionId > 0) + if ((propertyValue.PublishedValue != null || isSegmentValue) && publishedVersionId > 0) propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue)); // deal with edit value - if (propertyValue.EditedValue != null) + if (propertyValue.EditedValue != null || isSegmentValue) 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 +185,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 c134748047..dc1629e8f7 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs @@ -54,13 +54,16 @@ namespace Umbraco.Core.Persistence.Factories propertyType.Alias = typeDto.Alias; propertyType.DataTypeId = typeDto.DataTypeId; + propertyType.DataTypeKey = typeDto.DataTypeDto.NodeDto.UniqueId; propertyType.Description = typeDto.Description; propertyType.Id = typeDto.Id; 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; @@ -123,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..177a0494a2 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.ParentObjectType, dto.ChildObjectType); 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/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index dae70d502f..c64eb14bb7 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -36,6 +36,13 @@ namespace Umbraco.Core.Persistence.Factories user.InvitedDate = dto.InvitedDate; user.TourData = dto.TourData; + // we should never get user with ID zero from database, except + // when upgrading from v7 - mark that user so that we do not + // save it back to database (as that would create a *new* user) + // see also: UserRepository.PersistNewItem + if (dto.Id == 0) + user.AdditionalData["IS_V7_ZERO"] = true; + // reset dirty initial properties (U4-1946) user.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/IUmbracoDatabaseFactory.cs b/src/Umbraco.Core/Persistence/IUmbracoDatabaseFactory.cs index 0236fc4bd5..c2d65b824f 100644 --- a/src/Umbraco.Core/Persistence/IUmbracoDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/IUmbracoDatabaseFactory.cs @@ -10,22 +10,35 @@ namespace Umbraco.Core.Persistence /// /// Creates a new database. /// - /// The new database must be disposed after being used. + /// + /// The new database must be disposed after being used. + /// Creating a database causes the factory to initialize if it is not already initialized. + /// IUmbracoDatabase CreateDatabase(); /// - /// Gets a value indicating whether the database factory is configured. + /// Gets a value indicating whether the database factory is configured, i.e. whether + /// its connection string and provider name have been set. The factory may however not + /// be initialized (see ). /// bool Configured { get; } + /// + /// Gets a value indicating whether the database factory is initialized, i.e. whether + /// its internal state is ready and it has been possible to connect to the database. + /// + bool Initialized { get; } + /// /// Gets the connection string. /// - /// Throws if the factory is not configured. + /// May return null if the database factory is not configured. string ConnectionString { get; } /// - /// Gets a value indicating whether the database can connect. + /// Gets a value indicating whether the database factory is configured (see ), + /// and it is possible to connect to the database. The factory may however not be initialized (see + /// ). /// bool CanConnect { get; } @@ -37,6 +50,9 @@ namespace Umbraco.Core.Persistence /// /// Gets the Sql context. /// + /// + /// Getting the Sql context causes the factory to initialize if it is not already initialized. + /// ISqlContext SqlContext { get; } /// diff --git a/src/Umbraco.Core/Persistence/Mappers/AuditItemMapper.cs b/src/Umbraco.Core/Persistence/Mappers/AuditItemMapper.cs index 853cd9f99e..48e7afdc7e 100644 --- a/src/Umbraco.Core/Persistence/Mappers/AuditItemMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/AuditItemMapper.cs @@ -18,7 +18,8 @@ namespace Umbraco.Core.Persistence.Mappers DefineMap(nameof(AuditItem.Id), nameof(LogDto.NodeId)); DefineMap(nameof(AuditItem.CreateDate), nameof(LogDto.Datestamp)); DefineMap(nameof(AuditItem.UserId), nameof(LogDto.UserId)); - DefineMap(nameof(AuditItem.AuditType), nameof(LogDto.Header)); + // we cannot map that one - because AuditType is an enum but Header is a string + //DefineMap(nameof(AuditItem.AuditType), nameof(LogDto.Header)); DefineMap(nameof(AuditItem.Comment), nameof(LogDto.Comment)); } } 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 c9d85feb25..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. @@ -232,6 +246,14 @@ namespace Umbraco.Core.Persistence using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) { BulkCopyTimeout = 10000, DestinationTableName = tableName }) using (var bulkReader = new PocoDataDataReader(records, pocoData, syntax)) { + //we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared + //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) + { + copy.ColumnMappings.Add(col.DestinationColumn, col.DestinationColumn); + } + copy.WriteToServer(bulkReader); return bulkReader.RecordsAffected; } diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs index acfa51f895..152dcbe6d3 100644 --- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs @@ -18,6 +18,48 @@ namespace Umbraco.Core.Persistence /// public static partial class NPocoDatabaseExtensions { + /// + /// Iterates over the result of a paged data set with a db reader + /// + /// + /// + /// + /// The number of rows to load per page + /// + /// + /// + /// + /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to + /// iterate over each row with a reader using Query vs Fetch. + /// + internal static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) + { + var sqlString = sql.SQL; + var sqlArgs = sql.Arguments; + + int? itemCount = null; + long pageIndex = 0; + do + { + // Get the paged queries + database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var sqlCount, out var sqlPage); + + // get the item count once + if (itemCount == null) + { + itemCount = database.ExecuteScalar(sqlCount, sqlArgs); + } + pageIndex++; + + // iterate over rows without allocating all items to memory (Query vs Fetch) + foreach (var row in database.Query(sqlPage, sqlArgs)) + { + yield return row; + } + + } while ((pageIndex * pageSize) < itemCount); + } + // NOTE // // proper way to do it with TSQL and SQLCE diff --git a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs index 03d82a345f..c87937e41e 100644 --- a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs +++ b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs @@ -85,6 +85,17 @@ namespace Umbraco.Core.Persistence.Querying // I'm just unsure right now due to time constraints how to make it correct. It won't matter right now and has been working already with this bug but I've // only just discovered what it is actually doing. + // TODO + // in most cases we want to convert the value to a plain object, + // but for in some rare cases, we may want to do it differently, + // for instance a Models.AuditType (an enum) may in some cases + // need to be converted to its string value. + // but - we cannot have specific code here, really - and how would + // we configure this? is it even possible? + /* + var toString = typeof(object).GetMethod("ToString"); + var member = Expression.Call(m, toString); + */ var member = Expression.Convert(m, typeof(object)); var lambda = Expression.Lambda>(member); var getter = lambda.Compile(); diff --git a/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs index 7c8a82bb85..b2dd6a3297 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs @@ -33,5 +33,7 @@ namespace Umbraco.Core.Persistence.Repositories Direction orderDirection, AuditType[] auditTypeFilter, IQuery customFilter); + + IEnumerable Get(AuditType type, IQuery query); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs index 217719e144..ad9e2d27c1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; @@ -77,5 +78,7 @@ namespace Umbraco.Core.Persistence.Repositories /// Here, can be null but cannot. IEnumerable GetPage(IQuery query, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering); + + ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs index 69b0698a96..4020244733 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs @@ -26,5 +26,17 @@ namespace Umbraco.Core.Persistence.Repositories /// /// bool HasContainerInPath(string contentPath); + + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(params int[] ids); + + /// + /// 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/IInstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs new file mode 100644 index 0000000000..fbc7d2cfbc --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IInstallationRepository + { + Task SaveInstallLogAsync(InstallLog installLog); + } +} 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/IUpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs new file mode 100644 index 0000000000..6d56994781 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Semver; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IUpgradeCheckRepository + { + Task CheckUpgradeAsync(SemVersion version); + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditEntryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditEntryRepository.cs index 1486935e2a..c3d34cc3e9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditEntryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditEntryRepository.cs @@ -100,7 +100,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected override void PersistNewItem(IAuditEntry entity) { - ((EntityBase) entity).AddingEntity(); + entity.AddingEntity(); var dto = AuditEntryFactory.BuildDto(entity); Database.Insert(dto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs index cda89fd89a..4cb533e86f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs @@ -74,6 +74,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); } + public IEnumerable Get(AuditType type, IQuery query) + { + var sqlClause = GetBaseQuery(false) + .Where("(logHeader=@0)", type.ToString()); + + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + + var dtos = Database.Fetch(sql); + + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); + } + protected override Sql GetBaseQuery(bool isCount) { var sql = SqlContext.Sql(); @@ -162,7 +175,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/ConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs index 8df9bf686d..57d5dfa864 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs @@ -69,7 +69,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected override void PersistNewItem(IConsent entity) { - ((EntityBase) entity).AddingEntity(); + entity.AddingEntity(); var dto = ConsentFactory.BuildDto(entity); Database.Insert(dto); @@ -80,7 +80,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected override void PersistUpdatedItem(IConsent entity) { - ((EntityBase) entity).UpdatingEntity(); + entity.UpdatingEntity(); var dto = ConsentFactory.BuildDto(entity); Database.Update(dto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index aeb4c3774f..845006891d 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 @@ -60,7 +83,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets all version ids, current first public virtual IEnumerable GetVersionIds(int nodeId, int maxRows) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersionIds", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql => tsql.Select(x => x.Id) .From() .Where(x => x.NodeId == SqlTemplate.Arg("nodeId")) @@ -76,7 +99,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // TODO: test object node type? // get the version we want to delete - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersion", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersion, tsql => tsql.Select().From().Where(x => x.Id == SqlTemplate.Arg("versionId")) ); var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); @@ -98,7 +121,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // TODO: test object node type? // get the versions we want to delete, excluding the current one - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersions", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersions, tsql => tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) ); var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); @@ -380,7 +403,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // content type alias is invariant - if(ordering.OrderBy.InvariantEquals("contentTypeAlias")) + if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) { var joins = Sql() .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype"); @@ -454,6 +477,123 @@ namespace Umbraco.Core.Persistence.Repositories.Implement IQuery filter, Ordering ordering); + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) + { + var report = new Dictionary(); + + var sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + var nodesToRebuild = new Dictionary>(); + var validNodes = new Dictionary(); + var rootIds = new[] {Constants.System.Root, Constants.System.RecycleBinContent, Constants.System.RecycleBinMedia}; + var currentParentIds = new HashSet(rootIds); + var prevParentIds = currentParentIds; + var lastLevel = -1; + + // use a forward cursor (query) + foreach (var node in Database.Query(sql)) + { + if (node.Level != lastLevel) + { + // changing levels + prevParentIds = currentParentIds; + currentParentIds = null; + lastLevel = node.Level; + } + + if (currentParentIds == null) + { + // we're reset + currentParentIds = new HashSet(); + } + + currentParentIds.Add(node.NodeId); + + // paths parts without the roots + var pathParts = node.Path.Split(',').Where(x => !rootIds.Contains(int.Parse(x))).ToArray(); + + if (!prevParentIds.Contains(node.ParentId)) + { + // invalid, this will be because the level is wrong (which prob means path is wrong too) + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathAndLevelByParentId)); + AppendNodeToFix(nodesToRebuild, node); + } + else if (pathParts.Length == 0) + { + // invalid path + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty)); + AppendNodeToFix(nodesToRebuild, node); + } + else if (pathParts.Length != node.Level) + { + // invalid, either path or level is wrong + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathLevelMismatch)); + AppendNodeToFix(nodesToRebuild, node); + } + else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString()) + { + // invalid path + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathById)); + AppendNodeToFix(nodesToRebuild, node); + } + else if (!rootIds.Contains(node.ParentId) && pathParts[pathParts.Length - 2] != node.ParentId.ToString()) + { + // invalid path + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathByParentId)); + AppendNodeToFix(nodesToRebuild, node); + } + else + { + // it's valid! + + // don't track unless we are configured to fix + if (options.FixIssues) + validNodes.Add(node.NodeId, node); + } + } + + var updated = new List(); + + if (options.FixIssues) + { + // iterate all valid nodes to see if these are parents for invalid nodes + foreach (var (nodeId, node) in validNodes) + { + if (!nodesToRebuild.TryGetValue(nodeId, out var invalidNodes)) continue; + + // now we can try to rebuild the invalid paths. + + foreach (var invalidNode in invalidNodes) + { + invalidNode.Level = (short)(node.Level + 1); + invalidNode.Path = node.Path + "," + invalidNode.NodeId; + updated.Add(invalidNode); + } + } + + foreach (var node in updated) + { + Database.Update(node); + if (report.TryGetValue(node.NodeId, out var entry)) + entry.Fixed = true; + } + } + + return new ContentDataIntegrityReport(report); + } + + private static void AppendNodeToFix(IDictionary> nodesToRebuild, NodeDto node) + { + if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds)) + childIds.Add(node); + else + nodesToRebuild[node.ParentId] = new List { node }; + } + // here, filter can be null and ordering cannot protected IEnumerable GetPage(IQuery query, long pageIndex, int pageSize, out long totalRecords, @@ -512,31 +652,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(); @@ -762,7 +887,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.EnsureUniqueNodeName", tsql => tsql + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name")) .From() .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId"))); @@ -775,7 +900,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual int GetNewChildSortOrder(int parentId, int first) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetSortOrder", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql => tsql.Select($"COALESCE(MAX(sortOrder),{first - 1})").From().Where(x => x.ParentId == SqlTemplate.Arg("parentId") && x.NodeObjectType == NodeObjectTypeId) ); @@ -784,7 +909,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual NodeDto GetParentNodeDto(int parentId) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetParentNode", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql => tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("parentId")) ); @@ -793,7 +918,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual int GetReservedId(Guid uniqueId) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetReservedId", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql => tsql.Select(x => x.NodeId).From().Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation) ); var id = Database.ExecuteScalar(template.Sql(new { uniqueId = uniqueId })); @@ -812,5 +937,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 645ab9f924..7781e2e38a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -3,10 +3,12 @@ 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; using Umbraco.Core.Scoping; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -89,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 @@ -140,10 +142,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement while (templateDtoIx < templateDtos.Count && templateDtos[templateDtoIx].ContentTypeNodeId == contentType.Id) { var allowedDto = templateDtos[templateDtoIx]; + templateDtoIx++; if (!templates.TryGetValue(allowedDto.TemplateNodeId, out var template)) continue; allowedTemplates.Add(template); - templateDtoIx++; - + if (allowedDto.IsDefault) defaultTemplateId = template.Id; } @@ -188,10 +190,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var groupDtos = Database.Fetch(sql1); var sql2 = Sql() - .Select(r => r.Select(x => x.DataTypeDto)) + .Select(r => r.Select(x => x.DataTypeDto, r1 => r1.Select(x => x.NodeDto))) .AndSelect() .From() .InnerJoin().On((pt, dt) => pt.DataTypeId == dt.NodeId) + .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) .InnerJoin().On((pt, ct) => pt.ContentTypeId == ct.NodeId) .LeftJoin().On((pt, ptg) => pt.PropertyTypeGroupId == ptg.Id) .LeftJoin().On((pt, mpt) => pt.Id == mpt.PropertyTypeId) @@ -290,13 +293,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { Description = dto.Description, DataTypeId = dto.DataTypeId, + DataTypeKey = dto.DataTypeDto.NodeDto.UniqueId, 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 98ddcdcb17..359b967dab 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -3,8 +3,10 @@ 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; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; @@ -17,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; @@ -55,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(); } @@ -230,7 +232,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement throw ex; } - ((ContentType)entity).AddingEntity(); + entity.AddingEntity(); PersistNewBaseContentType(entity); PersistTemplates(entity, false); @@ -270,7 +272,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ValidateAlias(entity); //Updates Modified date - ((ContentType)entity).UpdatingEntity(); + entity.UpdatingEntity(); //Look up parent to get and set the correct Path if ParentId has changed if (entity.IsPropertyDirty("ParentId")) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 591fa2b660..357798a8a9 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) @@ -1013,8 +1228,9 @@ AND umbracoNode.id <> @id", if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false) { var sql = Sql() - .SelectAll() + .Select(dt => dt.Select(x => x.NodeDto)) .From() + .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) .Where("propertyEditorAlias = @propertyEditorAlias", new { propertyEditorAlias = propertyType.PropertyEditorAlias }) .OrderBy(typeDto => typeDto.NodeId); var datatype = Database.FirstOrDefault(sql); @@ -1022,6 +1238,7 @@ AND umbracoNode.id <> @id", if (datatype != null) { propertyType.DataTypeId = datatype.NodeId; + propertyType.DataTypeKey = datatype.NodeDto.UniqueId; } else { @@ -1092,20 +1309,33 @@ WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", return test; } - /// - /// Given the path of a content item, this will return true if the content item exists underneath a list view content item - /// - /// - /// + /// public bool HasContainerInPath(string contentPath) { - var ids = contentPath.Split(',').Select(int.Parse); + var ids = contentPath.Split(',').Select(int.Parse).ToArray(); + return HasContainerInPath(ids); + } + + /// + public bool HasContainerInPath(params int[] ids) + { var sql = new Sql($@"SELECT COUNT(*) FROM cmsContentType INNER JOIN {Constants.DatabaseSchema.Tables.Content} ON cmsContentType.nodeId={Constants.DatabaseSchema.Tables.Content}.contentTypeId WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", new { ids, isContainer = true }); 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 02ba4d1c13..9ccf6e9623 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; @@ -106,7 +107,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IDataType entity) { - ((DataType)entity).AddingEntity(); + entity.AddingEntity(); //ensure a datatype has a unique name before creating it entity.Name = EnsureUniqueNodeName(entity.Name); @@ -174,7 +175,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } //Updates Modified date - ((DataType)entity).UpdatingEntity(); + entity.UpdatingEntity(); //Look up parent to get and set the correct Path if ParentId has changed if (entity.IsPropertyDirty("ParentId")) @@ -278,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 @@ -290,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/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs index be1e28fcc1..0b58663952 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -148,7 +148,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IDictionaryItem entity) { - ((EntityBase)entity).UpdatingEntity(); + entity.UpdatingEntity(); foreach (var translation in entity.Translations) translation.Value = translation.Value.ToValidXmlString(); 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 6c08e05995..db1e2b350d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -3,14 +3,15 @@ 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; using Umbraco.Core.Models.Membership; 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; @@ -29,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)); @@ -205,7 +221,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.AccessRule + " WHERE accessId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id OR loginNodeId = @id OR noAccessNodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE loginNodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE noAccessNodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" }; return list; @@ -225,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) @@ -232,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) @@ -244,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 @@ -260,21 +345,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IContent entity) { - // TODO: https://github.com/umbraco/Umbraco-CMS/issues/4234 - sort out IContent vs Content - // however, it's not just so we have access to AddingEntity - // there are tons of things at the end of the methods, that can only work with a true Content - // and basically, the repository requires a Content, not an IContent - var content = (Content)entity; + entity.AddingEntity(); - content.AddingEntity(); - var publishing = content.PublishedState == PublishedState.Publishing; + var publishing = entity.PublishedState == PublishedState.Publishing; // ensure that the default template is assigned if (entity.TemplateId.HasValue == false) entity.TemplateId = entity.ContentType.DefaultTemplate?.Id; // sanitize names - SanitizeNames(content, publishing); + SanitizeNames(entity, publishing); // ensure that strings don't contain characters that are invalid in xml // TODO: do we really want to keep doing this here? @@ -324,11 +404,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = !publishing; Database.Insert(contentVersionDto); - content.VersionId = contentVersionDto.Id; + entity.VersionId = contentVersionDto.Id; // persist the document version dto var documentVersionDto = dto.DocumentVersionDto; - documentVersionDto.Id = content.VersionId; + documentVersionDto.Id = entity.VersionId; if (publishing) documentVersionDto.Published = true; Database.Insert(documentVersionDto); @@ -336,62 +416,62 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // and again in case we're publishing immediately if (publishing) { - content.PublishedVersionId = content.VersionId; + entity.PublishedVersionId = entity.VersionId; contentVersionDto.Id = 0; contentVersionDto.Current = true; - contentVersionDto.Text = content.Name; + contentVersionDto.Text = entity.Name; Database.Insert(contentVersionDto); - content.VersionId = contentVersionDto.Id; + entity.VersionId = contentVersionDto.Id; - documentVersionDto.Id = content.VersionId; + documentVersionDto.Id = entity.VersionId; documentVersionDto.Published = false; Database.Insert(documentVersionDto); } // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(content.ContentType.Variations, content.VersionId, content.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out var editedCultures); + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, entity.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out var editedCultures); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); // if !publishing, we may have a new name != current publish name, // also impacts 'edited' - if (!publishing && content.PublishName != content.Name) + if (!publishing && entity.PublishName != entity.Name) edited = true; // persist the document dto // at that point, when publishing, the entity still has its old Published value // so we need to explicitly update the dto to persist the correct value - if (content.PublishedState == PublishedState.Publishing) + if (entity.PublishedState == PublishedState.Publishing) dto.Published = true; dto.NodeId = nodeDto.NodeId; - content.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Insert(dto); //insert the schedule - PersistContentSchedule(content, false); + PersistContentSchedule(entity, false); // persist the variations - if (content.ContentType.VariesByCulture()) + if (entity.ContentType.VariesByCulture()) { // bump dates to align cultures to version if (publishing) - content.AdjustDates(contentVersionDto.VersionDate); + entity.AdjustDates(contentVersionDto.VersionDate); // names also impact 'edited' // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in content.CultureInfos) - if (cultureInfo.Name != content.GetPublishName(cultureInfo.Culture)) + foreach (var cultureInfo in entity.CultureInfos) + if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); // insert content variations - Database.BulkInsertRecords(GetContentVariationDtos(content, publishing)); + Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(content, publishing, editedCultures)); + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); } // refresh content - content.SetCultureEdited(editedCultures); + entity.SetCultureEdited(editedCultures); // trigger here, before we reset Published etc OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); @@ -399,27 +479,29 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // flip the entity's published property // this also flips its published state // note: what depends on variations (eg PublishNames) is managed directly by the content - if (content.PublishedState == PublishedState.Publishing) + if (entity.PublishedState == PublishedState.Publishing) { - content.Published = true; - content.PublishTemplateId = content.TemplateId; - content.PublisherId = content.WriterId; - content.PublishName = content.Name; - content.PublishDate = content.UpdateDate; + entity.Published = true; + entity.PublishTemplateId = entity.TemplateId; + entity.PublisherId = entity.WriterId; + entity.PublishName = entity.Name; + entity.PublishDate = entity.UpdateDate; SetEntityTags(entity, _tagRepository); } - else if (content.PublishedState == PublishedState.Unpublishing) + else if (entity.PublishedState == PublishedState.Unpublishing) { - content.Published = false; - content.PublishTemplateId = null; - content.PublisherId = null; - content.PublishName = null; - content.PublishDate = null; + entity.Published = false; + entity.PublishTemplateId = null; + entity.PublisherId = null; + entity.PublishName = null; + entity.PublishDate = null; ClearEntityTags(entity, _tagRepository); } + PersistRelations(entity); + entity.ResetDirtyProperties(); // troubleshooting @@ -437,46 +519,56 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IContent entity) { - // however, it's not just so we have access to AddingEntity - // there are tons of things at the end of the methods, that can only work with a true Content - // and basically, the repository requires a Content, not an IContent - var content = (Content)entity; + var isEntityDirty = entity.IsDirty(); // check if we need to make any database changes at all - if ((content.PublishedState == PublishedState.Published || content.PublishedState == PublishedState.Unpublished) - && !content.IsEntityDirty() && !content.IsAnyUserPropertyDirty()) + if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) + && !isEntityDirty && !entity.IsAnyUserPropertyDirty()) return; // no change to save, do nothing, don't even update dates // whatever we do, we must check that we are saving the current version - var version = Database.Fetch(SqlContext.Sql().Select().From().Where(x => x.Id == content.VersionId)).FirstOrDefault(); + var version = Database.Fetch(SqlContext.Sql().Select().From().Where(x => x.Id == entity.VersionId)).FirstOrDefault(); if (version == null || !version.Current) throw new InvalidOperationException("Cannot save a non-current version."); // update - content.UpdatingEntity(); - var publishing = content.PublishedState == PublishedState.Publishing; + entity.UpdatingEntity(); - // check if we need to create a new version - if (publishing && content.PublishedVersionId > 0) + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); + // TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below. + // There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost + // copy performance by 95% just like we did for Move + + + var publishing = entity.PublishedState == PublishedState.Publishing; + + if (!isMoving) { - // published version is not published anymore - Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == content.PublishedVersionId)); - } + // check if we need to create a new version + if (publishing && entity.PublishedVersionId > 0) + { + // published version is not published anymore + Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == entity.PublishedVersionId)); + } - // sanitize names - SanitizeNames(content, publishing); + // sanitize names + SanitizeNames(entity, publishing); - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) - { - var parent = GetParentNodeDto(entity.ParentId); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty("ParentId")) + { + var parent = GetParentNodeDto(entity.ParentId); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } } // create the dto @@ -487,144 +579,152 @@ namespace Umbraco.Core.Persistence.Repositories.Implement nodeDto.ValidatePathWithException(); Database.Update(nodeDto); - // update the content dto - Database.Update(dto.ContentDto); - - // update the content & document version dtos - var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; - var documentVersionDto = dto.DocumentVersionDto; - if (publishing) + if (!isMoving) { - documentVersionDto.Published = true; // now published - contentVersionDto.Current = false; // no more current - } - Database.Update(contentVersionDto); - Database.Update(documentVersionDto); + // update the content dto + Database.Update(dto.ContentDto); - // and, if publishing, insert new content & document version dtos - if (publishing) - { - content.PublishedVersionId = content.VersionId; - - contentVersionDto.Id = 0; // want a new id - contentVersionDto.Current = true; // current version - contentVersionDto.Text = content.Name; - Database.Insert(contentVersionDto); - content.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id - - documentVersionDto.Published = false; // non-published version - Database.Insert(documentVersionDto); - } - - // replace the property data (rather than updating) - // only need to delete for the version that existed, the new version (if any) has no property data yet - var versionToDelete = publishing ? content.PublishedVersionId : content.VersionId; - var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete); - Database.Execute(deletePropertyDataSql); - - // insert property data - var propertyDataDtos = PropertyFactory.BuildDtos(content.ContentType.Variations, content.VersionId, publishing ? content.PublishedVersionId : 0, - entity.Properties, LanguageRepository, out var edited, out var editedCultures); - foreach (var propertyDataDto in propertyDataDtos) - Database.Insert(propertyDataDto); - - // if !publishing, we may have a new name != current publish name, - // also impacts 'edited' - if (!publishing && content.PublishName != content.Name) - edited = true; - - if (content.ContentType.VariesByCulture()) - { - // bump dates to align cultures to version + // update the content & document version dtos + var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; + var documentVersionDto = dto.DocumentVersionDto; if (publishing) - content.AdjustDates(contentVersionDto.VersionDate); + { + documentVersionDto.Published = true; // now published + contentVersionDto.Current = false; // no more current + } + Database.Update(contentVersionDto); + Database.Update(documentVersionDto); - // names also impact 'edited' - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in content.CultureInfos) - if (cultureInfo.Name != content.GetPublishName(cultureInfo.Culture)) - { - edited = true; - (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); + // and, if publishing, insert new content & document version dtos + if (publishing) + { + entity.PublishedVersionId = entity.VersionId; - // TODO: change tracking - // at the moment, we don't do any dirty tracking on property values, so we don't know whether the - // culture has just been edited or not, so we don't update its update date - that date only changes - // when the name is set, and it all works because the controller does it - but, if someone uses a - // service to change a property value and save (without setting name), the update date does not change. - } + contentVersionDto.Id = 0; // want a new id + contentVersionDto.Current = true; // current version + contentVersionDto.Text = entity.Name; + Database.Insert(contentVersionDto); + entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id - // replace the content version variations (rather than updating) + documentVersionDto.Published = false; // non-published version + Database.Insert(documentVersionDto); + } + + // replace the property data (rather than updating) // only need to delete for the version that existed, the new version (if any) has no property data yet - var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); - Database.Execute(deleteContentVariations); + var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; + var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete); + Database.Execute(deletePropertyDataSql); - // replace the document version variations (rather than updating) - var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == content.Id); - Database.Execute(deleteDocumentVariations); + // insert property data + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0, + entity.Properties, LanguageRepository, out var edited, out var editedCultures); + foreach (var propertyDataDto in propertyDataDtos) + Database.Insert(propertyDataDto); - // TODO: NPoco InsertBulk issue? - // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) - // but by using SQL Server and updating a variants name will cause: Unable to cast object of type - // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. - // (same in PersistNewItem above) + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' + if (!publishing && entity.PublishName != entity.Name) + edited = true; - // insert content variations - Database.BulkInsertRecords(GetContentVariationDtos(content, publishing)); + if (entity.ContentType.VariesByCulture()) + { + // bump dates to align cultures to version + if (publishing) + entity.AdjustDates(contentVersionDto.VersionDate); - // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(content, publishing, editedCultures)); + // names also impact 'edited' + // ReSharper disable once UseDeconstruction + foreach (var cultureInfo in entity.CultureInfos) + if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) + { + edited = true; + (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); + + // TODO: change tracking + // at the moment, we don't do any dirty tracking on property values, so we don't know whether the + // culture has just been edited or not, so we don't update its update date - that date only changes + // when the name is set, and it all works because the controller does it - but, if someone uses a + // service to change a property value and save (without setting name), the update date does not change. + } + + // replace the content version variations (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); + Database.Execute(deleteContentVariations); + + // replace the document version variations (rather than updating) + var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == entity.Id); + Database.Execute(deleteDocumentVariations); + + // TODO: NPoco InsertBulk issue? + // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) + // but by using SQL Server and updating a variants name will cause: Unable to cast object of type + // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. + // (same in PersistNewItem above) + + // insert content variations + Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); + + // insert document variations + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); + } + + // refresh content + entity.SetCultureEdited(editedCultures); + + // update the document dto + // at that point, when un/publishing, the entity still has its old Published value + // so we need to explicitly update the dto to persist the correct value + if (entity.PublishedState == PublishedState.Publishing) + dto.Published = true; + else if (entity.PublishedState == PublishedState.Unpublishing) + dto.Published = false; + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + Database.Update(dto); + + //update the schedule + if (entity.IsPropertyDirty("ContentSchedule")) + PersistContentSchedule(entity, true); + + // if entity is publishing, update tags, else leave tags there + // means that implicitly unpublished, or trashed, entities *still* have tags in db + if (entity.PublishedState == PublishedState.Publishing) + SetEntityTags(entity, _tagRepository); } - // refresh content - content.SetCultureEdited(editedCultures); - - // update the document dto - // at that point, when un/publishing, the entity still has its old Published value - // so we need to explicitly update the dto to persist the correct value - if (content.PublishedState == PublishedState.Publishing) - dto.Published = true; - else if (content.PublishedState == PublishedState.Unpublishing) - dto.Published = false; - content.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited - Database.Update(dto); - - //update the schedule - if (content.IsPropertyDirty("ContentSchedule")) - PersistContentSchedule(content, true); - - // if entity is publishing, update tags, else leave tags there - // means that implicitly unpublished, or trashed, entities *still* have tags in db - if (content.PublishedState == PublishedState.Publishing) - SetEntityTags(entity, _tagRepository); - // trigger here, before we reset Published etc OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); - // flip the entity's published property - // this also flips its published state - if (content.PublishedState == PublishedState.Publishing) + if (!isMoving) { - content.Published = true; - content.PublishTemplateId = content.TemplateId; - content.PublisherId = content.WriterId; - content.PublishName = content.Name; - content.PublishDate = content.UpdateDate; + // flip the entity's published property + // this also flips its published state + if (entity.PublishedState == PublishedState.Publishing) + { + entity.Published = true; + entity.PublishTemplateId = entity.TemplateId; + entity.PublisherId = entity.WriterId; + entity.PublishName = entity.Name; + entity.PublishDate = entity.UpdateDate; - SetEntityTags(entity, _tagRepository); + SetEntityTags(entity, _tagRepository); + } + else if (entity.PublishedState == PublishedState.Unpublishing) + { + entity.Published = false; + entity.PublishTemplateId = null; + entity.PublisherId = null; + entity.PublishName = null; + entity.PublishDate = null; + + 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? } - else if (content.PublishedState == PublishedState.Unpublishing) - { - content.Published = false; - content.PublishTemplateId = null; - content.PublisherId = null; - content.PublishName = null; - content.PublishDate = null; - - ClearEntityTags(entity, _tagRepository); - } - - // 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(); @@ -877,32 +977,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."); } } @@ -1009,7 +1109,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(); @@ -1042,7 +1147,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; @@ -1067,49 +1172,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; @@ -1299,25 +1426,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 @@ -1335,7 +1465,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Utilities - private void SanitizeNames(Content content, bool publishing) + private void SanitizeNames(IContent content, bool publishing) { // a content item *must* have an invariant name, and invariant published name // else we just cannot write the invariant rows (node, content version...) to the database @@ -1400,7 +1530,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement x.NodeId != SqlTemplate.Arg("id")) .OrderBy(x => x.LanguageId)); - private void EnsureVariantNamesAreUnique(Content content, bool publishing) + private void EnsureVariantNamesAreUnique(IContent content, bool publishing) { if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureInfos.Count == 0) return; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DomainRepository.cs index 69523a860a..9aa28fb18a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DomainRepository.cs @@ -6,6 +6,7 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; @@ -101,7 +102,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (languageExists == 0) throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); } - ((UmbracoDomain)entity).AddingEntity(); + entity.AddingEntity(); var factory = new DomainModelFactory(); var dto = factory.BuildDto(entity); @@ -120,7 +121,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IDomain entity) { - ((UmbracoDomain)entity).UpdatingEntity(); + entity.UpdatingEntity(); var exists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName AND umbracoDomain.id <> @id", new { domainName = entity.DomainName, id = entity.Id }); 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 b4c0e51c22..24040bf393 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,56 +82,42 @@ 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 - { - 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()); - - // TODO: see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media - if (isMedia) - BuildProperties(entities, dtos.ToList()); + 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])); } - var dto = Database.FirstOrDefault(sql); + var dto = isMedia + ? Database.FirstOrDefault(sql) + : Database.FirstOrDefault(sql); + if (dto == null) return null; - var entity = BuildEntity(false, isMedia, dto); - - if (isMedia) - BuildProperties(entity, dto); + var entity = BuildEntity(dto); return entity; } @@ -127,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) @@ -162,26 +163,23 @@ namespace Umbraco.Core.Persistence.Repositories.Implement : PerformGetAll(objectType); } - private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool loadMediaProperties) + 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() : BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList(); } - var dtos = Database.Fetch(sql); - if (dtos.Count == 0) return Enumerable.Empty(); + var dtos = isMedia + ? (IEnumerable)Database.Fetch(sql) + : Database.Fetch(sql); - var entities = dtos.Select(x => BuildEntity(false, isMedia, x)).ToArray(); - - // TODO: See https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media - if (isMedia && loadMediaProperties) - BuildProperties(entities, dtos); + var entities = dtos.Select(BuildEntity).ToArray(); return entities; } @@ -190,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, true); + var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, filter); + return GetEntities(sql, isContent, isMedia, isMember); } public IEnumerable GetAllPaths(Guid objectType, params int[] ids) @@ -219,41 +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, true); - } - - // TODO: See https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media - internal IEnumerable GetMediaByQueryWithoutPropertyData(IQuery query) - { - var isContent = false; - var isMedia = true; - - var sql = GetBaseWhere(isContent, isMedia, false, null, Constants.ObjectTypes.Media); - - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, sql, true); - - return GetEntities(sql, isContent, isMedia, false); + return GetEntities(sql, isContent, isMedia, isMember); } public UmbracoObjectTypes GetObjectType(int id) @@ -280,41 +265,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return Database.ExecuteScalar(sql) > 0; } - // TODO: see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media - private void BuildProperties(EntitySlim entity, BaseDto dto) - { - var pdtos = Database.Fetch(GetPropertyData(dto.VersionId)); - foreach (var pdto in pdtos) - BuildProperty(entity, pdto); - } - - // TODO: see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media - private void BuildProperties(EntitySlim[] entities, List dtos) - { - var versionIds = dtos.Select(x => x.VersionId).Distinct().ToList(); - var pdtos = Database.FetchByGroups(versionIds, 2000, GetPropertyData); - - var xentity = entities.ToDictionary(x => x.Id, x => x); // nodeId -> entity - var xdto = dtos.ToDictionary(x => x.VersionId, x => x.NodeId); // versionId -> nodeId - foreach (var pdto in pdtos) - { - var nodeId = xdto[pdto.VersionId]; - var entity = xentity[nodeId]; - BuildProperty(entity, pdto); - } - } - - // TODO: see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media - private void BuildProperty(EntitySlim entity, PropertyDataDto pdto) - { - // explain ?! - var value = string.IsNullOrWhiteSpace(pdto.TextValue) - ? pdto.VarcharValue - : pdto.TextValue.ConvertToJsonIfPossible(); - - entity.AdditionalData[pdto.PropertyTypeDto.Alias] = new EntitySlim.PropertySlim(pdto.PropertyTypeDto.DataTypeDto.EditorAlias, value); - } - private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity) => BuildVariants(new[] { entity }).First(); @@ -380,50 +330,29 @@ 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 GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Action> filter) + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Action> filter) { - var sql = GetBaseWhere(isContent, isMedia, false, filter, objectType); - return AddGroupBy(isContent, isMedia, sql, true); - } - - private Sql GetPropertyData(int versionId) - { - return Sql() - .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x.DataTypeDto))) - .From() - .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) - .Where(x => x.VersionId == versionId); - } - - private Sql GetPropertyData(IEnumerable versionIds) - { - return Sql() - .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x.DataTypeDto))) - .From() - .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) - .WhereIn(x => x.VersionId, versionIds) - .OrderBy(x => x.VersionId); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] { objectType }); + return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version - protected Sql GetBase(bool isContent, bool isMedia, Action> filter, bool isCount = false) + protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action> filter, bool isCount = false) { var sql = Sql(); @@ -438,7 +367,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .AndSelect(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate) .Append(", COUNT(child.id) AS children"); - if (isContent || isMedia) + if (isContent || isMedia || isMember) sql .AndSelect(x => Alias(x.Id, "versionId")) .AndSelect(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); @@ -448,23 +377,35 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql .AndSelect(x => x.Published, x => x.Edited); } + + if (isMedia) + { + sql + .AndSelect(x => Alias(x.Path, "MediaPath")); + } } sql .From(); - if (isContent || isMedia) + if (isContent || isMedia || isMember) { sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.ContentTypeId == right.NodeId); + .LeftJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .LeftJoin().On((left, right) => left.NodeId == right.NodeId) + .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); } if (isContent) { sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId); + .LeftJoin().On((left, right) => left.NodeId == right.NodeId); + } + + if (isMedia) + { + sql + .LeftJoin().On((left, right) => left.Id == right.Id); } //Any LeftJoin statements need to come last @@ -482,49 +423,53 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the base SELECT + FROM [+ filter] + WHERE sql // for a given object type, with a given filter - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Action> filter, Guid objectType) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid[] objectTypes) { - return GetBase(isContent, isMedia, filter, isCount) - .Where(x => x.NodeObjectType == objectType); + var sql = GetBase(isContent, isMedia, isMember, filter, isCount); + if (objectTypes.Length > 0) + { + sql.WhereIn(x => x.NodeObjectType, objectTypes); + } + return sql; } // gets the base SELECT + FROM + WHERE sql // for a given node id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, int id) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, int id) { - var sql = GetBase(isContent, isMedia, null, isCount) + var sql = GetBase(isContent, isMedia, isMember, null, isCount) .Where(x => x.NodeId == id); - return AddGroupBy(isContent, isMedia, sql, true); + return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the base SELECT + FROM + WHERE sql // for a given unique id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid uniqueId) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid uniqueId) { - var sql = GetBase(isContent, isMedia, null, isCount) + var sql = GetBase(isContent, isMedia, isMember, null, isCount) .Where(x => x.UniqueId == uniqueId); - return AddGroupBy(isContent, isMedia, sql, true); + return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the base SELECT + FROM + WHERE sql // for a given object type and node id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, int nodeId) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, int nodeId) { - return GetBase(isContent, isMedia, null, isCount) + return GetBase(isContent, isMedia, isMember, null, isCount) .Where(x => x.NodeId == nodeId && x.NodeObjectType == objectType); } // gets the base SELECT + FROM + WHERE sql // for a given object type and unique id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, Guid uniqueId) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, Guid uniqueId) { - return GetBase(isContent, isMedia, null, isCount) + return GetBase(isContent, isMedia, isMember, null, isCount) .Where(x => x.UniqueId == uniqueId && x.NodeObjectType == objectType); } // gets the GROUP BY / ORDER BY sql // required in order to count children - protected Sql AddGroupBy(bool isContent, bool isMedia, Sql sql, bool defaultSort) + protected Sql AddGroupBy(bool isContent, bool isMedia, bool isMember, Sql sql, bool defaultSort) { sql .GroupBy(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) @@ -536,8 +481,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .AndBy(x => x.Published, x => x.Edited); } + if (isMedia) + { + sql + .AndBy(x => Alias(x.Path, "MediaPath")); + } - if (isContent || isMedia) + + if (isContent || isMedia || isMember) sql .AndBy(x => x.Id) .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); @@ -553,8 +504,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (sql == null) throw new ArgumentNullException(nameof(sql)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - // TODO: although this works for name, it probably doesn't work for others without an alias of some sort - var orderBy = ordering.OrderBy; + // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort + // As more things are attempted to be sorted we'll prob have to add more expressions here + string orderBy; + switch (ordering.OrderBy.ToUpperInvariant()) + { + case "PATH": + orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"); + break; + + default: + orderBy = ordering.OrderBy; + break; + } if (ordering.Direction == Direction.Ascending) sql.OrderBy(orderBy); @@ -566,27 +528,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Classes - [ExplicitColumns] - internal class UmbracoPropertyDto + /// + /// The DTO used to fetch results for a generic content item which could be either a document, media or a member + /// + private class GenericContentEntityDto : DocumentEntityDto { - [Column("propertyEditorAlias")] - public string PropertyEditorAlias { get; set; } - - [Column("propertyTypeAlias")] - public string PropertyAlias { get; set; } - - [Column("varcharValue")] - public string VarcharValue { get; set; } - - [Column("textValue")] - public string TextValue { get; set; } + public string MediaPath { get; set; } } - /// - /// The DTO used to fetch results for a content item with its variation info + /// The DTO used to fetch results for a document item with its variation info /// - private class ContentEntityDto : BaseDto + private class DocumentEntityDto : BaseDto { public ContentVariation Variations { get; set; } @@ -594,6 +547,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public bool Edited { get; set; } } + /// + /// The DTO used to fetch results for a media item with its media path info + /// + private class MediaEntityDto : BaseDto + { + public string MediaPath { get; set; } + } + + /// + /// The DTO used to fetch results for a member item + /// + private class MemberEntityDto : BaseDto + { + } + public class VariantInfoDto { public int NodeId { get; set; } @@ -640,12 +608,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Factory - private EntitySlim BuildEntity(bool isContent, bool isMedia, BaseDto dto) + private EntitySlim BuildEntity(BaseDto dto) { - if (isContent) + if (dto.NodeObjectType == Constants.ObjectTypes.Document) return BuildDocumentEntity(dto); - if (isMedia) - return BuildContentEntity(dto); + if (dto.NodeObjectType == Constants.ObjectTypes.Media) + return BuildMediaEntity(dto); + if (dto.NodeObjectType == Constants.ObjectTypes.Member) + return BuildMemberEntity(dto); // EntitySlim does not track changes var entity = new EntitySlim(); @@ -678,11 +648,22 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.ContentTypeThumbnail = dto.Thumbnail; } - private static EntitySlim BuildContentEntity(BaseDto dto) + private MediaEntitySlim BuildMediaEntity(BaseDto dto) { // EntitySlim does not track changes - var entity = new ContentEntitySlim(); + var entity = new MediaEntitySlim(); BuildContentEntity(entity, dto); + + // fill in the media info + if (dto is MediaEntityDto mediaEntityDto) + { + entity.MediaPath = mediaEntityDto.MediaPath; + } + else if (dto is GenericContentEntityDto genericContentEntityDto) + { + entity.MediaPath = genericContentEntityDto.MediaPath; + } + return entity; } @@ -692,7 +673,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entity = new DocumentEntitySlim(); BuildContentEntity(entity, dto); - if (dto is ContentEntityDto contentDto) + if (dto is DocumentEntityDto contentDto) { // fill in the invariant info entity.Edited = contentDto.Edited; @@ -703,6 +684,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return entity; } + private MemberEntitySlim BuildMemberEntity(BaseDto dto) + { + // EntitySlim does not track changes + var entity = new MemberEntitySlim(); + BuildEntity(entity, dto); + + entity.ContentTypeAlias = dto.Alias; + entity.ContentTypeIcon = dto.Icon; + entity.ContentTypeThumbnail = dto.Thumbnail; + + return entity; + } + #endregion } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 0fa48e5521..f708590ea8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -139,7 +139,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IIdentityUserLogin entity) { - ((EntityBase)entity).AddingEntity(); + entity.AddingEntity(); var dto = ExternalLoginFactory.BuildDto(entity); @@ -151,7 +151,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IIdentityUserLogin entity) { - ((EntityBase)entity).UpdatingEntity(); + entity.UpdatingEntity(); var dto = ExternalLoginFactory.BuildDto(entity); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/InstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/InstallationRepository.cs new file mode 100644 index 0000000000..49fbfe096b --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/InstallationRepository.cs @@ -0,0 +1,28 @@ +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Threading.Tasks; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories.Implement +{ + internal class InstallationRepository : IInstallationRepository + { + private static HttpClient _httpClient; + private const string RestApiInstallUrl = "https://our.umbraco.com/umbraco/api/Installation/Install"; + + + public async Task SaveInstallLogAsync(InstallLog installLog) + { + try + { + if (_httpClient == null) + _httpClient = new HttpClient(); + + await _httpClient.PostAsync(RestApiInstallUrl, installLog, new JsonMediaTypeFormatter()); + } + // this occurs if the server for Our is down or cannot be reached + catch (HttpRequestException) + { } + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 5a62c25df7..a905294417 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -102,17 +102,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override IEnumerable GetDeleteClauses() { - var list = new List { //NOTE: There is no constraint between the Language and cmsDictionary/cmsLanguageText tables (?) // but we still need to remove them - "DELETE FROM cmsLanguageText WHERE languageId = @id", - "DELETE FROM umbracoPropertyData WHERE languageId = @id", - "DELETE FROM umbracoContentVersionCultureVariation WHERE languageId = @id", - "DELETE FROM umbracoDocumentCultureVariation WHERE languageId = @id", - "DELETE FROM umbracoLanguage WHERE id = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id" + "DELETE FROM " + Constants.DatabaseSchema.Tables.DictionaryValue + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE tagId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Language + " WHERE id = @id" }; return list; } @@ -129,7 +129,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); - ((EntityBase) entity).AddingEntity(); + entity.AddingEntity(); // deal with entity becoming the new default entity if (entity.IsDefault) @@ -156,7 +156,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); - ((EntityBase) entity).UpdatingEntity(); + entity.UpdatingEntity(); if (entity.IsDefault) { @@ -182,6 +182,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement throw new InvalidOperationException($"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead."); } + if (entity.IsPropertyDirty(nameof(ILanguage.IsoCode))) + { + //if the iso code is changing, ensure there's not another lang with the same code already assigned + var sameCode = Sql() + .SelectCount() + .From() + .Where(x => x.IsoCode == entity.IsoCode && x.Id != entity.Id); + + var countOfSameCode = Database.ExecuteScalar(sameCode); + if (countOfSameCode > 0) + throw new InvalidOperationException($"Cannot update the language to a new culture: {entity.IsoCode} since that culture is already assigned to another language entity."); + } + // fallback cycles are detected at service level // update @@ -250,7 +263,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement lock (_codeIdMap) { if (_codeIdMap.TryGetValue(isoCode, out var id)) return id; - if (isoCode.Contains('-') && _codeIdMap.TryGetValue(isoCode.Split('-').First(), out var invariantId)) return invariantId; } if (throwOnNotFound) throw new ArgumentException($"Code {isoCode} does not correspond to an existing language.", nameof(isoCode)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs index 565917e078..1f0b45614b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs @@ -40,7 +40,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (macroDto == null) return null; - + var entity = MacroFactory.BuildEntity(macroDto); // reset dirty initial properties (U4-1946) @@ -132,7 +132,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMacro entity) { - ((EntityBase)entity).AddingEntity(); + entity.AddingEntity(); var dto = MacroFactory.BuildDto(entity); @@ -152,8 +152,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IMacro entity) { - ((EntityBase)entity).UpdatingEntity(); -; + entity.UpdatingEntity(); var dto = MacroFactory.BuildDto(entity); Database.Update(dto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 65043c4c67..242f21c749 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -8,9 +8,11 @@ using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; @@ -26,8 +28,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; - public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) - : base(scopeAccessor, cache, languageRepository, logger) + public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, + Lazy propertyEditorCollection, DataValueReferenceFactoryCollection dataValueReferenceFactories) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); @@ -216,8 +219,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMedia entity) { - var media = (Models.Media) entity; - media.AddingEntity(); + entity.AddingEntity(); // ensure unique name on the same level entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); @@ -227,7 +229,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.SanitizeEntityPropertiesForXmlStorage(); // create the dto - var dto = ContentBaseFactory.BuildDto(entity); + var dto = ContentBaseFactory.BuildDto(PropertyEditors, entity); // derive path and level from parent var parent = GetParentNodeDto(entity.ParentId); @@ -271,21 +273,23 @@ namespace Umbraco.Core.Persistence.Repositories.Implement contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = true; Database.Insert(contentVersionDto); - media.VersionId = contentVersionDto.Id; + entity.VersionId = contentVersionDto.Id; // persist the media version dto var mediaVersionDto = dto.MediaVersionDto; - mediaVersionDto.Id = media.VersionId; + mediaVersionDto.Id = entity.VersionId; Database.Insert(mediaVersionDto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); // set tags SetEntityTags(entity, _tagRepository); + PersistRelations(entity); + OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); entity.ResetDirtyProperties(); @@ -293,54 +297,65 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IMedia entity) { - var media = (Models.Media) entity; - // update - media.UpdatingEntity(); + entity.UpdatingEntity(); - // ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); - - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) + if (!isMoving) { - var parent = GetParentNodeDto(entity.ParentId); + // ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); + + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty(nameof(entity.ParentId))) + { + var parent = GetParentNodeDto(entity.ParentId); + + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } } // create the dto - var dto = ContentBaseFactory.BuildDto(entity); + var dto = ContentBaseFactory.BuildDto(PropertyEditors, entity); // update the node dto var nodeDto = dto.ContentDto.NodeDto; nodeDto.ValidatePathWithException(); Database.Update(nodeDto); - // update the content dto - Database.Update(dto.ContentDto); + if (!isMoving) + { + // update the content dto + Database.Update(dto.ContentDto); - // update the content & media version dtos - var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; - var mediaVersionDto = dto.MediaVersionDto; - contentVersionDto.Current = true; - Database.Update(contentVersionDto); - Database.Update(mediaVersionDto); + // update the content & media version dtos + var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; + var mediaVersionDto = dto.MediaVersionDto; + contentVersionDto.Current = true; + Database.Update(contentVersionDto); + Database.Update(mediaVersionDto); - // replace the property data - var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == media.VersionId); - Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); - foreach (var propertyDataDto in propertyDataDtos) - Database.Insert(propertyDataDto); + // replace the property data + var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == entity.VersionId); + Database.Execute(deletePropertyDataSql); + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); + foreach (var propertyDataDto in propertyDataDtos) + Database.Insert(propertyDataDto); - SetEntityTags(entity, _tagRepository); + SetEntityTags(entity, _tagRepository); + + PersistRelations(entity); + } OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); @@ -424,32 +439,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(IMedia entity) { - throw new WontImplementException(); + throw new InvalidOperationException("This method won't be implemented."); } protected override void PersistUpdatedItem(IMedia 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."); } } @@ -541,6 +556,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement media.ResetDirtyProperties(false); return media; } - + } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs index 281255e755..512011b52c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs @@ -3,8 +3,10 @@ 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; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; @@ -16,8 +18,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository { - public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) - : base(scopeAccessor, cache, logger, commonRepository) + public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository) { } protected override bool SupportsPublishing => MediaType.SupportsPublishingConst; @@ -49,7 +51,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(); } @@ -102,7 +104,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMediaType entity) { - ((MediaType)entity).AddingEntity(); + entity.AddingEntity(); PersistNewBaseContentType(entity); @@ -114,7 +116,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ValidateAlias(entity); //Updates Modified date - ((MediaType)entity).UpdatingEntity(); + entity.UpdatingEntity(); //Look up parent to get and set the correct Path if ParentId has changed if (entity.IsPropertyDirty("ParentId")) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs index ff7a79f98e..c138550de5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; @@ -91,8 +92,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMemberGroup entity) { //Save to db + entity.AddingEntity(); var group = (MemberGroup)entity; - group.AddingEntity(); var dto = MemberGroupFactory.BuildDto(group); var o = Database.IsNew(dto) ? Convert.ToInt32(Database.Insert(dto)) : Database.Update(dto); group.Id = dto.NodeId; //Set Id on entity to ensure an Id is set diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 808f61305a..42e7d1c32f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -6,9 +6,11 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; @@ -24,8 +26,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly IMemberGroupRepository _memberGroupRepository; - public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) - : base(scopeAccessor, cache, languageRepository, logger) + public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, + Lazy propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); @@ -132,7 +136,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // joining the type so we can do a query against the member type - not sure if this adds much overhead or not? // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content - // types by default on the document and media repo's so we can query by content type there too. + // types by default on the document and media repos so we can query by content type there too. .InnerJoin().On(left => left.ContentTypeId, right => right.NodeId); sql.Where(x => x.NodeObjectType == NodeObjectTypeId); @@ -232,8 +236,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMember entity) { + if (entity.ProviderUserKey == null) + { + entity.ProviderUserKey = entity.Key; + } + entity.AddingEntity(); + var member = (Member) entity; - member.AddingEntity(); // ensure that strings don't contain characters that are invalid in xml // TODO: do we really want to keep doing this here? @@ -315,6 +324,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement SetEntityTags(entity, _tagRepository); + PersistRelations(entity); + OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); entity.ResetDirtyProperties(); @@ -380,6 +391,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement SetEntityTags(entity, _tagRepository); + PersistRelations(entity); + OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); entity.ResetDirtyProperties(); @@ -540,6 +553,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (ordering.OrderBy.InvariantEquals("userName")) return SqlSyntax.GetFieldName(x => x.LoginName); + if (ordering.OrderBy.InvariantEquals("updateDate")) + return SqlSyntax.GetFieldName(x => x.VersionDate); + + if (ordering.OrderBy.InvariantEquals("createDate")) + return SqlSyntax.GetFieldName(x => x.CreateDate); + + if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) + return SqlSyntax.GetFieldName(x => x.Alias); + return base.ApplySystemOrdering(ref sql, ordering); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs index a32ec1b422..c3b95dbd8f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -3,8 +3,10 @@ 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; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; @@ -17,8 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository { - public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) - : base(scopeAccessor, cache, logger, commonRepository) + public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository) { } protected override bool SupportsPublishing => MemberType.SupportsPublishingConst; @@ -56,7 +58,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(); } @@ -131,12 +133,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { ValidateAlias(entity); - ((MemberType)entity).AddingEntity(); + entity.AddingEntity(); //set a default icon if one is not specified if (entity.Icon.IsNullOrWhiteSpace()) { - entity.Icon = "icon-user"; + entity.Icon = Constants.Icons.Member; } //By Convention we add 9 standard PropertyTypes to an Umbraco MemberType @@ -165,7 +167,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ValidateAlias(entity); //Updates Modified date - ((MemberType)entity).UpdatingEntity(); + entity.UpdatingEntity(); //Look up parent to get and set the correct Path if ParentId has changed if (entity.IsPropertyDirty("ParentId")) @@ -223,7 +225,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (builtinProperties.ContainsKey(propertyType.Alias)) { //this reset's its current data type reference which will be re-assigned based on the property editor assigned on the next line - propertyType.DataTypeId = 0; + var propDefinition = builtinProperties[propertyType.Alias]; + if (propDefinition != null) + { + propertyType.DataTypeId = propDefinition.DataTypeId; + propertyType.DataTypeKey = propDefinition.DataTypeKey; + } + else + { + propertyType.DataTypeId = 0; + propertyType.DataTypeKey = default; + } } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/PermissionRepository.cs index b4fd86c567..259f0b89c0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/PermissionRepository.cs @@ -252,27 +252,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override ContentPermissionSet PerformGet(int id) { - throw new WontImplementException(); + throw new InvalidOperationException("This method won't be implemented."); } protected override IEnumerable PerformGetAll(params int[] ids) { - throw new WontImplementException(); + throw new InvalidOperationException("This method won't be implemented."); } protected override IEnumerable PerformGetByQuery(IQuery query) { - 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."); } protected override IEnumerable GetDeleteClauses() @@ -280,11 +280,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return new List(); } - protected override Guid NodeObjectTypeId => throw new WontImplementException(); + protected override Guid NodeObjectTypeId => throw new InvalidOperationException("This property won't be implemented."); protected override void PersistDeletedItem(ContentPermissionSet entity) { - throw new WontImplementException(); + throw new InvalidOperationException("This method won't be implemented."); } #endregion diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/PublicAccessRepository.cs index bd2580b38f..1dc7aa478d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/PublicAccessRepository.cs @@ -5,6 +5,7 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs index baac02b6bf..acf6bb7df2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; @@ -105,7 +106,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); CreateDateUtc = redirectUrl.CreateDateUtc, Url = redirectUrl.Url, Culture = redirectUrl.Culture, - UrlHash = redirectUrl.Url.ToSHA1() + UrlHash = redirectUrl.Url.GenerateHash() }; } @@ -134,7 +135,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); public IRedirectUrl Get(string url, Guid contentKey, string culture) { - var urlHash = url.ToSHA1(); + var urlHash = url.GenerateHash(); var sql = GetBaseQuery(false).Where(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); var dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : Map(dto); @@ -157,7 +158,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); public IRedirectUrl GetMostRecentUrl(string url) { - var urlHash = url.ToSHA1(); + var urlHash = url.GenerateHash(); var sql = GetBaseQuery(false) .Where(x => x.Url == url && x.UrlHash == urlHash) .OrderByDescending(x => x.CreateDateUtc); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index c5ba24f385..56a6336f75 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -6,10 +6,14 @@ using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; +using Umbraco.Core.Services; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -19,11 +23,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement internal class RelationRepository : NPocoRepositoryBase, IRelationRepository { private readonly IRelationTypeRepository _relationTypeRepository; + private readonly IEntityRepository _entityRepository; - public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository) + public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository, IEntityRepository entityRepository) : base(scopeAccessor, AppCaches.NoCache, logger) { _relationTypeRepository = relationTypeRepository; + _entityRepository = entityRepository; } #region Overrides of RepositoryBase @@ -39,10 +45,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var relationType = _relationTypeRepository.Get(dto.RelationType); if (relationType == null) - throw new Exception(string.Format("RelationType with Id: {0} doesn't exist", dto.RelationType)); + throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", dto.RelationType)); - var factory = new RelationFactory(relationType); - return DtoToEntity(dto, factory); + return DtoToEntity(dto, relationType); } protected override IEnumerable PerformGetAll(params int[] ids) @@ -67,26 +72,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private IEnumerable DtosToEntities(IEnumerable dtos) { - // in most cases, the relation type will be the same for all of them, - // plus we've ordered the relations by type, so try to allocate as few - // factories as possible - bearing in mind that relation types are cached - RelationFactory factory = null; - var relationTypeId = -1; + //NOTE: This is N+1, BUT ALL relation types are cached so shouldn't matter - return dtos.Select(x => - { - if (relationTypeId != x.RelationType) - factory = new RelationFactory(_relationTypeRepository.Get(relationTypeId = x.RelationType)); - return DtoToEntity(x, factory); - }).ToList(); + return dtos.Select(x => DtoToEntity(x, _relationTypeRepository.Get(x.RelationType))).ToList(); } - private static IRelation DtoToEntity(RelationDto dto, RelationFactory factory) + private static IRelation DtoToEntity(RelationDto dto, IRelationType relationType) { - var entity = factory.BuildEntity(dto); + var entity = RelationFactory.BuildEntity(dto, relationType); // reset dirty initial properties (U4-1946) - ((BeingDirtyBase)entity).ResetDirtyProperties(false); + entity.ResetDirtyProperties(false); return entity; } @@ -97,14 +93,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override Sql GetBaseQuery(bool isCount) { - var sql = Sql(); + if (isCount) + { + return Sql().SelectCount().From(); + } - sql = isCount - ? sql.SelectCount() - : sql.Select(); + var sql = Sql().Select() + .AndSelect("uchild", x => Alias(x.NodeObjectType, "childObjectType")) + .AndSelect("uparent", x => Alias(x.NodeObjectType, "parentObjectType")) + .From() + .InnerJoin("uchild").On((rel, node) => rel.ChildId == node.NodeId, aliasRight: "uchild") + .InnerJoin("uparent").On((rel, node) => rel.ParentId == node.NodeId, aliasRight: "uparent"); - sql - .From(); return sql; } @@ -134,28 +134,208 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IRelation entity) { - ((EntityBase)entity).AddingEntity(); + entity.AddingEntity(); - var factory = new RelationFactory(entity.RelationType); - var dto = factory.BuildDto(entity); + var dto = RelationFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + PopulateObjectTypes(entity); entity.ResetDirtyProperties(); } protected override void PersistUpdatedItem(IRelation entity) { - ((EntityBase)entity).UpdatingEntity(); + entity.UpdatingEntity(); - var factory = new RelationFactory(entity.RelationType); - var dto = factory.BuildDto(entity); + var dto = RelationFactory.BuildDto(entity); Database.Update(dto); + PopulateObjectTypes(entity); + entity.ResetDirtyProperties(); } #endregion + + /// + /// Used for joining the entity query with relations for the paging methods + /// + /// + private void SqlJoinRelations(Sql sql) + { + // add left joins for relation tables (this joins on both child or parent, so beware that this will normally return entities for + // both sides of the relation type unless the IUmbracoEntity query passed in filters one side out). + sql.LeftJoin().On((left, right) => left.NodeId == right.ChildId || left.NodeId == right.ParentId); + sql.LeftJoin().On((left, right) => left.RelationType == right.Id); + } + + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + { + // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } + // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data + // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it + // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we + // will just return the bare minimum entity data. + + return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + { + SqlJoinRelations(sql); + + sql.Where(rel => rel.ChildId == childId); + sql.Where((rel, node) => rel.ParentId == childId || node.NodeId != childId); + }); + } + + public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + { + // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } + // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data + // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it + // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we + // will just return the bare minimum entity data. + + return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + { + SqlJoinRelations(sql); + + sql.Where(rel => rel.ParentId == parentId); + sql.Where((rel, node) => rel.ChildId == parentId || node.NodeId != parentId); + }); + } + + public void Save(IEnumerable relations) + { + foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) + { + if (hasIdentityGroup.Key) + { + // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation + // however we can bulk populate the object types. It might be possible to bulk update + // with SQL but would be pretty ugly and we're not really too worried about that for perf, + // it's the bulk inserts we care about. + var asArray = hasIdentityGroup.ToArray(); + foreach (var relation in hasIdentityGroup) + { + relation.UpdatingEntity(); + var dto = RelationFactory.BuildDto(relation); + Database.Update(dto); + } + PopulateObjectTypes(asArray); + } + else + { + // Do bulk inserts + var entitiesAndDtos = hasIdentityGroup.ToDictionary( + r => // key = entity + { + r.AddingEntity(); + return r; + }, + RelationFactory.BuildDto); // value = DTO + + // Use NPoco's own InsertBulk command which will automatically re-populate the new Ids on the entities, our own + // BulkInsertRecords does not cater for this. + Database.InsertBulk(entitiesAndDtos.Values); + + // All dtos now have IDs assigned + foreach (var de in entitiesAndDtos) + { + // re-assign ID to the entity + de.Key.Id = de.Value.Id; + } + + PopulateObjectTypes(entitiesAndDtos.Keys.ToArray()); + } + } + } + + public IEnumerable GetPagedRelationsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Ordering ordering) + { + var sql = GetBaseQuery(false); + + if (ordering == null || ordering.IsEmpty) + ordering = Ordering.By(SqlSyntax.GetQuotedColumn(Constants.DatabaseSchema.Tables.Relation, "id")); + + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + + // apply ordering + ApplyOrdering(ref sql, ordering); + + var pageIndexToFetch = pageIndex + 1; + var page = Database.Page(pageIndexToFetch, pageSize, sql); + var dtos = page.Items; + totalRecords = page.TotalItems; + + var relTypes = _relationTypeRepository.GetMany(dtos.Select(x => x.RelationType).Distinct().ToArray()) + .ToDictionary(x => x.Id, x => x); + + var result = dtos.Select(r => + { + if (!relTypes.TryGetValue(r.RelationType, out var relType)) + throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", r.RelationType)); + return DtoToEntity(r, relType); + }).ToList(); + + return result; + } + + + public void DeleteByParent(int parentId, params string[] relationTypeAliases) + { + var subQuery = Sql().Select(x => x.Id) + .From() + .InnerJoin().On(x => x.RelationType, x => x.Id) + .Where(x => x.ParentId == parentId); + + if (relationTypeAliases.Length > 0) + { + subQuery.WhereIn(x => x.Alias, relationTypeAliases); + } + + Database.Execute(Sql().Delete().WhereIn(x => x.Id, subQuery)); + } + + /// + /// Used to populate the object types after insert/update + /// + /// + private void PopulateObjectTypes(params IRelation[] entities) + { + var entityIds = entities.Select(x => x.ParentId).Concat(entities.Select(y => y.ChildId)).Distinct(); + + var nodes = Database.Fetch(Sql().Select().From() + .WhereIn(x => x.NodeId, entityIds)) + .ToDictionary(x => x.NodeId, x => x.NodeObjectType); + + foreach (var e in entities) + { + if (nodes.TryGetValue(e.ParentId, out var parentObjectType)) + { + e.ParentObjectType = parentObjectType.GetValueOrDefault(); + } + if (nodes.TryGetValue(e.ChildId, out var childObjectType)) + { + e.ChildObjectType = childObjectType.GetValueOrDefault(); + } + } + } + + private void ApplyOrdering(ref Sql sql, Ordering ordering) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (ordering == null) throw new ArgumentNullException(nameof(ordering)); + + // TODO: although this works for name, it probably doesn't work for others without an alias of some sort + var orderBy = ordering.OrderBy; + + if (ordering.Direction == Direction.Ascending) + sql.OrderBy(orderBy); + else + sql.OrderByDescending(orderBy); + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs index 4faf78bd0a..623b55b6f8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs @@ -133,8 +133,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IRelationType entity) { - ((EntityBase)entity).AddingEntity(); - + entity.AddingEntity(); + + CheckNullObjectTypeValues(entity); + var dto = RelationTypeFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); @@ -145,8 +147,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IRelationType entity) { - ((EntityBase)entity).UpdatingEntity(); - + entity.UpdatingEntity(); + + CheckNullObjectTypeValues(entity); + var dto = RelationTypeFactory.BuildDto(entity); Database.Update(dto); @@ -154,5 +158,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } #endregion + + private void CheckNullObjectTypeValues(IRelationType entity) + { + if (entity.ParentObjectType.HasValue && entity.ParentObjectType == Guid.Empty) + entity.ParentObjectType = null; + if (entity.ChildObjectType.HasValue && entity.ChildObjectType == Guid.Empty) + entity.ChildObjectType = null; + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs index 6b2dfddaeb..1497c2857c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs @@ -5,6 +5,7 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; @@ -96,7 +97,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IServerRegistration entity) { - ((ServerRegistration)entity).AddingEntity(); + entity.AddingEntity(); var dto = ServerRegistrationFactory.BuildDto(entity); @@ -108,7 +109,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IServerRegistration entity) { - ((ServerRegistration)entity).UpdatingEntity(); + entity.UpdatingEntity(); var dto = ServerRegistrationFactory.BuildDto(entity); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs index 9f27b6b9e3..99e824757d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs @@ -99,7 +99,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (uniqueing) { - if (name.NumPos > 0 && name.Name.StartsWith(nodeName) && name.NumVal == uniqueNumber) + if (name.NumPos > 0 && name.Name.InvariantStartsWith(nodeName) && name.NumVal == uniqueNumber) uniqueNumber++; else break; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/SimpleGetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/SimpleGetRepository.cs index 43233d0f31..f7e59820c3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/SimpleGetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/SimpleGetRepository.cs @@ -75,19 +75,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected sealed override IEnumerable GetDeleteClauses() { - throw new WontImplementException(); + throw new InvalidOperationException("This method won't be implemented."); } - protected sealed override Guid NodeObjectTypeId => throw new WontImplementException(); + protected sealed override Guid NodeObjectTypeId => throw new InvalidOperationException("This property won't be implemented."); protected sealed override void PersistNewItem(TEntity entity) { - throw new WontImplementException(); + throw new InvalidOperationException("This method won't be implemented."); } protected sealed override void PersistUpdatedItem(TEntity entity) { - throw new WontImplementException(); + throw new InvalidOperationException("This method won't be implemented."); } #endregion diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs index 4c02a8f4b5..698a9f4364 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs @@ -25,8 +25,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement path = path.EnsureEndsWith(".css"); - if (FileSystem.FileExists(path) == false) + // if the css directory is changed, references to the old path can still exist (ie in RTE config) + // these old references will throw an error, which breaks the RTE + // try-catch here makes the request fail silently, and allows RTE to load correctly + try + { + if (FileSystem.FileExists(path) == false) + return null; + } catch + { return null; + } // content will be lazy-loaded when required var created = FileSystem.GetCreated(path).UtcDateTime; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs index f26fcca81b..279e03b19e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs @@ -85,7 +85,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected override void PersistNewItem(ITag entity) { - ((EntityBase)entity).AddingEntity(); + entity.AddingEntity(); var dto = TagFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); @@ -97,7 +97,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected override void PersistUpdatedItem(ITag entity) { - ((EntityBase)entity).UpdatingEntity(); + entity.UpdatingEntity(); var dto = TagFactory.BuildDto(entity); Database.Update(dto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs index b348317989..2175780916 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs @@ -215,7 +215,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //Save updated entity to db template.UpdateDate = DateTime.Now; - ; var dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, templateDto.PrimaryKey); Database.Update(dto.NodeDto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs new file mode 100644 index 0000000000..365e8ba481 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs @@ -0,0 +1,49 @@ +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Threading.Tasks; +using Semver; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories.Implement +{ + internal class UpgradeCheckRepository : IUpgradeCheckRepository + { + private static HttpClient _httpClient; + private const string RestApiUpgradeChecklUrl = "https://our.umbraco.com/umbraco/api/UpgradeCheck/CheckUpgrade"; + + + public async Task CheckUpgradeAsync(SemVersion version) + { + try + { + if (_httpClient == null) + _httpClient = new HttpClient(); + + var task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl, new CheckUpgradeDto(version), new JsonMediaTypeFormatter()); + var result = await task.Content.ReadAsAsync(); + + return result ?? new UpgradeResult("None", "", ""); + } + catch (HttpRequestException) + { + // this occurs if the server for Our is down or cannot be reached + return new UpgradeResult("None", "", ""); + } + } + private class CheckUpgradeDto + { + public CheckUpgradeDto(SemVersion version) + { + VersionMajor = version.Major; + VersionMinor = version.Minor; + VersionPatch = version.Patch; + VersionComment = version.Prerelease; + } + + public int VersionMajor { get; } + public int VersionMinor { get; } + public int VersionPatch { get; } + public string VersionComment { get; } + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs index 3935027ada..0a66e29147 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -286,11 +286,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return list; } - protected override Guid NodeObjectTypeId => throw new WontImplementException(); + protected override Guid NodeObjectTypeId => throw new InvalidOperationException("This property won't be implemented."); protected override void PersistNewItem(IUserGroup entity) { - ((UserGroup) entity).AddingEntity(); + entity.AddingEntity(); var userGroupDto = UserGroupFactory.BuildDto(entity); @@ -304,7 +304,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IUserGroup entity) { - ((UserGroup) entity).UpdatingEntity(); + entity.UpdatingEntity(); var userGroupDto = UserGroupFactory.BuildDto(entity); @@ -370,35 +370,35 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override UserGroupWithUsers PerformGet(int id) { - throw new WontImplementException(); + throw new InvalidOperationException("This method won't be implemented."); } protected override IEnumerable PerformGetAll(params int[] ids) { - throw new WontImplementException(); + throw new InvalidOperationException("This method won't be implemented."); } protected override IEnumerable PerformGetByQuery(IQuery query) { - 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."); } protected override IEnumerable GetDeleteClauses() { - throw new WontImplementException(); + throw new InvalidOperationException("This method won't be implemented."); } - protected override Guid NodeObjectTypeId => throw new WontImplementException(); + protected override Guid NodeObjectTypeId => throw new InvalidOperationException("This property won't be implemented."); #endregion @@ -411,8 +411,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return; //now the user association - RemoveAllUsersFromGroup(entity.UserGroup.Id); - AddUsersToGroup(entity.UserGroup.Id, entity.UserIds); + RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); } protected override void PersistUpdatedItem(UserGroupWithUsers entity) @@ -424,8 +423,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return; //now the user association - RemoveAllUsersFromGroup(entity.UserGroup.Id); - AddUsersToGroup(entity.UserGroup.Id, entity.UserIds); + RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); + } + + /// + /// Adds a set of users to a group, first removing any that exist + /// + /// Id of group + /// Ids of users + private void RefreshUsersInGroup(int groupId, int[] userIds) + { + RemoveAllUsersFromGroup(groupId); + AddUsersToGroup(groupId, userIds); } /// @@ -444,7 +453,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// Ids of users private void AddUsersToGroup(int groupId, int[] userIds) { - // TODO: Check if the user exists? foreach (var userId in userIds) { var dto = new User2UserGroupDto diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index 9027e9269c..3be5102b83 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -434,7 +434,18 @@ ORDER BY colName"; protected override void PersistNewItem(IUser entity) { - ((User) entity).AddingEntity(); + // the use may have no identity, ie ID is zero, and be v7 super + // user - then it has been marked - and we must not persist it + // as new, as we do not want to create a new user - instead, persist + // it as updated + // see also: UserFactory.BuildEntity + if (((User) entity).AdditionalData.ContainsKey("IS_V7_ZERO")) + { + PersistUpdatedItem(entity); + return; + } + + entity.AddingEntity(); // ensure security stamp if missing if (entity.SecurityStamp.IsNullOrWhiteSpace()) @@ -484,7 +495,7 @@ ORDER BY colName"; protected override void PersistUpdatedItem(IUser entity) { // updates Modified date - ((User) entity).UpdatingEntity(); + entity.UpdatingEntity(); // ensure security stamp if missing if (entity.SecurityStamp.IsNullOrWhiteSpace()) @@ -546,6 +557,16 @@ ORDER BY colName"; } } + // If userlogin or the email has changed then need to reset security stamp + if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) + { + userDto.EmailConfirmedDate = null; + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + + changedCols.Add("emailConfirmedDate"); + changedCols.Add("securityStampToken"); + } + //only update the changed cols if (changedCols.Count > 0) { diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index c352e312ac..7ae001bf24 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Text.RegularExpressions; using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; @@ -44,7 +45,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax string TruncateTable { get; } string CreateConstraint { get; } string DeleteConstraint { get; } - + string DeleteDefaultConstraint { get; } string FormatDateTime(DateTime date, bool includeTime = true); string Format(TableDefinition table); @@ -76,6 +77,11 @@ namespace Umbraco.Core.Persistence.SqlSyntax string ConvertIntegerToOrderableString { get; } string ConvertDateToOrderableString { get; } string ConvertDecimalToOrderableString { get; } + + /// + /// Returns the default isolation level for the database + /// + IsolationLevel DefaultIsolationLevel { get; } IEnumerable GetTablesInSchema(IDatabase db); IEnumerable GetColumnsInSchema(IDatabase db); @@ -106,5 +112,23 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// A Tuple containing: TableName, IndexName, ColumnName, IsUnique /// IEnumerable> GetDefinedIndexes(IDatabase db); + + /// + /// Tries to gets the name of the default constraint on a column. + /// + /// The database. + /// The table name. + /// The column name. + /// The constraint name. + /// A value indicating whether a default constraint was found. + /// + /// Some database engines (e.g. SqlCe) may not have names for default constraints, + /// in which case the function may return true, but is + /// unspecified. + /// + bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName); + + void ReadLock(IDatabase db, params int[] lockIds); + void WriteLock(IDatabase db, params int[] lockIds); } } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 8f39e36fb9..2ed0fb878c 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Data; +using System.Data.SqlServerCe; using System.Linq; using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; @@ -52,6 +54,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax return "(" + string.Join("+", args) + ")"; } + public override System.Data.IsolationLevel DefaultIsolationLevel => System.Data.IsolationLevel.RepeatableRead; + public override string FormatColumnRename(string tableName, string oldName, string newName) { //NOTE Sql CE doesn't support renaming a column, so a new column needs to be created, then copy data and finally remove old column @@ -132,6 +136,17 @@ ORDER BY TABLE_NAME, INDEX_NAME"); item => new Tuple(item.TABLE_NAME, item.INDEX_NAME, item.COLUMN_NAME, item.UNIQUE)); } + /// + public override bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName) + { + // cannot return a true default constraint name (does not exist on SqlCe) + // but we won't really need it anyways - just check whether there is a constraint + constraintName = null; + var hasDefault = db.Fetch(@"select column_hasdefault from information_schema.columns +where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault(); + return hasDefault; + } + public override bool DoesTableExist(IDatabase db, string tableName) { var result = @@ -141,6 +156,39 @@ ORDER BY TABLE_NAME, INDEX_NAME"); return result > 0; } + public override void WriteLock(IDatabase db, params int[] lockIds) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + db.Execute(@"SET LOCK_TIMEOUT 1800;"); + // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + foreach (var lockId in lockIds) + { + var i = db.Execute(@"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); + if (i == 0) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + } + + public override void ReadLock(IDatabase db, params int[] lockIds) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + foreach (var lockId in lockIds) + { + var i = db.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new { id = lockId }); + if (i == null) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + } + protected override string FormatIdentity(ColumnDefinition column) { return column.IsIdentity ? GetIdentityString(column) : string.Empty; @@ -175,7 +223,7 @@ ORDER BY TABLE_NAME, INDEX_NAME"); { get { - return "ALTER TABLE [{0}] ALTER COLUMN [{1}] DROP DEFAULT"; + return "ALTER TABLE {0} ALTER COLUMN {1} DROP DEFAULT"; } } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index e51aa547b8..bb50fa98a1 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Data.SqlClient; using System.Linq; using NPoco; using Umbraco.Core.Logging; @@ -179,6 +180,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax return items.Select(x => x.TABLE_NAME).Cast().ToList(); } + public override IsolationLevel DefaultIsolationLevel => IsolationLevel.ReadCommitted; + public override IEnumerable GetColumnsInSchema(IDatabase db) { var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); @@ -225,6 +228,18 @@ order by T.name, I.name"); } + /// + public override bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName) + { + constraintName = db.Fetch(@"select con.[name] as [constraintName] +from sys.default_constraints con +join sys.columns col on con.object_id=col.default_object_id +join sys.tables tbl on col.object_id=tbl.object_id +where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) + .FirstOrDefault(); + return !constraintName.IsNullOrWhiteSpace(); + } + public override bool DoesTableExist(IDatabase db, string tableName) { var result = @@ -234,6 +249,46 @@ order by T.name, I.name"); return result > 0; } + public override void WriteLock(IDatabase db, params int[] lockIds) + { + WriteLock(db, TimeSpan.FromMilliseconds(1800), lockIds); + } + + public void WriteLock(IDatabase db, TimeSpan timeout, params int[] lockIds) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + + + // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + foreach (var lockId in lockIds) + { + db.Execute($"SET LOCK_TIMEOUT {timeout.TotalMilliseconds};"); + var i = db.Execute(@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); + if (i == 0) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + } + + + public override void ReadLock(IDatabase db, params int[] lockIds) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + + // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + foreach (var lockId in lockIds) + { + var i = db.ExecuteScalar("SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id", new { id = lockId }); + if (i == null) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist.", nameof(lockIds)); + } + } + public override string FormatColumnRename(string tableName, string oldName, string newName) { return string.Format(RenameColumn, tableName, oldName, newName); @@ -276,7 +331,7 @@ order by T.name, I.name"); return null; } - public override string DeleteDefaultConstraint => "ALTER TABLE [{0}] DROP CONSTRAINT [DF_{0}_{1}]"; + public override string DeleteDefaultConstraint => "ALTER TABLE {0} DROP CONSTRAINT {2}"; public override string DropIndex => "DROP INDEX {0} ON {1}"; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 5b6a9afb04..b2e03df96e 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -200,7 +200,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax return "NVARCHAR"; } - + + public abstract IsolationLevel DefaultIsolationLevel { get; } + public virtual IEnumerable GetTablesInSchema(IDatabase db) { return new List(); @@ -223,6 +225,11 @@ namespace Umbraco.Core.Persistence.SqlSyntax public abstract IEnumerable> GetDefinedIndexes(IDatabase db); + public abstract bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName); + + public abstract void ReadLock(IDatabase db, params int[] lockIds); + public abstract void WriteLock(IDatabase db, params int[] lockIds); + public virtual bool DoesTableExist(IDatabase db, string tableName) { return false; @@ -552,6 +559,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string CreateConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; public virtual string DeleteConstraint => "ALTER TABLE {0} DROP CONSTRAINT {1}"; public virtual string CreateForeignKeyConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; + public virtual string CreateDefaultConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} DEFAULT ({2}) FOR {3}"; public virtual string ConvertIntegerToOrderableString => "REPLACE(STR({0}, 8), SPACE(1), '0')"; public virtual string ConvertDateToOrderableString => "CONVERT(nvarchar, {0}, 102)"; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs index f7cf480830..b829f1fbc5 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs @@ -35,6 +35,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// public static Sql GetDeleteSubquery(this ISqlSyntaxProvider sqlProvider, string tableName, string columnName, Sql subQuery, WhereInType whereInType = WhereInType.In) { + //TODO: This is no longer necessary since this used to be a specific requirement for MySql! + // Now we can do a Delete + sub query, see RelationRepository.DeleteByParent for example return new Sql(string.Format( diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 51e0172f35..a95d95ea08 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -20,9 +20,6 @@ namespace Umbraco.Core.Persistence /// public class UmbracoDatabase : Database, IUmbracoDatabase { - // Umbraco's default isolation level is RepeatableRead - private const IsolationLevel DefaultIsolationLevel = IsolationLevel.RepeatableRead; - private readonly ILogger _logger; private readonly RetryPolicy _connectionRetryPolicy; private readonly RetryPolicy _commandRetryPolicy; @@ -38,7 +35,7 @@ namespace Umbraco.Core.Persistence /// Also used by DatabaseBuilder for creating databases and installing/upgrading. /// public UmbracoDatabase(string connectionString, ISqlContext sqlContext, DbProviderFactory provider, ILogger logger, RetryPolicy connectionRetryPolicy = null, RetryPolicy commandRetryPolicy = null) - : base(connectionString, sqlContext.DatabaseType, provider, DefaultIsolationLevel) + : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) { SqlContext = sqlContext; @@ -47,6 +44,8 @@ namespace Umbraco.Core.Persistence _commandRetryPolicy = commandRetryPolicy; EnableSqlTrace = EnableSqlTraceDefault; + + NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); } /// @@ -54,12 +53,14 @@ namespace Umbraco.Core.Persistence /// /// Internal for unit tests only. internal UmbracoDatabase(DbConnection connection, ISqlContext sqlContext, ILogger logger) - : base(connection, sqlContext.DatabaseType, DefaultIsolationLevel) + : base(connection, sqlContext.DatabaseType, sqlContext.SqlSyntax.DefaultIsolationLevel) { SqlContext = sqlContext; _logger = logger; EnableSqlTrace = EnableSqlTraceDefault; + + NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); } #endregion diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs index dc86ff060c..c502abc87c 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs @@ -4,7 +4,6 @@ using System.Data.Common; using System.Threading; using NPoco; using NPoco.FluentMappings; -using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.FaultHandling; using Umbraco.Core.Persistence.Mappers; @@ -28,7 +27,8 @@ namespace Umbraco.Core.Persistence { private readonly Lazy _mappers; private readonly ILogger _logger; - private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + + private object _lock = new object(); private DatabaseFactory _npocoDatabaseFactory; private IPocoDataFactory _pocoDataFactory; @@ -36,12 +36,13 @@ namespace Umbraco.Core.Persistence private string _providerName; private DbProviderFactory _dbProviderFactory; private DatabaseType _databaseType; - private bool _serverVersionDetected; private ISqlSyntaxProvider _sqlSyntax; private RetryPolicy _connectionRetryPolicy; private RetryPolicy _commandRetryPolicy; private NPoco.MapperCollection _pocoMappers; + private SqlContext _sqlContext; private bool _upgrading; + private bool _initialized; #region Constructors @@ -59,8 +60,8 @@ namespace Umbraco.Core.Persistence /// Used by the other ctor and in tests. public UmbracoDatabaseFactory(string connectionStringName, ILogger logger, Lazy mappers) { - if (string.IsNullOrWhiteSpace(connectionStringName)) - throw new ArgumentNullOrEmptyException(nameof(connectionStringName)); + if (connectionStringName == null) throw new ArgumentNullException(nameof(connectionStringName)); + if (string.IsNullOrWhiteSpace(connectionStringName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(connectionStringName)); _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -106,36 +107,30 @@ namespace Umbraco.Core.Persistence #endregion /// - public bool Configured { get; private set; } - - /// - public string ConnectionString + public bool Configured { get { - EnsureConfigured(); - return _connectionString; + lock (_lock) + { + return !_connectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace(); + } } } /// - public bool CanConnect - { - get - { - if (!Configured || !DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName)) return false; + public bool Initialized => Volatile.Read(ref _initialized); - if (_serverVersionDetected) return true; + /// + public string ConnectionString => _connectionString; - if (_databaseType.IsSqlServer()) - DetectSqlServerVersion(); - _serverVersionDetected = true; + /// + public bool CanConnect => + // actually tries to connect to the database (regardless of configured/initialized) + !_connectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace() && + DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName); - return true; - } - } - - private void DetectSqlServerVersion() + private void UpdateSqlServerDatabaseType() { // replace NPoco database type by a more efficient one @@ -171,7 +166,15 @@ namespace Umbraco.Core.Persistence } /// - public ISqlContext SqlContext { get; private set; } + public ISqlContext SqlContext + { + get + { + // must be initialized to have a context + EnsureInitialized(); + return _sqlContext; + } + } /// public void ConfigureForUpgrade() @@ -182,63 +185,79 @@ namespace Umbraco.Core.Persistence /// public void Configure(string connectionString, string providerName) { - try + if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString)); + if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName)); + + lock (_lock) { - _lock.EnterWriteLock(); - - _logger.Debug("Configuring."); - - if (Configured) throw new InvalidOperationException("Already configured."); - - if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString)); - if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName)); + if (Volatile.Read(ref _initialized)) + throw new InvalidOperationException("Already initialized."); _connectionString = connectionString; _providerName = providerName; - - _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionString); - _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionString); - - _dbProviderFactory = DbProviderFactories.GetFactory(_providerName); - if (_dbProviderFactory == null) - throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\"."); - _databaseType = DatabaseType.Resolve(_dbProviderFactory.GetType().Name, _providerName); - if (_databaseType == null) - throw new Exception($"Can't find an NPoco database type for provider name \"{_providerName}\"."); - - _sqlSyntax = GetSqlSyntaxProvider(_providerName); - if (_sqlSyntax == null) - throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\"."); - - // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database - // so that everything NPoco is properly cached for the lifetime of the application - _pocoMappers = new NPoco.MapperCollection { new PocoMapper() }; - var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver); - _pocoDataFactory = factory; - var config = new FluentConfig(xmappers => factory); - - // create the database factory - _npocoDatabaseFactory = DatabaseFactory.Config(x => x - .UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances - .WithFluentConfig(config)); // with proper configuration - - if (_npocoDatabaseFactory == null) throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance."); - - SqlContext = new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); - - _logger.Debug("Configured."); - Configured = true; - } - finally - { - if (_lock.IsWriteLockHeld) - _lock.ExitWriteLock(); } + + // rest to be lazy-initialized + } + + private void EnsureInitialized() + { + LazyInitializer.EnsureInitialized(ref _sqlContext, ref _initialized, ref _lock, Initialize); + } + + private SqlContext Initialize() + { + _logger.Debug("Initializing."); + + if (_connectionString.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper connection string."); + if (_providerName.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper provider name."); + + // cannot initialize without being able to talk to the database + if (!DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName)) + throw new Exception("Cannot connect to the database."); + + _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionString); + _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionString); + + _dbProviderFactory = DbProviderFactories.GetFactory(_providerName); + if (_dbProviderFactory == null) + throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\"."); + _databaseType = DatabaseType.Resolve(_dbProviderFactory.GetType().Name, _providerName); + if (_databaseType == null) + throw new Exception($"Can't find an NPoco database type for provider name \"{_providerName}\"."); + + _sqlSyntax = GetSqlSyntaxProvider(_providerName); + if (_sqlSyntax == null) + throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\"."); + + if (_databaseType.IsSqlServer()) + UpdateSqlServerDatabaseType(); + + // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database + // so that everything NPoco is properly cached for the lifetime of the application + _pocoMappers = new NPoco.MapperCollection { new PocoMapper() }; + var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver); + _pocoDataFactory = factory; + var config = new FluentConfig(xmappers => factory); + + // create the database factory + _npocoDatabaseFactory = DatabaseFactory.Config(x => x + .UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances + .WithFluentConfig(config)); // with proper configuration + + if (_npocoDatabaseFactory == null) + throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance."); + + _logger.Debug("Initialized."); + + return new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); } /// public IUmbracoDatabase CreateDatabase() { + // must be initialized to create a database + EnsureInitialized(); return (IUmbracoDatabase) _npocoDatabaseFactory.GetDatabase(); } @@ -260,22 +279,6 @@ namespace Umbraco.Core.Persistence } } - // ensures that the database is configured, else throws - private void EnsureConfigured() - { - _lock.EnterReadLock(); - try - { - if (Configured == false) - throw new InvalidOperationException("Not configured."); - } - finally - { - if (_lock.IsReadLockHeld) - _lock.ExitReadLock(); - } - } - // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance private UmbracoDatabase CreateDatabaseInstance() { @@ -292,7 +295,7 @@ namespace Umbraco.Core.Persistence //var db = _umbracoDatabaseAccessor.UmbracoDatabase; //_umbracoDatabaseAccessor.UmbracoDatabase = null; //db?.Dispose(); - Configured = false; + Volatile.Write(ref _initialized, false); } // during tests, the thread static var can leak between tests diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index 9471285148..87e0732d47 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -14,6 +14,7 @@ using System.Runtime.InteropServices; [assembly: InternalsVisibleTo("Umbraco.Web")] [assembly: InternalsVisibleTo("Umbraco.Web.UI")] [assembly: InternalsVisibleTo("Umbraco.Examine")] +[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.Embedded")] [assembly: InternalsVisibleTo("Umbraco.Tests")] [assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] @@ -32,7 +33,7 @@ using System.Runtime.InteropServices; [assembly: InternalsVisibleTo("Umbraco.Forms.Web")] // Umbraco Headless -[assembly: InternalsVisibleTo("Umbraco.Headless")] +[assembly: InternalsVisibleTo("Umbraco.Cloud.Headless")] // code analysis // IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs index 9d1193ac82..1654bb3f20 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs @@ -113,7 +113,7 @@ namespace Umbraco.Core.PropertyEditors } catch (Exception e) { - throw new Exception($"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", e); + throw new InvalidOperationException($"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", e); } } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs index d90aeef2e6..1852c742f0 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs @@ -1,5 +1,4 @@ using System; -using Umbraco.Core.Exceptions; namespace Umbraco.Core.PropertyEditors { @@ -28,13 +27,15 @@ namespace Umbraco.Core.PropertyEditors /// The view to use to render the field editor. public ConfigurationFieldAttribute(string key, string name, string view) { - if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullOrEmptyException(nameof(key)); + if (key == null) throw new ArgumentNullException(nameof(key)); + if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(key)); + 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 (view == null) throw new ArgumentNullException(nameof(view)); + if (string.IsNullOrWhiteSpace(view)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(view)); + Key = key; - - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); Name = name; - - if (string.IsNullOrWhiteSpace(view)) throw new ArgumentNullOrEmptyException(nameof(view)); View = view; } @@ -47,10 +48,12 @@ namespace Umbraco.Core.PropertyEditors /// from the name of the property marked with this attribute. public ConfigurationFieldAttribute(string name, string view) { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); - Name = 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 (view == null) throw new ArgumentNullException(nameof(view)); + if (string.IsNullOrWhiteSpace(view)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(view)); - if (string.IsNullOrWhiteSpace(view)) throw new ArgumentNullOrEmptyException(nameof(view)); + Name = name; View = view; } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationFieldsExtensions.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldsExtensions.cs new file mode 100644 index 0000000000..25fba622d5 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldsExtensions.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.PropertyEditors +{ + public static partial class ConfigurationFieldsExtensions + { + /// + /// Adds a configuration field. + /// + /// The list of configuration fields. + /// The key (alias) of the field. + /// The name (label) of the field. + /// The description for the field. + /// The path to the editor view to be used for the field. + /// Optional configuration used for field's editor. + public static void Add( + this List fields, + string key, + string name, + string description, + string view, + IDictionary config = null) + { + fields.Add(new ConfigurationField + { + Key = key, + Name = name, + Description = description, + View = view, + Config = config, + }); + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index 43f4b68b99..add523ecf6 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.PropertyEditors // defaults Type = type; Icon = Constants.Icons.PropertyEditor; - Group = "common"; + Group = Constants.PropertyEditors.Groups.Common; // assign properties based on the attribute, if it is found Attribute = GetType().GetCustomAttribute(false); @@ -104,7 +104,7 @@ namespace Umbraco.Core.PropertyEditors /// Technically, it could be cached by datatype but let's keep things /// simple enough for now. /// - public IDataValueEditor GetValueEditor(object configuration) + public virtual IDataValueEditor GetValueEditor(object configuration) { // if an explicit value editor has been set (by the manifest parser) // then return it, and ignore the configuration, which is going to be @@ -113,7 +113,7 @@ namespace Umbraco.Core.PropertyEditors return ExplicitValueEditor; var editor = CreateValueEditor(); - ((DataValueEditor) editor).Configuration = configuration; // TODO: casting is bad + ((DataValueEditor)editor).Configuration = configuration; // TODO: casting is bad return editor; } @@ -163,7 +163,7 @@ namespace Umbraco.Core.PropertyEditors protected virtual IDataValueEditor CreateValueEditor() { if (Attribute == null) - throw new InvalidOperationException("The editor does not specify a view."); + throw new InvalidOperationException($"The editor is not attributed with {nameof(DataEditorAttribute)}"); return new DataValueEditor(Attribute); } @@ -175,7 +175,7 @@ namespace Umbraco.Core.PropertyEditors { var editor = new ConfigurationEditor(); // pass the default configuration if this is not a property value editor - if((Type & EditorType.PropertyValue) == 0) + if ((Type & EditorType.PropertyValue) == 0) { editor.DefaultConfiguration = _defaultConfiguration; } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs index ca08127d51..7b3be7ea5f 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs @@ -1,5 +1,4 @@ using System; -using Umbraco.Core.Exceptions; namespace Umbraco.Core.PropertyEditors { @@ -53,17 +52,17 @@ namespace Umbraco.Core.PropertyEditors /// public DataEditorAttribute(string alias, EditorType type, string name, string view) { - if ((type & ~(EditorType.PropertyValue | EditorType.MacroParameter)) > 0) - throw new ArgumentOutOfRangeException(nameof(type), $"Not a valid {typeof(EditorType)} value."); + if (alias == null) throw new ArgumentNullException(nameof(alias)); + if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias)); + if ((type & ~(EditorType.PropertyValue | EditorType.MacroParameter)) > 0) throw new ArgumentOutOfRangeException(nameof(type), type, $"Not a valid {typeof(EditorType)} value."); + 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 (view == null) throw new ArgumentNullException(nameof(view)); + if (string.IsNullOrWhiteSpace(view)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(view)); + Type = type; - - if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentNullOrEmptyException(nameof(alias)); Alias = alias; - - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); Name = name; - - if (string.IsNullOrWhiteSpace(view)) throw new ArgumentNullOrEmptyException(nameof(view)); View = view == NullView ? null : view; } @@ -100,8 +99,10 @@ namespace Umbraco.Core.PropertyEditors get => _valueType; set { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullOrEmptyException(nameof(value)); - if (!ValueTypes.IsValue(value)) throw new ArgumentOutOfRangeException(nameof(value), $"Not a valid {typeof(ValueTypes)} value."); + if (value == null) throw new ArgumentNullException(nameof(value)); + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(value)); + if (!ValueTypes.IsValue(value)) throw new ArgumentOutOfRangeException(nameof(value), value, $"Not a valid {typeof(ValueTypes)} value."); + _valueType = value; } } @@ -121,7 +122,7 @@ namespace Umbraco.Core.PropertyEditors /// Gets or sets an optional group. /// /// The group can be used for example to group the editors by category. - public string Group { get; set; } = "common"; + public string Group { get; set; } = Constants.PropertyEditors.Groups.Common; /// /// Gets or sets a value indicating whether the value editor is deprecated. diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs new file mode 100644 index 0000000000..e7b051e17f --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceFactoryCollection : BuilderCollectionBase + { + public DataValueReferenceFactoryCollection(IEnumerable items) + : base(items) + { } + + public IEnumerable GetAllReferences(PropertyCollection properties, PropertyEditorCollection propertyEditors) + { + var trackedRelations = new HashSet(); + + foreach (var p in properties) + { + if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; + + //TODO: We will need to change this once we support tracking via variants/segments + // for now, we are tracking values from ALL variants + + foreach(var propertyVal in p.Values) + { + var val = propertyVal.EditedValue; + + var valueEditor = editor.GetValueEditor(); + if (valueEditor is IDataValueReference reference) + { + var refs = reference.GetReferences(val); + foreach(var r in refs) + trackedRelations.Add(r); + } + + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (var item in this) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has referecnes to media/content items + if (item.IsForEditor(editor)) + { + foreach(var r in item.GetDataValueReference().GetReferences(val)) + trackedRelations.Add(r); + } + + } + } + + + } + + return trackedRelations; + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs new file mode 100644 index 0000000000..2cf76712c8 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceFactoryCollectionBuilder : OrderedCollectionBuilderBase + { + protected override DataValueReferenceFactoryCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Core/PropertyEditors/GridEditor.cs b/src/Umbraco.Core/PropertyEditors/GridEditor.cs index 986eed9ccc..cc3561fbc2 100644 --- a/src/Umbraco.Core/PropertyEditors/GridEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/GridEditor.cs @@ -18,6 +18,9 @@ namespace Umbraco.Core.PropertyEditors [JsonProperty("name", Required = Required.Always)] public string Name { get; set; } + [JsonProperty("nameTemplate")] + public string NameTemplate { get; set; } + [JsonProperty("alias", Required = Required.Always)] public string Alias { get; set; } diff --git a/src/Umbraco.Core/PropertyEditors/IDataEditorWithMediaPath.cs b/src/Umbraco.Core/PropertyEditors/IDataEditorWithMediaPath.cs new file mode 100644 index 0000000000..e8af1b0ac3 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IDataEditorWithMediaPath.cs @@ -0,0 +1,19 @@ +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Must be implemented by property editors that store media and return media paths + /// + /// + /// Currently there are only 2x core editors that do this: upload and image cropper. + /// It would be possible for developers to know implement their own media property editors whereas previously this was not possible. + /// + public interface IDataEditorWithMediaPath + { + /// + /// Returns the media path for the value stored for a property + /// + /// + /// + string GetMediaPath(object value); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs index cb68531cc7..a02fa71ec7 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Services; namespace Umbraco.Core.PropertyEditors { + /// /// Represents an editor for editing data values. /// @@ -63,8 +64,26 @@ namespace Umbraco.Core.PropertyEditors // TODO: / deal with this when unplugging the xml cache // why property vs propertyType? services should be injected! etc... + + /// + /// Used for serializing an item for packaging + /// + /// + /// + /// + /// + /// IEnumerable ConvertDbToXml(Property property, IDataTypeService dataTypeService, ILocalizationService localizationService, bool published); + + /// + /// Used for serializing an item for packaging + /// + /// + /// + /// + /// XNode ConvertDbToXml(PropertyType propertyType, object value, IDataTypeService dataTypeService); + string ConvertDbToString(PropertyType propertyType, object value, IDataTypeService dataTypeService); } } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs new file mode 100644 index 0000000000..6377098bfc --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Umbraco.Core.Models.Editors; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Resolve references from values + /// + public interface IDataValueReference + { + /// + /// Returns any references contained in the value + /// + /// + /// + IEnumerable GetReferences(object value); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs new file mode 100644 index 0000000000..6587e071bf --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.PropertyEditors +{ + public interface IDataValueReferenceFactory + { + /// + /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). + /// + /// The datatype. + /// A value indicating whether the converter supports a datatype. + bool IsForEditor(IDataEditor dataEditor); + + /// + /// + /// + /// + IDataValueReference GetDataValueReference(); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs b/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs new file mode 100644 index 0000000000..bef3f42f46 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Marker interface for any editor configuration that supports Ignoring user start nodes + /// + internal interface IIgnoreUserStartNodesConfig + { + bool IgnoreUserStartNodes { get; set; } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs index f6a7cbf32f..0a9cf632bc 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.PropertyEditors /// /// The property type. /// A value indicating whether the converter supports a property type. - bool IsConverter(PublishedPropertyType propertyType); + bool IsConverter(IPublishedPropertyType propertyType); /// /// Determines whether a value is an actual value, or not a value. @@ -36,14 +36,14 @@ namespace Umbraco.Core.PropertyEditors /// The CLR type of values returned by the converter. /// Some of the CLR types may be generated, therefore this method cannot directly return /// a Type object (which may not exist yet). In which case it needs to return a ModelType instance. - Type GetPropertyValueType(PublishedPropertyType propertyType); + Type GetPropertyValueType(IPublishedPropertyType propertyType); /// /// Gets the property cache level. /// /// The property type. /// The property cache level. - PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType); + PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType); /// /// Converts a property source value to an intermediate value. @@ -64,7 +64,7 @@ namespace Umbraco.Core.PropertyEditors /// strings, and xml-whitespace strings appropriately, ie it should know whether to preserve /// white spaces. /// - object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview); + object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview); /// /// Converts a property intermediate value to an Object value. @@ -83,7 +83,7 @@ namespace Umbraco.Core.PropertyEditors /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage /// the cache levels of property values. It is not meant to be used by the converter. /// - object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); + object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); /// /// Converts a property intermediate value to an XPath value. @@ -107,6 +107,6 @@ namespace Umbraco.Core.PropertyEditors /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage /// the cache levels of property values. It is not meant to be used by the converter. /// - object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); + object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); } } diff --git a/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs index 3e4aea4385..9f29d65ffd 100644 --- a/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs @@ -21,4 +21,4 @@ namespace Umbraco.Core.PropertyEditors /// IEnumerable ValidateFormat(object value, string valueType, string format); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs index f8e62788c8..1b4074c96f 100644 --- a/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs @@ -19,4 +19,4 @@ namespace Umbraco.Core.PropertyEditors /// IEnumerable ValidateRequired(object value, string valueType); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs index d0c40b1e63..60b7d55c01 100644 --- a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs @@ -5,7 +5,11 @@ namespace Umbraco.Core.PropertyEditors /// /// Represents a property editor for label properties. /// - [DataEditor(Constants.PropertyEditors.Aliases.Label, "Label", "readonlyvalue", Icon = "icon-readonly")] + [DataEditor( + Constants.PropertyEditors.Aliases.Label, + "Label", + "readonlyvalue", + Icon = "icon-readonly")] public class LabelPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs index 712a66e55d..21854b63c1 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs @@ -4,6 +4,8 @@ using Umbraco.Core.Manifest; namespace Umbraco.Core.PropertyEditors { + + public class PropertyEditorCollection : BuilderCollectionBase { public PropertyEditorCollection(DataEditorCollection dataEditors, ManifestParser manifestParser) @@ -27,4 +29,4 @@ namespace Umbraco.Core.PropertyEditors return editor != null; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs index 48bfc49ed9..3b6ebc610c 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.PropertyEditors /// public abstract class PropertyValueConverterBase : IPropertyValueConverter { - public virtual bool IsConverter(PublishedPropertyType propertyType) + public virtual bool IsConverter(IPublishedPropertyType propertyType) => false; public virtual bool? IsValue(object value, PropertyValueLevel level) @@ -30,19 +30,19 @@ namespace Umbraco.Core.PropertyEditors return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); } - public virtual Type GetPropertyValueType(PublishedPropertyType propertyType) + public virtual Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (object); - public virtual PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public virtual PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - public virtual object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public virtual object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source; - public virtual object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public virtual object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) => inter; - public virtual object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public virtual object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) => inter?.ToString() ?? string.Empty; } } diff --git a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs index 2439c7c02e..6549f1a233 100644 --- a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs @@ -55,6 +55,7 @@ namespace Umbraco.Core.PropertyEditors /// /// Gets the type of the dynamic configuration provider. /// + //TODO: This is not used and should be implemented in a nicer way, see https://github.com/umbraco/Umbraco-CMS/issues/6017#issuecomment-516253562 public Type TagsConfigurationProviderType { get; } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs index e405fa3a3e..8e82d694a7 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using Umbraco.Core.Composing; -using Umbraco.Core.Exceptions; using Umbraco.Core.Services; namespace Umbraco.Core.PropertyEditors.Validators @@ -48,8 +47,9 @@ namespace Umbraco.Core.PropertyEditors.Validators get => _regex; set { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentNullOrEmptyException(nameof(value)); + if (value == null) throw new ArgumentNullException(nameof(value)); + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(value)); + _regex = value; } } @@ -58,7 +58,9 @@ namespace Umbraco.Core.PropertyEditors.Validators public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration) { if (_regex == null) + { throw new InvalidOperationException("The validator has not been configured."); + } return ValidateFormat(value, valueType, _regex); } @@ -66,9 +68,12 @@ namespace Umbraco.Core.PropertyEditors.Validators /// public IEnumerable ValidateFormat(object value, string valueType, string format) { - if (string.IsNullOrWhiteSpace(format)) throw new ArgumentNullOrEmptyException(nameof(format)); + if (format == null) throw new ArgumentNullException(nameof(format)); + if (string.IsNullOrWhiteSpace(format)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(format)); if (value == null || !new Regex(format).IsMatch(value.ToString())) + { yield return new ValidationResult(_textService.Localize("validation", "invalidPattern"), new[] { "value" }); + } } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs index c51f572817..1aa29870e5 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs @@ -35,14 +35,17 @@ namespace Umbraco.Core.PropertyEditors.Validators { if (value == null) { - yield return new ValidationResult(_textService.Localize("validation", "invalidNull"), new[] {"value"}); + yield return new ValidationResult(_textService.Localize("validation", "invalidNull"), new[] { "value" }); yield break; } if (valueType.InvariantEquals(ValueTypes.Json)) { if (value.ToString().DetectIsEmptyJson()) + { yield return new ValidationResult(_textService.Localize("validation", "invalidEmpty"), new[] { "value" }); + } + yield break; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs index 3d69c37b8b..dd2dfb49e7 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs @@ -9,16 +9,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class CheckboxListValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.CheckBoxList); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IEnumerable); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { var sourceString = source?.ToString() ?? string.Empty; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs index 9f260fc973..46dae3e4f0 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs @@ -8,16 +8,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class ColorPickerValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ColorPicker); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => UseLabel(propertyType) ? typeof(PickedColor) : typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { var useLabel = UseLabel(propertyType); @@ -39,7 +39,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters return ssource; } - private bool UseLabel(PublishedPropertyType propertyType) + private bool UseLabel(IPublishedPropertyType propertyType) { return ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration).UseLabel; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs index ffe9feb653..0206528bf7 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs @@ -8,16 +8,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class DatePickerValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DateTime); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (DateTime); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return DateTime.MinValue; @@ -39,7 +39,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters // default ConvertSourceToObject just returns source ie a DateTime value - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a DateTime already return XmlConvert.ToString((DateTime) inter, XmlDateTimeSerializationMode.Unspecified); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs index 6f7888aee3..7d6e7c0ce9 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs @@ -7,16 +7,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class DecimalValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.Decimal.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (decimal); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs index e4ef3a50a3..88061a559e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs @@ -6,16 +6,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class EmailAddressValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.EmailAddress); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { return source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index 29f6de0271..b3685457ec 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -28,16 +28,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters _config = config; } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Grid); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (JToken); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; var sourceString = source.ToString(); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs index b211272b51..ab217d3870 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs @@ -7,6 +7,8 @@ using System.Runtime.Serialization; using System.Text; using System.Web; using Newtonsoft.Json; +using Umbraco.Core.Composing; +using Umbraco.Core.Models; using Umbraco.Core.Serialization; namespace Umbraco.Core.PropertyEditors.ValueConverters @@ -59,38 +61,34 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters : Crops.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); } - internal void AppendCropBaseUrl(StringBuilder url, ImageCropperCrop crop, bool defaultCrop, bool preferFocalPoint) + internal ImageUrlGenerationOptions GetCropBaseOptions(string url, ImageCropperCrop crop, bool defaultCrop, bool preferFocalPoint) { if (preferFocalPoint && HasFocalPoint() || crop != null && crop.Coordinates == null && HasFocalPoint() || defaultCrop && HasFocalPoint()) { - url.Append("?center="); - url.Append(FocalPoint.Top.ToString(CultureInfo.InvariantCulture)); - url.Append(","); - url.Append(FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); - url.Append("&mode=crop"); + return new ImageUrlGenerationOptions(url) { FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition(FocalPoint.Top, FocalPoint.Left) }; } else if (crop != null && crop.Coordinates != null && preferFocalPoint == false) { - url.Append("?crop="); - url.Append(crop.Coordinates.X1.ToString(CultureInfo.InvariantCulture)).Append(","); - url.Append(crop.Coordinates.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); - url.Append(crop.Coordinates.X2.ToString(CultureInfo.InvariantCulture)).Append(","); - url.Append(crop.Coordinates.Y2.ToString(CultureInfo.InvariantCulture)); - url.Append("&cropmode=percentage"); + return new ImageUrlGenerationOptions(url) { Crop = new ImageUrlGenerationOptions.CropCoordinates(crop.Coordinates.X1, crop.Coordinates.Y1, crop.Coordinates.X2, crop.Coordinates.Y2) }; } else { - url.Append("?anchor=center"); - url.Append("&mode=crop"); + return new ImageUrlGenerationOptions(url) { DefaultCrop = true }; } } /// /// Gets the value image url for a specified crop. /// - public string GetCropUrl(string alias, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) + [Obsolete("Use the overload that takes an IImageUrlGenerator")] + public string GetCropUrl(string alias, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) => GetCropUrl(alias, Current.ImageUrlGenerator, useCropDimensions, useFocalPoint, cacheBusterValue); + + /// + /// Gets the value image url for a specified crop. + /// + public string GetCropUrl(string alias, IImageUrlGenerator imageUrlGenerator, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) { var crop = GetCrop(alias); @@ -98,38 +96,37 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters if (crop == null && !string.IsNullOrWhiteSpace(alias)) return null; - var url = new StringBuilder(); - - AppendCropBaseUrl(url, crop, string.IsNullOrWhiteSpace(alias), useFocalPoint); + var options = GetCropBaseOptions(string.Empty, crop, string.IsNullOrWhiteSpace(alias), useFocalPoint); if (crop != null && useCropDimensions) { - url.Append("&width=").Append(crop.Width); - url.Append("&height=").Append(crop.Height); + options.Width = crop.Width; + options.Height = crop.Height; } - if (cacheBusterValue != null) - url.Append("&rnd=").Append(cacheBusterValue); + options.CacheBusterValue = cacheBusterValue; - return url.ToString(); + return imageUrlGenerator.GetImageUrl(options); } /// /// Gets the value image url for a specific width and height. /// - public string GetCropUrl(int width, int height, bool useFocalPoint = false, string cacheBusterValue = null) + [Obsolete("Use the overload that takes an IImageUrlGenerator")] + public string GetCropUrl(int width, int height, bool useFocalPoint = false, string cacheBusterValue = null) => GetCropUrl(width, height, Current.ImageUrlGenerator, useFocalPoint, cacheBusterValue); + + /// + /// Gets the value image url for a specific width and height. + /// + public string GetCropUrl(int width, int height, IImageUrlGenerator imageUrlGenerator, bool useFocalPoint = false, string cacheBusterValue = null) { - var url = new StringBuilder(); + var options = GetCropBaseOptions(string.Empty, null, true, useFocalPoint); - AppendCropBaseUrl(url, null, true, useFocalPoint); + options.Width = width; + options.Height = height; + options.CacheBusterValue = cacheBusterValue; - url.Append("&width=").Append(width); - url.Append("&height=").Append(height); - - if (cacheBusterValue != null) - url.Append("&rnd=").Append(cacheBusterValue); - - return url.ToString(); + return imageUrlGenerator.GetImageUrl(options); } /// diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs index 79cb748960..36ffddb863 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -14,19 +14,19 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters public class ImageCropperValueConverter : PropertyValueConverterBase { /// - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ImageCropper); /// - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (ImageCropperValue); /// - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; /// - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; var sourceString = source.ToString(); @@ -47,7 +47,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters value = new ImageCropperValue { Src = sourceString }; } - value.ApplyConfiguration(propertyType.DataType.ConfigurationAs()); + value?.ApplyConfiguration(propertyType.DataType.ConfigurationAs()); return value; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs index e0abf17a7e..ca8f23bca2 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs @@ -6,16 +6,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class IntegerValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.Integer.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (int); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { return source.TryConvertTo().Result; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs index e04893716a..12e6238705 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs @@ -31,19 +31,19 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters /// /// /// - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) { return _propertyEditors.TryGet(propertyType.EditorAlias, out var editor) && editor.GetValueEditor().ValueType.InvariantEquals(ValueTypes.Json); } - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (JToken); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; var sourceString = source.ToString(); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs index 05a5f15aaf..eb461b4920 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs @@ -16,10 +16,10 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class LabelValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.Label.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var valueType = ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); switch (valueType.ValueType) @@ -40,10 +40,10 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters } } - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { var valueType = ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); switch (valueType.ValueType) @@ -65,6 +65,8 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters if (source is decimal sourceDecimal) return sourceDecimal; if (source is string sourceDecimalString) return decimal.TryParse(sourceDecimalString, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0; + if (source is double sourceDouble) + return Convert.ToDecimal(sourceDouble); return (decimal) 0; case ValueTypes.Integer: if (source is int sourceInt) return sourceInt; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index aeacf33eef..a062561ab1 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -7,17 +7,17 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MarkdownEditorValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MarkdownEditor.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IHtmlString); // PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { // in xml a string is: string // in the database a string is: string @@ -25,13 +25,13 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters return source; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return new HtmlString(inter == null ? string.Empty : (string) inter); } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter?.ToString() ?? string.Empty; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs index bdd09ea33b..cd7f48f510 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs @@ -6,16 +6,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MemberGroupPickerValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberGroupPicker); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { return source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs index 1d5f0b1ca3..15e7ce4caf 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs @@ -9,18 +9,18 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MultipleTextStringValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MultipleTextstring.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IEnumerable); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" }; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { // data is (both in database and xml): // @@ -58,7 +58,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters : values.ToArray(); } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { var d = new XmlDocument(); var e = d.CreateElement("values"); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs index c9528c3e8b..b9c61bb169 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs @@ -22,16 +22,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters Constants.PropertyEditors.Aliases.MultiNodeTreePicker }; - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Aliases.Contains(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { return source?.ToString(); } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs index b99cc7e0e3..61adc9a93e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs @@ -6,16 +6,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class RadioButtonListValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.RadioButtonList); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { var attempt = source.TryConvertTo(); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs index 31ab47223f..11502687b7 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs @@ -18,16 +18,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => IsRangeDataType(propertyType.DataType.Id) ? typeof (Range) : typeof (decimal); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { if (source == null) return null; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs index 9b857c2dff..b54c693c14 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs @@ -19,16 +19,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Tags); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IEnumerable); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return Array.Empty(); @@ -43,7 +43,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters return source.ToString().Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { return (string[]) source; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs index 2368a1d034..7caa9a90cc 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -13,16 +13,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters Constants.PropertyEditors.Aliases.TextArea }; - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => PropertyTypeAliases.Contains(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { // in xml a string is: string // in the database a string is: string @@ -30,13 +30,13 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters return source; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter ?? string.Empty; } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs index 46f660d829..9938af671d 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs @@ -10,17 +10,17 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class TinyMceValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IHtmlString); // PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { // in xml a string is: string // in the database a string is: string @@ -28,13 +28,13 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters return source; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return new HtmlString(inter == null ? string.Empty : (string)inter); } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs index cfa247edaa..407ed13ddf 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs @@ -9,16 +9,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class UploadPropertyConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.UploadField); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { return source?.ToString() ?? ""; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs index 8ad09733f8..153462ccf5 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs @@ -6,16 +6,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class YesNoValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.Boolean; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (bool); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { // in xml a boolean is: string // in the database a boolean is: string "1" or "0" or empty @@ -49,7 +49,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters // default ConvertSourceToObject just returns source ie a boolean value - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a boolean already return (bool)inter ? "1" : "0"; diff --git a/src/Umbraco.Core/PublishedContentExtensions.cs b/src/Umbraco.Core/PublishedContentExtensions.cs new file mode 100644 index 0000000000..921883b822 --- /dev/null +++ b/src/Umbraco.Core/PublishedContentExtensions.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Composing; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core +{ + public static class PublishedContentExtensions + { + private static IVariationContextAccessor VariationContextAccessor => Current.VariationContextAccessor; + + /// + /// Determines whether the content has a culture. + /// + /// Culture is case-insensitive. + public static bool HasCulture(this IPublishedContent content, string culture) + => content.Cultures.ContainsKey(culture ?? string.Empty); + + /// + /// Determines whether the content is invariant, or has a culture. + /// + /// Culture is case-insensitive. + public static bool IsInvariantOrHasCulture(this IPublishedContent content, string culture) + => !content.ContentType.VariesByCulture() || content.Cultures.ContainsKey(culture ?? ""); + + /// + /// Filters a sequence of to return invariant items, and items that are published for the specified culture. + /// + /// The content items. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null). + internal static IEnumerable WhereIsInvariantOrHasCulture(this IEnumerable contents, string culture = null) + where T : class, IPublishedContent + { + if (contents == null) throw new ArgumentNullException(nameof(contents)); + + culture = culture ?? Current.VariationContextAccessor.VariationContext?.Culture ?? ""; + + // either does not vary by culture, or has the specified culture + return contents.Where(x => !x.ContentType.VariesByCulture() || HasCulture(x, culture)); + } + + /// + /// Gets the name of the content item. + /// + /// The content item. + /// The specific culture to get the name for. If null is used the current culture is used (Default is null). + public static string Name(this IPublishedContent content, string culture = null) + { + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) + return content.Cultures.TryGetValue("", out var invariantInfos) ? invariantInfos.Name : null; + + // handle context culture for variant + if (culture == null) + culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; + + // get + return culture != "" && content.Cultures.TryGetValue(culture, out var infos) ? infos.Name : null; + } + + + /// + /// Gets the url segment of the content item. + /// + /// The content item. + /// The specific culture to get the url segment for. If null is used the current culture is used (Default is null). + public static string UrlSegment(this IPublishedContent content, string culture = null) + { + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) + return content.Cultures.TryGetValue("", out var invariantInfos) ? invariantInfos.UrlSegment : null; + + // handle context culture for variant + if (culture == null) + culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; + + // get + return culture != "" && content.Cultures.TryGetValue(culture, out var infos) ? infos.UrlSegment : null; + } + + /// + /// Gets the culture date of the content item. + /// + /// The content item. + /// The specific culture to get the name for. If null is used the current culture is used (Default is null). + public static DateTime CultureDate(this IPublishedContent content, string culture = null) + { + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) + return content.UpdateDate; + + // handle context culture for variant + if (culture == null) + culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; + + // get + return culture != "" && content.Cultures.TryGetValue(culture, out var infos) ? infos.Date : DateTime.MinValue; + } + + + /// + /// Gets the children of the content item. + /// + /// The content item. + /// + /// The specific culture to get the url children for. Default is null which will use the current culture in + /// + /// + /// Gets children that are available for the specified culture. + /// Children are sorted by their sortOrder. + /// + /// For culture, + /// if null is used the current culture is used. + /// If an empty string is used only invariant children are returned. + /// If "*" is used all children are returned. + /// + /// + /// If a variant culture is specified or there is a current culture in the then the Children returned + /// will include both the variant children matching the culture AND the invariant children because the invariant children flow with the current culture. + /// However, if an empty string is specified only invariant children are returned. + /// + /// + public static IEnumerable Children(this IPublishedContent content, string culture = null) + { + // handle context culture for variant + if (culture == null) + culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; + + var children = content.ChildrenForAllCultures; + return culture == "*" + ? children + : children.Where(x => x.IsInvariantOrHasCulture(culture)); + } + } +} diff --git a/src/Umbraco.Core/PublishedModelFactoryExtensions.cs b/src/Umbraco.Core/PublishedModelFactoryExtensions.cs index 4e026490a4..697e3fd11f 100644 --- a/src/Umbraco.Core/PublishedModelFactoryExtensions.cs +++ b/src/Umbraco.Core/PublishedModelFactoryExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core @@ -9,17 +11,35 @@ namespace Umbraco.Core public static class PublishedModelFactoryExtensions { /// - /// Executes an action with a safe live factory/ + /// Returns true if the current is an implementation of /// - /// - /// If the factory is a live factory, ensures it is refreshed and locked while executing the action. - /// + /// + /// + public static bool IsLiveFactory(this IPublishedModelFactory factory) => factory is ILivePublishedModelFactory; + + /// + /// Returns true if the current is an implementation of and is enabled + /// + /// + /// + public static bool IsLiveFactoryEnabled(this IPublishedModelFactory factory) + { + if (factory is ILivePublishedModelFactory2 liveFactory2) + return liveFactory2.Enabled; + + // if it's not ILivePublishedModelFactory2 we can't determine if it's enabled or not so return true + return factory is ILivePublishedModelFactory; + } + + [Obsolete("This method is no longer used or necessary and will be removed from future")] + [EditorBrowsable(EditorBrowsableState.Never)] public static void WithSafeLiveFactory(this IPublishedModelFactory factory, Action action) { if (factory is ILivePublishedModelFactory liveFactory) { lock (liveFactory.SyncRoot) { + //Call refresh on the live factory to re-compile the models liveFactory.Refresh(); action(); } @@ -29,5 +49,40 @@ namespace Umbraco.Core action(); } } + + /// + /// Sets a flag to reset the ModelsBuilder models if the is + /// + /// + /// + /// + /// This does not recompile the pure live models, only sets a flag to tell models builder to recompile when they are requested. + /// + internal static void WithSafeLiveFactoryReset(this IPublishedModelFactory factory, Action action) + { + if (factory is ILivePublishedModelFactory liveFactory) + { + lock (liveFactory.SyncRoot) + { + if (liveFactory is ILivePublishedModelFactory2 liveFactory2) + { + liveFactory2.Reset(); + } + else + { + // This is purely here for backwards compat and to avoid breaking changes but this code will probably never get executed + var resetMethod = liveFactory.GetType().GetMethod("ResetModels", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + if (resetMethod != null) + resetMethod.Invoke(liveFactory, null); + } + action(); + } + } + else + { + action(); + } + } + } } diff --git a/src/Umbraco.Core/ReflectionUtilities.cs b/src/Umbraco.Core/ReflectionUtilities.cs index 622d81f5f2..a8e6836ca1 100644 --- a/src/Umbraco.Core/ReflectionUtilities.cs +++ b/src/Umbraco.Core/ReflectionUtilities.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; -using Umbraco.Core.Exceptions; namespace Umbraco.Core { @@ -29,10 +28,14 @@ namespace Umbraco.Core /// The declaring type. /// The field type. /// The name of the field. - /// A field getter function. - /// Occurs when is null or empty. - /// Occurs when the field does not exist. - /// Occurs when does not match the type of the field. + /// + /// A field getter function. + /// + /// fieldName + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . type. + /// Could not find field .. public static Func EmitFieldGetter(string fieldName) { var field = GetField(fieldName); @@ -45,10 +48,14 @@ namespace Umbraco.Core /// The declaring type. /// The field type. /// The name of the field. - /// A field setter action. - /// Occurs when is null or empty. - /// Occurs when the field does not exist. - /// Occurs when does not match the type of the field. + /// + /// A field setter action. + /// + /// fieldName + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . type. + /// Could not find field .. public static Action EmitFieldSetter(string fieldName) { var field = GetField(fieldName); @@ -61,19 +68,36 @@ namespace Umbraco.Core /// The declaring type. /// The field type. /// The name of the field. - /// A field getter and setter functions. - /// Occurs when is null or empty. - /// Occurs when the field does not exist. - /// Occurs when does not match the type of the field. + /// + /// A field getter and setter functions. + /// + /// fieldName + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . type. + /// Could not find field .. public static (Func, Action) EmitFieldGetterAndSetter(string fieldName) { var field = GetField(fieldName); return (EmitFieldGetter(field), EmitFieldSetter(field)); } + /// + /// Gets the field. + /// + /// The type of the declaring. + /// The type of the value. + /// Name of the field. + /// + /// fieldName + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . type. + /// Could not find field .. private static FieldInfo GetField(string fieldName) { - if (string.IsNullOrWhiteSpace(fieldName)) throw new ArgumentNullOrEmptyException(nameof(fieldName)); + if (fieldName == null) throw new ArgumentNullException(nameof(fieldName)); + if (string.IsNullOrWhiteSpace(fieldName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(fieldName)); // get the field var field = typeof(TDeclaring).GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); @@ -120,13 +144,18 @@ namespace Umbraco.Core /// The property type. /// The name of the property. /// A value indicating whether the property and its getter must exist. - /// A property getter function. If is false, returns null when the property or its getter does not exist. - /// Occurs when is null or empty. - /// Occurs when the property or its getter does not exist. - /// Occurs when does not match the type of the property. + /// + /// A property getter function. If is false, returns null when the property or its getter does not exist. + /// + /// propertyName + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match property . type. + /// Could not find property getter for .. public static Func EmitPropertyGetter(string propertyName, bool mustExist = true) { - if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentNullOrEmptyException(nameof(propertyName)); + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); @@ -146,13 +175,18 @@ namespace Umbraco.Core /// The property type. /// The name of the property. /// A value indicating whether the property and its setter must exist. - /// A property setter function. If is false, returns null when the property or its setter does not exist. - /// Occurs when is null or empty. - /// Occurs when the property or its setter does not exist. - /// Occurs when does not match the type of the property. + /// + /// A property setter function. If is false, returns null when the property or its setter does not exist. + /// + /// propertyName + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match property . type. + /// Could not find property setter for .. public static Action EmitPropertySetter(string propertyName, bool mustExist = true) { - if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentNullOrEmptyException(nameof(propertyName)); + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); @@ -172,13 +206,18 @@ namespace Umbraco.Core /// The property type. /// The name of the property. /// A value indicating whether the property and its getter and setter must exist. - /// A property getter and setter functions. If is false, returns null when the property or its getter or setter does not exist. - /// Occurs when is null or empty. - /// Occurs when the property or its getter or setter does not exist. - /// Occurs when does not match the type of the property. + /// + /// A property getter and setter functions. If is false, returns null when the property or its getter or setter does not exist. + /// + /// propertyName + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match property . type. + /// Could not find property getter and setter for .. public static (Func, Action) EmitPropertyGetterAndSetter(string propertyName, bool mustExist = true) { - if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentNullOrEmptyException(nameof(propertyName)); + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); @@ -402,13 +441,17 @@ namespace Umbraco.Core /// A lambda representing the method. /// The name of the method. /// A value indicating whether the constructor must exist. - /// The method. If is false, returns null when the method does not exist. + /// + /// The method. If is false, returns null when the method does not exist. + /// + /// methodName + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Occurs when does not match the method signature.. + /// Occurs when no proper method with name could be found. /// - /// The method arguments are determined by generic arguments. + /// The method arguments are determined by generic arguments. /// - /// Occurs when is null or empty. - /// Occurs when no proper method with name could be found. - /// Occurs when Occurs when does not match the method signature. public static TLambda EmitMethod(string methodName, bool mustExist = true) { return EmitMethod(typeof(TDeclaring), methodName, mustExist); @@ -421,16 +464,21 @@ namespace Umbraco.Core /// The declaring type. /// The name of the method. /// A value indicating whether the constructor must exist. - /// The method. If is false, returns null when the method does not exist. + /// + /// The method. If is false, returns null when the method does not exist. + /// + /// methodName + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Occurs when does not match the method signature.. + /// Occurs when no proper method with name could be found. /// - /// The method arguments are determined by generic arguments. + /// The method arguments are determined by generic arguments. /// - /// Occurs when is null or empty. - /// Occurs when no proper method with name could be found. - /// Occurs when Occurs when does not match the method signature. public static TLambda EmitMethod(Type declaring, string methodName, bool mustExist = true) { - if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentNullOrEmptyException(nameof(methodName)); + if (methodName == null) throw new ArgumentNullException(nameof(methodName)); + if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, out var isFunction); @@ -510,17 +558,21 @@ namespace Umbraco.Core /// A lambda representing the method. /// The name of the method. /// A value indicating whether the constructor must exist. - /// The method. If is false, returns null when the method does not exist. + /// + /// The method. If is false, returns null when the method does not exist. + /// + /// methodName + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Occurs when does not match the method signature.. + /// Occurs when no proper method with name could be found. /// - /// The method arguments are determined by generic arguments. + /// The method arguments are determined by generic arguments. /// - /// Occurs when is null or empty. - /// Occurs when no proper method with name could be found. - /// Occurs when Occurs when does not match the method signature. public static TLambda EmitMethod(string methodName, bool mustExist = true) { - if (string.IsNullOrWhiteSpace(methodName)) - throw new ArgumentNullOrEmptyException(nameof(methodName)); + if (methodName == null) throw new ArgumentNullException(nameof(methodName)); + if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); // validate lambda type var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(false, out var isFunction); diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index f32a46f3f8..617dbcd9ea 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -18,6 +18,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.Validators; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Core.Sync; using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidator; @@ -43,7 +44,7 @@ namespace Umbraco.Core.Runtime // register persistence mappers - required by database factory so needs to be done here // means the only place the collection can be modified is in a runtime - afterwards it // has been frozen and it is too late - composition.WithCollectionBuilder().AddCoreMappers(); + composition.Mappers().AddCoreMappers(); // register the scope provider composition.RegisterUnique(); // implements both IScopeProvider and IScopeAccessor @@ -58,7 +59,7 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(); // register our predefined validators - composition.WithCollectionBuilder() + composition.ManifestValueValidators() .Add() .Add() .Add() @@ -66,12 +67,19 @@ namespace Umbraco.Core.Runtime .Add() .Add(); + // register the manifest filter collection builder (collection is empty by default) + composition.ManifestFilters(); + // properties and parameters derive from data editors - composition.WithCollectionBuilder() + composition.DataEditors() .Add(() => composition.TypeLoader.GetDataEditors()); composition.RegisterUnique(); composition.RegisterUnique(); + // Used to determine if a datatype/editor should be storing/tracking + // references to media item/s + composition.DataValueReferenceFactories(); + // register a server registrar, by default it's the db registrar composition.RegisterUnique(f => { @@ -98,13 +106,13 @@ namespace Umbraco.Core.Runtime factory.GetInstance(), true, new DatabaseServerMessengerOptions())); - composition.WithCollectionBuilder() + composition.CacheRefreshers() .Add(() => composition.TypeLoader.GetCacheRefreshers()); - composition.WithCollectionBuilder() + composition.PackageActions() .Add(() => composition.TypeLoader.GetPackageActions()); - composition.WithCollectionBuilder() + composition.PropertyValueConverters() .Append(composition.TypeLoader.GetTypes()); composition.RegisterUnique(); @@ -112,7 +120,7 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(factory => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetInstance()))); - composition.WithCollectionBuilder() + composition.UrlSegmentProviders() .Append(); composition.RegisterUnique(factory => new MigrationBuilder(factory)); @@ -122,6 +130,10 @@ namespace Umbraco.Core.Runtime // by default, register a noop rebuilder composition.RegisterUnique(); + + // will be injected in controllers when needed to invoke rest endpoints on Our + composition.RegisterUnique(); + composition.RegisterUnique(); } } } diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index f9a41b4f66..b852aff2ff 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -27,6 +27,17 @@ namespace Umbraco.Core.Runtime private IFactory _factory; private RuntimeState _state; + [Obsolete("Use the ctor with all parameters instead")] + public CoreRuntime() + { + } + + public CoreRuntime(ILogger logger, IMainDom mainDom) + { + MainDom = mainDom; + Logger = logger; + } + /// /// Gets the logger. /// @@ -45,14 +56,22 @@ namespace Umbraco.Core.Runtime /// public IRuntimeState State => _state; + public IMainDom MainDom { get; private set; } + /// public virtual IFactory Boot(IRegister register) { // create and register the essential services // ie the bare minimum required to boot +#pragma warning disable CS0618 // Type or member is obsolete // loggers - var logger = Logger = GetLogger(); + // TODO: Removes this in netcore, this is purely just backwards compat ugliness + var logger = GetLogger(); + if (logger != Logger) + Logger = logger; +#pragma warning restore CS0618 // Type or member is obsolete + var profiler = Profiler = GetProfiler(); var profilingLogger = ProfilingLogger = new ProfilingLogger(logger, profiler); @@ -125,12 +144,16 @@ namespace Umbraco.Core.Runtime Level = RuntimeLevel.Boot }; - // main dom - var mainDom = new MainDom(Logger); + // TODO: remove this in netcore, this is purely backwards compat hacks with the empty ctor + if (MainDom == null) + { + MainDom = new MainDom(Logger, new MainDomSemaphoreLock(Logger)); + } + // create the composition composition = new Composition(register, typeLoader, ProfilingLogger, _state, configs); - composition.RegisterEssentials(Logger, Profiler, ProfilingLogger, mainDom, appCaches, databaseFactory, typeLoader, _state); + composition.RegisterEssentials(Logger, Profiler, ProfilingLogger, MainDom, appCaches, databaseFactory, typeLoader, _state); // run handlers RuntimeOptions.DoRuntimeEssentials(composition, appCaches, typeLoader, databaseFactory); @@ -139,16 +162,22 @@ namespace Umbraco.Core.Runtime // there should be none, really - this is here "just in case" Compose(composition); - // acquire the main domain - AcquireMainDom(mainDom); + // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate + AcquireMainDom(MainDom); // determine our runtime level DetermineRuntimeLevel(databaseFactory, ProfilingLogger); // get composers, and compose var composerTypes = ResolveComposerTypes(typeLoader); - composition.WithCollectionBuilder(); - var composers = new Composers(composition, composerTypes, ProfilingLogger); + + IEnumerable enableDisableAttributes; + using (ProfilingLogger.DebugDuration("Scanning enable/disable composer attributes")) + { + enableDisableAttributes = typeLoader.GetAssemblyAttributes(typeof(EnableComposerAttribute), typeof(DisableComposerAttribute)); + } + + var composers = new Composers(composition, composerTypes, enableDisableAttributes, ProfilingLogger); composers.Compose(); // create the factory @@ -157,6 +186,8 @@ namespace Umbraco.Core.Runtime // create & initialize the components _components = _factory.GetInstance(); _components.Initialize(); + + } catch (Exception e) { @@ -218,13 +249,13 @@ namespace Umbraco.Core.Runtime IOHelper.SetRootDirectory(path); } - private void AcquireMainDom(MainDom mainDom) + private bool AcquireMainDom(IMainDom mainDom) { using (var timer = ProfilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) { try { - mainDom.Acquire(); + return mainDom.IsMainDom; } catch { @@ -301,11 +332,9 @@ namespace Umbraco.Core.Runtime protected virtual IEnumerable GetComposerTypes(TypeLoader typeLoader) => typeLoader.GetTypes(); - /// - /// Gets a logger. - /// + [Obsolete("Don't use this method, the logger should be injected into the " + nameof(CoreRuntime))] protected virtual ILogger GetLogger() - => SerilogLogger.CreateWithDefaultConfiguration(); + => Logger ?? SerilogLogger.CreateWithDefaultConfiguration(); // TODO: Remove this in netcore, this purely just backwards compat ugliness /// /// Gets a profiler. diff --git a/src/Umbraco.Core/IMainDom.cs b/src/Umbraco.Core/Runtime/IMainDom.cs similarity index 88% rename from src/Umbraco.Core/IMainDom.cs rename to src/Umbraco.Core/Runtime/IMainDom.cs index 3a8cd13ff1..444fc1c7d0 100644 --- a/src/Umbraco.Core/IMainDom.cs +++ b/src/Umbraco.Core/Runtime/IMainDom.cs @@ -1,5 +1,6 @@ using System; +// TODO: Can't change namespace due to breaking changes, change in netcore namespace Umbraco.Core { /// @@ -14,6 +15,9 @@ namespace Umbraco.Core /// /// Gets a value indicating whether the current domain is the main domain. /// + /// + /// When the first call is made to this there will generally be some logic executed to acquire a distributed lock lease. + /// bool IsMainDom { get; } /// @@ -35,4 +39,4 @@ namespace Umbraco.Core /// is guaranteed to execute before the AppDomain releases the main domain status. bool Register(Action install, Action release, int weight = 100); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Runtime/IMainDomLock.cs b/src/Umbraco.Core/Runtime/IMainDomLock.cs new file mode 100644 index 0000000000..6a62f48194 --- /dev/null +++ b/src/Umbraco.Core/Runtime/IMainDomLock.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; + +namespace Umbraco.Core.Runtime +{ + /// + /// An application-wide distributed lock + /// + /// + /// Disposing releases the lock + /// + public interface IMainDomLock : IDisposable + { + /// + /// Acquires an application-wide distributed lock + /// + /// + /// + /// An awaitable boolean value which will be false if the elapsed millsecondsTimeout value is exceeded + /// + Task AcquireLockAsync(int millisecondsTimeout); + + /// + /// Wait on a background thread to receive a signal from another AppDomain + /// + /// + Task ListenAsync(); + } +} diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs new file mode 100644 index 0000000000..e6780ec876 --- /dev/null +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Web.Hosting; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Runtime +{ + + /// + /// 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, IDisposable + { + #region Vars + + private readonly ILogger _logger; + private readonly IMainDomLock _mainDomLock; + + // our own lock for local consistency + private object _locko = new object(); + + private bool _isInitialized; + // indicates whether... + private 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 = 40000; // 40 seconds + + #endregion + + #region Ctor + + // initializes a new instance of MainDom + public MainDom(ILogger logger, IMainDomLock systemLock) + { + HostingEnvironment.RegisterObject(this); + + _logger = logger; + _mainDomLock = systemLock; + } + + #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; + if (_isMainDom == false) + { + _logger.Warn("Register called when MainDom has not been acquired"); + 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"); + continue; + } + } + + _logger.Debug("Stopped ({SignalSource})", source); + } + finally + { + // in any case... + _isMainDom = false; + _mainDomLock.Dispose(); + _logger.Info("Released ({SignalSource})", source); + } + + } + } + + // acquires the main domain + private bool Acquire() + { + // 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."); + + // Get the lock + var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).GetAwaiter().GetResult(); + + if (!acquired) + { + _logger.Info("Cannot acquire (timeout)."); + + // In previous versions we'd let a TimeoutException be thrown + // and the appdomain would not start. We have the opportunity to allow it to + // start without having MainDom? This would mean that it couldn't write + // to nucache/examine and would only be ok if this was a super short lived appdomain. + // maybe safer to just keep throwing in this case. + + throw new TimeoutException("Cannot acquire MainDom"); + // return false; + } + + try + { + // Listen for the signal from another AppDomain coming online to release the lock + _mainDomLock.ListenAsync().ContinueWith(_ => OnSignal("signal")); + } + catch (OperationCanceledException ex) + { + // the waiting task could be canceled if this appdomain is naturally shutting down, we'll just swallow this exception + _logger.Warn(ex, ex.Message); + } + + _logger.Info("Acquired."); + return true; + } + + /// + /// Gets a value indicating whether the current domain is the main domain. + /// + /// + /// The lazy initializer call will only call the Acquire callback when it's not been initialized, else it will just return + /// the value from _isMainDom which means when we set _isMainDom to false again after being signaled, this will return false; + /// + public bool IsMainDom => LazyInitializer.EnsureInitialized(ref _isMainDom, ref _isInitialized, ref _locko, () => Acquire()); + + // IRegisteredObject + void IRegisteredObject.Stop(bool immediate) + { + OnSignal("environment"); // will run once + + // The web app is stopping, need to wind down + Dispose(true); + + HostingEnvironment.UnregisterObject(this); + } + + #region IDisposable Support + + // This code added to correctly implement the disposable pattern. + + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _mainDomLock.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + + #endregion + + public static string GetMainDomId() + { + // HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail + var appId = HostingEnvironment.ApplicationID?.ReplaceNonAlphanumericChars(string.Empty) ?? 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?.ToLowerInvariant() ?? string.Empty; + var hash = (appId + ":::" + appPath).GenerateHash(); + + return hash; + } + } +} diff --git a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs new file mode 100644 index 0000000000..2dcc37e25f --- /dev/null +++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs @@ -0,0 +1,95 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.Runtime +{ + /// + /// Uses a system-wide Semaphore and EventWaitHandle to synchronize the current AppDomain + /// + internal class MainDomSemaphoreLock : IMainDomLock + { + private readonly SystemLock _systemLock; + + // 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; + private readonly ILogger _logger; + private IDisposable _lockRelease; + + public MainDomSemaphoreLock(ILogger logger) + { + var lockName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-LCK"; + _systemLock = new SystemLock(lockName); + + var eventName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-EVT"; + _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + _logger = logger; + } + + //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread + public Task ListenAsync() => _signal.WaitOneAsync(); + + public Task AcquireLockAsync(int millisecondsTimeout) + { + // signal other instances that we want the lock, then wait on 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 + + //This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset. + try + { + _lockRelease = _systemLock.Lock(millisecondsTimeout); + return Task.FromResult(true); + } + catch (TimeoutException ex) + { + _logger.Error(ex); + return Task.FromResult(false); + } + finally + { + // 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(); + } + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _lockRelease?.Dispose(); + _signal.Close(); + _signal.Dispose(); + } + + disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + #endregion + } +} diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs new file mode 100644 index 0000000000..f3bfe4eefc --- /dev/null +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -0,0 +1,429 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Runtime +{ + internal class SqlMainDomLock : IMainDomLock + { + private string _lockId; + private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; + private const string UpdatedSuffix = "_updated"; + private readonly ILogger _logger; + private IUmbracoDatabase _db; + private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); + private bool _mainDomChanging = false; + private readonly UmbracoDatabaseFactory _dbFactory; + private bool _hasError; + private object _locker = new object(); + + public SqlMainDomLock(ILogger logger) + { + // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer + _lockId = Guid.NewGuid().ToString(); + _logger = logger; + + _dbFactory = new UmbracoDatabaseFactory( + Constants.System.UmbracoConnectionName, + _logger, + new Lazy(() => new Persistence.Mappers.MapperCollection(Enumerable.Empty()))); + } + + public async Task AcquireLockAsync(int millisecondsTimeout) + { + if (!_dbFactory.Configured) + { + // if we aren't configured, then we're in an install state, in which case we have no choice but to assume we can acquire + return true; + } + + if (!(_dbFactory.SqlContext.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider)) + throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server"); + + _sqlServerSyntax = sqlServerSyntaxProvider; + + _logger.Debug("Acquiring lock..."); + + var db = GetDatabase(); + + var tempId = Guid.NewGuid().ToString(); + + try + { + db.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + // wait to get a write lock + _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom); + } + catch (Exception ex) + { + if (IsLockTimeoutException(ex)) + { + _logger.Error(ex, "Sql timeout occurred, could not acquire MainDom."); + _hasError = true; + return false; + } + + // unexpected (will be caught below) + throw; + } + + var result = InsertLockRecord(tempId); //we change the row to a random Id to signal other MainDom to shutdown + if (result == RecordPersistenceType.Insert) + { + // if we've inserted, then there was no MainDom so we can instantly acquire + + // TODO: see the other TODO, could we just delete the row and that would indicate that we + // are MainDom? then we don't leave any orphan rows behind. + + InsertLockRecord(_lockId); // so update with our appdomain id + _logger.Debug("Acquired with ID {LockId}", _lockId); + return true; + } + + // if we've updated, this means there is an active MainDom, now we need to wait to + // for the current MainDom to shutdown which also requires releasing our write lock + } + catch (Exception ex) + { + ResetDatabase(); + // unexpected + _logger.Error(ex, "Unexpected error, cannot acquire MainDom"); + _hasError = true; + return false; + } + finally + { + db?.CompleteTransaction(); + } + + return await WaitForExistingAsync(tempId, millisecondsTimeout); + } + + public Task ListenAsync() + { + if (_hasError) + { + _logger.Warn("Could not acquire MainDom, listening is canceled."); + return Task.CompletedTask; + } + + // Create a long running task (dedicated thread) + // to poll to check if we are still the MainDom registered in the DB + return Task.Factory.StartNew(ListeningLoop, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + + } + + private void ListeningLoop() + { + while (true) + { + // poll every 1 second + Thread.Sleep(1000); + + if (!_dbFactory.Configured) + { + // if we aren't configured, we just keep looping since we can't query the db + continue; + } + + lock (_locker) + { + // If cancellation has been requested we will just exit. Depending on timing of the shutdown, + // we will have already flagged _mainDomChanging = true, or we're shutting down faster than + // the other MainDom is taking to startup. In this case the db row will just be deleted and the + // new MainDom will just take over. + if (_cancellationTokenSource.IsCancellationRequested) + return; + + var db = GetDatabase(); + + try + { + db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a read lock + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + + // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that + // we are still the maindom. An empty value might be better because then we won't have any orphan rows + // if the app is terminated. Could that work? + + if (!IsMainDomValue(_lockId)) + { + // we are no longer main dom, another one has come online, exit + _mainDomChanging = true; + _logger.Debug("Detected new booting application, releasing MainDom lock."); + return; + } + } + catch (Exception ex) + { + ResetDatabase(); + // unexpected + _logger.Error(ex, "Unexpected error, listening is canceled."); + _hasError = true; + return; + } + finally + { + db?.CompleteTransaction(); + } + } + + } + } + + private void ResetDatabase() + { + if (_db.InTransaction) + _db.AbortTransaction(); + _db.Dispose(); + _db = null; + } + + private IUmbracoDatabase GetDatabase() + { + if (_db != null) + return _db; + + _db = _dbFactory.CreateDatabase(); + return _db; + } + + /// + /// Wait for any existing MainDom to release so we can continue booting + /// + /// + /// + /// + private Task WaitForExistingAsync(string tempId, int millisecondsTimeout) + { + var updatedTempId = tempId + UpdatedSuffix; + + return Task.Run(() => + { + var db = GetDatabase(); + var watch = new Stopwatch(); + watch.Start(); + while(true) + { + // poll very often, we need to take over as fast as we can + Thread.Sleep(100); + + try + { + db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a read lock + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + + // the row + var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + + if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) + { + // the other main dom has updated our record + // Or the other maindom shutdown super fast and just deleted the record + // which indicates that we + // can acquire it and it has shutdown. + + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); + return true; + } + else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) + { + // in this case, the prefixed ID is different which means + // another new AppDomain has come online and is wanting to take over. In that case, we will not + // acquire. + + _logger.Debug("Cannot acquire, another booting application detected."); + + return false; + } + } + catch (Exception ex) + { + ResetDatabase(); + + if (IsLockTimeoutException(ex)) + { + _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); + _hasError = true; + return false; + } + // unexpected + _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); + _hasError = true; + return false; + } + finally + { + db?.CompleteTransaction(); + } + + if (watch.ElapsedMilliseconds >= millisecondsTimeout) + { + // if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown, + // or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row + // and it's just been left as an orphan row. + // There's really know way of knowing unless we are constantly updating the row for the current maindom + // which isn't ideal. + // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok + + _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); + + try + { + db.BeginTransaction(IsolationLevel.ReadCommitted); + + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); + return true; + } + catch (Exception ex) + { + ResetDatabase(); + + if (IsLockTimeoutException(ex)) + { + // something is wrong, we cannot acquire, not much we can do + _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); + _hasError = true; + return false; + } + _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); + _hasError = true; + return false; + } + finally + { + db?.CompleteTransaction(); + } + } + } + }, _cancellationTokenSource.Token); + } + + /// + /// Inserts or updates the key/value row + /// + private RecordPersistenceType InsertLockRecord(string id) + { + var db = GetDatabase(); + return db.InsertOrUpdate(new KeyValueDto + { + Key = MainDomKey, + Value = id, + Updated = DateTime.Now + }); + } + + /// + /// Checks if the DB row value is equals the value + /// + /// + private bool IsMainDomValue(string val) + { + var db = GetDatabase(); + return db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", + new { key = MainDomKey, val = val }) == 1; + } + + /// + /// Checks if the exception is an SQL timeout + /// + /// + /// + private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222; + + #region IDisposable Support + private bool _disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + lock (_locker) + { + // immediately cancel all sub-tasks, we don't want them to keep querying + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + + if (_dbFactory.Configured) + { + var db = GetDatabase(); + try + { + db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a write lock + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // When we are disposed, it means we have released the MainDom lock + // and called all MainDom release callbacks, in this case + // if another maindom is actually coming online we need + // to signal to the MainDom coming online that we have shutdown. + // To do that, we update the existing main dom DB record with a suffixed "_updated" string. + // Otherwise, if we are just shutting down, we want to just delete the row. + if (_mainDomChanging) + { + _logger.Debug("Releasing MainDom, updating row, new application is booting."); + db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); + } + else + { + _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); + db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + } + } + catch (Exception ex) + { + ResetDatabase(); + _logger.Error(ex, "Unexpected error during dipsose."); + _hasError = true; + } + finally + { + db?.CompleteTransaction(); + ResetDatabase(); + } + } + } + } + + _disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + #endregion + + } +} diff --git a/src/Umbraco.Core/RuntimeState.cs b/src/Umbraco.Core/RuntimeState.cs index 6fb8a04c0d..74ec70828b 100644 --- a/src/Umbraco.Core/RuntimeState.cs +++ b/src/Umbraco.Core/RuntimeState.cs @@ -1,8 +1,8 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Web; using Semver; +using Umbraco.Core.Collections; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Exceptions; @@ -22,7 +22,7 @@ namespace Umbraco.Core private readonly ILogger _logger; private readonly IUmbracoSettingsSection _settings; private readonly IGlobalSettings _globalSettings; - private readonly HashSet _applicationUrls = new HashSet(); + private readonly ConcurrentHashSet _applicationUrls = new ConcurrentHashSet(); private readonly Lazy _mainDom; private readonly Lazy _serverRegistrar; @@ -97,6 +97,11 @@ namespace Umbraco.Core /// internal void EnsureApplicationUrl(HttpRequestBase request = null) { + //Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that + // it changes the URL to `localhost:80` which actually doesn't work for pinging itself, it only works internally in Azure. The ironic part + // about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626 + + // see U4-10626 - in some cases we want to reset the application url // (this is a simplified version of what was in 7.x) // note: should this be optional? is it expensive? diff --git a/src/Umbraco.Core/Scoping/IScopeProvider.cs b/src/Umbraco.Core/Scoping/IScopeProvider.cs index 6c9eb63ba0..96bb939f8e 100644 --- a/src/Umbraco.Core/Scoping/IScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/IScopeProvider.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Scoping /// Provides scopes. /// public interface IScopeProvider - { + { /// /// Creates an ambient scope. /// diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index e9dd04c5fa..84273e23da 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -33,8 +33,6 @@ namespace Umbraco.Core.Scoping private ICompletable _fscope; private IEventDispatcher _eventDispatcher; - private const IsolationLevel DefaultIsolationLevel = IsolationLevel.RepeatableRead; - // initializes a new scope private Scope(ScopeProvider scopeProvider, ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable, @@ -205,7 +203,7 @@ namespace Umbraco.Core.Scoping { if (_isolationLevel != IsolationLevel.Unspecified) return _isolationLevel; if (ParentScope != null) return ParentScope.IsolationLevel; - return DefaultIsolationLevel; + return Database.SqlContext.SqlSyntax.DefaultIsolationLevel; } } @@ -488,37 +486,9 @@ namespace Umbraco.Core.Scoping ?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value; /// - public void ReadLock(params int[] lockIds) - { - // soon as we get Database, a transaction is started - - if (Database.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks - foreach (var lockId in lockIds) - { - var i = Database.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new { id = lockId }); - if (i == null) // ensure we are actually locking! - throw new Exception($"LockObject with id={lockId} does not exist."); - } - } + public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds); /// - public void WriteLock(params int[] lockIds) - { - // soon as we get Database, a transaction is started - - if (Database.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks - foreach (var lockId in lockIds) - { - var i = Database.Execute("UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); - if (i == 0) // ensure we are actually locking! - throw new Exception($"LockObject with id={lockId} does not exist."); - } - } + public void WriteLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.WriteLock(Database, lockIds); } } diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 085a7b7a5b..dc271452e1 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using System.Web.Security; using Microsoft.AspNet.Identity; using Umbraco.Core.Configuration; -using Umbraco.Core.Exceptions; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.Identity; @@ -217,7 +216,8 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); - if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentNullOrEmptyException(nameof(passwordHash)); + if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash)); + if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash)); user.PasswordHash = passwordHash; @@ -329,7 +329,7 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); - if (login == null) throw new ArgumentNullException("login"); + if (login == null) throw new ArgumentNullException(nameof(login)); var logins = user.Logins; var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id); @@ -348,7 +348,7 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); - if (login == null) throw new ArgumentNullException("login"); + if (login == null) throw new ArgumentNullException(nameof(login)); var provider = login.LoginProvider; var key = login.ProviderKey; @@ -379,7 +379,7 @@ namespace Umbraco.Core.Security public Task FindAsync(UserLoginInfo login) { ThrowIfDisposed(); - if (login == null) throw new ArgumentNullException("login"); + if (login == null) throw new ArgumentNullException(nameof(login)); //get all logins associated with the login id var result = _externalLoginService.Find(login).ToArray(); @@ -413,7 +413,8 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); - if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value cannot be null or whitespace.", "roleName"); + if (roleName == null) throw new ArgumentNullException(nameof(roleName)); + if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(roleName)); var userRole = user.Roles.SingleOrDefault(r => r.RoleId == roleName); @@ -434,7 +435,8 @@ namespace Umbraco.Core.Security { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); - if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value cannot be null or whitespace.", "roleName"); + if (roleName == null) throw new ArgumentNullException(nameof(roleName)); + if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(roleName)); var userRole = user.Roles.SingleOrDefault(r => r.RoleId == roleName); diff --git a/src/Umbraco.Core/Serialization/JsonNetSerializer.cs b/src/Umbraco.Core/Serialization/JsonNetSerializer.cs index 1b5a9c6caf..46f23ca35e 100644 --- a/src/Umbraco.Core/Serialization/JsonNetSerializer.cs +++ b/src/Umbraco.Core/Serialization/JsonNetSerializer.cs @@ -60,7 +60,7 @@ namespace Umbraco.Core.Serialization /// public IStreamedResult ToStream(object input) { - string s = JsonConvert.SerializeObject(input, Formatting.Indented, _settings); + string s = JsonConvert.SerializeObject(input, Formatting.None, _settings); byte[] bytes = Encoding.UTF8.GetBytes(s); MemoryStream ms = new MemoryStream(bytes); diff --git a/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs b/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs index 497f7d47a9..bf7f87fd1a 100644 --- a/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs +++ b/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs @@ -6,9 +6,25 @@ namespace Umbraco.Core.Services.Changes public enum ContentTypeChangeTypes : byte { None = 0, - Create = 1, // item type has been created, no impact - RefreshMain = 2, // changed, impacts content (adding property or composition does NOT) - RefreshOther = 4, // changed, other changes - Remove = 8 // item type has been removed + + /// + /// Item type has been created, no impact + /// + Create = 1, + + /// + /// Content type changes impact only the Content type being saved + /// + RefreshMain = 2, + + /// + /// Content type changes impacts the content type being saved and others used that are composed of it + /// + RefreshOther = 4, // changed, other change + + /// + /// Content type was removed + /// + Remove = 8 } } diff --git a/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs b/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs index b3d15bd612..a5f5efdba9 100644 --- a/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs +++ b/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs @@ -2,9 +2,9 @@ namespace Umbraco.Core.Services.Changes { - internal static class TreeChangeExtensions + public static class TreeChangeExtensions { - public static TreeChange.EventArgs ToEventArgs(this IEnumerable> changes) + internal static TreeChange.EventArgs ToEventArgs(this IEnumerable> changes) { return new TreeChange.EventArgs(changes); } diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index 1175df81dc..dfe02ba690 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -11,6 +12,42 @@ namespace Umbraco.Core.Services /// public static class ContentServiceExtensions { + #region RTE Anchor values + + private static readonly Regex AnchorRegex = new Regex("", RegexOptions.Compiled); + + internal static IEnumerable GetAnchorValuesFromRTEs(this IContentService contentService, int id, string culture = "*") + { + var result = new List(); + var content = contentService.GetById(id); + + foreach (var contentProperty in content.Properties) + { + if (contentProperty.PropertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.TinyMce)) + { + var value = contentProperty.GetValue(culture)?.ToString(); + if (!string.IsNullOrEmpty(value)) + { + result.AddRange(contentService.GetAnchorValuesFromRTEContent(value)); + } + } + } + return result; + } + + + internal static IEnumerable GetAnchorValuesFromRTEContent(this IContentService contentService, string rteContent) + { + var result = new List(); + var matches = AnchorRegex.Matches(rteContent); + foreach (Match match in matches) + { + result.Add(match.Value.Split('\"')[1]); + } + return result; + } + #endregion + public static IEnumerable GetByIds(this IContentService contentService, IEnumerable ids) { var guids = new List(); diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index f643039dca..37f1e5127f 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -7,6 +7,21 @@ namespace Umbraco.Core.Services { public static class ContentTypeServiceExtensions { + /// + /// Gets all of the element types (e.g. content types that have been marked as an element type). + /// + /// The content type service. + /// Returns all the element types. + public static IEnumerable GetAllElementTypes(this IContentTypeService contentTypeService) + { + if (contentTypeService == null) + { + return Enumerable.Empty(); + } + + return contentTypeService.GetAll().Where(x => x.IsElement); + } + /// /// Returns the available composite content types for a given content type /// @@ -22,12 +37,14 @@ namespace Umbraco.Core.Services /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot /// be looked up via the db, they need to be passed in. /// + /// Wether the composite content types should be applicable for an element type /// internal static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(this IContentTypeService ctService, IContentTypeComposition source, IContentTypeComposition[] allContentTypes, string[] filterContentTypes = null, - string[] filterPropertyTypes = null) + string[] filterPropertyTypes = null, + bool isElement = false) { filterContentTypes = filterContentTypes == null ? Array.Empty() @@ -46,7 +63,7 @@ namespace Umbraco.Core.Services .Select(c => c.Alias) .Union(filterPropertyTypes) .ToArray(); - + var sourceId = source?.Id ?? 0; // find out if any content type uses this content type @@ -64,8 +81,9 @@ namespace Umbraco.Core.Services x => x.Id)); // usable types are those that are top-level + // do not allow element types to be composed by non-element types as this will break the model generation in ModelsBuilder var usableContentTypes = allContentTypes - .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); + .Where(x => x.ContentTypeComposition.Any() == false && (isElement == false || x.IsElement)).ToArray(); foreach (var x in usableContentTypes) list.Add(x); diff --git a/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs new file mode 100644 index 0000000000..3b72a6f258 --- /dev/null +++ b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Services +{ + internal static class DateTypeServiceExtensions + { + public static bool IsDataTypeIgnoringUserStartNodes(this IDataTypeService dataTypeService, Guid key) + { + if (DataTypeExtensions.IsBuildInDataType(key)) return false; //built in ones can never be ignoring start nodes + + var dataType = dataTypeService.GetDataType(key); + + if (dataType != null && dataType.Configuration is IIgnoreUserStartNodesConfig ignoreStartNodesConfig) + return ignoreStartNodesConfig.IgnoreUserStartNodes; + + return false; + } + } +} diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 48e577a8f0..58279fb4da 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -526,5 +526,6 @@ namespace Umbraco.Core.Services OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId); #endregion + } } diff --git a/src/Umbraco.Core/Services/IContentServiceBase.cs b/src/Umbraco.Core/Services/IContentServiceBase.cs index 439c55d0d0..c40f49347f 100644 --- a/src/Umbraco.Core/Services/IContentServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentServiceBase.cs @@ -1,9 +1,16 @@ -namespace Umbraco.Core.Services +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services { /// /// Placeholder for sharing logic between the content, media (and member) services /// TODO: Start sharing the logic! /// public interface IContentServiceBase : IService - { } + { + /// + /// Checks/fixes the data integrity of node paths/levels stored in the database + /// + ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options); + } } diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs index b86494adb5..4c818aa87c 100644 --- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs @@ -25,7 +25,7 @@ namespace Umbraco.Core.Services /// /// Gets a content type. /// - TItem Get(int id); + new TItem Get(int id); /// /// Gets a content type. @@ -39,13 +39,22 @@ namespace Umbraco.Core.Services int Count(); + /// + /// Returns true or false depending on whether content nodes have been created based on the provided content type id. + /// + bool HasContentNodes(int id); + IEnumerable GetAll(params int[] ids); + IEnumerable GetAll(IEnumerable ids); IEnumerable GetDescendants(int id, bool andSelf); // parent-child axis IEnumerable GetComposedOf(int id); // composition axis IEnumerable GetChildren(int id); + IEnumerable GetChildren(Guid id); + bool HasChildren(int id); + bool HasChildren(Guid id); void Save(TItem item, int userId = Constants.Security.SuperUserId); void Save(IEnumerable items, int userId = Constants.Security.SuperUserId); @@ -63,6 +72,13 @@ namespace Umbraco.Core.Services /// bool HasContainerInPath(string contentPath); + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(params int[] ids); + Attempt> CreateContainer(int parentContainerId, string name, int userId = Constants.Security.SuperUserId); Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId); EntityContainer GetContainer(int containerId); diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index 3dc530e250..3ebfa95bfb 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -4,11 +4,19 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Services { + /// /// Defines the DataType Service, which is an easy access to operations involving /// public interface IDataTypeService : IService { + /// + /// Returns a dictionary of content type s and the property type aliases that use a + /// + /// + /// + IReadOnlyDictionary> GetReferences(int id); + Attempt> CreateContainer(int parentId, string name, int userId = Constants.Security.SuperUserId); Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId); EntityContainer GetContainer(int containerId); diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 9d0399f324..f03bc640ec 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -15,92 +15,40 @@ namespace Umbraco.Core.Services /// The identifier of the entity. IEntitySlim Get(int id); - /// - /// Gets an entity. - /// - /// The identifier of the entity. - /// A value indicating whether to load a light entity, or the full entity. - /// Returns either a , or an actual entity, depending on . - IUmbracoEntity Get(int id, bool full); - /// /// Gets an entity. /// /// The unique key of the entity. IEntitySlim Get(Guid key); - /// - /// Gets an entity. - /// - /// The unique key of the entity. - /// A value indicating whether to load a light entity, or the full entity. - /// Returns either a , or an actual entity, depending on . - IUmbracoEntity Get(Guid key, bool full); - /// /// Gets an entity. /// /// The identifier of the entity. /// The object type of the entity. IEntitySlim Get(int id, UmbracoObjectTypes objectType); - - /// - /// Gets an entity. - /// - /// The identifier of the entity. - /// The object type of the entity. - /// A value indicating whether to load a light entity, or the full entity. - /// Returns either a , or an actual entity, depending on . - IUmbracoEntity Get(int id, UmbracoObjectTypes objectType, bool full); - + /// /// Gets an entity. /// /// The unique key of the entity. /// The object type of the entity. IEntitySlim Get(Guid key, UmbracoObjectTypes objectType); - - /// - /// Gets an entity. - /// - /// The unique key of the entity. - /// The object type of the entity. - /// A value indicating whether to load a light entity, or the full entity. - /// Returns either a , or an actual entity, depending on . - IUmbracoEntity Get(Guid key, UmbracoObjectTypes objectType, bool full); - + /// /// Gets an entity. /// /// The type used to determine the object type of the entity. /// The identifier of the entity. IEntitySlim Get(int id) where T : IUmbracoEntity; - - /// - /// Gets an entity. - /// - /// The type used to determine the object type of the entity. - /// The identifier of the entity. - /// A value indicating whether to load a light entity, or the full entity. - /// Returns either a , or an actual entity, depending on . - IUmbracoEntity Get(int id, bool full) where T : IUmbracoEntity; - + /// /// Gets an entity. /// /// The type used to determine the object type of the entity. /// The unique key of the entity. IEntitySlim Get(Guid key) where T : IUmbracoEntity; - - /// - /// Gets an entity. - /// - /// The type used to determine the object type of the entity. - /// The unique key of the entity. - /// A value indicating whether to load a light entity, or the full entity. - /// Returns either a , or an actual entity, depending on . - IUmbracoEntity Get(Guid key, bool full) where T : IUmbracoEntity; - + /// /// Determines whether an entity exists. /// diff --git a/src/Umbraco.Core/Services/IInstallationService.cs b/src/Umbraco.Core/Services/IInstallationService.cs new file mode 100644 index 0000000000..334088f8ae --- /dev/null +++ b/src/Umbraco.Core/Services/IInstallationService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + public interface IInstallationService + { + Task LogInstall(InstallLog installLog); + } +} diff --git a/src/Umbraco.Core/Services/IMembershipRoleService.cs b/src/Umbraco.Core/Services/IMembershipRoleService.cs index 30531b5031..7389bb9799 100644 --- a/src/Umbraco.Core/Services/IMembershipRoleService.cs +++ b/src/Umbraco.Core/Services/IMembershipRoleService.cs @@ -11,6 +11,9 @@ namespace Umbraco.Core.Services IEnumerable GetAllRoles(); IEnumerable GetAllRoles(int memberId); IEnumerable GetAllRoles(string username); + IEnumerable GetAllRolesIds(); + IEnumerable GetAllRolesIds(int memberId); + IEnumerable GetAllRolesIds(string username); IEnumerable GetMembersInRole(string roleName); IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); bool DeleteRole(string roleName, bool throwIfBeingUsed); diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index e2733a311d..bf8bcd5b2a 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -8,186 +8,223 @@ namespace Umbraco.Core.Services public interface IRelationService : IService { /// - /// Gets a by its Id + /// Gets a by its Id /// - /// Id of the - /// A object + /// Id of the + /// A object IRelation GetById(int id); /// - /// Gets a by its Id + /// Gets a by its Id /// - /// Id of the - /// A object + /// Id of the + /// A object IRelationType GetRelationTypeById(int id); /// - /// Gets a by its Id + /// Gets a by its Id /// - /// Id of the - /// A object + /// Id of the + /// A object IRelationType GetRelationTypeById(Guid id); /// - /// Gets a by its Alias + /// Gets a by its Alias /// - /// Alias of the - /// A object + /// Alias of the + /// A object IRelationType GetRelationTypeByAlias(string alias); /// - /// Gets all objects + /// Gets all objects /// /// Optional array of integer ids to return relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetAllRelations(params int[] ids); /// - /// Gets all objects by their + /// Gets all objects by their /// - /// to retrieve Relations for - /// An enumerable list of objects - IEnumerable GetAllRelationsByRelationType(RelationType relationType); + /// to retrieve Relations for + /// An enumerable list of objects + IEnumerable GetAllRelationsByRelationType(IRelationType relationType); /// - /// Gets all objects by their 's Id + /// Gets all objects by their 's Id /// - /// Id of the to retrieve Relations for - /// An enumerable list of objects + /// Id of the to retrieve Relations for + /// An enumerable list of objects IEnumerable GetAllRelationsByRelationType(int relationTypeId); /// - /// Gets all objects + /// Gets all objects /// /// Optional array of integer ids to return relationtypes for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetAllRelationTypes(params int[] ids); /// - /// Gets a list of objects by their parent Id + /// Gets a list of objects by their parent Id /// /// Id of the parent to retrieve relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByParentId(int id); /// - /// Gets a list of objects by their parent entity + /// Gets a list of objects by their parent Id + /// + /// Id of the parent to retrieve relations for + /// Alias of the type of relation to retrieve + /// An enumerable list of objects + IEnumerable GetByParentId(int id, string relationTypeAlias); + + /// + /// Gets a list of objects by their parent entity /// /// Parent Entity to retrieve relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByParent(IUmbracoEntity parent); /// - /// Gets a list of objects by their parent entity + /// Gets a list of objects by their parent entity /// /// Parent Entity to retrieve relations for /// Alias of the type of relation to retrieve - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias); /// - /// Gets a list of objects by their child Id + /// Gets a list of objects by their child Id /// /// Id of the child to retrieve relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByChildId(int id); /// - /// Gets a list of objects by their child Entity + /// Gets a list of objects by their child Id + /// + /// Id of the child to retrieve relations for + /// Alias of the type of relation to retrieve + /// An enumerable list of objects + IEnumerable GetByChildId(int id, string relationTypeAlias); + + /// + /// Gets a list of objects by their child Entity /// /// Child Entity to retrieve relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByChild(IUmbracoEntity child); /// - /// Gets a list of objects by their child Entity + /// Gets a list of objects by their child Entity /// /// Child Entity to retrieve relations for /// Alias of the type of relation to retrieve - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias); /// - /// Gets a list of objects by their child or parent Id. + /// Gets a list of objects by their child or parent Id. /// Using this method will get you all relations regards of it being a child or parent relation. /// /// Id of the child or parent to retrieve relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByParentOrChildId(int id); IEnumerable GetByParentOrChildId(int id, string relationTypeAlias); /// - /// Gets a list of objects by the Name of the + /// Gets a list of objects by the Name of the /// - /// Name of the to retrieve Relations for - /// An enumerable list of objects + /// Name of the to retrieve Relations for + /// An enumerable list of objects IEnumerable GetByRelationTypeName(string relationTypeName); /// - /// Gets a list of objects by the Alias of the + /// Gets a list of objects by the Alias of the /// - /// Alias of the to retrieve Relations for - /// An enumerable list of objects + /// Alias of the to retrieve Relations for + /// An enumerable list of objects IEnumerable GetByRelationTypeAlias(string relationTypeAlias); /// - /// Gets a list of objects by the Id of the + /// Gets a list of objects by the Id of the /// - /// Id of the to retrieve Relations for - /// An enumerable list of objects + /// Id of the to retrieve Relations for + /// An enumerable list of objects IEnumerable GetByRelationTypeId(int relationTypeId); + /// + /// Gets a paged result of + /// + /// + /// + /// + /// + /// + IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering ordering = null); + /// /// Gets the Child object from a Relation as an /// /// Relation to retrieve child object from - /// Optional bool to load the complete object graph when set to False /// An - IUmbracoEntity GetChildEntityFromRelation(IRelation relation, bool loadBaseType = false); + IUmbracoEntity GetChildEntityFromRelation(IRelation relation); /// /// Gets the Parent object from a Relation as an /// /// Relation to retrieve parent object from - /// Optional bool to load the complete object graph when set to False /// An - IUmbracoEntity GetParentEntityFromRelation(IRelation relation, bool loadBaseType = false); + IUmbracoEntity GetParentEntityFromRelation(IRelation relation); /// /// Gets the Parent and Child objects from a Relation as a "/> with . /// /// Relation to retrieve parent and child object from - /// Optional bool to load the complete object graph when set to False /// Returns a Tuple with Parent (item1) and Child (item2) - Tuple GetEntitiesFromRelation(IRelation relation, bool loadBaseType = false); + Tuple GetEntitiesFromRelation(IRelation relation); /// /// Gets the Child objects from a list of Relations as a list of objects. /// /// List of relations to retrieve child objects from - /// Optional bool to load the complete object graph when set to False /// An enumerable list of - IEnumerable GetChildEntitiesFromRelations(IEnumerable relations, bool loadBaseType = false); + IEnumerable GetChildEntitiesFromRelations(IEnumerable relations); /// /// Gets the Parent objects from a list of Relations as a list of objects. /// /// List of relations to retrieve parent objects from - /// Optional bool to load the complete object graph when set to False /// An enumerable list of - IEnumerable GetParentEntitiesFromRelations(IEnumerable relations, - bool loadBaseType = false); + IEnumerable GetParentEntitiesFromRelations(IEnumerable relations); + + /// + /// Returns paged parent entities for a related child id + /// + /// + /// + /// + /// + /// + IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes); + + /// + /// Returns paged child entities for a related parent id + /// + /// + /// + /// + /// + /// + IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes); /// /// Gets the Parent and Child objects from a list of Relations as a list of objects. /// /// List of relations to retrieve parent and child objects from - /// Optional bool to load the complete object graph when set to False /// An enumerable list of with - IEnumerable> GetEntitiesFromRelations( - IEnumerable relations, - bool loadBaseType = false); + IEnumerable> GetEntitiesFromRelations(IEnumerable relations); /// /// Relates two objects by their entity Ids. @@ -195,7 +232,7 @@ namespace Umbraco.Core.Services /// Id of the parent /// Id of the child /// The type of relation to create - /// The created + /// The created IRelation Relate(int parentId, int childId, IRelationType relationType); /// @@ -204,7 +241,7 @@ namespace Umbraco.Core.Services /// Parent entity /// Child entity /// The type of relation to create - /// The created + /// The created IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType); /// @@ -213,7 +250,7 @@ namespace Umbraco.Core.Services /// Id of the parent /// Id of the child /// Alias of the type of relation to create - /// The created + /// The created IRelation Relate(int parentId, int childId, string relationTypeAlias); /// @@ -222,14 +259,14 @@ namespace Umbraco.Core.Services /// Parent entity /// Child entity /// Alias of the type of relation to create - /// The created + /// The created IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias); /// - /// Checks whether any relations exists for the passed in . + /// Checks whether any relations exists for the passed in . /// - /// to check for relations - /// Returns True if any relations exists for the given , otherwise False + /// to check for relations + /// Returns True if any relations exists for the given , otherwise False bool HasRelations(IRelationType relationType); /// @@ -274,33 +311,35 @@ namespace Umbraco.Core.Services bool AreRelated(int parentId, int childId, string relationTypeAlias); /// - /// Saves a + /// Saves a /// /// Relation to save void Save(IRelation relation); + void Save(IEnumerable relations); + /// - /// Saves a + /// Saves a /// /// RelationType to Save void Save(IRelationType relationType); /// - /// Deletes a + /// Deletes a /// /// Relation to Delete void Delete(IRelation relation); /// - /// Deletes a + /// Deletes a /// /// RelationType to Delete void Delete(IRelationType relationType); /// - /// Deletes all objects based on the passed in + /// Deletes all objects based on the passed in /// - /// to Delete Relations for + /// to Delete Relations for void DeleteRelationsOfType(IRelationType relationType); } } diff --git a/src/Umbraco.Core/Services/IUpgradeService.cs b/src/Umbraco.Core/Services/IUpgradeService.cs new file mode 100644 index 0000000000..70bbb68085 --- /dev/null +++ b/src/Umbraco.Core/Services/IUpgradeService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Semver; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + public interface IUpgradeService + { + Task CheckUpgrade(SemVersion version); + } +} diff --git a/src/Umbraco.Core/Services/Implement/AuditService.cs b/src/Umbraco.Core/Services/Implement/AuditService.cs index 46c851a789..7d3be1d52b 100644 --- a/src/Umbraco.Core/Services/Implement/AuditService.cs +++ b/src/Umbraco.Core/Services/Implement/AuditService.cs @@ -51,8 +51,8 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var result = sinceDate.HasValue == false - ? _auditRepository.Get(Query().Where(x => x.UserId == userId && x.AuditType == type)) - : _auditRepository.Get(Query().Where(x => x.UserId == userId && x.AuditType == type && x.CreateDate >= sinceDate.Value)); + ? _auditRepository.Get(type, Query().Where(x => x.UserId == userId)) + : _auditRepository.Get(type, Query().Where(x => x.UserId == userId && x.CreateDate >= sinceDate.Value)); scope.Complete(); return result; } @@ -63,8 +63,8 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var result = sinceDate.HasValue == false - ? _auditRepository.Get(Query().Where(x => x.AuditType == type)) - : _auditRepository.Get(Query().Where(x => x.AuditType == type && x.CreateDate >= sinceDate.Value)); + ? _auditRepository.Get(type, Query()) + : _auditRepository.Get(type, Query().Where(x => x.CreateDate >= sinceDate.Value)); scope.Complete(); return result; } @@ -142,7 +142,7 @@ namespace Umbraco.Core.Services.Implement if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); - if (userId < 0) + if (userId < Constants.Security.SuperUserId) { totalRecords = 0; return Enumerable.Empty(); diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index bd77daffbe..068864a558 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -11,7 +11,6 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Core.Services.Changes; @@ -31,6 +30,7 @@ namespace Umbraco.Core.Services.Implement private IQuery _queryNotTrashed; //TODO: The non-lazy object should be injected private readonly Lazy _propertyValidationService = new Lazy(() => new PropertyValidationService()); + #region Constructors @@ -601,23 +601,27 @@ namespace Umbraco.Core.Services.Implement totalChildren = 0; return Enumerable.Empty(); } - return GetPagedDescendantsLocked(contentPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering); } - return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); } } - private IEnumerable GetPagedDescendantsLocked(string contentPath, long pageIndex, int pageSize, out long totalChildren, + private IQuery GetPagedDescendantQuery(string contentPath) + { + var query = Query(); + if (!contentPath.IsNullOrWhiteSpace()) + query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar)); + return query; + } + + private IEnumerable GetPagedLocked(IQuery query, long pageIndex, int pageSize, out long totalChildren, IQuery filter, Ordering ordering) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - var query = Query(); - if (!contentPath.IsNullOrWhiteSpace()) - query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar)); - return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } @@ -755,7 +759,12 @@ namespace Umbraco.Core.Services.Implement { var publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) - throw new InvalidOperationException("Cannot save (un)publishing content, use the dedicated SavePublished method."); + throw new InvalidOperationException($"Cannot save (un)publishing content with name: {content.Name} - and state: {content.PublishedState}, use the dedicated SavePublished method."); + + if (content.Name != null && content.Name.Length > 255) + { + throw new InvalidOperationException($"Content with the name {content.Name} cannot be more than 255 characters in length."); + } var evtMsgs = EventMessagesFactory.Get(); @@ -870,10 +879,17 @@ namespace Umbraco.Core.Services.Implement throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types."); } + if(content.Name != null && content.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); @@ -882,13 +898,13 @@ namespace Umbraco.Core.Services.Implement // if culture is '*', then publish them all (including variants) //this will create the correct culture impact even if culture is * or null - var impact = CultureImpact.Create(culture, _languageRepository.IsDefault(culture), content); + var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content); // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. content.PublishCulture(impact); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } @@ -900,10 +916,17 @@ namespace Umbraco.Core.Services.Implement if (content == null) throw new ArgumentNullException(nameof(content)); if (cultures == null) throw new ArgumentNullException(nameof(cultures)); + if (content.Name != null && content.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var evtMsgs = EventMessagesFactory.Get(); var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) @@ -911,25 +934,23 @@ namespace Umbraco.Core.Services.Implement var varies = content.ContentType.VariesByCulture(); - if (cultures.Length == 0) + if (cultures.Length == 0 && !varies) { //no cultures specified and doesn't vary, so publish it, else nothing to publish - return !varies - ? SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents) - : new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + return SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents); } if (cultures.Any(x => x == null || x == "*")) throw new InvalidOperationException("Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed"); - var impacts = cultures.Select(x => CultureImpact.Explicit(x, _languageRepository.IsDefault(x))); + var impacts = cultures.Select(x => CultureImpact.Explicit(x, IsDefaultCulture(allLangs, x))); // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. foreach (var impact in impacts) content.PublishCulture(impact); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } @@ -969,6 +990,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); @@ -976,26 +999,39 @@ namespace Umbraco.Core.Services.Implement // all cultures = unpublish whole if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null)) { + // It's important to understand that when the document varies by culture but the "*" is used, + // we are just unpublishing the whole document but leaving all of the culture's as-is. This is expected + // because we don't want to actually unpublish every culture and then the document, we just want everything + // to be non-routable so that when it's re-published all variants were as they were. + content.PublishedState = PublishedState.Unpublishing; + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId); + scope.Complete(); + return result; } else { - // If the culture we want to unpublish was already unpublished, nothing to do. - // To check for that we need to lookup the persisted content item - var persisted = content.HasIdentity ? GetById(content.Id) : null; + // Unpublish the culture, this will change the document state to Publishing! ... which is expected because this will + // essentially be re-publishing the document with the requested culture removed. + // The call to CommitDocumentChangesInternal will perform all the checks like if this is a mandatory culture or the last culture being unpublished + // and will then unpublish the document accordingly. + // If the result of this is false it means there was no culture to unpublish (i.e. it was already unpublished or it did not exist) + var removed = content.UnpublishCulture(culture); - if (persisted != null && !persisted.IsCulturePublished(culture)) + //save and publish any changes + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId); + + scope.Complete(); + + // In one case the result will be PublishStatusType.FailedPublishNothingToPublish which means that no cultures + // were specified to be published which will be the case when removed is false. In that case + // we want to swap the result type to PublishResultType.SuccessUnpublishAlready (that was the expectation before). + if (result.Result == PublishResultType.FailedPublishNothingToPublish && !removed) return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); - // unpublish the culture - content.UnpublishCulture(culture); + return result; } - - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId); - scope.Complete(); - return result; } - } /// @@ -1030,15 +1066,35 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var allLangs = _languageRepository.GetMany().ToList(); + + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } } + /// + /// Handles a lot of business logic cases for how the document should be persisted + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for pending scheduled publishing, etc... is dealt with in this method. + /// There is quite a lot of cases to take into account along with logic that needs to deal with scheduled saving/publishing, branch saving/publishing, etc... + /// + /// private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content, - ContentSavingEventArgs saveEventArgs, - int userId = Constants.Security.SuperUserId, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) + ContentSavingEventArgs saveEventArgs, IReadOnlyCollection allLangs, + int userId = Constants.Security.SuperUserId, + bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) { if (scope == null) throw new ArgumentNullException(nameof(scope)); if (content == null) throw new ArgumentNullException(nameof(content)); @@ -1053,8 +1109,8 @@ namespace Umbraco.Core.Services.Implement if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing) content.PublishedState = PublishedState.Publishing; - // state here is either Publishing or Unpublishing - // (even though, Publishing to unpublish a culture may end up unpublishing everything) + // State here is either Publishing or Unpublishing + // Publishing to unpublish a culture may end up unpublishing everything so these flags can be flipped later var publishing = content.PublishedState == PublishedState.Publishing; var unpublishing = content.PublishedState == PublishedState.Unpublishing; @@ -1071,6 +1127,18 @@ namespace Umbraco.Core.Services.Implement var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch; var previouslyPublished = content.HasIdentity && content.Published; + //inline method to persist the document with the documentRepository since this logic could be called a couple times below + void SaveDocument(IContent c) + { + // save, always + if (c.HasIdentity == false) + c.CreatorId = userId; + c.WriterId = userId; + + // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing + _documentRepository.Save(c); + } + if (publishing) { //determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo @@ -1080,11 +1148,25 @@ namespace Umbraco.Core.Services.Implement : null; // ensure that the document can be published, and publish handling events, business rules, etc - publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs, saveEventArgs); + publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs, saveEventArgs, allLangs); if (publishResult.Success) { // note: StrategyPublish flips the PublishedState to Publishing! publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, evtMsgs); + + //check if a culture has been unpublished and if there are no cultures left, and then unpublish document as a whole + if (publishResult.Result == PublishResultType.SuccessUnpublishCulture && content.PublishCultureInfos.Count == 0) + { + // This is a special case! We are unpublishing the last culture and to persist that we need to re-publish without any cultures + // so the state needs to remain Publishing to do that. However, we then also need to unpublish the document and to do that + // the state needs to be Unpublishing and it cannot be both. This state is used within the documentRepository to know how to + // persist certain things. So before proceeding below, we need to save the Publishing state to publish no cultures, then we can + // mark the document for Unpublishing. + SaveDocument(content); + + //set the flag to unpublish and continue + unpublishing = content.Published; // if not published yet, nothing to do + } } else { @@ -1145,13 +1227,8 @@ namespace Umbraco.Core.Services.Implement } } - // save, always - if (content.HasIdentity == false) - content.CreatorId = userId; - content.WriterId = userId; - - // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing - _documentRepository.Save(content); + //Persist the document + SaveDocument(content); // raise the Saved event, always if (raiseEvents) @@ -1169,17 +1246,34 @@ namespace Umbraco.Core.Services.Implement if (culturesUnpublishing != null) { - //If we are here, it means we tried unpublishing a culture but it was mandatory so now everything is unpublished - var langs = string.Join(", ", _languageRepository.GetMany() + // This will mean that that we unpublished a mandatory culture or we unpublished the last culture. + + var langs = string.Join(", ", allLangs .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); - //log that the whole content item has been unpublished due to mandatory culture unpublished - Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)"); - } - else - Audit(AuditType.Unpublish, userId, content.Id); + if (publishResult == null) + throw new PanicException("publishResult == null - should not happen"); + + switch(publishResult.Result) + { + case PublishResultType.FailedPublishMandatoryCultureMissing: + //occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture) + + //log that the whole content item has been unpublished due to mandatory culture unpublished + Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)"); + return new PublishResult(PublishResultType.SuccessUnpublishMandatoryCulture, evtMsgs, content); + case PublishResultType.SuccessUnpublishCulture: + //occurs when the last culture is unpublished + + Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)"); + return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, evtMsgs, content); + } + + } + + Audit(AuditType.Unpublish, userId, content.Id); return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); } @@ -1194,6 +1288,9 @@ namespace Umbraco.Core.Services.Implement { if (isNew == false && previouslyPublished == false) changeType = TreeChangeTypes.RefreshBranch; // whole branch + else if (isNew == false && previouslyPublished) + changeType = TreeChangeTypes.RefreshNode; // single node + // invalidate the node/branch if (!branchOne) // for branches, handled by SaveAndPublishBranch @@ -1219,7 +1316,7 @@ namespace Umbraco.Core.Services.Implement case PublishResultType.SuccessPublishCulture: if (culturesPublishing != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesPublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", langs); @@ -1228,7 +1325,7 @@ namespace Umbraco.Core.Services.Implement case PublishResultType.SuccessUnpublishCulture: if (culturesUnpublishing != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); @@ -1242,14 +1339,14 @@ namespace Umbraco.Core.Services.Implement // should not happen if (branchOne && !branchRoot) - throw new Exception("panic"); + throw new PanicException("branchOne && !branchRoot - should not happen"); //if publishing didn't happen or if it has failed, we still need to log which cultures were saved if (!branchOne && (publishResult == null || !publishResult.Success)) { if (culturesChanging != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesChanging.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs); @@ -1280,6 +1377,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + foreach (var d in _documentRepository.GetContentForRelease(date)) { PublishResult result; @@ -1308,7 +1407,7 @@ namespace Umbraco.Core.Services.Implement //publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed Property[] invalidProperties = null; - var impact = CultureImpact.Explicit(culture, _languageRepository.IsDefault(culture)); + var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs, culture)); var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact); if (invalidProperties != null && invalidProperties.Length > 0) Logger.Warn("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}", @@ -1323,7 +1422,7 @@ namespace Umbraco.Core.Services.Implement else if (!publishing) result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); else - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId); + result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); if (result.Success == false) @@ -1373,7 +1472,7 @@ namespace Umbraco.Core.Services.Implement d.UnpublishCulture(c); } - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId); + result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); yield return result; @@ -1399,7 +1498,7 @@ namespace Umbraco.Core.Services.Implement } // utility 'PublishCultures' func used by SaveAndPublishBranch - private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish) + private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish, IReadOnlyCollection allLangs) { //TODO: This does not support being able to return invalid property details to bubble up to the UI @@ -1409,7 +1508,7 @@ namespace Umbraco.Core.Services.Implement { return culturesToPublish.All(culture => { - var impact = CultureImpact.Create(culture, _languageRepository.IsDefault(culture), content); + var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content); return content.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact); }); } @@ -1518,7 +1617,7 @@ namespace Umbraco.Core.Services.Implement internal IEnumerable SaveAndPublishBranch(IContent document, bool force, Func> shouldPublish, - Func, bool> publishCultures, + Func, IReadOnlyCollection, bool> publishCultures, int userId = Constants.Security.SuperUserId) { if (shouldPublish == null) throw new ArgumentNullException(nameof(shouldPublish)); @@ -1532,6 +1631,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + if (!document.HasIdentity) throw new InvalidOperationException("Cannot not branch-publish a new document."); @@ -1540,7 +1641,7 @@ namespace Umbraco.Core.Services.Implement throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch."); // deal with the branch root - if it fails, abort - var result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId); + var result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId, allLangs); if (result != null) { results.Add(result); @@ -1571,7 +1672,7 @@ namespace Umbraco.Core.Services.Implement } // no need to check path here, parent has to be published here - result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId); + result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId, allLangs); if (result != null) { results.Add(result); @@ -1603,10 +1704,10 @@ namespace Umbraco.Core.Services.Implement // publishValues: a function publishing values (using the appropriate PublishCulture calls) private PublishResult SaveAndPublishBranchItem(IScope scope, IContent document, Func> shouldPublish, - Func, bool> publishCultures, + Func, IReadOnlyCollection, bool> publishCultures, bool isRoot, ICollection publishedDocuments, - EventMessages evtMsgs, int userId) + EventMessages evtMsgs, int userId, IReadOnlyCollection allLangs) { var culturesToPublish = shouldPublish(document); if (culturesToPublish == null) // null = do not include @@ -1619,13 +1720,13 @@ namespace Umbraco.Core.Services.Implement return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document); // publish & check if values are valid - if (!publishCultures(document, culturesToPublish)) + if (!publishCultures(document, culturesToPublish, allLangs)) { //TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document); } - var result = CommitDocumentChangesInternal(scope, document, saveEventArgs, userId, branchOne: true, branchRoot: isRoot); + var result = CommitDocumentChangesInternal(scope, document, saveEventArgs, allLangs, userId, branchOne: true, branchRoot: isRoot); if (result.Success) publishedDocuments.Add(document); return result; @@ -1680,12 +1781,11 @@ namespace Umbraco.Core.Services.Implement } const int pageSize = 500; - var page = 0; var total = long.MaxValue; - while (page * pageSize < total) + while (total > 0) { //get descendants - ordered from deepest to shallowest - var descendants = GetPagedDescendants(content.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); + var descendants = GetPagedDescendants(content.Id, 0, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); foreach (var c in descendants) DoDelete(c); } @@ -1752,7 +1852,7 @@ namespace Umbraco.Core.Services.Implement scope.WriteLock(Constants.Locks.ContentTree); var c = _documentRepository.Get(id); - if (c.VersionId != versionId) // don't delete the current version + if (c.VersionId != versionId && c.PublishedVersionId != versionId) // don't delete the current or published version _documentRepository.DeleteVersion(versionId); scope.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId)); @@ -1770,7 +1870,7 @@ namespace Umbraco.Core.Services.Implement public OperationResult MoveToRecycleBin(IContent content, int userId) { var evtMsgs = EventMessagesFactory.Get(); - var moves = new List>(); + var moves = new List<(IContent, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -1829,7 +1929,7 @@ namespace Umbraco.Core.Services.Implement return; } - var moves = new List>(); + var moves = new List<(IContent, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -1882,7 +1982,7 @@ namespace Umbraco.Core.Services.Implement // MUST be called from within WriteLock // trash indicates whether we are trashing, un-trashing, or not changing anything private void PerformMoveLocked(IContent content, int parentId, IContent parent, int userId, - ICollection> moves, + ICollection<(IContent, string)> moves, bool? trash) { content.WriterId = userId; @@ -1894,7 +1994,7 @@ namespace Umbraco.Core.Services.Implement var paths = new Dictionary(); - moves.Add(Tuple.Create(content, content.Path)); // capture original path + moves.Add((content, content.Path)); // capture original path //need to store the original path to lookup descendants based on it below var originalPath = content.Path; @@ -1911,21 +2011,24 @@ namespace Umbraco.Core.Services.Implement paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString) : parent.Path) + "," + content.Id; const int pageSize = 500; - var page = 0; - var total = long.MaxValue; - while (page * pageSize < total) + var query = GetPagedDescendantQuery(originalPath); + long total; + do { - var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced + var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + foreach (var descendant in descendants) { - moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path + moves.Add((descendant, descendant.Path)); // capture original path // update path and level since we do not update parentId descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; descendant.Level += levelDelta; PerformMoveContentLocked(descendant, userId, trash); } - } + + } while (total > pageSize); } @@ -2280,6 +2383,25 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Succeed(evtMsgs); } + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.WriteLock(Constants.Locks.ContentTree); + + var report = _documentRepository.CheckDataIntegrity(options); + + if (report.FixedIssues.Count > 0) + { + //The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref + var root = new Content("root", -1, new ContentType(-1)) {Id = -1, Key = Guid.Empty}; + scope.Events.Dispatch(TreeChanged, this, new TreeChange.EventArgs(new TreeChange(root, TreeChangeTypes.RefreshAll))); + } + + return report; + } + } + #endregion #region Internal Methods @@ -2328,6 +2450,9 @@ namespace Umbraco.Core.Services.Implement _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Document), message, parameters)); } + private bool IsDefaultCulture(IReadOnlyCollection langs, string culture) => langs.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)); + private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) => langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture)); + #endregion #region Event Handlers @@ -2482,7 +2607,9 @@ namespace Umbraco.Core.Services.Implement /// /// /// - private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, IReadOnlyList culturesPublishing, IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs, ContentSavingEventArgs savingEventArgs) + private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, IReadOnlyList culturesPublishing, + IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs, ContentSavingEventArgs savingEventArgs, + IReadOnlyCollection allLangs) { // raise Publishing event if (scope.Events.DispatchCancelable(Publishing, this, savingEventArgs.ToContentPublishingEventArgs())) @@ -2494,8 +2621,8 @@ namespace Umbraco.Core.Services.Implement var variesByCulture = content.ContentType.VariesByCulture(); var impactsToPublish = culturesPublishing == null - ? new[] {CultureImpact.Invariant} //if it's null it's invariant - : culturesPublishing.Select(x => CultureImpact.Explicit(x, _languageRepository.IsDefault(x))).ToArray(); + ? new[] { CultureImpact.Invariant } //if it's null it's invariant + : culturesPublishing.Select(x => CultureImpact.Explicit(x, allLangs.Any(lang => lang.IsoCode.InvariantEquals(x) && lang.IsMandatory))).ToArray(); // publish the culture(s) if (!impactsToPublish.All(content.PublishCulture)) @@ -2516,11 +2643,17 @@ namespace Umbraco.Core.Services.Implement if (culturesPublishing == null) throw new InvalidOperationException("Internal error, variesByCulture but culturesPublishing is null."); - if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing.Count == 0) // no published cultures = cannot be published + if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing.Count == 0) + { + // no published cultures = cannot be published + // This will occur if for example, a culture that is already unpublished is sent to be unpublished again, or vice versa, in that case + // there will be nothing to publish/unpublish. return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + } + // missing mandatory culture = cannot be published - var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); + var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode); var mandatoryMissing = mandatoryCultures.Any(x => !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); if (mandatoryMissing) return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content); @@ -2661,6 +2794,7 @@ namespace Umbraco.Core.Services.Implement { var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); + //TODO: What is this check?? we just created this attempt and of course it is Success?! if (attempt.Success == false) return attempt; @@ -2705,7 +2839,7 @@ namespace Umbraco.Core.Services.Implement // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. var changes = new List>(); - var moves = new List>(); + var moves = new List<(IContent, string)>(); var contentTypeIdsA = contentTypeIds.ToArray(); // using an immediate uow here because we keep making changes with @@ -2778,7 +2912,8 @@ namespace Umbraco.Core.Services.Implement private IContentType GetContentType(IScope scope, 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)); scope.ReadLock(Constants.Locks.ContentTypes); @@ -2793,7 +2928,8 @@ namespace Umbraco.Core.Services.Implement private IContentType GetContentType(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)); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { @@ -2879,13 +3015,27 @@ namespace Umbraco.Core.Services.Implement content.CreatorId = userId; content.WriterId = userId; + IEnumerable cultures = ArrayOfOneNullString; + if (blueprint.CultureInfos.Count > 0) + { + cultures = blueprint.CultureInfos.Values.Select(x => x.Culture); + using (var scope = ScopeProvider.CreateScope()) + { + if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out var defaultCulture)) + { + defaultCulture.Name = name; + } + + scope.Complete(); + } + } + var now = DateTime.Now; - var cultures = blueprint.CultureInfos.Count > 0 ? blueprint.CultureInfos.Values.Select(x => x.Culture) : ArrayOfOneNullString; foreach (var culture in cultures) { foreach (var property in blueprint.Properties) { - var propertyCulture = property.PropertyType.VariesByCulture() ? culture : null; + var propertyCulture = property.PropertyType.VariesByCulture() ? culture : null; content.SetValue(property.Alias, property.GetValue(propertyCulture), propertyCulture); } @@ -2962,7 +3112,7 @@ namespace Umbraco.Core.Services.Implement var version = GetVersion(versionId); //Good ole null checks - if (content == null || version == null) + if (content == null || version == null || content.Trashed) { return new OperationResult(OperationResultType.FailedCannot, evtMsgs); } @@ -3011,5 +3161,8 @@ namespace Umbraco.Core.Services.Implement } #endregion + + + } } diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 1f1f0d9ac3..fdd2d9ceae 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -252,12 +252,12 @@ namespace Umbraco.Core.Services.Implement } } - public IEnumerable GetAll(params Guid[] ids) + public IEnumerable GetAll(IEnumerable ids) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(ReadLockIds); - return Repository.GetMany(ids); + return Repository.GetMany(ids.ToArray()); } } @@ -321,6 +321,15 @@ namespace Umbraco.Core.Services.Implement } } + public bool HasContainerInPath(params int[] ids) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + // can use same repo for both content and media + return Repository.HasContainerInPath(ids); + } + } + public IEnumerable GetDescendants(int id, bool andSelf) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -372,6 +381,15 @@ namespace Umbraco.Core.Services.Implement } } + public bool HasContentNodes(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(ReadLockIds); + return Repository.HasContentNodes(id); + } + } + #endregion #region Save @@ -390,6 +408,11 @@ namespace Umbraco.Core.Services.Implement if (string.IsNullOrWhiteSpace(item.Name)) throw new ArgumentException("Cannot save item with empty name."); + if (item.Name != null && item.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + scope.WriteLock(WriteLockIds); // validate the DAG transform, within the lock @@ -590,10 +613,9 @@ namespace Umbraco.Core.Services.Implement public TItem Copy(TItem original, string alias, string name, TItem parent) { if (original == null) throw new ArgumentNullException(nameof(original)); - if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentNullOrEmptyException(nameof(alias)); - - if (parent != null && parent.HasIdentity == false) - throw new InvalidOperationException("Parent must have an identity."); + if (alias == null) throw new ArgumentNullException(nameof(alias)); + if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias)); + if (parent != null && parent.HasIdentity == false) throw new InvalidOperationException("Parent must have an identity."); // this is illegal //var originalb = (ContentTypeCompositionBase)original; diff --git a/src/Umbraco.Core/Services/Implement/DataTypeService.cs b/src/Umbraco.Core/Services/Implement/DataTypeService.cs index 3552b2d8fc..5a93fb91b1 100644 --- a/src/Umbraco.Core/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Core/Services/Implement/DataTypeService.cs @@ -349,6 +349,11 @@ namespace Umbraco.Core.Services.Implement throw new ArgumentException("Cannot save datatype with empty name."); } + if (dataType.Name != null && dataType.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + _dataTypeRepository.Save(dataType); saveEventArgs.CanCancel = false; @@ -461,6 +466,14 @@ namespace Umbraco.Core.Services.Implement } } + public IReadOnlyDictionary> GetReferences(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete:true)) + { + return _dataTypeRepository.FindUsages(id); + } + } + private void Audit(AuditType type, int userId, int objectId) { _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.DataType))); diff --git a/src/Umbraco.Core/Services/Implement/EntityService.cs b/src/Umbraco.Core/Services/Implement/EntityService.cs index 00edde48f3..04e2624592 100644 --- a/src/Umbraco.Core/Services/Implement/EntityService.cs +++ b/src/Umbraco.Core/Services/Implement/EntityService.cs @@ -7,11 +7,9 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; namespace Umbraco.Core.Services.Implement @@ -19,30 +17,25 @@ namespace Umbraco.Core.Services.Implement public class EntityService : ScopeRepositoryService, IEntityService { private readonly IEntityRepository _entityRepository; - private readonly Dictionary GetById, Func GetByKey)> _objectTypes; + private readonly Dictionary _objectTypes; private IQuery _queryRootEntity; private readonly IdkMap _idkMap; - public EntityService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, - IContentService contentService, IContentTypeService contentTypeService, - IMediaService mediaService, IMediaTypeService mediaTypeService, - IDataTypeService dataTypeService, - IMemberService memberService, IMemberTypeService memberTypeService, IdkMap idkMap, - IEntityRepository entityRepository) + public EntityService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IdkMap idkMap, IEntityRepository entityRepository) : base(provider, logger, eventMessagesFactory) { _idkMap = idkMap; _entityRepository = entityRepository; - _objectTypes = new Dictionary, Func)> + _objectTypes = new Dictionary { - { typeof (IDataType).FullName, (UmbracoObjectTypes.DataType, dataTypeService.GetDataType, dataTypeService.GetDataType) }, - { typeof (IContent).FullName, (UmbracoObjectTypes.Document, contentService.GetById, contentService.GetById) }, - { typeof (IContentType).FullName, (UmbracoObjectTypes.DocumentType, contentTypeService.Get, contentTypeService.Get) }, - { typeof (IMedia).FullName, (UmbracoObjectTypes.Media, mediaService.GetById, mediaService.GetById) }, - { typeof (IMediaType).FullName, (UmbracoObjectTypes.MediaType, mediaTypeService.Get, mediaTypeService.Get) }, - { typeof (IMember).FullName, (UmbracoObjectTypes.Member, memberService.GetById, memberService.GetByKey) }, - { typeof (IMemberType).FullName, (UmbracoObjectTypes.MemberType, memberTypeService.Get, memberTypeService.Get) }, + { typeof (IDataType).FullName, UmbracoObjectTypes.DataType }, + { typeof (IContent).FullName, UmbracoObjectTypes.Document }, + { typeof (IContentType).FullName, UmbracoObjectTypes.DocumentType }, + { typeof (IMedia).FullName, UmbracoObjectTypes.Media }, + { typeof (IMediaType).FullName, UmbracoObjectTypes.MediaType }, + { typeof (IMember).FullName, UmbracoObjectTypes.Member }, + { typeof (IMemberType).FullName, UmbracoObjectTypes.MemberType }, }; } @@ -54,162 +47,68 @@ namespace Umbraco.Core.Services.Implement #endregion - // gets the getters, throws if not supported - private (UmbracoObjectTypes ObjectType, Func GetById, Func GetByKey) GetGetters(Type type) + // gets the object type, throws if not supported + private UmbracoObjectTypes GetObjectType(Type type) { - if (type?.FullName == null || !_objectTypes.TryGetValue(type.FullName, out var getters)) + if (type?.FullName == null || !_objectTypes.TryGetValue(type.FullName, out var objType)) throw new NotSupportedException($"Type \"{type?.FullName ?? ""}\" is not supported here."); - return getters; + return objType; } /// public IEntitySlim Get(int id) { - return (IEntitySlim) Get(id, false); - } - - /// - public IUmbracoEntity Get(int id, bool full) - { - if (!full) + using (ScopeProvider.CreateScope(autoComplete: true)) { - // get the light entity - using (ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.Get(id); - } + return _entityRepository.Get(id); } - - // get the full entity - var objectType = GetObjectType(id); - var entityType = objectType.GetClrType(); - var getters = GetGetters(entityType); - return getters.GetById(id); } /// public IEntitySlim Get(Guid key) { - return (IEntitySlim) Get(key, false); - } - - /// - public IUmbracoEntity Get(Guid key, bool full) - { - if (!full) + using (ScopeProvider.CreateScope(autoComplete: true)) { - // get the light entity - using (ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.Get(key); - } + return _entityRepository.Get(key); } - - // get the full entity - var objectType = GetObjectType(key); - var entityType = objectType.GetClrType(); - var getters = GetGetters(entityType); - return getters.GetByKey(key); } /// public virtual IEntitySlim Get(int id, UmbracoObjectTypes objectType) { - return (IEntitySlim) Get(id, objectType, false); - } - - /// - public virtual IUmbracoEntity Get(int id, UmbracoObjectTypes objectType, bool full) - { - if (!full) + using (ScopeProvider.CreateScope(autoComplete: true)) { - // get the light entity - using (ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.Get(id, objectType.GetGuid()); - } + return _entityRepository.Get(id, objectType.GetGuid()); } - - // get the full entity - var entityType = objectType.GetClrType(); - var getters = GetGetters(entityType); - return getters.GetById(id); } /// public IEntitySlim Get(Guid key, UmbracoObjectTypes objectType) { - return (IEntitySlim) Get(key, objectType, false); - } - - /// - public IUmbracoEntity Get(Guid key, UmbracoObjectTypes objectType, bool full) - { - if (!full) + using (ScopeProvider.CreateScope(autoComplete: true)) { - // get the light entity - using (ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.Get(key, objectType.GetGuid()); - } + return _entityRepository.Get(key, objectType.GetGuid()); } - - // get the full entity - var entityType = objectType.GetClrType(); - var getters = GetGetters(entityType); - return getters.GetByKey(key); } /// public virtual IEntitySlim Get(int id) where T : IUmbracoEntity { - return (IEntitySlim) Get(id, false); - } - - /// - public virtual IUmbracoEntity Get(int id, bool full) - where T : IUmbracoEntity - { - if (!full) + using (ScopeProvider.CreateScope(autoComplete: true)) { - // get the light entity - using (ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.Get(id); - } + return _entityRepository.Get(id); } - - // get the full entity - var entityType = typeof (T); - var getters = GetGetters(entityType); - return getters.GetById(id); } /// public virtual IEntitySlim Get(Guid key) where T : IUmbracoEntity { - return (IEntitySlim) Get(key, false); - } - - /// - public IUmbracoEntity Get(Guid key, bool full) - where T : IUmbracoEntity - { - if (!full) + using (ScopeProvider.CreateScope(autoComplete: true)) { - // get the light entity - using (ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.Get(key); - } + return _entityRepository.Get(key); } - - // get the full entity - var entityType = typeof (T); - var getters = GetGetters(entityType); - return getters.GetByKey(key); } /// @@ -240,8 +139,7 @@ namespace Umbraco.Core.Services.Implement where T : IUmbracoEntity { var entityType = typeof (T); - var getters = GetGetters(entityType); - var objectType = getters.ObjectType; + var objectType = GetObjectType(entityType); var objectTypeId = objectType.GetGuid(); using (ScopeProvider.CreateScope(autoComplete: true)) @@ -261,7 +159,7 @@ namespace Umbraco.Core.Services.Implement if (entityType == null) throw new NotSupportedException($"Type \"{objectType}\" is not supported here."); - GetGetters(entityType); + GetObjectType(entityType); using (ScopeProvider.CreateScope(autoComplete: true)) { @@ -277,7 +175,7 @@ namespace Umbraco.Core.Services.Implement public virtual IEnumerable GetAll(Guid objectType, params int[] ids) { var entityType = ObjectTypes.GetClrType(objectType); - GetGetters(entityType); + GetObjectType(entityType); using (ScopeProvider.CreateScope(autoComplete: true)) { @@ -290,8 +188,7 @@ namespace Umbraco.Core.Services.Implement where T : IUmbracoEntity { var entityType = typeof (T); - var getters = GetGetters(entityType); - var objectType = getters.ObjectType; + var objectType = GetObjectType(entityType); var objectTypeId = objectType.GetGuid(); using (ScopeProvider.CreateScope(autoComplete: true)) @@ -304,7 +201,7 @@ namespace Umbraco.Core.Services.Implement public IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys) { var entityType = objectType.GetClrType(); - GetGetters(entityType); + GetObjectType(entityType); using (ScopeProvider.CreateScope(autoComplete: true)) { @@ -316,7 +213,7 @@ namespace Umbraco.Core.Services.Implement public virtual IEnumerable GetAll(Guid objectType, params Guid[] keys) { var entityType = ObjectTypes.GetClrType(objectType); - GetGetters(entityType); + GetObjectType(entityType); using (ScopeProvider.CreateScope(autoComplete: true)) { @@ -377,22 +274,6 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a collection of children by the parent's Id and UmbracoObjectType without adding property data - /// - /// Id of the parent to retrieve children for - /// An enumerable list of objects - internal IEnumerable GetMediaChildrenWithoutPropertyData(int parentId) - { - using (ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == parentId); - - // TODO: see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media - return ((EntityRepository)_entityRepository).GetMediaByQueryWithoutPropertyData(query); - } - } - /// public virtual IEnumerable GetDescendants(int id) { @@ -578,7 +459,7 @@ namespace Umbraco.Core.Services.Implement public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[] ids) { var entityType = objectType.GetClrType(); - GetGetters(entityType); + GetObjectType(entityType); using (ScopeProvider.CreateScope(autoComplete: true)) { @@ -590,7 +471,7 @@ namespace Umbraco.Core.Services.Implement public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys) { var entityType = objectType.GetClrType(); - GetGetters(entityType); + GetObjectType(entityType); using (ScopeProvider.CreateScope(autoComplete: true)) { diff --git a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs index 863ecc0b1b..5189b3422e 100644 --- a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs @@ -172,7 +172,7 @@ namespace Umbraco.Core.Services.Implement xml.Add(new XAttribute("Id", dataType.EditorAlias)); xml.Add(new XAttribute("Definition", dataType.Key)); xml.Add(new XAttribute("DatabaseType", dataType.DatabaseType.ToString())); - xml.Add(new XAttribute("Configuration", JsonConvert.SerializeObject(dataType.Configuration))); + xml.Add(new XAttribute("Configuration", JsonConvert.SerializeObject(dataType.Configuration, PropertyEditors.ConfigurationEditor.ConfigurationJsonSettings))); var folderNames = string.Empty; if (dataType.Level != 1) @@ -360,7 +360,9 @@ namespace Umbraco.Core.Services.Implement new XElement("Definition", definition.Key), new XElement("Tab", propertyGroup == null ? "" : propertyGroup.Name), new XElement("Mandatory", propertyType.Mandatory.ToString()), + new XElement("MandatoryMessage", propertyType.MandatoryMessage), new XElement("Validation", propertyType.ValidationRegExp), + new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage), new XElement("Description", new XCData(propertyType.Description))); genericProperties.Add(genericProperty); } @@ -437,7 +439,8 @@ namespace Umbraco.Core.Services.Implement new XElement("Description", contentType.Description), new XElement("AllowAtRoot", contentType.AllowedAsRoot.ToString()), new XElement("IsListView", contentType.IsContainer.ToString()), - new XElement("IsElement", contentType.IsElement.ToString())); + new XElement("IsElement", contentType.IsElement.ToString()), + new XElement("Variations", contentType.Variations.ToString())); var masterContentType = contentType.ContentTypeComposition.FirstOrDefault(x => x.Id == contentType.ParentId); if(masterContentType != null) @@ -486,8 +489,11 @@ namespace Umbraco.Core.Services.Implement new XElement("Tab", propertyGroup == null ? "" : propertyGroup.Name), new XElement("SortOrder", propertyType.SortOrder), new XElement("Mandatory", propertyType.Mandatory.ToString()), + propertyType.MandatoryMessage != null ? new XElement("MandatoryMessage", propertyType.MandatoryMessage) : null, propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null, - propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null); + propertyType.ValidationRegExpMessage != null ? new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage) : null, + propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null, + new XElement("Variations", propertyType.Variations.ToString())); genericProperties.Add(genericProperty); } diff --git a/src/Umbraco.Core/Services/Implement/FileService.cs b/src/Umbraco.Core/Services/Implement/FileService.cs index 79d5b35775..ce8600d798 100644 --- a/src/Umbraco.Core/Services/Implement/FileService.cs +++ b/src/Umbraco.Core/Services/Implement/FileService.cs @@ -358,6 +358,11 @@ namespace Umbraco.Core.Services.Implement { "ContentTypeAlias", contentTypeAlias }, }; + if (contentTypeAlias != null && contentTypeAlias.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + // check that the template hasn't been created on disk before creating the content type // if it exists, set the new template content to the existing file content string content = GetViewContent(contentTypeAlias); @@ -365,7 +370,10 @@ namespace Umbraco.Core.Services.Implement { template.Content = content; } + + + using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(template, true, evtMsgs, additionalData); @@ -397,6 +405,21 @@ namespace Umbraco.Core.Services.Implement /// public ITemplate CreateTemplateWithIdentity(string name, string alias, string content, ITemplate masterTemplate = null, int userId = Constants.Security.SuperUserId) { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Name cannot be empty or contain only white-space characters", nameof(name)); + } + + if (name.Length > 255) + { + throw new ArgumentOutOfRangeException(nameof(name), "Name cannot be more than 255 characters in length."); + } + // file might already be on disk, if so grab the content to avoid overwriting var template = new Template(name, alias) { @@ -531,6 +554,17 @@ namespace Umbraco.Core.Services.Implement /// public void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId) { + if (template == null) + { + throw new ArgumentNullException(nameof(template)); + } + + if (string.IsNullOrWhiteSpace(template.Name) || template.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be null, empty, contain only white-space characters or be more than 255 characters in length."); + } + + using (var scope = ScopeProvider.CreateScope()) { if (scope.Events.DispatchCancelable(SavingTemplate, this, new SaveEventArgs(template))) diff --git a/src/Umbraco.Core/Services/Implement/InstallationService.cs b/src/Umbraco.Core/Services/Implement/InstallationService.cs new file mode 100644 index 0000000000..a1f74e0862 --- /dev/null +++ b/src/Umbraco.Core/Services/Implement/InstallationService.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories; + +namespace Umbraco.Core.Services.Implement +{ + public class InstallationService : IInstallationService + { + private readonly IInstallationRepository _installationRepository; + + public InstallationService(IInstallationRepository installationRepository) + { + _installationRepository = installationRepository; + } + + public async Task LogInstall(InstallLog installLog) + { + await _installationRepository.SaveInstallLogAsync(installLog); + } + } +} diff --git a/src/Umbraco.Core/Services/Implement/KeyValueService.cs b/src/Umbraco.Core/Services/Implement/KeyValueService.cs index eb68cea25c..b3f3f2468d 100644 --- a/src/Umbraco.Core/Services/Implement/KeyValueService.cs +++ b/src/Umbraco.Core/Services/Implement/KeyValueService.cs @@ -28,7 +28,6 @@ namespace Umbraco.Core.Services.Implement { if (_initialized) return; Initialize(); - _initialized = true; } } @@ -41,7 +40,10 @@ namespace Umbraco.Core.Services.Implement // then everything should be ok (the table should exist, etc) if (UmbracoVersion.LocalVersion != null && UmbracoVersion.LocalVersion.Major >= 8) + { + _initialized = true; return; + } // else we are upgrading from 7, we can assume that the locks table // exists, but we need to create everything for key/value @@ -53,6 +55,10 @@ namespace Umbraco.Core.Services.Implement initMigration.Migrate(); scope.Complete(); } + + // but don't assume we are initializing + // we are upgrading from v7 and if anything goes wrong, + // the table and everything will be rolled back } /// diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 2ff39f7f7d..ecd4cccc8d 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.IO; using System.Linq; using Umbraco.Core.Events; -using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -139,6 +138,10 @@ namespace Umbraco.Core.Services.Implement var parent = parentId > 0 ? GetById(parentId) : null; if (parentId > 0 && parent == null) throw new ArgumentException("No media with that id.", nameof(parentId)); + if (name != null && name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } var media = new Models.Media(name, parentId, mediaType); using (var scope = ScopeProvider.CreateScope()) @@ -168,6 +171,10 @@ namespace Umbraco.Core.Services.Implement var mediaType = GetMediaType(mediaTypeAlias); if (mediaType == null) throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); + if (name != null && name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } var media = new Models.Media(name, -1, mediaType); using (var scope = ScopeProvider.CreateScope()) @@ -201,7 +208,11 @@ namespace Umbraco.Core.Services.Implement var mediaType = GetMediaType(mediaTypeAlias); if (mediaType == null) - throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback // causes rollback + throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback + if (name != null && name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } var media = new Models.Media(name, parent, mediaType); CreateMedia(scope, media, parent, userId, false); @@ -227,13 +238,13 @@ namespace Umbraco.Core.Services.Implement // locking the media tree secures media types too scope.WriteLock(Constants.Locks.MediaTree); - var mediaType = GetMediaType(mediaTypeAlias); // + locks // + locks + var mediaType = GetMediaType(mediaTypeAlias); // + locks if (mediaType == null) - throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback // causes rollback + throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback - var parent = parentId > 0 ? GetById(parentId) : null; // + locks // + locks + var parent = parentId > 0 ? GetById(parentId) : null; // + locks if (parentId > 0 && parent == null) - throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback // causes rollback + throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback var media = parentId > 0 ? new Models.Media(name, parent, mediaType) : new Models.Media(name, parentId, mediaType); CreateMedia(scope, media, parent, userId, true); @@ -261,9 +272,9 @@ namespace Umbraco.Core.Services.Implement // locking the media tree secures media types too scope.WriteLock(Constants.Locks.MediaTree); - var mediaType = GetMediaType(mediaTypeAlias); // + locks // + locks + var mediaType = GetMediaType(mediaTypeAlias); // + locks if (mediaType == null) - throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback // causes rollback + throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback var media = new Models.Media(name, parent, mediaType); CreateMedia(scope, media, parent, userId, true); @@ -519,23 +530,27 @@ namespace Umbraco.Core.Services.Implement totalChildren = 0; return Enumerable.Empty(); } - return GetPagedDescendantsLocked(mediaPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(GetPagedDescendantQuery(mediaPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering); } - return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering); } } - private IEnumerable GetPagedDescendantsLocked(string mediaPath, long pageIndex, int pageSize, out long totalChildren, + private IQuery GetPagedDescendantQuery(string mediaPath) + { + var query = Query(); + if (!mediaPath.IsNullOrWhiteSpace()) + query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar)); + return query; + } + + private IEnumerable GetPagedLocked(IQuery query, long pageIndex, int pageSize, out long totalChildren, IQuery filter, Ordering ordering) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - var query = Query(); - if (!mediaPath.IsNullOrWhiteSpace()) - query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar)); - return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } @@ -645,11 +660,14 @@ namespace Umbraco.Core.Services.Implement } // poor man's validation? - // poor man's validation? - if (string.IsNullOrWhiteSpace(media.Name)) throw new ArgumentException("Media has no name.", nameof(media)); + if (media.Name != null && media.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + scope.WriteLock(Constants.Locks.MediaTree); if (media.HasIdentity == false) media.CreatorId = userId; @@ -762,7 +780,7 @@ namespace Umbraco.Core.Services.Implement const int pageSize = 500; var page = 0; var total = long.MaxValue; - while(page * pageSize < total) + while (page * pageSize < total) { //get descendants - ordered from deepest to shallowest var descendants = GetPagedDescendants(media.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); @@ -874,7 +892,7 @@ namespace Umbraco.Core.Services.Implement public Attempt MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId) { var evtMsgs = EventMessagesFactory.Get(); - var moves = new List>(); + var moves = new List<(IMedia, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -926,7 +944,7 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Attempt.Succeed(evtMsgs); } - var moves = new List>(); + var moves = new List<(IMedia, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -934,7 +952,7 @@ namespace Umbraco.Core.Services.Implement var parent = parentId == Constants.System.Root ? null : GetById(parentId); if (parentId != Constants.System.Root && (parent == null || parent.Trashed)) - throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback // causes rollback + throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback var moveEventInfo = new MoveEventInfo(media, media.Path, parentId); var moveEventArgs = new MoveEventArgs(true, evtMsgs, moveEventInfo); @@ -947,13 +965,7 @@ namespace Umbraco.Core.Services.Implement // if media was trashed, and since we're not moving to the recycle bin, // indicate that the trashed status should be changed to false, else just // leave it unchanged - // if media was trashed, and since we're not moving to the recycle bin, - - // indicate that the trashed status should be changed to false, else just - - // leave it unchanged - - var trashed = media.Trashed ? false : (bool?) null; + var trashed = media.Trashed ? false : (bool?)null; PerformMoveLocked(media, parentId, parent, userId, moves, trashed); scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, TreeChangeTypes.RefreshBranch).ToEventArgs()); @@ -971,7 +983,7 @@ namespace Umbraco.Core.Services.Implement // MUST be called from within WriteLock // trash indicates whether we are trashing, un-trashing, or not changing anything - private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection> moves, bool? trash) + private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection<(IMedia, string)> moves, bool? trash) { media.ParentId = parentId; @@ -981,7 +993,7 @@ namespace Umbraco.Core.Services.Implement var paths = new Dictionary(); - moves.Add(Tuple.Create(media, media.Path)); // capture original path + moves.Add((media, media.Path)); // capture original path //need to store the original path to lookup descendants based on it below var originalPath = media.Path; @@ -998,26 +1010,30 @@ namespace Umbraco.Core.Services.Implement paths[media.Id] = (parent == null ? (parentId == Constants.System.RecycleBinMedia ? "-1,-21" : Constants.System.RootString) : parent.Path) + "," + media.Id; const int pageSize = 500; - var page = 0; - var total = long.MaxValue; - while (page * pageSize < total) + var query = GetPagedDescendantQuery(originalPath); + long total; + do { - var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced + var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + foreach (var descendant in descendants) { - moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path + moves.Add((descendant, descendant.Path)); // capture original path // update path and level since we do not update parentId descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; descendant.Level += levelDelta; PerformMoveMediaLocked(descendant, userId, trash); } - } + + } while (total > pageSize); + } private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash) { - if (trash.HasValue) ((ContentBase) media).Trashed = trash.Value; + if (trash.HasValue) ((ContentBase)media).Trashed = trash.Value; _mediaRepository.Save(media); } @@ -1042,17 +1058,11 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.MediaTree); + // no idea what those events are for, keep a simplified version + // v7 EmptyingRecycleBin and EmptiedRecycleBin events are greatly simplified since // each deleted items will have its own deleting/deleted events. so, files and such // are managed by Delete, and not here. - - // no idea what those events are for, keep a simplified version - // v7 EmptyingRecycleBin and EmptiedRecycleBin events are greatly simplified since - // each deleted items will have its own deleting/deleted events. so, files and such - - // emptying the recycle bin means deleting whatever is in there - do it properly! - // are managed by Delete, and not here. - // no idea what those events are for, keep a simplified version var args = new RecycleBinEventArgs(nodeObjectType, evtMsgs); if (scope.Events.DispatchCancelable(EmptyingRecycleBin, this, args)) @@ -1113,11 +1123,6 @@ namespace Umbraco.Core.Services.Implement { // if the current sort order equals that of the media we don't // need to update it, so just increment the sort order and continue. - // if the current sort order equals that of the media we don't - - // else update - // need to update it, so just increment the sort order and continue. - // save if (media.SortOrder == sortOrder) { sortOrder++; @@ -1142,6 +1147,26 @@ namespace Umbraco.Core.Services.Implement } return true; + + } + + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.WriteLock(Constants.Locks.MediaTree); + + var report = _mediaRepository.CheckDataIntegrity(options); + + if (report.FixedIssues.Count > 0) + { + //The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref + var root = new Models.Media("root", -1, new MediaType(-1)) { Id = -1, Key = Guid.Empty }; + scope.Events.Dispatch(TreeChanged, this, new TreeChange.EventArgs(new TreeChange(root, TreeChangeTypes.RefreshAll))); + } + + return report; + } } #endregion @@ -1280,7 +1305,7 @@ namespace Umbraco.Core.Services.Implement // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. var changes = new List>(); - var moves = new List>(); + var moves = new List<(IMedia, string)>(); var mediaTypeIdsA = mediaTypeIds.ToArray(); using (var scope = ScopeProvider.CreateScope()) @@ -1342,7 +1367,8 @@ namespace Umbraco.Core.Services.Implement private IMediaType GetMediaType(string mediaTypeAlias) { - if (string.IsNullOrWhiteSpace(mediaTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(mediaTypeAlias)); + if (mediaTypeAlias == null) throw new ArgumentNullException(nameof(mediaTypeAlias)); + if (string.IsNullOrWhiteSpace(mediaTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(mediaTypeAlias)); using (var scope = ScopeProvider.CreateScope()) { @@ -1352,7 +1378,7 @@ namespace Umbraco.Core.Services.Implement var mediaType = _mediaTypeRepository.Get(query).FirstOrDefault(); if (mediaType == null) - throw new Exception($"No MediaType matching the passed in Alias: '{mediaTypeAlias}' was found"); // causes rollback // causes rollback + throw new InvalidOperationException($"No media type matched the specified alias '{mediaTypeAlias}'."); scope.Complete(); return mediaType; @@ -1360,5 +1386,7 @@ namespace Umbraco.Core.Services.Implement } #endregion + + } } diff --git a/src/Umbraco.Core/Services/Implement/MemberService.cs b/src/Umbraco.Core/Services/Implement/MemberService.cs index 8c69664712..9b97c6b161 100644 --- a/src/Umbraco.Core/Services/Implement/MemberService.cs +++ b/src/Umbraco.Core/Services/Implement/MemberService.cs @@ -331,7 +331,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } - + if (withIdentity == false) return; @@ -971,6 +971,35 @@ namespace Umbraco.Core.Services.Implement } } + public IEnumerable GetAllRolesIds() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.MemberTree); + return _memberGroupRepository.GetMany().Select(x => x.Id).Distinct(); + } + } + + public IEnumerable GetAllRolesIds(int memberId) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.MemberTree); + var result = _memberGroupRepository.GetMemberGroupsForMember(memberId); + return result.Select(x => x.Id).Distinct(); + } + } + + public IEnumerable GetAllRolesIds(string username) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.MemberTree); + var result = _memberGroupRepository.GetMemberGroupsForMember(username); + return result.Select(x => x.Id).Distinct(); + } + } + public IEnumerable GetMembersInRole(string roleName) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -1241,7 +1270,7 @@ namespace Umbraco.Core.Services.Implement /// Exports a member. /// /// - /// This is internal for now and is used to export a member in the member editor, + /// This is internal for now and is used to export a member in the member editor, /// it will raise an event so that auditing logs can be created. /// internal MemberExportModel ExportMember(Guid key) @@ -1344,7 +1373,8 @@ namespace Umbraco.Core.Services.Implement private IMemberType GetMemberType(IScope scope, string memberTypeAlias) { - if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(memberTypeAlias)); + if (memberTypeAlias == null) throw new ArgumentNullException(nameof(memberTypeAlias)); + if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); scope.ReadLock(Constants.Locks.MemberTypes); @@ -1358,7 +1388,8 @@ namespace Umbraco.Core.Services.Implement private IMemberType GetMemberType(string memberTypeAlias) { - if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(memberTypeAlias)); + if (memberTypeAlias == null) throw new ArgumentNullException(nameof(memberTypeAlias)); + if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { diff --git a/src/Umbraco.Core/Services/Implement/PackagingService.cs b/src/Umbraco.Core/Services/Implement/PackagingService.cs index b4dcc70a96..d85c688a0d 100644 --- a/src/Umbraco.Core/Services/Implement/PackagingService.cs +++ b/src/Umbraco.Core/Services/Implement/PackagingService.cs @@ -57,7 +57,7 @@ namespace Umbraco.Core.Services.Implement } catch (HttpRequestException ex) { - throw new ConnectionException("An error occurring downloading the package from " + url, ex); + throw new HttpRequestException("An error occurring downloading the package from " + url, ex); } //successful diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index a316d04f8e..4b53709de9 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -25,11 +25,7 @@ namespace Umbraco.Core.Services.Implement _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); } - /// - /// Gets a by its Id - /// - /// Id of the - /// A object + /// public IRelation GetById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -38,11 +34,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a by its Id - /// - /// Id of the - /// A object + /// public IRelationType GetRelationTypeById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -51,11 +43,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a by its Id - /// - /// Id of the - /// A object + /// public IRelationType GetRelationTypeById(Guid id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -64,25 +52,10 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a by its Alias - /// - /// Alias of the - /// A object - public IRelationType GetRelationTypeByAlias(string alias) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query().Where(x => x.Alias == alias); - return _relationTypeRepository.Get(query).FirstOrDefault(); - } - } + /// + public IRelationType GetRelationTypeByAlias(string alias) => GetRelationType(alias); - /// - /// Gets all objects - /// - /// Optional array of integer ids to return relations for - /// An enumerable list of objects + /// public IEnumerable GetAllRelations(params int[] ids) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -91,21 +64,13 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets all objects by their - /// - /// to retrieve Relations for - /// An enumerable list of objects - public IEnumerable GetAllRelationsByRelationType(RelationType relationType) + /// + public IEnumerable GetAllRelationsByRelationType(IRelationType relationType) { return GetAllRelationsByRelationType(relationType.Id); } - /// - /// Gets all objects by their 's Id - /// - /// Id of the to retrieve Relations for - /// An enumerable list of objects + /// public IEnumerable GetAllRelationsByRelationType(int relationTypeId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -115,11 +80,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets all objects - /// - /// Optional array of integer ids to return relationtypes for - /// An enumerable list of objects + /// public IEnumerable GetAllRelationTypes(params int[] ids) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -128,82 +89,65 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a list of objects by their parent Id - /// - /// Id of the parent to retrieve relations for - /// An enumerable list of objects - public IEnumerable GetByParentId(int id) + /// + public IEnumerable GetByParentId(int id) => GetByParentId(id, null); + + /// + public IEnumerable GetByParentId(int id, string relationTypeAlias) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - var query = Query().Where(x => x.ParentId == id); - return _relationRepository.Get(query); + if (relationTypeAlias.IsNullOrWhiteSpace()) + { + var qry1 = Query().Where(x => x.ParentId == id); + return _relationRepository.Get(qry1); + } + + var relationType = GetRelationType(relationTypeAlias); + if (relationType == null) + return Enumerable.Empty(); + + var qry2 = Query().Where(x => x.ParentId == id && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(qry2); } } - /// - /// Gets a list of objects by their parent entity - /// - /// Parent Entity to retrieve relations for - /// An enumerable list of objects - public IEnumerable GetByParent(IUmbracoEntity parent) - { - return GetByParentId(parent.Id); - } + /// + public IEnumerable GetByParent(IUmbracoEntity parent) => GetByParentId(parent.Id); - /// - /// Gets a list of objects by their parent entity - /// - /// Parent Entity to retrieve relations for - /// Alias of the type of relation to retrieve - /// An enumerable list of objects - public IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias) - { - return GetByParent(parent).Where(relation => relation.RelationType.Alias == relationTypeAlias); - } + /// + public IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias) => GetByParentId(parent.Id, relationTypeAlias); - /// - /// Gets a list of objects by their child Id - /// - /// Id of the child to retrieve relations for - /// An enumerable list of objects - public IEnumerable GetByChildId(int id) + /// + public IEnumerable GetByChildId(int id) => GetByChildId(id, null); + + /// + public IEnumerable GetByChildId(int id, string relationTypeAlias) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - var query = Query().Where(x => x.ChildId == id); - return _relationRepository.Get(query); + if (relationTypeAlias.IsNullOrWhiteSpace()) + { + var qry1 = Query().Where(x => x.ChildId == id); + return _relationRepository.Get(qry1); + } + + var relationType = GetRelationType(relationTypeAlias); + if (relationType == null) + return Enumerable.Empty(); + + var qry2 = Query().Where(x => x.ChildId == id && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(qry2); } } - /// - /// Gets a list of objects by their child Entity - /// - /// Child Entity to retrieve relations for - /// An enumerable list of objects - public IEnumerable GetByChild(IUmbracoEntity child) - { - return GetByChildId(child.Id); - } + /// + public IEnumerable GetByChild(IUmbracoEntity child) => GetByChildId(child.Id); - /// - /// Gets a list of objects by their child Entity - /// - /// Child Entity to retrieve relations for - /// Alias of the type of relation to retrieve - /// An enumerable list of objects - public IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias) - { - return GetByChild(child).Where(relation => relation.RelationType.Alias == relationTypeAlias); - } + /// + public IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias) => GetByChildId(child.Id, relationTypeAlias); - /// - /// Gets a list of objects by their child or parent Id. - /// Using this method will get you all relations regards of it being a child or parent relation. - /// - /// Id of the child or parent to retrieve relations for - /// An enumerable list of objects + /// public IEnumerable GetByParentOrChildId(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -217,8 +161,7 @@ namespace Umbraco.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - var rtQuery = Query().Where(x => x.Alias == relationTypeAlias); - var relationType = _relationTypeRepository.Get(rtQuery).FirstOrDefault(); + var relationType = GetRelationType(relationTypeAlias); if (relationType == null) return Enumerable.Empty(); @@ -227,16 +170,13 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a list of objects by the Name of the - /// - /// Name of the to retrieve Relations for - /// An enumerable list of objects + /// public IEnumerable GetByRelationTypeName(string relationTypeName) { List relationTypeIds; using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { + //This is a silly query - but i guess it's needed in case someone has more than one relation with the same Name (not alias), odd. var query = Query().Where(x => x.Name == relationTypeName); var relationTypes = _relationTypeRepository.Get(query); relationTypeIds = relationTypes.Select(x => x.Id).ToList(); @@ -247,31 +187,17 @@ namespace Umbraco.Core.Services.Implement : GetRelationsByListOfTypeIds(relationTypeIds); } - /// - /// Gets a list of objects by the Alias of the - /// - /// Alias of the to retrieve Relations for - /// An enumerable list of objects + /// public IEnumerable GetByRelationTypeAlias(string relationTypeAlias) { - List relationTypeIds; - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query().Where(x => x.Alias == relationTypeAlias); - var relationTypes = _relationTypeRepository.Get(query); - relationTypeIds = relationTypes.Select(x => x.Id).ToList(); - } - - return relationTypeIds.Count == 0 + var relationType = GetRelationType(relationTypeAlias); + + return relationType == null ? Enumerable.Empty() - : GetRelationsByListOfTypeIds(relationTypeIds); + : GetRelationsByListOfTypeIds(new[] { relationType.Id }); } - /// - /// Gets a list of objects by the Id of the - /// - /// Id of the to retrieve Relations for - /// An enumerable list of objects + /// public IEnumerable GetByRelationTypeId(int relationTypeId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -281,110 +207,116 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets the Child object from a Relation as an - /// - /// Relation to retrieve child object from - /// Optional bool to load the complete object graph when set to False - /// An - public IUmbracoEntity GetChildEntityFromRelation(IRelation relation, bool loadBaseType = false) + /// + public IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering ordering = null) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType); - return _entityService.Get(relation.ChildId, objectType, loadBaseType); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.RelationTypeId == relationTypeId); + return _relationRepository.GetPagedRelationsByQuery(query, pageIndex, pageSize, out totalRecords, ordering); + } } - /// - /// Gets the Parent object from a Relation as an - /// - /// Relation to retrieve parent object from - /// Optional bool to load the complete object graph when set to False - /// An - public IUmbracoEntity GetParentEntityFromRelation(IRelation relation, bool loadBaseType = false) + /// + public IUmbracoEntity GetChildEntityFromRelation(IRelation relation) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType); - return _entityService.Get(relation.ParentId, objectType, loadBaseType); + var objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + return _entityService.Get(relation.ChildId, objectType); } - /// - /// Gets the Parent and Child objects from a Relation as a "/> with . - /// - /// Relation to retrieve parent and child object from - /// Optional bool to load the complete object graph when set to False - /// Returns a Tuple with Parent (item1) and Child (item2) - public Tuple GetEntitiesFromRelation(IRelation relation, bool loadBaseType = false) + /// + public IUmbracoEntity GetParentEntityFromRelation(IRelation relation) { - var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType); - var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType); + var objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); + return _entityService.Get(relation.ParentId, objectType); + } - var child = _entityService.Get(relation.ChildId, childObjectType, loadBaseType); - var parent = _entityService.Get(relation.ParentId, parentObjectType, loadBaseType); + /// + public Tuple GetEntitiesFromRelation(IRelation relation) + { + var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); + + var child = _entityService.Get(relation.ChildId, childObjectType); + var parent = _entityService.Get(relation.ParentId, parentObjectType); return new Tuple(parent, child); } - /// - /// Gets the Child objects from a list of Relations as a list of objects. - /// - /// List of relations to retrieve child objects from - /// Optional bool to load the complete object graph when set to False - /// An enumerable list of - public IEnumerable GetChildEntitiesFromRelations(IEnumerable relations, bool loadBaseType = false) + /// + public IEnumerable GetChildEntitiesFromRelations(IEnumerable relations) { - foreach (var relation in relations) + // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll + // method to lookup batches of entities for each parent object type + + foreach (var groupedRelations in relations.GroupBy(x => ObjectTypes.GetUmbracoObjectType(x.ChildObjectType))) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType); - yield return _entityService.Get(relation.ChildId, objectType, loadBaseType); + var objectType = groupedRelations.Key; + var ids = groupedRelations.Select(x => x.ChildId).ToArray(); + foreach (var e in _entityService.GetAll(objectType, ids)) + yield return e; } } - /// - /// Gets the Parent objects from a list of Relations as a list of objects. - /// - /// List of relations to retrieve parent objects from - /// Optional bool to load the complete object graph when set to False - /// An enumerable list of - public IEnumerable GetParentEntitiesFromRelations(IEnumerable relations, bool loadBaseType = false) + /// + public IEnumerable GetParentEntitiesFromRelations(IEnumerable relations) { - foreach (var relation in relations) + // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll + // method to lookup batches of entities for each parent object type + + foreach (var groupedRelations in relations.GroupBy(x => ObjectTypes.GetUmbracoObjectType(x.ParentObjectType))) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType); - yield return _entityService.Get(relation.ParentId, objectType, loadBaseType); + var objectType = groupedRelations.Key; + var ids = groupedRelations.Select(x => x.ParentId).ToArray(); + foreach (var e in _entityService.GetAll(objectType, ids)) + yield return e; } } - /// - /// Gets the Parent and Child objects from a list of Relations as a list of objects. - /// - /// List of relations to retrieve parent and child objects from - /// Optional bool to load the complete object graph when set to False - /// An enumerable list of with - public IEnumerable> GetEntitiesFromRelations(IEnumerable relations, bool loadBaseType = false) + /// + public IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); + } + } + + /// + public IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); + } + } + + /// + public IEnumerable> GetEntitiesFromRelations(IEnumerable relations) + { + //TODO: Argh! N+1 + foreach (var relation in relations) { - var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType); - var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType); + var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); - var child = _entityService.Get(relation.ChildId, childObjectType, loadBaseType); - var parent = _entityService.Get(relation.ParentId, parentObjectType, loadBaseType); + var child = _entityService.Get(relation.ChildId, childObjectType); + var parent = _entityService.Get(relation.ParentId, parentObjectType); yield return new Tuple(parent, child); } } - /// - /// Relates two objects by their entity Ids. - /// - /// Id of the parent - /// Id of the child - /// The type of relation to create - /// The created + /// public IRelation Relate(int parentId, int childId, IRelationType relationType) { // Ensure that the RelationType has an identity before using it to relate two entities if (relationType.HasIdentity == false) Save(relationType); + //TODO: We don't check if this exists first, it will throw some sort of data integrity exception if it already exists, is that ok? + var relation = new Relation(parentId, childId, relationType); using (var scope = ScopeProvider.CreateScope()) @@ -404,25 +336,13 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Relates two objects that are based on the interface. - /// - /// Parent entity - /// Child entity - /// The type of relation to create - /// The created + /// public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) { return Relate(parent.Id, child.Id, relationType); } - /// - /// Relates two objects by their entity Ids. - /// - /// Id of the parent - /// Id of the child - /// Alias of the type of relation to create - /// The created + /// public IRelation Relate(int parentId, int childId, string relationTypeAlias) { var relationType = GetRelationTypeByAlias(relationTypeAlias); @@ -432,13 +352,7 @@ namespace Umbraco.Core.Services.Implement return Relate(parentId, childId, relationType); } - /// - /// Relates two objects that are based on the interface. - /// - /// Parent entity - /// Child entity - /// Alias of the type of relation to create - /// The created + /// public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) { var relationType = GetRelationTypeByAlias(relationTypeAlias); @@ -448,11 +362,7 @@ namespace Umbraco.Core.Services.Implement return Relate(parent.Id, child.Id, relationType); } - /// - /// Checks whether any relations exists for the passed in . - /// - /// to check for relations - /// Returns True if any relations exists for the given , otherwise False + /// public bool HasRelations(IRelationType relationType) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -462,11 +372,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Checks whether any relations exists for the passed in Id. - /// - /// Id of an object to check relations for - /// Returns True if any relations exists with the given Id, otherwise False + /// public bool IsRelated(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -476,12 +382,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Checks whether two items are related - /// - /// Id of the Parent relation - /// Id of the Child relation - /// Returns True if any relations exists with the given Ids, otherwise False + /// public bool AreRelated(int parentId, int childId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -491,13 +392,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Checks whether two items are related with a given relation type alias - /// - /// Id of the Parent relation - /// Id of the Child relation - /// Alias of the relation type - /// Returns True if any relations exists with the given Ids and relation type, otherwise False + /// public bool AreRelated(int parentId, int childId, string relationTypeAlias) { var relType = GetRelationTypeByAlias(relationTypeAlias); @@ -508,13 +403,7 @@ namespace Umbraco.Core.Services.Implement } - /// - /// Checks whether two items are related with a given relation type - /// - /// Id of the Parent relation - /// Id of the Child relation - /// Type of relation - /// Returns True if any relations exists with the given Ids and relation type, otherwise False + /// public bool AreRelated(int parentId, int childId, IRelationType relationType) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -524,34 +413,20 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Checks whether two items are related - /// - /// Parent entity - /// Child entity - /// Returns True if any relations exist between the entities, otherwise False + /// public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child) { return AreRelated(parent.Id, child.Id); } - /// - /// Checks whether two items are related - /// - /// Parent entity - /// Child entity - /// Alias of the type of relation to create - /// Returns True if any relations exist between the entities, otherwise False + /// public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) { return AreRelated(parent.Id, child.Id, relationTypeAlias); } - /// - /// Saves a - /// - /// Relation to save + /// public void Save(IRelation relation) { using (var scope = ScopeProvider.CreateScope()) @@ -570,10 +445,25 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Saves a - /// - /// RelationType to Save + public void Save(IEnumerable relations) + { + using (var scope = ScopeProvider.CreateScope()) + { + var saveEventArgs = new SaveEventArgs(relations); + if (scope.Events.DispatchCancelable(SavingRelation, this, saveEventArgs)) + { + scope.Complete(); + return; + } + + _relationRepository.Save(relations); + scope.Complete(); + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(SavedRelation, this, saveEventArgs); + } + } + + /// public void Save(IRelationType relationType) { using (var scope = ScopeProvider.CreateScope()) @@ -592,10 +482,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Deletes a - /// - /// Relation to Delete + /// public void Delete(IRelation relation) { using (var scope = ScopeProvider.CreateScope()) @@ -614,10 +501,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Deletes a - /// - /// RelationType to Delete + /// public void Delete(IRelationType relationType) { using (var scope = ScopeProvider.CreateScope()) @@ -636,10 +520,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Deletes all objects based on the passed in - /// - /// to Delete Relations for + /// public void DeleteRelationsOfType(IRelationType relationType) { var relations = new List(); @@ -648,6 +529,8 @@ namespace Umbraco.Core.Services.Implement var query = Query().Where(x => x.RelationTypeId == relationType.Id); relations.AddRange(_relationRepository.Get(query).ToList()); + //TODO: N+1, we should be able to do this in a single call + foreach (var relation in relations) _relationRepository.Delete(relation); @@ -659,6 +542,15 @@ namespace Umbraco.Core.Services.Implement #region Private Methods + private IRelationType GetRelationType(string relationTypeAlias) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.Alias == relationTypeAlias); + return _relationTypeRepository.Get(query).FirstOrDefault(); + } + } + private IEnumerable GetRelationsByListOfTypeIds(IEnumerable relationTypeIds) { var relations = new List(); diff --git a/src/Umbraco.Core/Services/Implement/UserService.cs b/src/Umbraco.Core/Services/Implement/UserService.cs index 0ea77dedcc..6f5cbfca56 100644 --- a/src/Umbraco.Core/Services/Implement/UserService.cs +++ b/src/Umbraco.Core/Services/Implement/UserService.cs @@ -1,15 +1,11 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Data.Common; -using System.Data.SqlClient; -using System.Data.SqlServerCe; using System.Globalization; using System.Linq; using System.Linq.Expressions; using Umbraco.Core.Configuration; using Umbraco.Core.Events; -using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -17,7 +13,6 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; -using Umbraco.Core.Security; namespace Umbraco.Core.Services.Implement { @@ -107,7 +102,8 @@ namespace Umbraco.Core.Services.Implement /// private IUser CreateUserWithIdentity(string username, string email, string passwordValue, bool isApproved = true) { - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentNullOrEmptyException(nameof(username)); + 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)); // TODO: PUT lock here!! @@ -376,7 +372,7 @@ namespace Umbraco.Core.Services.Implement /// public string GetDefaultMemberType() { - return "writer"; + return Constants.Security.WriterGroupAlias; } /// @@ -1201,12 +1197,12 @@ namespace Umbraco.Core.Services.Implement /// /// Occurs before Save /// - internal static event TypedEventHandler> SavingUserGroup; + public static event TypedEventHandler> SavingUserGroup; /// /// Occurs after Save /// - internal static event TypedEventHandler> SavedUserGroup; + public static event TypedEventHandler> SavedUserGroup; /// /// Occurs before Delete diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs index 51fdb7e8e3..ce5b3ef8c4 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs @@ -83,10 +83,10 @@ namespace Umbraco.Core.Services internal static string UmbracoDictionaryTranslate(this ILocalizedTextService manager, string text) { var cultureDictionary = CultureDictionary; - return UmbracoDictionaryTranslate(text, cultureDictionary); + return manager.UmbracoDictionaryTranslate(text, cultureDictionary); } - private static string UmbracoDictionaryTranslate(string text, ICultureDictionary cultureDictionary) + private static string UmbracoDictionaryTranslate(this ILocalizedTextService manager, string text, ICultureDictionary cultureDictionary) { if (text == null) return null; @@ -95,7 +95,14 @@ namespace Umbraco.Core.Services return text; text = text.Substring(1); - return cultureDictionary[text].IfNullOrWhiteSpace(text); + var value = cultureDictionary[text]; + if (value.IsNullOrWhiteSpace() == false) + { + return value; + } + + value = manager.Localize(text.Replace('_', '/')); + return value.StartsWith("[") ? text : value; } private static ICultureDictionary CultureDictionary diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index b846095bd1..a037a83920 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -131,6 +131,12 @@ namespace Umbraco.Core.Services private bool IsPropertyValueValid(PropertyType propertyType, object value) { var editor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (editor == null) + { + // nothing much we can do validation wise if the property editor has been removed. + // the property will be displayed as a label, so flagging it as invalid would be pointless. + return true; + } var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId).Configuration; var valueEditor = editor.GetValueEditor(configuration); return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any(); diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index 1a2b52f9c9..66c1e38267 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -49,6 +49,11 @@ /// SuccessUnpublishMandatoryCulture = 6, + /// + /// The specified document culture was unpublished, and was the last published culture in the document, therefore the document itself was unpublished. + /// + SuccessUnpublishLastCulture = 8, + #endregion #region Success - Mixed @@ -113,9 +118,9 @@ FailedPublishContentInvalid = FailedPublish | 8, /// - /// The document could not be published because it has no publishing flags or values. + /// The document could not be published because it has no publishing flags or values or if its a variant document, no cultures were specified to be published. /// - FailedPublishNothingToPublish = FailedPublish | 9, // TODO: in ContentService.StrategyCanPublish - weird + FailedPublishNothingToPublish = FailedPublish | 9, /// /// The document could not be published because some mandatory cultures are missing. diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 31c446352e..c365f1ccc2 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Web.Security; using Umbraco.Core.Models.Membership; @@ -104,5 +105,29 @@ namespace Umbraco.Core.Services return found; } + + /// + /// Gets the concrete assigned permissions for the provided user and node + /// + /// + /// + /// + internal static string[] GetAssignedPermissions(this IUserService userService, IUser user, int nodeId) + { + var permissionCollection = userService.GetPermissions(user, nodeId); + return permissionCollection.SelectMany(c => c.AssignedPermissions).Distinct().ToArray(); + } + + internal static IEnumerable GetProfilesById(this IUserService userService, params int[] ids) + { + var fullUsers = userService.GetUsersById(ids); + + return fullUsers.Select(user => + { + var asProfile = user as IProfile; + return asProfile ?? new UserProfile(user.Id, user.Name); + }); + + } } } diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 4df1105bf7..c37f8bdf35 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -10,9 +10,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Web.Security; using Newtonsoft.Json; -using Umbraco.Core.Configuration; using Umbraco.Core.Composing; -using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Strings; @@ -81,6 +79,21 @@ namespace Umbraco.Core } + /// + /// Determines the extension of the path or URL + /// + /// + /// Extension of the file + public static string GetFileExtension(this string file) + { + //Find any characters between the last . and the start of a query string or the end of the string + const string pattern = @"(?\.[^\.\?]+)(\?.*|$)"; + var match = Regex.Match(file, pattern); + return match.Success + ? match.Groups["extension"].Value + : string.Empty; + } + /// /// Based on the input string, this will detect if the string is a JS path or a JS snippet. /// If a path cannot be determined, then it is assumed to be a snippet the original text is returned @@ -724,67 +737,56 @@ namespace Umbraco.Core /// /// Generates a hash of a string based on the FIPS compliance setting. /// - /// Refers to itself - /// The hashed string + /// The to hash. + /// + /// The hashed string. + /// public static string GenerateHash(this string str) { - return CryptoConfig.AllowOnlyFipsAlgorithms - ? str.ToSHA1() - : str.ToMd5(); + return str.GenerateHash(CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5"); } /// - /// Converts the string to MD5 + /// Generate a hash of a string based on the specified hash algorithm. /// - /// Refers to itself - /// The MD5 hashed string - [Obsolete("Please use the GenerateHash method instead. This may be removed in future versions")] - internal static string ToMd5(this string stringToConvert) + /// The hash algorithm implementation to use. + /// The to hash. + /// + /// The hashed string. + /// + internal static string GenerateHash(this string str) + where T : HashAlgorithm { - return stringToConvert.GenerateHash("MD5"); + return str.GenerateHash(typeof(T).FullName); } /// - /// Converts the string to SHA1 + /// Generate a hash of a string based on the specified . /// - /// refers to itself - /// The SHA1 hashed string - [Obsolete("Please use the GenerateHash method instead. This may be removed in future versions")] - internal static string ToSHA1(this string stringToConvert) + /// The to hash. + /// The hash algorithm implementation to use. + /// + /// The hashed string. + /// + /// No hashing type found by name . + /// + internal static string GenerateHash(this string str, string hashType) { - return stringToConvert.GenerateHash("SHA1"); - } - - /// Generate a hash of a string based on the hashType passed in - /// - /// Refers to itself - /// String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a list of possible values. - /// The hashed string - private static string GenerateHash(this string str, string hashType) - { - //create an instance of the correct hashing provider based on the type passed in var hasher = HashAlgorithm.Create(hashType); - if (hasher == null) throw new InvalidOperationException("No hashing type found by name " + hashType); + if (hasher == null) throw new InvalidOperationException($"No hashing type found by name {hashType}."); + using (hasher) { - //convert our string into byte array var byteArray = Encoding.UTF8.GetBytes(str); - - //get the hashed values created by our selected provider var hashedByteArray = hasher.ComputeHash(byteArray); - //create a StringBuilder object - var stringBuilder = new StringBuilder(); - - //loop to each byte + var sb = new StringBuilder(); foreach (var b in hashedByteArray) { - //append it to our StringBuilder - stringBuilder.Append(b.ToString("x2")); + sb.Append(b.ToString("x2")); } - //return the hashed value - return stringBuilder.ToString(); + return sb.ToString(); } } @@ -1102,7 +1104,8 @@ namespace Umbraco.Core /// The safe url segment. public static string ToUrlSegment(this 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)); return Current.ShortStringHelper.CleanStringForUrlSegment(text); } @@ -1115,7 +1118,8 @@ namespace Umbraco.Core /// The safe url segment. public static string ToUrlSegment(this string text, string culture) { - 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)); return Current.ShortStringHelper.CleanStringForUrlSegment(text, culture); } diff --git a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs index 9cd4a58c65..2a558f85aa 100644 --- a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs +++ b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs @@ -97,8 +97,7 @@ namespace Umbraco.Core.Sync ? ":" + request.ServerVariables["SERVER_PORT"] : ""; - var useSsl = globalSettings.UseHttps || port == "443"; - var ssl = useSsl ? "s" : ""; // force, whatever the first request + var ssl = globalSettings.UseHttps ? "s" : ""; // force, whatever the first request var url = "http" + ssl + "://" + request.ServerVariables["SERVER_NAME"] + port + IOHelper.ResolveUrl(SystemDirectories.Umbraco); return url.TrimEnd('/'); diff --git a/src/Umbraco.Core/AsyncLock.cs b/src/Umbraco.Core/SystemLock.cs similarity index 80% rename from src/Umbraco.Core/AsyncLock.cs rename to src/Umbraco.Core/SystemLock.cs index 158b132f26..4eaae7082b 100644 --- a/src/Umbraco.Core/AsyncLock.cs +++ b/src/Umbraco.Core/SystemLock.cs @@ -21,18 +21,18 @@ namespace Umbraco.Core // been closed, the Semaphore system object is destroyed - so in any case // an iisreset should clean up everything // - internal class AsyncLock + internal class SystemLock { private readonly SemaphoreSlim _semaphore; private readonly Semaphore _semaphore2; private readonly IDisposable _releaser; private readonly Task _releaserTask; - public AsyncLock() - : this (null) + public SystemLock() + : this(null) { } - public AsyncLock(string name) + public SystemLock(string name) { // WaitOne() waits until count > 0 then decrements count // Release() increments count @@ -67,32 +67,6 @@ namespace Umbraco.Core : new NamedSemaphoreReleaser(_semaphore2); } - public Task LockAsync() - { - var wait = _semaphore != null - ? _semaphore.WaitAsync() - : _semaphore2.WaitOneAsync(); - - return wait.IsCompleted - ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named - : wait.ContinueWith((_, state) => (((AsyncLock) state).CreateReleaser()), - this, CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } - - public Task LockAsync(int millisecondsTimeout) - { - var wait = _semaphore != null - ? _semaphore.WaitAsync(millisecondsTimeout) - : _semaphore2.WaitOneAsync(millisecondsTimeout); - - return wait.IsCompleted - ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named - : wait.ContinueWith((_, state) => (((AsyncLock)state).CreateReleaser()), - this, CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } - public IDisposable Lock() { if (_semaphore != null) @@ -118,14 +92,18 @@ namespace Umbraco.Core private class NamedSemaphoreReleaser : CriticalFinalizerObject, IDisposable { private readonly Semaphore _semaphore; - private GCHandle _handle; internal NamedSemaphoreReleaser(Semaphore semaphore) { _semaphore = semaphore; - _handle = GCHandle.Alloc(_semaphore); } + #region IDisposable Support + + // This code added to correctly implement the disposable pattern. + + private bool disposedValue = false; // To detect redundant calls + public void Dispose() { Dispose(true); @@ -134,10 +112,22 @@ namespace Umbraco.Core private void Dispose(bool disposing) { - // critical - _handle.Free(); - _semaphore.Release(); - _semaphore.Dispose(); + if (!disposedValue) + { + try + { + _semaphore.Release(); + } + finally + { + try + { + _semaphore.Dispose(); + } + catch { } + } + disposedValue = true; + } } // we WANT to release the semaphore because it's a system object, ie a critical @@ -168,6 +158,9 @@ namespace Umbraco.Core // we do NOT want the finalizer to throw - never ever } } + + #endregion + } private class SemaphoreSlimReleaser : IDisposable diff --git a/src/Umbraco.Core/Udi.cs b/src/Umbraco.Core/Udi.cs index c7297b8c09..b11ce250ad 100644 --- a/src/Umbraco.Core/Udi.cs +++ b/src/Umbraco.Core/Udi.cs @@ -233,7 +233,7 @@ namespace Umbraco.Core // just pick every service connectors - just making sure that not two of them // would register the same entity type, with different udi types (would not make // much sense anyways). - var connectors = Current.TypeLoader.GetTypes(); + var connectors = Current.HasFactory ? (Current.TypeLoader?.GetTypes() ?? Enumerable.Empty()) : Enumerable.Empty(); var result = new Dictionary(); foreach (var connector in connectors) { @@ -316,7 +316,7 @@ namespace Umbraco.Core if (udiType == UdiType.GuidUdi) return new GuidUdi(uri); - if (udiType == UdiType.GuidUdi) + if (udiType == UdiType.StringUdi) return new StringUdi(uri); throw new ArgumentException(string.Format("Uri \"{0}\" is not a valid udi.", uri)); @@ -368,7 +368,7 @@ namespace Umbraco.Core return (udi1 == udi2) == false; } - private class UnknownTypeUdi : Udi + internal class UnknownTypeUdi : Udi { private UnknownTypeUdi() : base("unknown", "umb://unknown/") diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8eeb8773ea..f15b098473 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -10,10 +10,11 @@ Umbraco.Core ..\ + $(AdditionalFileItemNames);Content true - full + portable false bin\Debug\ TRACE;DEBUG @@ -23,7 +24,7 @@ latest - pdbonly + portable true bin\Release\ TRACE @@ -55,6 +56,16 @@ + + 1.0.0-beta2-19324-01 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 3.3.0 + runtime; build; native; contentfiles; analyzers + all + 1.3.0 @@ -117,7 +128,31 @@ --> - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -199,6 +234,7 @@ + @@ -218,16 +254,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -235,6 +324,7 @@ + @@ -295,6 +385,7 @@ + @@ -304,6 +395,7 @@ + @@ -342,7 +434,7 @@ - + @@ -386,23 +478,6 @@ - - - - - - - - - - - - - - - - - @@ -415,14 +490,13 @@ - - + @@ -432,6 +506,7 @@ + @@ -642,7 +717,6 @@ - @@ -691,7 +765,7 @@ - + @@ -704,7 +778,6 @@ - @@ -811,7 +884,7 @@ - + @@ -956,7 +1029,6 @@ - @@ -1029,7 +1101,6 @@ - @@ -1138,30 +1209,7 @@ - - - - - - - - - - - - - - - - - - - - - - - @@ -1514,12 +1562,13 @@ - + + diff --git a/src/Umbraco.Core/UpgradeService.cs b/src/Umbraco.Core/UpgradeService.cs new file mode 100644 index 0000000000..8116c0e738 --- /dev/null +++ b/src/Umbraco.Core/UpgradeService.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using Semver; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Services; + +namespace Umbraco.Core +{ + internal class UpgradeService : IUpgradeService + { + private readonly IUpgradeCheckRepository _upgradeCheckRepository; + + public UpgradeService(IUpgradeCheckRepository upgradeCheckRepository) + { + _upgradeCheckRepository = upgradeCheckRepository; + } + + public async Task CheckUpgrade(SemVersion version) + { + return await _upgradeCheckRepository.CheckUpgradeAsync(version); + } + } +} diff --git a/src/Umbraco.Core/WaitHandleExtensions.cs b/src/Umbraco.Core/WaitHandleExtensions.cs index 0d840a2496..7a9294c113 100644 --- a/src/Umbraco.Core/WaitHandleExtensions.cs +++ b/src/Umbraco.Core/WaitHandleExtensions.cs @@ -23,6 +23,8 @@ namespace Umbraco.Core handle, (state, timedOut) => { + //TODO: We aren't checking if this is timed out + tcs.SetResult(null); // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. diff --git a/src/Umbraco.Core/Xml/DynamicContext.cs b/src/Umbraco.Core/Xml/DynamicContext.cs index d39c4e133b..e5b90a1622 100644 --- a/src/Umbraco.Core/Xml/DynamicContext.cs +++ b/src/Umbraco.Core/Xml/DynamicContext.cs @@ -236,7 +236,7 @@ namespace Umbraco.Core.Xml _name = name; _value = value; - if (value is String) + if (value is string) _type = XPathResultType.String; else if (value is bool) _type = XPathResultType.Boolean; diff --git a/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs index b31fa6a8df..24fb070663 100644 --- a/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs +++ b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core.Exceptions; namespace Umbraco.Core.Xml { @@ -37,7 +34,8 @@ namespace Umbraco.Core.Xml // allowed 'inline', not just at the beginning... whether or not we want to support that is up // for discussion. - if (string.IsNullOrWhiteSpace(xpathExpression)) throw new ArgumentNullOrEmptyException(nameof(xpathExpression)); + if (xpathExpression == null) throw new ArgumentNullException(nameof(xpathExpression)); + if (string.IsNullOrWhiteSpace(xpathExpression)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(xpathExpression)); if (getPath == null) throw new ArgumentNullException(nameof(getPath)); if (publishedContentExists == null) throw new ArgumentNullException(nameof(publishedContentExists)); diff --git a/src/Umbraco.Core/Xml/XmlHelper.cs b/src/Umbraco.Core/Xml/XmlHelper.cs index 3fd8dcaea5..d6461ec8c6 100644 --- a/src/Umbraco.Core/Xml/XmlHelper.cs +++ b/src/Umbraco.Core/Xml/XmlHelper.cs @@ -4,9 +4,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Xml; -using System.Xml.Linq; using System.Xml.XPath; -using Umbraco.Core.Exceptions; using Umbraco.Core.IO; namespace Umbraco.Core.Xml @@ -25,9 +23,10 @@ namespace Umbraco.Core.Xml /// public static void SetAttribute(XmlDocument xml, XmlNode n, string name, string value) { - if (xml == null) throw new ArgumentNullException("xml"); - if (n == null) throw new ArgumentNullException("n"); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); + if (xml == null) throw new ArgumentNullException(nameof(xml)); + if (n == null) throw new ArgumentNullException(nameof(n)); + 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 (n.Attributes == null) { @@ -215,9 +214,9 @@ namespace Umbraco.Core.Xml var xmlDoc = new XmlDocument(); //Load the file into the XmlDocument xmlDoc.Load(reader); - + return xmlDoc; - } + } } /// @@ -229,8 +228,9 @@ namespace Umbraco.Core.Xml /// a XmlAttribute public static XmlAttribute AddAttribute(XmlDocument xd, string name, string value) { - if (xd == null) throw new ArgumentNullException("xd"); - if (string.IsNullOrEmpty(name)) throw new ArgumentNullOrEmptyException(nameof(name)); + if (xd == null) throw new ArgumentNullException(nameof(xd)); + 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)); var temp = xd.CreateAttribute(name); temp.Value = value; @@ -246,8 +246,9 @@ namespace Umbraco.Core.Xml /// a XmlNode public static XmlNode AddTextNode(XmlDocument xd, string name, string value) { - if (xd == null) throw new ArgumentNullException("xd"); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); + if (xd == null) throw new ArgumentNullException(nameof(xd)); + 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)); var temp = xd.CreateNode(XmlNodeType.Element, name, ""); temp.AppendChild(xd.CreateTextNode(value)); @@ -264,9 +265,10 @@ namespace Umbraco.Core.Xml /// a XmlNode public static XmlNode SetTextNode(XmlDocument xd, XmlNode parent, string name, string value) { - if (xd == null) throw new ArgumentNullException("xd"); - if (parent == null) throw new ArgumentNullException("parent"); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); + if (xd == null) throw new ArgumentNullException(nameof(xd)); + if (parent == null) throw new ArgumentNullException(nameof(parent)); + 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)); var child = parent.SelectSingleNode(name); if (child != null) @@ -289,7 +291,8 @@ namespace Umbraco.Core.Xml { if (xd == null) throw new ArgumentNullException(nameof(xd)); if (parent == null) throw new ArgumentNullException(nameof(parent)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", 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)); var child = parent.SelectSingleNode(name) ?? xd.CreateNode(XmlNodeType.Element, name, ""); child.InnerXml = value; @@ -305,8 +308,9 @@ namespace Umbraco.Core.Xml /// A XmlNode public static XmlNode AddCDataNode(XmlDocument xd, string name, string value) { - if (xd == null) throw new ArgumentNullException("xd"); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); + if (xd == null) throw new ArgumentNullException(nameof(xd)); + 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)); var temp = xd.CreateNode(XmlNodeType.Element, name, ""); temp.AppendChild(xd.CreateCDataSection(value)); @@ -323,14 +327,15 @@ namespace Umbraco.Core.Xml /// a XmlNode public static XmlNode SetCDataNode(XmlDocument xd, XmlNode parent, string name, string value) { - if (xd == null) throw new ArgumentNullException("xd"); - if (parent == null) throw new ArgumentNullException("parent"); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); + if (xd == null) throw new ArgumentNullException(nameof(xd)); + if (parent == null) throw new ArgumentNullException(nameof(parent)); + 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)); var child = parent.SelectSingleNode(name); if (child != null) { - child.InnerXml = ""; ; + child.InnerXml = ""; return child; } return AddCDataNode(xd, name, value); diff --git a/src/Umbraco.Examine/BaseValueSetBuilder.cs b/src/Umbraco.Examine/BaseValueSetBuilder.cs index 22d379d148..93cee88231 100644 --- a/src/Umbraco.Examine/BaseValueSetBuilder.cs +++ b/src/Umbraco.Examine/BaseValueSetBuilder.cs @@ -45,7 +45,7 @@ namespace Umbraco.Examine continue; case string strVal: { - if (strVal.IsNullOrWhiteSpace()) return; + if (strVal.IsNullOrWhiteSpace()) continue; var key = $"{keyVal.Key}{cultureSuffix}"; if (values.TryGetValue(key, out var v)) values[key] = new List(v) { val }.ToArray(); diff --git a/src/Umbraco.Examine/ContentIndexPopulator.cs b/src/Umbraco.Examine/ContentIndexPopulator.cs index 51b9de4a0b..99ff4d7f87 100644 --- a/src/Umbraco.Examine/ContentIndexPopulator.cs +++ b/src/Umbraco.Examine/ContentIndexPopulator.cs @@ -15,7 +15,7 @@ namespace Umbraco.Examine /// /// Performs the data lookups required to rebuild a content index /// - public class ContentIndexPopulator : IndexPopulator + public class ContentIndexPopulator : IndexPopulator { private readonly IContentService _contentService; private readonly IValueSetBuilder _contentValueSetBuilder; @@ -36,7 +36,7 @@ namespace Umbraco.Examine /// public ContentIndexPopulator(IContentService contentService, ISqlContext sqlContext, IContentValueSetBuilder contentValueSetBuilder) : this(false, null, contentService, sqlContext, contentValueSetBuilder) - { + { } /// @@ -52,7 +52,7 @@ namespace Umbraco.Examine if (sqlContext == null) throw new ArgumentNullException(nameof(sqlContext)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); _contentValueSetBuilder = contentValueSetBuilder ?? throw new ArgumentNullException(nameof(contentValueSetBuilder)); - if (_publishedQuery != null) + if (_publishedQuery == null) _publishedQuery = sqlContext.Query().Where(x => x.Published); _publishedValuesOnly = publishedValuesOnly; _parentId = parentId; diff --git a/src/Umbraco.Examine/ContentValueSetBuilder.cs b/src/Umbraco.Examine/ContentValueSetBuilder.cs index 44cef08813..b8477a9047 100644 --- a/src/Umbraco.Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Examine/ContentValueSetBuilder.cs @@ -1,9 +1,13 @@ using Examine; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; @@ -16,20 +20,46 @@ namespace Umbraco.Examine { private readonly UrlSegmentProviderCollection _urlSegmentProviders; private readonly IUserService _userService; + private readonly IScopeProvider _scopeProvider; + + [Obsolete("Use the other ctor instead")] + public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + IUserService userService, + bool publishedValuesOnly) + : this(propertyEditors, urlSegmentProviders, userService, Current.ScopeProvider, publishedValuesOnly) + { + } public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, UrlSegmentProviderCollection urlSegmentProviders, IUserService userService, + IScopeProvider scopeProvider, bool publishedValuesOnly) : base(propertyEditors, publishedValuesOnly) { _urlSegmentProviders = urlSegmentProviders; _userService = userService; + _scopeProvider = scopeProvider; } /// public override IEnumerable GetValueSets(params IContent[] content) { + Dictionary creatorIds; + Dictionary writerIds; + + // We can lookup all of the creator/writer names at once which can save some + // processing below instead of one by one. + using (var scope = _scopeProvider.CreateScope()) + { + creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) + .ToDictionary(x => x.Id, x => x); + writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) + .ToDictionary(x => x.Id, x => x); + scope.Complete(); + } + // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` @@ -54,12 +84,12 @@ namespace Umbraco.Examine {"updateDate", new object[] {c.UpdateDate}}, //Always add invariant updateDate {"nodeName", (PublishedValuesOnly //Always add invariant nodeName ? c.PublishName?.Yield() - : c?.Name.Yield()) ?? Enumerable.Empty()}, + : c.Name?.Yield()) ?? Enumerable.Empty()}, {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName {"path", c.Path?.Yield() ?? Enumerable.Empty()}, {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, - {"creatorName", (c.GetCreatorProfile(_userService)?.Name ?? "??").Yield() }, - {"writerName",(c.GetWriterProfile(_userService)?.Name ?? "??").Yield() }, + {"creatorName", (creatorIds.TryGetValue(c.CreatorId, out var creatorProfile) ? creatorProfile.Name : "??").Yield() }, + {"writerName", (writerIds.TryGetValue(c.WriterId, out var writerProfile) ? writerProfile.Name : "??").Yield() }, {"writerID", new object[] {c.WriterId}}, {"templateID", new object[] {c.TemplateId ?? 0}}, {UmbracoContentIndex.VariesByCultureFieldName, new object[] {"n"}}, diff --git a/src/Umbraco.Examine/ExamineExtensions.cs b/src/Umbraco.Examine/ExamineExtensions.cs index 43a3ccc196..d231a86f69 100644 --- a/src/Umbraco.Examine/ExamineExtensions.cs +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -12,6 +12,7 @@ using Lucene.Net.Store; using Umbraco.Core; using Version = Lucene.Net.Util.Version; using Umbraco.Core.Logging; +using System.Threading; namespace Umbraco.Examine { @@ -28,6 +29,29 @@ namespace Umbraco.Examine /// internal static readonly Regex CultureIsoCodeFieldNameMatchExpression = new Regex("^([_\\w]+)_([a-z]{2}-[a-z0-9]{2,4})$", RegexOptions.Compiled); + private static bool _isConfigured = false; + private static object _configuredInit = null; + private static object _isConfiguredLocker = new object(); + + /// + /// Called on startup to configure each index. + /// + /// + /// Configures and unlocks all Lucene based indexes registered with the . + /// + internal static void ConfigureIndexes(this IExamineManager examineManager, IMainDom mainDom, ILogger logger) + { + LazyInitializer.EnsureInitialized( + ref _configuredInit, + ref _isConfigured, + ref _isConfiguredLocker, + () => + { + examineManager.ConfigureLuceneIndexes(logger, !mainDom.IsMainDom); + return null; + }); + } + //TODO: We need a public method here to just match a field name against CultureIsoCodeFieldNameMatchExpression /// @@ -48,6 +72,31 @@ namespace Umbraco.Examine } } + /// + /// Returns all index fields that are culture specific (suffixed) or invariant + /// + /// + /// + /// + public static IEnumerable GetCultureAndInvariantFields(this IUmbracoIndex index, string culture) + { + var allFields = index.GetFields(); + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var field in allFields) + { + var match = CultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && match.Groups.Count == 3 && culture.InvariantEquals(match.Groups[2].Value)) + { + yield return field; //matches this culture field + } + else if (!match.Success) + { + yield return field; //matches no culture field (invariant) + } + + } + } + internal static bool TryParseLuceneQuery(string query) { // TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet, for now we'll @@ -77,7 +126,7 @@ namespace Umbraco.Examine /// /// This is not thread safe, use with care /// - internal static void UnlockLuceneIndexes(this IExamineManager examineManager, ILogger logger) + internal static void ConfigureLuceneIndexes(this IExamineManager examineManager, ILogger logger, bool disableExamineIndexing) { foreach (var luceneIndexer in examineManager.Indexes.OfType()) { @@ -86,6 +135,8 @@ namespace Umbraco.Examine //that could end up halting shutdown for a very long time causing overlapping appdomains and many other problems. luceneIndexer.WaitForIndexQueueOnShutdown = false; + if (disableExamineIndexing) continue; //exit if not enabled, we don't need to unlock them if we're not maindom + //we should check if the index is locked ... it shouldn't be! We are using simple fs lock now and we are also ensuring that //the indexes are not operational unless MainDom is true var dir = luceneIndexer.GetLuceneDirectory(); diff --git a/src/Umbraco.Examine/IUmbracoContentIndex.cs b/src/Umbraco.Examine/IUmbracoContentIndex.cs new file mode 100644 index 0000000000..3181ff663e --- /dev/null +++ b/src/Umbraco.Examine/IUmbracoContentIndex.cs @@ -0,0 +1,9 @@ +using Examine; + +namespace Umbraco.Examine +{ + public interface IUmbracoContentIndex : IIndex + { + + } +} diff --git a/src/Umbraco.Examine/IUmbracoIndexConfig.cs b/src/Umbraco.Examine/IUmbracoIndexConfig.cs new file mode 100644 index 0000000000..02c6c51d0c --- /dev/null +++ b/src/Umbraco.Examine/IUmbracoIndexConfig.cs @@ -0,0 +1,12 @@ +using Examine; + +namespace Umbraco.Examine +{ + public interface IUmbracoIndexConfig + { + IContentValueSetValidator GetContentValueSetValidator(); + IContentValueSetValidator GetPublishedContentValueSetValidator(); + IValueSetValidator GetMemberValueSetValidator(); + + } +} diff --git a/src/Umbraco.Examine/IUmbracoMemberIndex.cs b/src/Umbraco.Examine/IUmbracoMemberIndex.cs new file mode 100644 index 0000000000..b1f325b2e9 --- /dev/null +++ b/src/Umbraco.Examine/IUmbracoMemberIndex.cs @@ -0,0 +1,9 @@ +using Examine; + +namespace Umbraco.Examine +{ + public interface IUmbracoMemberIndex : IIndex + { + + } +} diff --git a/src/Umbraco.Examine/IndexRebuilder.cs b/src/Umbraco.Examine/IndexRebuilder.cs index 43c309b9c5..786aecac71 100644 --- a/src/Umbraco.Examine/IndexRebuilder.cs +++ b/src/Umbraco.Examine/IndexRebuilder.cs @@ -5,7 +5,8 @@ using System.Threading.Tasks; using Examine; namespace Umbraco.Examine -{ +{ + /// /// Utility to rebuild all indexes ensuring minimal data queries /// diff --git a/src/Umbraco.Examine/LuceneIndexDiagnostics.cs b/src/Umbraco.Examine/LuceneIndexDiagnostics.cs new file mode 100644 index 0000000000..96363904b4 --- /dev/null +++ b/src/Umbraco.Examine/LuceneIndexDiagnostics.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using Examine.LuceneEngine.Providers; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Lucene.Net.Store; +using Umbraco.Core.IO; +using System.Linq; + +namespace Umbraco.Examine +{ + public class LuceneIndexDiagnostics : IIndexDiagnostics + { + public LuceneIndexDiagnostics(LuceneIndex index, ILogger logger) + { + Index = index; + Logger = logger; + } + + public LuceneIndex Index { get; } + public ILogger Logger { get; } + + public int DocumentCount + { + get + { + try + { + return Index.GetIndexDocumentCount(); + } + catch (AlreadyClosedException) + { + Logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexDocumentCount, the writer is already closed"); + return 0; + } + } + } + + public int FieldCount + { + get + { + try + { + return Index.GetIndexFieldCount(); + } + catch (AlreadyClosedException) + { + Logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexFieldCount, the writer is already closed"); + return 0; + } + } + } + + public Attempt IsHealthy() + { + var isHealthy = Index.IsHealthy(out var indexError); + return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError.Message); + } + + public virtual IReadOnlyDictionary Metadata + { + get + { + var luceneDir = Index.GetLuceneDirectory(); + var d = new Dictionary + { + [nameof(UmbracoExamineIndex.CommitCount)] = Index.CommitCount, + [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = Index.DefaultAnalyzer.GetType().Name, + ["LuceneDirectory"] = luceneDir.GetType().Name + }; + + if (luceneDir is FSDirectory fsDir) + { + d[nameof(UmbracoExamineIndex.LuceneIndexFolder)] = fsDir.Directory.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'); + } + + return d; + } + } + + + } +} diff --git a/src/Umbraco.Examine/MediaIndexPopulator.cs b/src/Umbraco.Examine/MediaIndexPopulator.cs index 6dadcbe4b3..1f5b11e54f 100644 --- a/src/Umbraco.Examine/MediaIndexPopulator.cs +++ b/src/Umbraco.Examine/MediaIndexPopulator.cs @@ -10,7 +10,7 @@ namespace Umbraco.Examine /// /// Performs the data lookups required to rebuild a media index /// - public class MediaIndexPopulator : IndexPopulator + public class MediaIndexPopulator : IndexPopulator { private readonly int? _parentId; private readonly IMediaService _mediaService; @@ -69,6 +69,6 @@ namespace Umbraco.Examine pageIndex++; } while (media.Length == pageSize); } - + } } diff --git a/src/Umbraco.Examine/MediaValueSetBuilder.cs b/src/Umbraco.Examine/MediaValueSetBuilder.cs index 3839d008b3..03e7f4944b 100644 --- a/src/Umbraco.Examine/MediaValueSetBuilder.cs +++ b/src/Umbraco.Examine/MediaValueSetBuilder.cs @@ -1,9 +1,13 @@ -using Examine; +using System; +using Examine; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using Umbraco.Core; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Services; using Umbraco.Core.Strings; @@ -13,14 +17,16 @@ namespace Umbraco.Examine { private readonly UrlSegmentProviderCollection _urlSegmentProviders; private readonly IUserService _userService; + private readonly ILogger _logger; public MediaValueSetBuilder(PropertyEditorCollection propertyEditors, UrlSegmentProviderCollection urlSegmentProviders, - IUserService userService) + IUserService userService, ILogger logger) : base(propertyEditors, false) { _urlSegmentProviders = urlSegmentProviders; _userService = userService; + _logger = logger; } /// @@ -29,6 +35,42 @@ namespace Umbraco.Examine foreach (var m in media) { var urlValue = m.GetUrlSegment(_urlSegmentProviders); + + var umbracoFilePath = string.Empty; + var umbracoFile = string.Empty; + + var umbracoFileSource = m.GetValue(Constants.Conventions.Media.File); + + if (umbracoFileSource.DetectIsJson()) + { + ImageCropperValue cropper = null; + try + { + cropper = JsonConvert.DeserializeObject( + m.GetValue(Constants.Conventions.Media.File)); + } + catch (Exception ex) + { + _logger.Error(ex, $"Could not Deserialize ImageCropperValue for item with key {m.Key} "); + } + + if (cropper != null) + { + umbracoFilePath = cropper.Src; + } + } + else + { + umbracoFilePath = umbracoFileSource; + } + + if (!string.IsNullOrEmpty(umbracoFilePath)) + { + // intentional dummy Uri + var uri = new Uri("https://localhost/" + umbracoFilePath); + umbracoFile = uri.Segments.Last(); + } + var values = new Dictionary> { {"icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, @@ -44,7 +86,8 @@ namespace Umbraco.Examine {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, {"path", m.Path?.Yield() ?? Enumerable.Empty()}, {"nodeType", m.ContentType.Id.ToString().Yield() }, - {"creatorName", (m.GetCreatorProfile(_userService)?.Name ?? "??").Yield()} + {"creatorName", (m.GetCreatorProfile(_userService)?.Name ?? "??").Yield()}, + {UmbracoExamineIndex.UmbracoFileFieldName, umbracoFile.Yield()} }; foreach (var property in m.Properties) diff --git a/src/Umbraco.Examine/MemberIndexPopulator.cs b/src/Umbraco.Examine/MemberIndexPopulator.cs index e20dab91ca..26a3b0aedd 100644 --- a/src/Umbraco.Examine/MemberIndexPopulator.cs +++ b/src/Umbraco.Examine/MemberIndexPopulator.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Services; namespace Umbraco.Examine { - public class MemberIndexPopulator : IndexPopulator + public class MemberIndexPopulator : IndexPopulator { private readonly IMemberService _memberService; private readonly IValueSetBuilder _valueSetBuilder; diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 05b209f927..0e0ee62139 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -10,10 +10,11 @@ Umbraco.Examine ..\ + $(AdditionalFileItemNames);Content true - full + portable false bin\Debug\ DEBUG;TRACE @@ -23,7 +24,7 @@ latest - pdbonly + portable true bin\Release\ TRACE @@ -48,9 +49,19 @@ - + + + 1.0.0-beta2-19324-01 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 3.3.0 + runtime; build; native; contentfiles; analyzers + all + @@ -59,14 +70,19 @@ + + + + + diff --git a/src/Umbraco.Examine/UmbracoContentIndex.cs b/src/Umbraco.Examine/UmbracoContentIndex.cs index a9e2c72cb6..e266ca789d 100644 --- a/src/Umbraco.Examine/UmbracoContentIndex.cs +++ b/src/Umbraco.Examine/UmbracoContentIndex.cs @@ -17,13 +17,13 @@ namespace Umbraco.Examine /// /// An indexer for Umbraco content and media /// - public class UmbracoContentIndex : UmbracoExamineIndex + public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex { public const string VariesByCultureFieldName = SpecialFieldPrefix + "VariesByCulture"; protected ILocalizationService LanguageService { get; } #region Constructors - + /// /// Create an index at runtime /// @@ -141,6 +141,6 @@ namespace Umbraco.Examine base.PerformDeleteFromIndex(idsAsList, onComplete); } - + } } diff --git a/src/Umbraco.Examine/UmbracoExamineIndex.cs b/src/Umbraco.Examine/UmbracoExamineIndex.cs index 24952050da..e1dd77b994 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndex.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndex.cs @@ -32,6 +32,7 @@ namespace Umbraco.Examine /// public const string IndexPathFieldName = SpecialFieldPrefix + "Path"; public const string NodeKeyFieldName = SpecialFieldPrefix + "Key"; + public const string UmbracoFileFieldName = "umbracoFileSrc"; public const string IconFieldName = SpecialFieldPrefix + "Icon"; public const string PublishedFieldName = SpecialFieldPrefix + "Published"; diff --git a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs index fed5b9bae7..4a926deebe 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs @@ -7,73 +7,24 @@ using Umbraco.Core.Logging; namespace Umbraco.Examine { - public class UmbracoExamineIndexDiagnostics : IIndexDiagnostics + public class UmbracoExamineIndexDiagnostics : LuceneIndexDiagnostics { private readonly UmbracoExamineIndex _index; - private readonly ILogger _logger; public UmbracoExamineIndexDiagnostics(UmbracoExamineIndex index, ILogger logger) + : base(index, logger) { _index = index; - _logger = logger; } - public int DocumentCount + public override IReadOnlyDictionary Metadata { get { - try - { - return _index.GetIndexDocumentCount(); - } - catch (AlreadyClosedException) - { - _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexDocumentCount, the writer is already closed"); - return 0; - } - } - } + var d = base.Metadata.ToDictionary(x => x.Key, x => x.Value); - public int FieldCount - { - get - { - try - { - return _index.GetIndexFieldCount(); - } - catch (AlreadyClosedException) - { - _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexFieldCount, the writer is already closed"); - return 0; - } - } - } - - public Attempt IsHealthy() - { - var isHealthy = _index.IsHealthy(out var indexError); - return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError.Message); - } - - public virtual IReadOnlyDictionary Metadata - { - get - { - var d = new Dictionary - { - [nameof(UmbracoExamineIndex.CommitCount)] = _index.CommitCount, - [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = _index.DefaultAnalyzer.GetType().Name, - ["LuceneDirectory"] = _index.GetLuceneDirectory().GetType().Name, - [nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler, - [nameof(UmbracoExamineIndex.LuceneIndexFolder)] = - _index.LuceneIndexFolder == null - ? string.Empty - : _index.LuceneIndexFolder.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'), - [nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly, - //There's too much info here - //[nameof(UmbracoExamineIndexer.FieldDefinitionCollection)] = _index.FieldDefinitionCollection, - }; + d[nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler; + d[nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly; if (_index.ValueSetValidator is ValueSetValidator vsv) { diff --git a/src/Umbraco.Examine/UmbracoIndexConfig.cs b/src/Umbraco.Examine/UmbracoIndexConfig.cs new file mode 100644 index 0000000000..7ad9c638d3 --- /dev/null +++ b/src/Umbraco.Examine/UmbracoIndexConfig.cs @@ -0,0 +1,34 @@ +using Examine; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; + +namespace Umbraco.Examine +{ + public class UmbracoIndexConfig : IUmbracoIndexConfig + { + public UmbracoIndexConfig(IPublicAccessService publicAccessService) + { + PublicAccessService = publicAccessService; + } + + protected IPublicAccessService PublicAccessService { get; } + public IContentValueSetValidator GetContentValueSetValidator() + { + return new ContentValueSetValidator(false, true, PublicAccessService); + } + + public IContentValueSetValidator GetPublishedContentValueSetValidator() + { + return new ContentValueSetValidator(true, false, PublicAccessService); + } + + /// + /// Returns the for the member indexer + /// + /// + public IValueSetValidator GetMemberValueSetValidator() + { + return new MemberValueSetValidator(); + } + } +} diff --git a/src/Umbraco.Examine/UmbracoMemberIndex.cs b/src/Umbraco.Examine/UmbracoMemberIndex.cs index fbf8a1cc0f..445707ab0c 100644 --- a/src/Umbraco.Examine/UmbracoMemberIndex.cs +++ b/src/Umbraco.Examine/UmbracoMemberIndex.cs @@ -11,7 +11,7 @@ namespace Umbraco.Examine /// /// Custom indexer for members /// - public class UmbracoMemberIndex : UmbracoExamineIndex + public class UmbracoMemberIndex : UmbracoExamineIndex, IUmbracoMemberIndex { /// /// Constructor to allow for creating an indexer at runtime @@ -32,6 +32,6 @@ namespace Umbraco.Examine base(name, luceneDirectory, fieldDefinitions, analyzer, profilingLogger, validator) { } - + } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs b/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs new file mode 100644 index 0000000000..22347edd60 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/ApiVersion.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; +using Semver; + +namespace Umbraco.ModelsBuilder.Embedded +{ + /// + /// Manages API version handshake between client and server. + /// + public class ApiVersion + { + /// + /// Initializes a new instance of the class. + /// + /// The currently executing version. + /// + internal ApiVersion(SemVersion executingVersion) + { + Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); + } + + private static SemVersion CurrentAssemblyVersion + => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute().InformationalVersion); + + /// + /// Gets the currently executing API version. + /// + public static ApiVersion Current { get; } + = new ApiVersion(CurrentAssemblyVersion); + + /// + /// Gets the executing version of the API. + /// + public SemVersion Version { get; } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs new file mode 100644 index 0000000000..1fdb64c62a --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidator.cs @@ -0,0 +1,17 @@ +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + /// + /// Used to validate the aliases for the content type when MB is enabled to ensure that + /// no illegal aliases are used + /// + // ReSharper disable once UnusedMember.Global - This is typed scanned + public class ContentTypeModelValidator : ContentTypeModelValidatorBase + { + public ContentTypeModelValidator(IModelsBuilderConfig config) : base(config) + { + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs new file mode 100644 index 0000000000..15ca2cca24 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Editors; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + public abstract class ContentTypeModelValidatorBase : EditorValidator + where TModel : ContentTypeSave + where TProperty : PropertyTypeBasic + { + private readonly IModelsBuilderConfig _config; + + public ContentTypeModelValidatorBase(IModelsBuilderConfig config) + { + _config = config; + } + + protected override IEnumerable Validate(TModel model) + { + //don't do anything if we're not enabled + if (!_config.Enable) yield break; + + var properties = model.Groups.SelectMany(x => x.Properties) + .Where(x => x.Inherited == false) + .ToArray(); + + foreach (var prop in properties) + { + var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop)); + + if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant()) + yield return new ValidationResult(string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), new[] + { + $"Groups[{model.Groups.IndexOf(propertyGroup)}].Properties[{propertyGroup.Properties.IndexOf(prop)}].Alias" + }); + + //we need to return the field name with an index so it's wired up correctly + var groupIndex = model.Groups.IndexOf(propertyGroup); + var propertyIndex = propertyGroup.Properties.IndexOf(prop); + + var validationResult = ValidateProperty(prop, groupIndex, propertyIndex); + if (validationResult != null) + yield return validationResult; + } + } + + private ValidationResult ValidateProperty(PropertyTypeBasic property, int groupIndex, int propertyIndex) + { + //don't let them match any properties or methods in IPublishedContent + //TODO: There are probably more! + var reservedProperties = typeof(IPublishedContent).GetProperties().Select(x => x.Name).ToArray(); + var reservedMethods = typeof(IPublishedContent).GetMethods().Select(x => x.Name).ToArray(); + + var alias = property.Alias; + + if (reservedProperties.InvariantContains(alias) || reservedMethods.InvariantContains(alias)) + return new ValidationResult( + $"The alias {alias} is a reserved term and cannot be used", new[] + { + $"Groups[{groupIndex}].Properties[{propertyIndex}].Alias" + }); + + return null; + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs new file mode 100644 index 0000000000..25ddc838e8 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs @@ -0,0 +1,61 @@ +using System.Text; +using Umbraco.ModelsBuilder.Embedded.Configuration; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + internal class DashboardReport + { + private readonly IModelsBuilderConfig _config; + private readonly OutOfDateModelsStatus _outOfDateModels; + private readonly ModelsGenerationError _mbErrors; + + public DashboardReport(IModelsBuilderConfig config, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors) + { + _config = config; + _outOfDateModels = outOfDateModels; + _mbErrors = mbErrors; + } + + public bool CanGenerate() => _config.ModelsMode.SupportsExplicitGeneration(); + + public bool AreModelsOutOfDate() => _outOfDateModels.IsOutOfDate; + + public string LastError() => _mbErrors.GetLastError(); + + public string Text() + { + if (!_config.Enable) + return "Version: " + ApiVersion.Current.Version + "
 
ModelsBuilder is disabled
(the .Enable key is missing, or its value is not 'true')."; + + var sb = new StringBuilder(); + + sb.Append("Version: "); + sb.Append(ApiVersion.Current.Version); + sb.Append("
 
"); + + sb.Append("ModelsBuilder is enabled, with the following configuration:"); + + sb.Append("
    "); + + sb.Append("
  • The models factory is "); + sb.Append(_config.EnableFactory || _config.ModelsMode == ModelsMode.PureLive + ? "enabled" + : "not enabled. Umbraco will not use models"); + sb.Append(".
  • "); + + sb.Append(_config.ModelsMode != ModelsMode.Nothing + ? $"
  • {_config.ModelsMode} models are enabled.
  • " + : "
  • No models mode is specified: models will not be generated.
  • "); + + sb.Append($"
  • Models namespace is {_config.ModelsNamespace}.
  • "); + + sb.Append("
  • Tracking of out-of-date models is "); + sb.Append(_config.FlagOutOfDateModels ? "enabled" : "not enabled"); + sb.Append(".
  • "); + + sb.Append("
"); + + return sb.ToString(); + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs new file mode 100644 index 0000000000..9dc1ea6c20 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MediaTypeModelValidator.cs @@ -0,0 +1,17 @@ +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + /// + /// Used to validate the aliases for the content type when MB is enabled to ensure that + /// no illegal aliases are used + /// + // ReSharper disable once UnusedMember.Global - This is typed scanned + public class MediaTypeModelValidator : ContentTypeModelValidatorBase + { + public MediaTypeModelValidator(IModelsBuilderConfig config) : base(config) + { + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs new file mode 100644 index 0000000000..8d0a98eeab --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/MemberTypeModelValidator.cs @@ -0,0 +1,17 @@ +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + /// + /// Used to validate the aliases for the content type when MB is enabled to ensure that + /// no illegal aliases are used + /// + // ReSharper disable once UnusedMember.Global - This is typed scanned + public class MemberTypeModelValidator : ContentTypeModelValidatorBase + { + public MemberTypeModelValidator(IModelsBuilderConfig config) : base(config) + { + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs new file mode 100644 index 0000000000..1d9de265e9 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs @@ -0,0 +1,146 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Runtime.Serialization; +using System.Web.Hosting; +using Umbraco.Core.Exceptions; +using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Editors; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.ModelsBuilder.Embedded.BackOffice +{ + /// + /// API controller for use in the Umbraco back office with Angular resources + /// + /// + /// We've created a different controller for the backoffice/angular specifically this is to ensure that the + /// correct CSRF security is adhered to for angular and it also ensures that this controller is not subseptipal to + /// global WebApi formatters being changed since this is always forced to only return Angular JSON Specific formats. + /// + [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] + public class ModelsBuilderDashboardController : UmbracoAuthorizedJsonController + { + private readonly IModelsBuilderConfig _config; + private readonly ModelsGenerator _modelGenerator; + private readonly OutOfDateModelsStatus _outOfDateModels; + private readonly ModelsGenerationError _mbErrors; + private readonly DashboardReport _dashboardReport; + + public ModelsBuilderDashboardController(IModelsBuilderConfig config, ModelsGenerator modelsGenerator, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors) + { + //_umbracoServices = umbracoServices; + _config = config; + _modelGenerator = modelsGenerator; + _outOfDateModels = outOfDateModels; + _mbErrors = mbErrors; + _dashboardReport = new DashboardReport(config, outOfDateModels, mbErrors); + } + + // invoked by the dashboard + // requires that the user is logged into the backoffice and has access to the settings section + // beware! the name of the method appears in modelsbuilder.controller.js + [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! + public HttpResponseMessage BuildModels() + { + try + { + var config = _config; + + if (!config.ModelsMode.SupportsExplicitGeneration()) + { + var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." }; + return Request.CreateResponse(HttpStatusCode.OK, result2, Configuration.Formatters.JsonFormatter); + } + + var bin = HostingEnvironment.MapPath("~/bin"); + if (bin == null) + throw new PanicException("bin is null."); + + // EnableDllModels will recycle the app domain - but this request will end properly + _modelGenerator.GenerateModels(); + _mbErrors.Clear(); + } + catch (Exception e) + { + _mbErrors.Report("Failed to build models.", e); + } + + return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); + } + + // invoked by the back-office + // requires that the user is logged into the backoffice and has access to the settings section + [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! + public HttpResponseMessage GetModelsOutOfDateStatus() + { + var status = _outOfDateModels.IsEnabled + ? _outOfDateModels.IsOutOfDate + ? new OutOfDateStatus { Status = OutOfDateType.OutOfDate } + : new OutOfDateStatus { Status = OutOfDateType.Current } + : new OutOfDateStatus { Status = OutOfDateType.Unknown }; + + return Request.CreateResponse(HttpStatusCode.OK, status, Configuration.Formatters.JsonFormatter); + } + + // invoked by the back-office + // requires that the user is logged into the backoffice and has access to the settings section + // beware! the name of the method appears in modelsbuilder.controller.js + [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! + public HttpResponseMessage GetDashboard() + { + return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); + } + + private Dashboard GetDashboardResult() + { + return new Dashboard + { + Enable = _config.Enable, + Text = _dashboardReport.Text(), + CanGenerate = _dashboardReport.CanGenerate(), + OutOfDateModels = _dashboardReport.AreModelsOutOfDate(), + LastError = _dashboardReport.LastError(), + }; + } + + [DataContract] + internal class BuildResult + { + [DataMember(Name = "success")] + public bool Success; + [DataMember(Name = "message")] + public string Message; + } + + [DataContract] + internal class Dashboard + { + [DataMember(Name = "enable")] + public bool Enable; + [DataMember(Name = "text")] + public string Text; + [DataMember(Name = "canGenerate")] + public bool CanGenerate; + [DataMember(Name = "outOfDateModels")] + public bool OutOfDateModels; + [DataMember(Name = "lastError")] + public string LastError; + } + + internal enum OutOfDateType + { + OutOfDate, + Current, + Unknown = 100 + } + + [DataContract] + internal class OutOfDateStatus + { + [DataMember(Name = "status")] + public OutOfDateType Status { get; set; } + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Building/Builder.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs similarity index 54% rename from src/Umbraco.ModelsBuilder/Building/Builder.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs index acfa402355..ffd56d4312 100644 --- a/src/Umbraco.ModelsBuilder/Building/Builder.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs @@ -1,14 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Embedded.Configuration; -namespace Umbraco.ModelsBuilder.Building +namespace Umbraco.ModelsBuilder.Embedded.Building { // NOTE // The idea was to have different types of builder, because I wanted to experiment with @@ -22,10 +17,10 @@ namespace Umbraco.ModelsBuilder.Building ///
internal abstract class Builder { + private readonly IList _typeModels; protected Dictionary ModelsMap { get; } = new Dictionary(); - protected ParseResult ParseResult { get; } // the list of assemblies that will be 'using' by default protected readonly IList TypesUsing = new List @@ -37,8 +32,7 @@ namespace Umbraco.ModelsBuilder.Building "Umbraco.Core.Models", "Umbraco.Core.Models.PublishedContent", "Umbraco.Web", - "Umbraco.ModelsBuilder", - "Umbraco.ModelsBuilder.Umbraco", + "Umbraco.ModelsBuilder.Embedded" }; /// @@ -55,10 +49,10 @@ namespace Umbraco.ModelsBuilder.Building /// /// Gets the list of models to generate. /// - /// The models to generate, ie those that are not ignored. + /// The models to generate public IEnumerable GetModelsToGenerate() { - return _typeModels.Where(x => !x.IsContentIgnored); + return _typeModels; } /// @@ -67,90 +61,39 @@ namespace Umbraco.ModelsBuilder.Building /// Includes those that are ignored. internal IList TypeModels => _typeModels; - /// - /// Initializes a new instance of the class with a list of models to generate - /// and the result of code parsing. - /// - /// The list of models to generate. - /// The result of code parsing. - protected Builder(IList typeModels, ParseResult parseResult) - { - _typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); - ParseResult = parseResult ?? throw new ArgumentNullException(nameof(parseResult)); - - Prepare(); - } - /// /// Initializes a new instance of the class with a list of models to generate, /// the result of code parsing, and a models namespace. /// /// The list of models to generate. - /// The result of code parsing. /// The models namespace. - protected Builder(IList typeModels, ParseResult parseResult, string modelsNamespace) - : this(typeModels, parseResult) + protected Builder(IModelsBuilderConfig config, IList typeModels) { + _typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); + + Config = config ?? throw new ArgumentNullException(nameof(config)); + // can be null or empty, we'll manage - ModelsNamespace = modelsNamespace; + ModelsNamespace = Config.ModelsNamespace; + + // but we want it to prepare + Prepare(); } // for unit tests only protected Builder() { } + protected IModelsBuilderConfig Config { get; } + /// /// Prepares generation by processing the result of code parsing. /// - /// - /// Preparation includes figuring out from the existing code which models or properties should - /// be ignored or renamed, etc. -- anything that comes from the attributes in the existing code. - /// private void Prepare() { - var pureLive = UmbracoConfig.For.ModelsBuilder().ModelsMode == ModelsMode.PureLive; + TypeModel.MapModelTypes(_typeModels, ModelsNamespace); - // mark IsContentIgnored models that we discovered should be ignored - // then propagate / ignore children of ignored contents - // ignore content = don't generate a class for it, don't generate children - foreach (var typeModel in _typeModels.Where(x => ParseResult.IsIgnored(x.Alias))) - typeModel.IsContentIgnored = true; - foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored && x.EnumerateBaseTypes().Any(xx => xx.IsContentIgnored))) - typeModel.IsContentIgnored = true; - - // handle model renames - foreach (var typeModel in _typeModels.Where(x => ParseResult.IsContentRenamed(x.Alias))) - { - typeModel.ClrName = ParseResult.ContentClrName(typeModel.Alias); - typeModel.IsRenamed = true; - ModelsMap[typeModel.Alias] = typeModel.ClrName; - } - - // handle implement - foreach (var typeModel in _typeModels.Where(x => ParseResult.HasContentImplement(x.Alias))) - { - typeModel.HasImplement = true; - } - - // mark OmitBase models that we discovered already have a base class - foreach (var typeModel in _typeModels.Where(x => ParseResult.HasContentBase(ParseResult.ContentClrName(x.Alias) ?? x.ClrName))) - typeModel.HasBase = true; - - foreach (var typeModel in _typeModels) - { - // mark IsRemoved properties that we discovered should be ignored - // ie is marked as ignored on type, or on any parent type - var tm = typeModel; - foreach (var property in typeModel.Properties - .Where(property => tm.EnumerateBaseTypes(true).Any(x => ParseResult.IsPropertyIgnored(ParseResult.ContentClrName(x.Alias) ?? x.ClrName, property.Alias)))) - { - property.IsIgnored = true; - } - - // handle property renames - foreach (var property in typeModel.Properties) - property.ClrName = ParseResult.PropertyClrName(ParseResult.ContentClrName(typeModel.Alias) ?? typeModel.ClrName, property.Alias) ?? property.ClrName; - } + var pureLive = Config.ModelsMode == ModelsMode.PureLive; // for the first two of these two tests, // always throw, even in purelive: cannot happen unless ppl start fidling with attributes to rename @@ -158,22 +101,22 @@ namespace Umbraco.ModelsBuilder.Building // for the last one, don't throw in purelive, see comment // ensure we have no duplicates type names - foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) + foreach (var xx in _typeModels.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) throw new InvalidOperationException($"Type name \"{xx.Key}\" is used" + $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique." + " Consider using an attribute to assign different names to conflicting types."); // ensure we have no duplicates property names - foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored)) - foreach (var xx in typeModel.Properties.Where(x => !x.IsIgnored).GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) + foreach (var typeModel in _typeModels) + foreach (var xx in typeModel.Properties.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) throw new InvalidOperationException($"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\"" + $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique." + " Consider using an attribute to assign different names to conflicting properties."); // ensure content & property type don't have identical name (csharp hates it) - foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored)) + foreach (var typeModel in _typeModels) { - foreach (var xx in typeModel.Properties.Where(x => !x.IsIgnored && x.ClrName == typeModel.ClrName)) + foreach (var xx in typeModel.Properties.Where(x => x.ClrName == typeModel.ClrName)) { if (!pureLive) throw new InvalidOperationException($"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"." @@ -204,7 +147,7 @@ namespace Umbraco.ModelsBuilder.Building // collect all the (non-removed) types implemented at parent level // ie the parent content types and the mixins content types, recursively var parentImplems = new List(); - if (typeModel.BaseType != null && !typeModel.BaseType.IsContentIgnored) + if (typeModel.BaseType != null) TypeModel.CollectImplems(parentImplems, typeModel.BaseType); // interfaces we must declare we implement (initially empty) @@ -212,7 +155,6 @@ namespace Umbraco.ModelsBuilder.Building // and except those that are already declared at the parent level // in other words, DeclaringInterfaces is "local mixins" var declaring = typeModel.MixinTypes - .Where(x => !x.IsContentIgnored) .Except(parentImplems); typeModel.DeclaringInterfaces.AddRange(declaring); @@ -227,43 +169,16 @@ namespace Umbraco.ModelsBuilder.Building typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems)); } - // register using types - foreach (var usingNamespace in ParseResult.UsingNamespaces) + // ensure elements don't inherit from non-elements + foreach (var typeModel in _typeModels.Where(x => x.IsElement)) { - if (!TypesUsing.Contains(usingNamespace)) - TypesUsing.Add(usingNamespace); + if (typeModel.BaseType != null && !typeModel.BaseType.IsElement) + throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but its parent type '{typeModel.BaseType.Alias}' is not."); + + var errs = typeModel.MixinTypes.Where(x => !x.IsElement).ToList(); + if (errs.Count > 0) + throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but it is composed of {string.Join(", ", errs.Select(x => "'" + x.Alias + "'"))} which {(errs.Count == 1 ? "is" : "are")} not."); } - - // discover static mixin methods - foreach (var typeModel in _typeModels) - typeModel.StaticMixinMethods.AddRange(ParseResult.StaticMixinMethods(typeModel.ClrName)); - - // handle ctor - foreach (var typeModel in _typeModels.Where(x => ParseResult.HasCtor(x.ClrName))) - typeModel.HasCtor = true; - } - - private SemanticModel _ambiguousSymbolsModel; - private int _ambiguousSymbolsPos; - - // internal for tests - internal void PrepareAmbiguousSymbols() - { - var codeBuilder = new StringBuilder(); - foreach (var t in TypesUsing) - codeBuilder.AppendFormat("using {0};\n", t); - - codeBuilder.AppendFormat("namespace {0}\n{{ }}\n", GetModelsNamespace()); - - var compiler = new Compiler(); - SyntaxTree[] trees; - var compilation = compiler.GetCompilation("MyCompilation", new Dictionary { { "code", codeBuilder.ToString() } }, out trees); - var tree = trees[0]; - _ambiguousSymbolsModel = compilation.GetSemanticModel(tree); - - var namespaceSyntax = tree.GetRoot().DescendantNodes().OfType().First(); - //var namespaceSymbol = model.GetDeclaredSymbol(namespaceSyntax); - _ambiguousSymbolsPos = namespaceSyntax.OpenBraceToken.SpanStart; } // looking for a simple symbol eg 'Umbraco' or 'String' @@ -273,20 +188,12 @@ namespace Umbraco.ModelsBuilder.Building // - 1 symbol is found BUT not matching (implicitely ambiguous) protected bool IsAmbiguousSymbol(string symbol, string match) { - if (_ambiguousSymbolsModel == null) - PrepareAmbiguousSymbols(); - if (_ambiguousSymbolsModel == null) - throw new Exception("Could not prepare ambiguous symbols."); - var symbols = _ambiguousSymbolsModel.LookupNamespacesAndTypes(_ambiguousSymbolsPos, null, symbol); + // cannot figure out is a symbol is ambiguous without Roslyn + // so... let's say everything is ambiguous - code won't be + // pretty but it'll work - if (symbols.Length > 1) return true; - if (symbols.Length == 0) return false; // what else? - - // only 1 - ensure it matches - var found = symbols[0].ToDisplayString(); - var pos = found.IndexOf('<'); // generic? - if (pos > 0) found = found.Substring(0, pos); // strip - return found != match; // and compare + // Essentially this means that a `global::` syntax will be output for the generated models + return true; } internal string ModelsNamespaceForTests; @@ -296,25 +203,18 @@ namespace Umbraco.ModelsBuilder.Building if (ModelsNamespaceForTests != null) return ModelsNamespaceForTests; - // code attribute overrides everything - if (ParseResult.HasModelsNamespace) - return ParseResult.ModelsNamespace; - // if builder was initialized with a namespace, use that one if (!string.IsNullOrWhiteSpace(ModelsNamespace)) return ModelsNamespace; - // default - // fixme - should NOT reference config here, should make ModelsNamespace mandatory - return UmbracoConfig.For.ModelsBuilder().ModelsNamespace; + // use configured else fallback to default + return string.IsNullOrWhiteSpace(Config.ModelsNamespace) + ? ModelsBuilderConfig.DefaultModelsNamespace + : Config.ModelsNamespace; } protected string GetModelsBaseClassName(TypeModel type) { - // code attribute overrides everything - if (ParseResult.HasModelsBaseClassName) - return ParseResult.ModelsBaseClassName; - // default return type.IsElement ? "PublishedElementModel" : "PublishedContentModel"; } diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs new file mode 100644 index 0000000000..8a3bc5a5b5 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs @@ -0,0 +1,54 @@ +using System.IO; +using System.Text; +using Umbraco.ModelsBuilder.Embedded.Configuration; + +namespace Umbraco.ModelsBuilder.Embedded.Building +{ + public class ModelsGenerator + { + private readonly UmbracoServices _umbracoService; + private readonly IModelsBuilderConfig _config; + private readonly OutOfDateModelsStatus _outOfDateModels; + + public ModelsGenerator(UmbracoServices umbracoService, IModelsBuilderConfig config, OutOfDateModelsStatus outOfDateModels) + { + _umbracoService = umbracoService; + _config = config; + _outOfDateModels = outOfDateModels; + } + + internal void GenerateModels() + { + if (!Directory.Exists(_config.ModelsDirectory)) + Directory.CreateDirectory(_config.ModelsDirectory); + + foreach (var file in Directory.GetFiles(_config.ModelsDirectory, "*.generated.cs")) + File.Delete(file); + + var typeModels = _umbracoService.GetAllTypes(); + + var builder = new TextBuilder(_config, typeModels); + + foreach (var typeModel in builder.GetModelsToGenerate()) + { + var sb = new StringBuilder(); + builder.Generate(sb, typeModel); + var filename = Path.Combine(_config.ModelsDirectory, typeModel.ClrName + ".generated.cs"); + File.WriteAllText(filename, sb.ToString()); + } + + // the idea was to calculate the current hash and to add it as an extra file to the compilation, + // in order to be able to detect whether a DLL is consistent with an environment - however the + // environment *might not* contain the local partial files, and thus it could be impossible to + // calculate the hash. So... maybe that's not a good idea after all? + /* + var currentHash = HashHelper.Hash(ourFiles, typeModels); + ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; +[assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")] +"; + */ + + _outOfDateModels.Clear(); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs similarity index 89% rename from src/Umbraco.ModelsBuilder/Building/PropertyModel.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs index 1595b3f888..af5445b175 100644 --- a/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/PropertyModel.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Umbraco.ModelsBuilder.Building +namespace Umbraco.ModelsBuilder.Embedded.Building { /// /// Represents a model property. @@ -41,11 +41,6 @@ namespace Umbraco.ModelsBuilder.Building /// public string ClrTypeName; - /// - /// Gets a value indicating whether this property should be excluded from generation. - /// - public bool IsIgnored; - /// /// Gets the generation errors for the property. /// diff --git a/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs similarity index 83% rename from src/Umbraco.ModelsBuilder/Building/TextBuilder.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs index 85ccd541b7..7ef8112d11 100644 --- a/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs @@ -3,11 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Embedded.Configuration; -namespace Umbraco.ModelsBuilder.Building +namespace Umbraco.ModelsBuilder.Embedded.Building { /// /// Implements a builder that works by writing text. @@ -19,20 +17,8 @@ namespace Umbraco.ModelsBuilder.Building /// and the result of code parsing. /// /// The list of models to generate. - /// The result of code parsing. - public TextBuilder(IList typeModels, ParseResult parseResult) - : base(typeModels, parseResult) - { } - - /// - /// Initializes a new instance of the class with a list of models to generate, - /// the result of code parsing, and a models namespace. - /// - /// The list of models to generate. - /// The result of code parsing. - /// The models namespace. - public TextBuilder(IList typeModels, ParseResult parseResult, string modelsNamespace) - : base(typeModels, parseResult, modelsNamespace) + public TextBuilder(IModelsBuilderConfig config, IList typeModels) + : base(config, typeModels) { } // internal for unit tests only @@ -97,6 +83,20 @@ namespace Umbraco.ModelsBuilder.Building TextHeaderWriter.WriteHeader(sb); } + // writes an attribute that identifies code generated by a tool + // (helps reduce warnings, tools such as FxCop use it) + // see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107 + // see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute + // see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/ + // + // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class." + // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself. + // + private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) + { + sb.AppendFormat("{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n", tabs, ApiVersion.Current.Version); + } + private void WriteContentType(StringBuilder sb, TypeModel type) { string sep; @@ -104,11 +104,11 @@ namespace Umbraco.ModelsBuilder.Building if (type.IsMixin) { // write the interface declaration - sb.AppendFormat("\t// Mixin content Type {0} with alias \"{1}\"\n", type.Id, type.Alias); + sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias); if (!string.IsNullOrWhiteSpace(type.Name)) sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); - var implements = type.BaseType == null || type.BaseType.IsContentIgnored + var implements = type.BaseType == null ? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent")) : type.BaseType.ClrName; if (implements != null) @@ -126,7 +126,7 @@ namespace Umbraco.ModelsBuilder.Building // write the properties - only the local (non-ignored) ones, we're an interface var more = false; - foreach (var prop in type.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) + foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) { if (more) sb.Append("\n"); more = true; @@ -137,8 +137,6 @@ namespace Umbraco.ModelsBuilder.Building } // write the class declaration - if (type.IsRenamed) - sb.AppendFormat("\t// Content Type {0} with alias \"{1}\"\n", type.Id, type.Alias); if (!string.IsNullOrWhiteSpace(type.Name)) sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); // cannot do it now. see note in ImplementContentTypeAttribute @@ -148,7 +146,7 @@ namespace Umbraco.ModelsBuilder.Building sb.AppendFormat("\tpublic partial class {0}", type.ClrName); var inherits = type.HasBase ? null // has its own base already - : (type.BaseType == null || type.BaseType.IsContentIgnored + : (type.BaseType == null ? GetModelsBaseClassName(type) : type.BaseType.ClrName); if (inherits != null) @@ -178,22 +176,25 @@ namespace Umbraco.ModelsBuilder.Building // as 'new' since parent has its own - or maybe not - disable warning sb.Append("\t\t// helpers\n"); sb.Append("#pragma warning disable 0109 // new is redundant\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", type.Alias); var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", itemType); - sb.Append("\t\tpublic new static PublishedContentType GetModelContentType()\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.Append("\t\tpublic new static IPublishedContentType GetModelContentType()\n"); sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);\n"); - sb.AppendFormat("\t\tpublic static PublishedPropertyType GetModelPropertyType(Expression> selector)\n", + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType(Expression> selector)\n", type.ClrName); sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector);\n"); sb.Append("#pragma warning restore 0109\n\n"); // write the ctor - if (!type.HasCtor) - sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n", - type.ClrName, type.IsElement ? "Element" : "Content"); + sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n", + type.ClrName, type.IsElement ? "Element" : "Content"); // write the properties sb.Append("\t\t// properties\n"); @@ -205,10 +206,10 @@ namespace Umbraco.ModelsBuilder.Building private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) { - var staticMixinGetters = UmbracoConfig.For.ModelsBuilder().StaticMixinGetters; + var staticMixinGetters = true; // write the properties - foreach (var prop in type.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) + foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); // no need to write the parent properties since we inherit from the parent @@ -217,7 +218,7 @@ namespace Umbraco.ModelsBuilder.Building // write the mixins properties foreach (var mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) - foreach (var prop in mixinType.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) + foreach (var prop in mixinType.Properties.OrderBy(x => x.ClrName)) if (staticMixinGetters) WriteMixinProperty(sb, prop, mixinType.ClrName); else @@ -242,6 +243,7 @@ namespace Umbraco.ModelsBuilder.Building sb.Append("\t\t///\n"); } + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); sb.Append("\t\tpublic "); @@ -256,7 +258,7 @@ namespace Umbraco.ModelsBuilder.Building private static string MixinStaticGetterName(string clrName) { - return string.Format(UmbracoConfig.For.ModelsBuilder().StaticMixinGetterPattern, clrName); + return string.Format("Get{0}", clrName); } private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string mixinClrName = null) @@ -300,6 +302,7 @@ namespace Umbraco.ModelsBuilder.Building sb.Append("\t\t///\n"); } + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); if (mixinStatic) @@ -336,13 +339,14 @@ namespace Umbraco.ModelsBuilder.Building var mixinStaticGetterName = MixinStaticGetterName(property.ClrName); - if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; + //if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; sb.Append("\n"); if (!string.IsNullOrWhiteSpace(property.Name)) sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.Append("\t\tpublic static "); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0}(I{1} that) => that.Value", @@ -397,6 +401,7 @@ namespace Umbraco.ModelsBuilder.Building if (!string.IsNullOrWhiteSpace(property.Name)) sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + WriteGeneratedCodeAttribute(sb, "\t\t"); sb.Append("\t\t"); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0} {{ get; }}\n", @@ -461,7 +466,7 @@ namespace Umbraco.ModelsBuilder.Building s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); // takes care eg of "System.Int32" vs. "int" - if (TypesMap.TryGetValue(s.ToLowerInvariant(), out string typeName)) + if (TypesMap.TryGetValue(s, out string typeName)) { sb.Append(typeName); return; @@ -481,6 +486,11 @@ namespace Umbraco.ModelsBuilder.Building typeName = typeName.Substring(p + 1); typeUsing = x; } + else if (x == ModelsNamespace) // that one is used by default + { + typeName = typeName.Substring(p + 1); + typeUsing = ModelsNamespace; + } } // nested types *after* using @@ -531,24 +541,24 @@ namespace Umbraco.ModelsBuilder.Building return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); } - private static readonly IDictionary TypesMap = new Dictionary + private static readonly IDictionary TypesMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "system.int16", "short" }, - { "system.int32", "int" }, - { "system.int64", "long" }, - { "system.string", "string" }, - { "system.object", "object" }, - { "system.boolean", "bool" }, - { "system.void", "void" }, - { "system.char", "char" }, - { "system.byte", "byte" }, - { "system.uint16", "ushort" }, - { "system.uint32", "uint" }, - { "system.uint64", "ulong" }, - { "system.sbyte", "sbyte" }, - { "system.single", "float" }, - { "system.double", "double" }, - { "system.decimal", "decimal" } + { "System.Int16", "short" }, + { "System.Int32", "int" }, + { "System.Int64", "long" }, + { "System.String", "string" }, + { "System.Object", "object" }, + { "System.Boolean", "bool" }, + { "System.Void", "void" }, + { "System.Char", "char" }, + { "System.Byte", "byte" }, + { "System.UInt16", "ushort" }, + { "System.UInt32", "uint" }, + { "System.UInt64", "ulong" }, + { "System.SByte", "sbyte" }, + { "System.Single", "float" }, + { "System.Double", "double" }, + { "System.Decimal", "decimal" } }; } } diff --git a/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs similarity index 80% rename from src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs index d165f03907..0ffad1c5bc 100644 --- a/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TextHeaderWriter.cs @@ -1,9 +1,8 @@ using System.Text; -using Umbraco.ModelsBuilder.Api; -namespace Umbraco.ModelsBuilder.Building +namespace Umbraco.ModelsBuilder.Embedded.Building { - public static class TextHeaderWriter + internal static class TextHeaderWriter { /// /// Outputs an "auto-generated" header to a string builder. @@ -15,7 +14,7 @@ namespace Umbraco.ModelsBuilder.Building sb.Append("// \n"); sb.Append("// This code was generated by a tool.\n"); sb.Append("//\n"); - sb.AppendFormat("// Umbraco.ModelsBuilder v{0}\n", ApiVersion.Current.Version); + sb.AppendFormat("// Umbraco.ModelsBuilder.Embedded v{0}\n", ApiVersion.Current.Version); sb.Append("//\n"); sb.Append("// Changes to this file will be lost if the code is regenerated.\n"); sb.Append("// \n"); diff --git a/src/Umbraco.ModelsBuilder/Building/TypeModel.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs similarity index 85% rename from src/Umbraco.ModelsBuilder/Building/TypeModel.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs index 5ada8e881c..95356cf3ff 100644 --- a/src/Umbraco.ModelsBuilder/Building/TypeModel.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModel.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Models.PublishedContent; -namespace Umbraco.ModelsBuilder.Building +namespace Umbraco.ModelsBuilder.Embedded.Building { /// /// Represents a model. @@ -76,10 +77,10 @@ namespace Umbraco.ModelsBuilder.Building /// public readonly List ImplementingInterfaces = new List(); - /// - /// Gets the list of existing static mixin method candidates. - /// - public readonly List StaticMixinMethods = new List(); + ///// + ///// Gets the list of existing static mixin method candidates. + ///// + //public readonly List StaticMixinMethods = new List(); //TODO: Do we need this? it isn't used /// /// Gets a value indicating whether this model has a base class. @@ -88,16 +89,6 @@ namespace Umbraco.ModelsBuilder.Building /// or because the existing user's code declares a base class for this model. public bool HasBase; - /// - /// Gets a value indicating whether this model has been renamed. - /// - public bool IsRenamed; - - /// - /// Gets a value indicating whether this model has [ImplementContentType] already. - /// - public bool HasImplement; - /// /// Gets a value indicating whether this model is used as a mixin by another model. /// @@ -108,16 +99,6 @@ namespace Umbraco.ModelsBuilder.Building /// public bool IsParent; - /// - /// Gets a value indicating whether this model should be excluded from generation. - /// - public bool IsContentIgnored; - - /// - /// Gets a value indicating whether the ctor is already defined in a partial. - /// - public bool HasCtor; - /// /// Gets a value indicating whether the type is an element. /// @@ -181,11 +162,11 @@ namespace Umbraco.ModelsBuilder.Building /// Includes the specified type. internal static void CollectImplems(ICollection types, TypeModel type) { - if (!type.IsContentIgnored && types.Contains(type) == false) + if (types.Contains(type) == false) types.Add(type); - if (type.BaseType != null && !type.BaseType.IsContentIgnored) + if (type.BaseType != null) CollectImplems(types, type.BaseType); - foreach (var mixin in type.MixinTypes.Where(x => !x.IsContentIgnored)) + foreach (var mixin in type.MixinTypes) CollectImplems(types, mixin); } @@ -204,5 +185,21 @@ namespace Umbraco.ModelsBuilder.Building typeModel = typeModel.BaseType; } } + + /// + /// Maps ModelType. + /// + public static void MapModelTypes(IList typeModels, string ns) + { + var hasNs = !string.IsNullOrWhiteSpace(ns); + var map = typeModels.ToDictionary(x => x.Alias, x => hasNs ? (ns + "." + x.ClrName) : x.ClrName); + foreach (var typeModel in typeModels) + { + foreach (var propertyModel in typeModel.Properties) + { + propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); + } + } + } } } diff --git a/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs similarity index 81% rename from src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs rename to src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs index c530cbbd6b..2f14bec875 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs @@ -1,18 +1,14 @@ using System.Collections.Generic; using System.Linq; -using Umbraco.ModelsBuilder.Building; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded.Building { - class HashHelper + internal class TypeModelHasher { - public static string Hash(IDictionary ourFiles, IEnumerable typeModels) + public static string Hash(IEnumerable typeModels) { var hash = new HashCombiner(); - foreach (var kvp in ourFiles) - hash.Add(kvp.Key + "::" + kvp.Value); - // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference @@ -39,6 +35,9 @@ namespace Umbraco.ModelsBuilder.Umbraco } } + // Include the MB version in the hash so that if the MB version changes, models are rebuilt + hash.Add(ApiVersion.Current.Version.ToString()); + return hash.GetCombinedHashCode(); } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs new file mode 100644 index 0000000000..c599785711 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs @@ -0,0 +1,30 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.ModelsBuilder.Embedded.BackOffice; +using Umbraco.Web.Features; + +namespace Umbraco.ModelsBuilder.Embedded.Compose +{ + /// + /// Special component used for when MB is disabled with the legacy MB is detected + /// + internal class DisabledModelsBuilderComponent : IComponent + { + private readonly UmbracoFeatures _features; + + public DisabledModelsBuilderComponent(UmbracoFeatures features) + { + _features = features; + } + + public void Initialize() + { + //disable the embedded dashboard controller + _features.Disabled.Controllers.Add(); + } + + public void Terminate() + { + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs new file mode 100644 index 0000000000..0e41c9ac62 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; +using Umbraco.Core.Composing; +using Umbraco.Core.IO; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; +using Umbraco.ModelsBuilder.Embedded.BackOffice; +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web; +using Umbraco.Web.JavaScript; +using Umbraco.Web.Mvc; + +namespace Umbraco.ModelsBuilder.Embedded.Compose +{ + + internal class ModelsBuilderComponent : IComponent + { + + private readonly IModelsBuilderConfig _config; + private readonly LiveModelsProvider _liveModelsProvider; + private readonly OutOfDateModelsStatus _outOfDateModels; + + public ModelsBuilderComponent(IModelsBuilderConfig config, LiveModelsProvider liveModelsProvider, OutOfDateModelsStatus outOfDateModels) + { + _config = config; + _liveModelsProvider = liveModelsProvider; + _outOfDateModels = outOfDateModels; + } + + public void Initialize() + { + // always setup the dashboard + // note: UmbracoApiController instances are automatically registered + InstallServerVars(); + + ContentModelBinder.ModelBindingException += ContentModelBinder_ModelBindingException; + + if (_config.Enable) + FileService.SavingTemplate += FileService_SavingTemplate; + + if (_config.ModelsMode.IsLiveNotPure()) + _liveModelsProvider.Install(); + + if (_config.FlagOutOfDateModels) + _outOfDateModels.Install(); + } + + public void Terminate() + { } + + private void InstallServerVars() + { + // register our url - for the backoffice api + ServerVariablesParser.Parsing += (sender, serverVars) => + { + if (!serverVars.ContainsKey("umbracoUrls")) + throw new ArgumentException("Missing umbracoUrls."); + var umbracoUrlsObject = serverVars["umbracoUrls"]; + if (umbracoUrlsObject == null) + throw new ArgumentException("Null umbracoUrls"); + if (!(umbracoUrlsObject is Dictionary umbracoUrls)) + throw new ArgumentException("Invalid umbracoUrls"); + + if (!serverVars.ContainsKey("umbracoPlugins")) + throw new ArgumentException("Missing umbracoPlugins."); + if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) + throw new ArgumentException("Invalid umbracoPlugins"); + + if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null"); + var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); + + umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); + umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); + }; + } + + private Dictionary GetModelsBuilderSettings() + { + var settings = new Dictionary + { + {"enabled", _config.Enable} + }; + + return settings; + } + + /// + /// Used to check if a template is being created based on a document type, in this case we need to + /// ensure the template markup is correct based on the model name of the document type + /// + /// + /// + private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs e) + { + // don't do anything if the factory is not enabled + // because, no factory = no models (even if generation is enabled) + if (!_config.EnableFactory) return; + + // don't do anything if this special key is not found + if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return; + + // ensure we have the content type alias + if (!e.AdditionalData.ContainsKey("ContentTypeAlias")) + throw new InvalidOperationException("The additionalData key: ContentTypeAlias was not found"); + + foreach (var template in e.SavedEntities) + // if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key + // is found, then it means a new template is being created based on the creation of a document type + if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content)) + { + // ensure is safe and always pascal cased, per razor standard + // + this is how we get the default model name in Umbraco.ModelsBuilder.Umbraco.Application + var alias = e.AdditionalData["ContentTypeAlias"].ToString(); + var name = template.Name; // will be the name of the content type since we are creating + var className = UmbracoServices.GetClrName(name, alias); + + var modelNamespace = _config.ModelsNamespace; + + // we do not support configuring this at the moment, so just let Umbraco use its default value + //var modelNamespaceAlias = ...; + + var markup = ViewHelper.GetDefaultFileContent( + modelClassName: className, + modelNamespace: modelNamespace/*, + modelNamespaceAlias: modelNamespaceAlias*/); + + //set the template content to the new markup + template.Content = markup; + } + } + + private void ContentModelBinder_ModelBindingException(object sender, ContentModelBinder.ModelBindingArgs args) + { + var sourceAttr = args.SourceType.Assembly.GetCustomAttribute(); + var modelAttr = args.ModelType.Assembly.GetCustomAttribute(); + + // if source or model is not a ModelsBuider type... + if (sourceAttr == null || modelAttr == null) + { + // if neither are ModelsBuilder types, give up entirely + if (sourceAttr == null && modelAttr == null) + return; + + // else report, but better not restart (loops?) + args.Message.Append(" The "); + args.Message.Append(sourceAttr == null ? "view model" : "source"); + args.Message.Append(" is a ModelsBuilder type, but the "); + args.Message.Append(sourceAttr != null ? "view model" : "source"); + args.Message.Append(" is not. The application is in an unstable state and should be restarted."); + return; + } + + // both are ModelsBuilder types + var pureSource = sourceAttr.PureLive; + var pureModel = modelAttr.PureLive; + + if (sourceAttr.PureLive || modelAttr.PureLive) + if (pureSource == false || pureModel == false) + { + // only one is pure - report, but better not restart (loops?) + args.Message.Append(pureSource + ? " The content model is PureLive, but the view model is not." + : " The view model is PureLive, but the content model is not."); + args.Message.Append(" The application is in an unstable state and should be restarted."); + } + else + { + // both are pure - report, and if different versions, restart + // if same version... makes no sense... and better not restart (loops?) + var sourceVersion = args.SourceType.Assembly.GetName().Version; + var modelVersion = args.ModelType.Assembly.GetName().Version; + args.Message.Append(" Both view and content models are PureLive, with "); + args.Message.Append(sourceVersion == modelVersion + ? "same version. The application is in an unstable state and should be restarted." + : "different versions. The application is in an unstable state and is going to be restarted."); + args.Restart = sourceVersion != modelVersion; + } + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs new file mode 100644 index 0000000000..45c4de5d2a --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs @@ -0,0 +1,103 @@ +using System.Linq; +using System.Reflection; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Composing; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web; +using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.Features; + +namespace Umbraco.ModelsBuilder.Embedded.Compose +{ + + + [ComposeBefore(typeof(NuCacheComposer))] + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public sealed class ModelsBuilderComposer : ICoreComposer + { + public void Compose(Composition composition) + { + var isLegacyModelsBuilderInstalled = IsLegacyModelsBuilderInstalled(); + + + composition.Configs.Add(() => new ModelsBuilderConfig()); + + if (isLegacyModelsBuilderInstalled) + { + ComposeForLegacyModelsBuilder(composition); + return; + } + + composition.Components().Append(); + composition.Register(Lifetime.Singleton); + + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + + if (composition.Configs.ModelsBuilder().ModelsMode == ModelsMode.PureLive) + ComposeForLiveModels(composition); + else if (composition.Configs.ModelsBuilder().EnableFactory) + ComposeForDefaultModelsFactory(composition); + } + + private static bool IsLegacyModelsBuilderInstalled() + { + Assembly legacyMbAssembly = null; + try + { + legacyMbAssembly = Assembly.Load("Umbraco.ModelsBuilder"); + } + catch (System.Exception) + { + //swallow exception, DLL must not be there + } + + return legacyMbAssembly != null; + } + + private void ComposeForLegacyModelsBuilder(Composition composition) + { + composition.Logger.Info("ModelsBuilder.Embedded is disabled, the external ModelsBuilder was detected."); + composition.Components().Append(); + composition.Dashboards().Remove(); + } + + private void ComposeForDefaultModelsFactory(Composition composition) + { + composition.RegisterUnique(factory => + { + var typeLoader = factory.GetInstance(); + var types = typeLoader + .GetTypes() // element models + .Concat(typeLoader.GetTypes()); // content models + return new PublishedModelFactory(types); + }); + } + + private void ComposeForLiveModels(Composition composition) + { + composition.RegisterUnique(); + + // the following would add @using statement in every view so user's don't + // have to do it - however, then noone understands where the @using statement + // comes from, and it cannot be avoided / removed --- DISABLED + // + /* + // no need for @using in views + // note: + // we are NOT using the in-code attribute here, config is required + // because that would require parsing the code... and what if it changes? + // we can AddGlobalImport not sure we can remove one anyways + var modelsNamespace = Configuration.Config.ModelsNamespace; + if (string.IsNullOrWhiteSpace(modelsNamespace)) + modelsNamespace = Configuration.Config.DefaultModelsNamespace; + System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace); + */ + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs new file mode 100644 index 0000000000..a86669b135 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs @@ -0,0 +1,28 @@ +using System.Web; +using System.Web.Compilation; +using Umbraco.ModelsBuilder.Embedded.Compose; + +[assembly: PreApplicationStartMethod(typeof(ModelsBuilderInitializer), "Initialize")] + +namespace Umbraco.ModelsBuilder.Embedded.Compose +{ + public static class ModelsBuilderInitializer + { + public static void Initialize() + { + // for some reason, netstandard is missing from BuildManager.ReferencedAssemblies and yet, is part of + // the references that CSharpCompiler receives - in some cases eg when building views - but not when + // using BuildManager to build the PureLive models - where is it coming from? cannot figure it out + + // so... cheating here + + // this is equivalent to adding + // + // to web.config system.web/compilation/assemblies + + var netStandard = ReferencedAssemblies.GetNetStandardAssembly(); + if (netStandard != null) + BuildManager.AddReferencedAssembly(netStandard); + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/ConfigsExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/ConfigsExtensions.cs new file mode 100644 index 0000000000..d634547a49 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/ConfigsExtensions.cs @@ -0,0 +1,20 @@ +using Umbraco.Core.Configuration; +using Umbraco.ModelsBuilder.Embedded.Configuration; + +namespace Umbraco.ModelsBuilder.Embedded +{ + /// + /// Provides extension methods for the class. + /// + public static class ConfigsExtensions + { + /// + /// Gets the models builder configuration. + /// + /// Getting the models builder configuration freezes its state, + /// and any attempt at modifying the configuration using the Setup method + /// will be ignored. + public static IModelsBuilderConfig ModelsBuilder(this Configs configs) + => configs.GetConfig(); + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Configuration/IModelsBuilderConfig.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/IModelsBuilderConfig.cs new file mode 100644 index 0000000000..7e96aec60e --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/IModelsBuilderConfig.cs @@ -0,0 +1,15 @@ +namespace Umbraco.ModelsBuilder.Embedded.Configuration +{ + public interface IModelsBuilderConfig + { + bool Enable { get; } + bool AcceptUnsafeModelsDirectory { get; } + int DebugLevel { get; } + bool EnableFactory { get; } + bool FlagOutOfDateModels { get; } + bool IsDebug { get; } + string ModelsDirectory { get; } + ModelsMode ModelsMode { get; } + string ModelsNamespace { get; } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs new file mode 100644 index 0000000000..179fcecfcb --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs @@ -0,0 +1,226 @@ +using System; +using System.Configuration; +using System.IO; +using System.Threading; +using System.Web.Configuration; +using Umbraco.Core; +using Umbraco.Core.IO; + +namespace Umbraco.ModelsBuilder.Embedded.Configuration +{ + /// + /// Represents the models builder configuration. + /// + public class ModelsBuilderConfig : IModelsBuilderConfig + { + private const string Prefix = "Umbraco.ModelsBuilder."; + private object _modelsModelLock; + private bool _modelsModelConfigured; + private ModelsMode _modelsMode; + private object _flagOutOfDateModelsLock; + private bool _flagOutOfDateModelsConfigured; + private bool _flagOutOfDateModels; + public const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels"; + public const string DefaultModelsDirectory = "~/App_Data/Models"; + + /// + /// Initializes a new instance of the class. + /// + public ModelsBuilderConfig() + { + // giant kill switch, default: false + // must be explicitely set to true for anything else to happen + Enable = ConfigurationManager.AppSettings[Prefix + "Enable"] == "true"; + + // ensure defaults are initialized for tests + ModelsNamespace = DefaultModelsNamespace; + ModelsDirectory = IOHelper.MapPath(DefaultModelsDirectory); + DebugLevel = 0; + + // stop here, everything is false + if (!Enable) return; + + // default: false + AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[Prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true"); + + // default: true + EnableFactory = !ConfigurationManager.AppSettings[Prefix + "EnableFactory"].InvariantEquals("false"); + + // default: initialized above with DefaultModelsNamespace const + var value = ConfigurationManager.AppSettings[Prefix + "ModelsNamespace"]; + if (!string.IsNullOrWhiteSpace(value)) + ModelsNamespace = value; + + // default: initialized above with DefaultModelsDirectory const + value = ConfigurationManager.AppSettings[Prefix + "ModelsDirectory"]; + if (!string.IsNullOrWhiteSpace(value)) + { + var root = IOHelper.MapPath("~/"); + if (root == null) + throw new ConfigurationErrorsException("Could not determine root directory."); + + // GetModelsDirectory will ensure that the path is safe + ModelsDirectory = GetModelsDirectory(root, value, AcceptUnsafeModelsDirectory); + } + + // default: 0 + value = ConfigurationManager.AppSettings[Prefix + "DebugLevel"]; + if (!string.IsNullOrWhiteSpace(value)) + { + if (!int.TryParse(value, out var debugLevel)) + throw new ConfigurationErrorsException($"Invalid debug level \"{value}\"."); + DebugLevel = debugLevel; + } + + } + + /// + /// Initializes a new instance of the class. + /// + public ModelsBuilderConfig( + bool enable = false, + ModelsMode modelsMode = ModelsMode.Nothing, + string modelsNamespace = null, + bool enableFactory = true, + bool flagOutOfDateModels = true, + string modelsDirectory = null, + bool acceptUnsafeModelsDirectory = false, + int debugLevel = 0) + { + Enable = enable; + _modelsMode = modelsMode; + + ModelsNamespace = string.IsNullOrWhiteSpace(modelsNamespace) ? DefaultModelsNamespace : modelsNamespace; + EnableFactory = enableFactory; + _flagOutOfDateModels = flagOutOfDateModels; + ModelsDirectory = string.IsNullOrWhiteSpace(modelsDirectory) ? DefaultModelsDirectory : modelsDirectory; + AcceptUnsafeModelsDirectory = acceptUnsafeModelsDirectory; + DebugLevel = debugLevel; + } + + // internal for tests + internal static string GetModelsDirectory(string root, string config, bool acceptUnsafe) + { + // making sure it is safe, ie under the website root, + // unless AcceptUnsafeModelsDirectory and then everything is OK. + + if (!Path.IsPathRooted(root)) + throw new ConfigurationErrorsException($"Root is not rooted \"{root}\"."); + + if (config.StartsWith("~/")) + { + var dir = Path.Combine(root, config.TrimStart("~/")); + + // sanitize - GetFullPath will take care of any relative + // segments in path, eg '../../foo.tmp' - it may throw a SecurityException + // if the combined path reaches illegal parts of the filesystem + dir = Path.GetFullPath(dir); + root = Path.GetFullPath(root); + + if (!dir.StartsWith(root) && !acceptUnsafe) + throw new ConfigurationErrorsException($"Invalid models directory \"{config}\"."); + + return dir; + } + + if (acceptUnsafe) + return Path.GetFullPath(config); + + throw new ConfigurationErrorsException($"Invalid models directory \"{config}\"."); + } + + /// + /// Gets a value indicating whether the whole models experience is enabled. + /// + /// + /// If this is false then absolutely nothing happens. + /// Default value is false which means that unless we have this setting, nothing happens. + /// + public bool Enable { get; } + + /// + /// Gets the models mode. + /// + public ModelsMode ModelsMode => + LazyInitializer.EnsureInitialized(ref _modelsMode, ref _modelsModelConfigured, ref _modelsModelLock, () => + { + // mode + var modelsMode = ConfigurationManager.AppSettings[Prefix + "ModelsMode"]; + if (string.IsNullOrWhiteSpace(modelsMode)) return ModelsMode.Nothing; //default + switch (modelsMode) + { + case nameof(ModelsMode.Nothing): + return ModelsMode.Nothing; + case nameof(ModelsMode.PureLive): + return ModelsMode.PureLive; + case nameof(ModelsMode.AppData): + return ModelsMode.AppData; + case nameof(ModelsMode.LiveAppData): + return ModelsMode.LiveAppData; + default: + throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." + " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode)))); + } + }); + + /// + /// Gets a value indicating whether system.web/compilation/@debug is true. + /// + public bool IsDebug + { + get + { + var section = (CompilationSection)ConfigurationManager.GetSection("system.web/compilation"); + return section != null && section.Debug; + } + } + + /// + /// Gets the models namespace. + /// + /// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied. + public string ModelsNamespace { get; } + + /// + /// Gets a value indicating whether we should enable the models factory. + /// + /// Default value is true because no factory is enabled by default in Umbraco. + public bool EnableFactory { get; } + + /// + /// Gets a value indicating whether we should flag out-of-date models. + /// + /// Models become out-of-date when data types or content types are updated. When this + /// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are + /// generated through the dashboard, the files is cleared. Default value is false. + public bool FlagOutOfDateModels + => LazyInitializer.EnsureInitialized(ref _flagOutOfDateModels, ref _flagOutOfDateModelsConfigured, ref _flagOutOfDateModelsLock, () => + { + var flagOutOfDateModels = !ConfigurationManager.AppSettings[Prefix + "FlagOutOfDateModels"].InvariantEquals("false"); + if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive()) + { + flagOutOfDateModels = false; + } + + return flagOutOfDateModels; + }); + + /// + /// Gets the models directory. + /// + /// Default is ~/App_Data/Models but that can be changed. + public string ModelsDirectory { get; } + + /// + /// Gets a value indicating whether to accept an unsafe value for ModelsDirectory. + /// + /// An unsafe value is an absolute path, or a relative path pointing outside + /// of the website root. + public bool AcceptUnsafeModelsDirectory { get; } + + /// + /// Gets a value indicating the debug log level. + /// + /// 0 means minimal (safe on live site), anything else means more and more details (maybe not safe). + public int DebugLevel { get; } + } +} diff --git a/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsMode.cs similarity index 56% rename from src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs rename to src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsMode.cs index e04c4dee90..e0286fdab1 100644 --- a/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsMode.cs @@ -1,4 +1,4 @@ -namespace Umbraco.ModelsBuilder.Configuration +namespace Umbraco.ModelsBuilder.Embedded.Configuration { /// /// Defines the models generation modes. @@ -8,7 +8,7 @@ /// /// Do not generate models. /// - Nothing = 0, // default value + Nothing = 0, // default value /// /// Generate models in memory. @@ -31,22 +31,6 @@ /// /// Generation can be triggered from the dashboard. The app does not restart. /// Models are not compiled and thus are not available to the project. - LiveAppData, - - /// - /// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts). - /// When: generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does restart. Models - /// are available to the entire project. - Dll, - - /// - /// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts). - /// When: a content type change occurs, or generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does restart. Models - /// are available to the entire project. - LiveDll + LiveAppData } } diff --git a/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsModeExtensions.cs similarity index 61% rename from src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs rename to src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsModeExtensions.cs index be609c0548..be638729ea 100644 --- a/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsModeExtensions.cs @@ -1,4 +1,4 @@ -namespace Umbraco.ModelsBuilder.Configuration +namespace Umbraco.ModelsBuilder.Embedded.Configuration { /// /// Provides extensions for the enumeration. @@ -12,7 +12,6 @@ { return modelsMode == ModelsMode.PureLive - || modelsMode == ModelsMode.LiveDll || modelsMode == ModelsMode.LiveAppData; } @@ -22,18 +21,7 @@ public static bool IsLiveNotPure(this ModelsMode modelsMode) { return - modelsMode == ModelsMode.LiveDll - || modelsMode == ModelsMode.LiveAppData; - } - - /// - /// Gets a value indicating whether the mode is [Live]Dll. - /// - public static bool IsAnyDll(this ModelsMode modelsMode) - { - return - modelsMode == ModelsMode.Dll - || modelsMode == ModelsMode.LiveDll; + modelsMode == ModelsMode.LiveAppData; } /// @@ -42,10 +30,8 @@ public static bool SupportsExplicitGeneration(this ModelsMode modelsMode) { return - modelsMode == ModelsMode.Dll - || modelsMode == ModelsMode.LiveDll - || modelsMode == ModelsMode.AppData + modelsMode == ModelsMode.AppData || modelsMode == ModelsMode.LiveAppData; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs b/src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs similarity index 76% rename from src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs rename to src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs index e11662eb24..1c1fca6f73 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs @@ -1,17 +1,17 @@ using System; using System.Globalization; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { // because, of course, it's internal in Umbraco // see also System.Web.Util.HashCodeCombiner - class HashCombiner + internal class HashCombiner { private long _combinedHash = 5381L; public void Add(int i) { - _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; + _combinedHash = (_combinedHash << 5) + _combinedHash ^ i; } public void Add(object o) @@ -27,7 +27,7 @@ namespace Umbraco.ModelsBuilder.Umbraco public void Add(string s) { if (s == null) return; - Add((StringComparer.InvariantCulture).GetHashCode(s)); + Add(StringComparer.InvariantCulture.GetHashCode(s)); } public string GetCombinedHashCode() diff --git a/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs similarity index 69% rename from src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs rename to src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs index c5d8f8cad4..b7b2695a08 100644 --- a/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs @@ -1,17 +1,13 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace Umbraco.ModelsBuilder +namespace Umbraco.ModelsBuilder.Embedded { /// /// Indicates that a property implements a given property alias. /// /// And therefore it should not be generated. [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - public sealed class ImplementPropertyTypeAttribute : Attribute + public class ImplementPropertyTypeAttribute : Attribute { public ImplementPropertyTypeAttribute(string alias) { diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs new file mode 100644 index 0000000000..333181f27c --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs @@ -0,0 +1,110 @@ +using System; +using System.Threading; +using System.Web.Hosting; +using Umbraco.Core.Logging; +using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.ModelsBuilder.Embedded.Configuration; +using Umbraco.Web.Cache; + +namespace Umbraco.ModelsBuilder.Embedded +{ + // supports LiveAppData - but not PureLive + public sealed class LiveModelsProvider + { + private static Mutex _mutex; + private static int _req; + private readonly ILogger _logger; + private readonly IModelsBuilderConfig _config; + private readonly ModelsGenerator _modelGenerator; + private readonly ModelsGenerationError _mbErrors; + + // we do not manage pure live here + internal bool IsEnabled => _config.ModelsMode.IsLiveNotPure(); + + public LiveModelsProvider(ILogger logger, IModelsBuilderConfig config, ModelsGenerator modelGenerator, ModelsGenerationError mbErrors) + { + _logger = logger; + _config = config ?? throw new ArgumentNullException(nameof(config)); + _modelGenerator = modelGenerator; + _mbErrors = mbErrors; + } + + internal void Install() + { + // just be sure + if (!IsEnabled) + return; + + // initialize mutex + // ApplicationId will look like "/LM/W3SVC/1/Root/AppName" + // name is system-wide and must be less than 260 chars + var name = HostingEnvironment.ApplicationID + "/UmbracoLiveModelsProvider"; + + _mutex = new Mutex(false, name); //TODO: Replace this with MainDom? Seems we now have 2x implementations of almost the same thing + + // anything changes, and we want to re-generate models. + ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; + DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; + + // at the end of a request since we're restarting the pool + // NOTE - this does NOT trigger - see module below + //umbracoApplication.EndRequest += GenerateModelsIfRequested; + } + + // NOTE + // Using HttpContext Items fails because CacheUpdated triggers within + // some asynchronous backend task where we seem to have no HttpContext. + + // So we use a static (non request-bound) var to register that models + // need to be generated. Could be by another request. Anyway. We could + // have collisions but... you know the risk. + + private void RequestModelsGeneration(object sender, EventArgs args) + { + //HttpContext.Current.Items[this] = true; + _logger.Debug("Requested to generate models."); + Interlocked.Exchange(ref _req, 1); + } + + public void GenerateModelsIfRequested(object sender, EventArgs args) + { + //if (HttpContext.Current.Items[this] == null) return; + if (Interlocked.Exchange(ref _req, 0) == 0) return; + + // cannot use a simple lock here because we don't want another AppDomain + // to generate while we do... and there could be 2 AppDomains if the app restarts. + + try + { + _logger.Debug("Generate models..."); + const int timeout = 2 * 60 * 1000; // 2 mins + _mutex.WaitOne(timeout); // wait until it is safe, and acquire + _logger.Info("Generate models now."); + GenerateModels(); + _mbErrors.Clear(); + _logger.Info("Generated."); + } + catch (TimeoutException) + { + _logger.Warn("Timeout, models were NOT generated."); + } + catch (Exception e) + { + _mbErrors.Report("Failed to build Live models.", e); + _logger.Error("Failed to generate models.", e); + } + finally + { + _mutex.ReleaseMutex(); // release + } + } + + private void GenerateModels() + { + // EnableDllModels will recycle the app domain - but this request will end properly + _modelGenerator.GenerateModels(); + } + + + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs new file mode 100644 index 0000000000..678ff241b0 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs @@ -0,0 +1,47 @@ +using System; +using System.Web; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.ModelsBuilder.Embedded; + +// will install only if configuration says it needs to be installed +[assembly: PreApplicationStartMethod(typeof(LiveModelsProviderModule), "Install")] + +namespace Umbraco.ModelsBuilder.Embedded +{ + // have to do this because it's the only way to subscribe to EndRequest, + // module is installed by assembly attribute at the top of this file + public class LiveModelsProviderModule : IHttpModule + { + private static LiveModelsProvider _liveModelsProvider; + + public void Init(HttpApplication app) + { + app.EndRequest += App_EndRequest; + } + + private void App_EndRequest(object sender, EventArgs e) + { + if (((HttpApplication)sender).Request.Url.IsClientSideRequest()) + return; + + // here we're using "Current." since we're in a module, it is possible in a round about way to inject into a module but for now we'll just use Current + if (_liveModelsProvider == null) + _liveModelsProvider = Current.Factory.TryGetInstance(); // will be null in upgrade mode or if embedded MB is disabled + + if (_liveModelsProvider?.IsEnabled ?? false) + _liveModelsProvider.GenerateModelsIfRequested(sender, e); + } + + public void Dispose() + { + // nothing + } + + public static void Install() + { + // always - don't read config in PreApplicationStartMethod + HttpApplication.RegisterModule(typeof(LiveModelsProviderModule)); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs similarity index 95% rename from src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs rename to src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs index ed956852f8..7570c0b5b2 100644 --- a/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderAssemblyAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.ModelsBuilder +namespace Umbraco.ModelsBuilder.Embedded { /// /// Indicates that an Assembly is a Models Builder assembly. diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs new file mode 100644 index 0000000000..b8b1945f32 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs @@ -0,0 +1,19 @@ +using System; +using Umbraco.Core.Composing; +using Umbraco.Core.Dashboards; + +namespace Umbraco.ModelsBuilder.Embedded +{ + [Weight(40)] + public class ModelsBuilderDashboard : IDashboard + { + public string Alias => "settingsModelsBuilder"; + + public string[] Sections => new [] { "settings" }; + + public string View => "views/dashboard/settings/modelsbuildermanagement.html"; + + public IAccessRule[] AccessRules => Array.Empty(); + } + +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs similarity index 68% rename from src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs rename to src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs index 7102190b5e..a692f633a5 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs @@ -1,14 +1,20 @@ using System; using System.IO; using System.Text; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Embedded.Configuration; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { - internal static class ModelsGenerationError + public sealed class ModelsGenerationError { - public static void Clear() + private readonly IModelsBuilderConfig _config; + + public ModelsGenerationError(IModelsBuilderConfig config) + { + _config = config; + } + + public void Clear() { var errFile = GetErrFile(); if (errFile == null) return; @@ -17,7 +23,7 @@ namespace Umbraco.ModelsBuilder.Umbraco File.Delete(errFile); } - public static void Report(string message, Exception e) + public void Report(string message, Exception e) { var errFile = GetErrFile(); if (errFile == null) return; @@ -33,7 +39,7 @@ namespace Umbraco.ModelsBuilder.Umbraco File.WriteAllText(errFile, sb.ToString()); } - public static string GetLastError() + public string GetLastError() { var errFile = GetErrFile(); if (errFile == null) return null; @@ -48,9 +54,9 @@ namespace Umbraco.ModelsBuilder.Umbraco } } - private static string GetErrFile() + private string GetErrFile() { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; + var modelsDirectory = _config.ModelsDirectory; if (!Directory.Exists(modelsDirectory)) return null; diff --git a/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs b/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs similarity index 55% rename from src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs rename to src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs index a047f21edb..5425c31c77 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs @@ -1,55 +1,58 @@ -using System; -using System.IO; -using System.Web.Hosting; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; +using System.IO; +using Umbraco.ModelsBuilder.Embedded.Configuration; using Umbraco.Web.Cache; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { public sealed class OutOfDateModelsStatus { - internal static void Install() + private readonly IModelsBuilderConfig _config; + + public OutOfDateModelsStatus(IModelsBuilderConfig config) + { + _config = config; + } + + internal void Install() { // just be sure - if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) + if (_config.FlagOutOfDateModels == false) return; ContentTypeCacheRefresher.CacheUpdated += (sender, args) => Write(); DataTypeCacheRefresher.CacheUpdated += (sender, args) => Write(); } - private static string GetFlagPath() + private string GetFlagPath() { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; + var modelsDirectory = _config.ModelsDirectory; if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); return Path.Combine(modelsDirectory, "ood.flag"); } - private static void Write() + private void Write() { var path = GetFlagPath(); if (path == null || File.Exists(path)) return; File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n"); } - public static void Clear() + public void Clear() { - if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return; + if (_config.FlagOutOfDateModels == false) return; var path = GetFlagPath(); if (path == null || !File.Exists(path)) return; File.Delete(path); } - public static bool IsEnabled => UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels; + public bool IsEnabled => _config.FlagOutOfDateModels; - public static bool IsOutOfDate + public bool IsOutOfDate { get { - if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return false; + if (_config.FlagOutOfDateModels == false) return false; var path = GetFlagPath(); return path != null && File.Exists(path); } diff --git a/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs b/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..5fa17d3c77 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Umbraco.ModelsBuilder.Embedded")] +[assembly: AssemblyDescription("Umbraco ModelsBuilder")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Umbraco CMS")] + +[assembly: ComVisible(false)] +[assembly: Guid("52ac0ba8-a60e-4e36-897b-e8b97a54ed1c")] + +[assembly: InternalsVisibleTo("Umbraco.Tests")] diff --git a/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs similarity index 74% rename from src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs rename to src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs index f3320b5dfb..8a0a688942 100644 --- a/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs @@ -2,9 +2,12 @@ using System.Linq.Expressions; using System.Reflection; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web; +using Umbraco.ModelsBuilder; +using Umbraco.ModelsBuilder.Embedded; -namespace Umbraco.ModelsBuilder +// same namespace as original Umbraco.Web PublishedElementExtensions +// ReSharper disable once CheckNamespace +namespace Umbraco.Web { /// /// Provides extension methods to models. @@ -14,13 +17,14 @@ namespace Umbraco.ModelsBuilder /// /// Gets the value of a property. /// - public static TValue Value(this TModel model, Expression> property, string culture = ".", string segment = ".") + public static TValue ValueFor(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) where TModel : IPublishedElement { var alias = GetAlias(model, property); - return model.Value(alias, culture, segment); + return model.Value(alias, culture, segment, fallback, defaultValue); } + // fixme that one should be public so ppl can use it private static string GetAlias(TModel model, Expression> property) { if (property.NodeType != ExpressionType.Lambda) @@ -41,7 +45,7 @@ namespace Umbraco.ModelsBuilder var attribute = member.GetCustomAttribute(); if (attribute == null) throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); - + return attribute.Alias; } } diff --git a/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs similarity index 77% rename from src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs rename to src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs index c70e8a3b65..8a6ed83ce9 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs @@ -1,11 +1,17 @@ using System; using System.Linq; using System.Linq.Expressions; -using Umbraco.Web.Composing; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Composing; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { + /// + /// This is called from within the generated model classes + /// + /// + /// DO NOT REMOVE - although there are not code references this is used directly by the generated models. + /// public static class PublishedModelUtility { // looks safer but probably useless... ppl should not call these methods directly @@ -24,7 +30,7 @@ namespace Umbraco.ModelsBuilder.Umbraco // // etc... //} - public static PublishedContentType GetModelContentType(PublishedItemType itemType, string alias) + public static IPublishedContentType GetModelContentType(PublishedItemType itemType, string alias) { var facade = Current.UmbracoContext.PublishedSnapshot; // fixme inject! switch (itemType) @@ -40,8 +46,8 @@ namespace Umbraco.ModelsBuilder.Umbraco } } - public static PublishedPropertyType GetModelPropertyType(PublishedContentType contentType, Expression> selector) - //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel + public static IPublishedPropertyType GetModelPropertyType(IPublishedContentType contentType, Expression> selector) + //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel { // fixme therefore, missing a check on TModel here @@ -54,7 +60,7 @@ namespace Umbraco.ModelsBuilder.Umbraco // see note above : accepted risk... var attr = expr.Member - .GetCustomAttributes(typeof (ImplementPropertyTypeAttribute), false) + .GetCustomAttributes(typeof(ImplementPropertyTypeAttribute), false) .OfType() .SingleOrDefault(); diff --git a/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs similarity index 72% rename from src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs rename to src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs index 9558c0140e..0e125759c6 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs @@ -3,59 +3,60 @@ using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; using System.Text; using System.Text.RegularExpressions; using System.Threading; +using System.Web; using System.Web.Compilation; using System.Web.Hosting; using System.Web.WebPages.Razor; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Cache; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.ModelsBuilder.Embedded.Configuration; using File = System.IO.File; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { - internal class PureLiveModelFactory : IPublishedModelFactory, IRegisteredObject + internal class PureLiveModelFactory : ILivePublishedModelFactory2, IRegisteredObject { private Assembly _modelsAssembly; private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); private volatile bool _hasModels; // volatile 'cos reading outside lock private bool _pendingRebuild; - private readonly ProfilingLogger _logger; + private readonly IProfilingLogger _logger; private readonly FileSystemWatcher _watcher; private int _ver, _skipver; private readonly int _debugLevel; private BuildManager _theBuildManager; - private readonly Lazy _umbracoServices; + private readonly Lazy _umbracoServices; // fixme: this is because of circular refs :( private UmbracoServices UmbracoServices => _umbracoServices.Value; private static readonly Regex AssemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled); private const string ProjVirt = "~/App_Data/Models/all.generated.cs"; private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err" }; - public PureLiveModelFactory(Lazy umbracoServices, ProfilingLogger logger) + private readonly IModelsBuilderConfig _config; + private readonly ModelsGenerationError _errors; + + public PureLiveModelFactory(Lazy umbracoServices, IProfilingLogger logger, IModelsBuilderConfig config) { _umbracoServices = umbracoServices; _logger = logger; + _config = config; + _errors = new ModelsGenerationError(config); _ver = 1; // zero is for when we had no version _skipver = -1; // nothing to skip - ContentTypeCacheRefresher.CacheUpdated += (sender, args) => ResetModels(); - DataTypeCacheRefresher.CacheUpdated += (sender, args) => ResetModels(); + RazorBuildProvider.CodeGenerationStarted += RazorBuildProvider_CodeGenerationStarted; if (!HostingEnvironment.IsHosted) return; - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; + var modelsDirectory = _config.ModelsDirectory; if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); @@ -68,9 +69,23 @@ namespace Umbraco.ModelsBuilder.Umbraco _watcher.EnableRaisingEvents = true; // get it here, this need to be fast - _debugLevel = UmbracoConfig.For.ModelsBuilder().DebugLevel; + _debugLevel = _config.DebugLevel; } + #region ILivePublishedModelFactory + + /// + public object SyncRoot { get; } = new object(); + + /// + public void Refresh() + { + ResetModels(); + EnsureModels(); + } + + #endregion + #region IPublishedModelFactory public IPublishedElement CreateModel(IPublishedElement element) @@ -84,7 +99,7 @@ namespace Umbraco.ModelsBuilder.Umbraco var contentTypeAlias = element.ContentType.Alias; // lookup model constructor (else null) - infos.TryGetValue(contentTypeAlias, out ModelInfo info); + infos.TryGetValue(contentTypeAlias, out var info); // create model return info == null ? element : info.Ctor(element); @@ -115,10 +130,20 @@ namespace Umbraco.ModelsBuilder.Umbraco if (ctor != null) return ctor(); var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); - ctor = modelInfo.ListCtor = ReflectionUtilities.EmitCtor>(declaring: listType); + ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); return ctor(); } + /// + public bool Enabled => _config.Enable; + + /// + public void Reset() + { + if (_config.Enable) + ResetModels(); + } + #endregion #region Compilation @@ -163,7 +188,7 @@ namespace Umbraco.ModelsBuilder.Umbraco if (_modelsAssembly == null) return; if (_debugLevel > 0) - _logger.Logger.Debug("RazorBuildProvider.CodeGenerationStarted"); + _logger.Debug("RazorBuildProvider.CodeGenerationStarted"); if (!(sender is RazorBuildProvider provider)) return; // add the assembly, and add a dependency to a text file that will change on each @@ -182,7 +207,7 @@ namespace Umbraco.ModelsBuilder.Umbraco // tells the factory that it should build a new generation of models private void ResetModels() { - _logger.Logger.Debug("Resetting models."); + _logger.Debug("Resetting models."); try { @@ -190,6 +215,19 @@ namespace Umbraco.ModelsBuilder.Umbraco _hasModels = false; _pendingRebuild = true; + + var modelsDirectory = _config.ModelsDirectory; + if (!Directory.Exists(modelsDirectory)) + Directory.CreateDirectory(modelsDirectory); + + // clear stuff + var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); + //var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs"); + //var projFile = Path.Combine(modelsDirectory, "all.generated.cs"); + var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path"); + + if (File.Exists(dllPathFile)) File.Delete(dllPathFile); + if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile); } finally { @@ -204,10 +242,10 @@ namespace Umbraco.ModelsBuilder.Umbraco get { if (_theBuildManager != null) return _theBuildManager; - var prop = typeof (BuildManager).GetProperty("TheBuildManager", BindingFlags.NonPublic | BindingFlags.Static); + var prop = typeof(BuildManager).GetProperty("TheBuildManager", BindingFlags.NonPublic | BindingFlags.Static); if (prop == null) throw new InvalidOperationException("Could not get BuildManager.TheBuildManager property."); - _theBuildManager = (BuildManager) prop.GetValue(null); + _theBuildManager = (BuildManager)prop.GetValue(null); return _theBuildManager; } } @@ -216,7 +254,7 @@ namespace Umbraco.ModelsBuilder.Umbraco internal Infos EnsureModels() { if (_debugLevel > 0) - _logger.Logger.Debug("Ensuring models."); + _logger.Debug("Ensuring models."); // don't use an upgradeable lock here because only 1 thread at a time could enter it try @@ -264,15 +302,15 @@ namespace Umbraco.ModelsBuilder.Umbraco var types = assembly.ExportedTypes.Where(x => x.Inherits() || x.Inherits()); _infos = RegisterModels(types); - ModelsGenerationError.Clear(); + _errors.Clear(); } catch (Exception e) { try { - _logger.Logger.Error("Failed to build models.", e); - _logger.Logger.Warn("Running without models."); // be explicit - ModelsGenerationError.Report("Failed to build PureLive models.", e); + _logger.Error("Failed to build models.", e); + _logger.Warn("Running without models."); // be explicit + _errors.Report("Failed to build PureLive models.", e); } finally { @@ -300,19 +338,12 @@ namespace Umbraco.ModelsBuilder.Umbraco private Assembly GetModelsAssembly(bool forceRebuild) { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; + var modelsDirectory = _config.ModelsDirectory; if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); - // must filter out *.generated.cs because we haven't deleted them yet! - var ourFiles = Directory.Exists(modelsDirectory) - ? Directory.GetFiles(modelsDirectory, "*.cs") - .Where(x => !x.EndsWith(".generated.cs")) - .ToDictionary(x => x, File.ReadAllText) - : new Dictionary(); - var typeModels = UmbracoServices.GetAllTypes(); - var currentHash = HashHelper.Hash(ourFiles, typeModels); + var currentHash = TypeModelHasher.Hash(typeModels); var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs"); var projFile = Path.Combine(modelsDirectory, "all.generated.cs"); @@ -323,31 +354,48 @@ namespace Umbraco.ModelsBuilder.Umbraco if (!forceRebuild) { - _logger.Logger.Debug("Looking for cached models."); + _logger.Debug("Looking for cached models."); if (File.Exists(modelsHashFile) && File.Exists(projFile)) { var cachedHash = File.ReadAllText(modelsHashFile); if (currentHash != cachedHash) { - _logger.Logger.Debug("Found obsolete cached models."); + _logger.Debug("Found obsolete cached models."); forceRebuild = true; } + + // else cachedHash matches currentHash, we can try to load an existing dll } else { - _logger.Logger.Debug("Could not find cached models."); + _logger.Debug("Could not find cached models."); forceRebuild = true; } } Assembly assembly; - if (forceRebuild == false) + if (!forceRebuild) { // try to load the dll directly (avoid rebuilding) + // + // ensure that the .dll file does not have a corresponding .dll.delete file + // as that would mean the the .dll file is going to be deleted and should not + // be re-used - that should not happen in theory, but better be safe + // + // ensure that the .dll file is in the current codegen directory - when IIS + // or Express does a full restart, it can switch to an entirely new codegen + // directory, and then we end up referencing a dll which is *not* in that + // directory, and BuildManager fails to instantiate views ("the view found + // at ... was not created"). + // if (File.Exists(dllPathFile)) { var dllPath = File.ReadAllText(dllPathFile); - if (File.Exists(dllPath)) + var codegen = HttpRuntime.CodegenDir; + + _logger.Debug($"Cached models dll at {dllPath}."); + + if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete") && dllPath.StartsWith(codegen)) { assembly = Assembly.LoadFile(dllPath); var attr = assembly.GetCustomAttribute(); @@ -359,13 +407,23 @@ namespace Umbraco.ModelsBuilder.Umbraco // with the "same but different" version of the assembly in memory _skipver = assembly.GetName().Version.Revision; - _logger.Logger.Debug("Loading cached models (dll)."); + _logger.Debug("Loading cached models (dll)."); return assembly; } + + _logger.Debug("Cached models dll cannot be loaded (invalid assembly)."); } + else if (!File.Exists(dllPath)) + _logger.Debug("Cached models dll does not exist."); + else if (File.Exists(dllPath + ".delete")) + _logger.Debug("Cached models dll is marked for deletion."); + else if (!dllPath.StartsWith(codegen)) + _logger.Debug("Cached models dll is in a different codegen directory."); + else + _logger.Debug("Cached models dll cannot be loaded (why?)."); } - // mmust reset the version in the file else it would keep growing + // must reset the version in the file else it would keep growing // loading cached modules only happens when the app restarts var text = File.ReadAllText(projFile); var match = AssemblyVersionRegex.Match(text); @@ -381,47 +439,80 @@ namespace Umbraco.ModelsBuilder.Umbraco //File.WriteAllText(Path.Combine(modelsDirectory, "models.dep"), "VER:" + _ver); _ver++; - assembly = BuildManager.GetCompiledAssembly(ProjVirt); - File.WriteAllText(dllPathFile, assembly.Location); + try + { + assembly = BuildManager.GetCompiledAssembly(ProjVirt); + File.WriteAllText(dllPathFile, assembly.Location); + } + catch + { + ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile); + throw; + } - _logger.Logger.Debug("Loading cached models (source)."); + _logger.Debug("Loading cached models (source)."); return assembly; } // need to rebuild - _logger.Logger.Debug("Rebuilding models."); + _logger.Debug("Rebuilding models."); // generate code, save - var code = GenerateModelsCode(ourFiles, typeModels); + var code = GenerateModelsCode(typeModels); // add extra attributes, // PureLiveAssembly helps identifying Assemblies that contain PureLive models // AssemblyVersion is so that we have a different version for each rebuild var ver = _ver == _skipver ? ++_ver : _ver; _ver++; - code = code.Replace("//ASSATTR", $@"[assembly: PureLiveAssembly] -[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")] + code = code.Replace("//ASSATTR", $@"[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")] [assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]"); File.WriteAllText(modelsSrcFile, code); // generate proj, save - ourFiles["models.generated.cs"] = code; - var proj = GenerateModelsProj(ourFiles); + var projFiles = new Dictionary + { + { "models.generated.cs", code } + }; + var proj = GenerateModelsProj(projFiles); File.WriteAllText(projFile, proj); // compile and register - assembly = BuildManager.GetCompiledAssembly(ProjVirt); - File.WriteAllText(dllPathFile, assembly.Location); + try + { + assembly = BuildManager.GetCompiledAssembly(ProjVirt); + File.WriteAllText(dllPathFile, assembly.Location); + File.WriteAllText(modelsHashFile, currentHash); + } + catch + { + ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile); + throw; + } - // assuming we can write and it's not going to cause exceptions... - File.WriteAllText(modelsHashFile, currentHash); - - _logger.Logger.Debug("Done rebuilding."); + _logger.Debug("Done rebuilding."); return assembly; } + private void ClearOnFailingToCompile(string dllPathFile, string modelsHashFile, string projFile) + { + _logger.Debug("Failed to compile."); + + // the dll file reference still points to the previous dll, which is obsolete + // now and will be deleted by ASP.NET eventually, so better clear that reference. + // also touch the proj file to force views to recompile - don't delete as it's + // useful to have the source around for debugging. + try + { + if (File.Exists(dllPathFile)) File.Delete(dllPathFile); + if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile); + if (File.Exists(projFile)) File.SetLastWriteTime(projFile, DateTime.Now); + } + catch { /* enough */ } + } + private static Infos RegisterModels(IEnumerable types) { - var ctorArgTypes = new[] { typeof (IPublishedElement) }; + var ctorArgTypes = new[] { typeof(IPublishedElement) }; var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var map = new Dictionary(); @@ -433,7 +524,7 @@ namespace Umbraco.ModelsBuilder.Umbraco foreach (var ctor in type.GetConstructors()) { var parms = ctor.GetParameters(); - if (parms.Length == 1 && typeof (IPublishedElement).IsAssignableFrom(parms[0].ParameterType)) + if (parms.Length == 1 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType)) { if (constructor != null) throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPropertySet."); @@ -448,16 +539,17 @@ namespace Umbraco.ModelsBuilder.Umbraco var attribute = type.GetCustomAttribute(false); var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; - if (modelInfos.TryGetValue(typeName, out ModelInfo modelInfo)) + if (modelInfos.TryGetValue(typeName, out var modelInfo)) throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\"."); // fixme use Core's ReflectionUtilities.EmitCtor !! - var meth = new DynamicMethod(string.Empty, typeof (IPublishedElement), ctorArgTypes, type.Module, true); + // Yes .. DynamicMethod is uber slow + var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true); var gen = meth.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Newobj, constructor); gen.Emit(OpCodes.Ret); - var func = (Func) meth.CreateDelegate(typeof (Func)); + var func = (Func)meth.CreateDelegate(typeof(Func)); modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, Ctor = func, ModelType = type }; map[typeName] = type; @@ -466,26 +558,16 @@ namespace Umbraco.ModelsBuilder.Umbraco return new Infos { ModelInfos = modelInfos.Count > 0 ? modelInfos : null, ModelTypeMap = map }; } - private static string GenerateModelsCode(IDictionary ourFiles, IList typeModels) + private string GenerateModelsCode(IList typeModels) { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; + var modelsDirectory = _config.ModelsDirectory; if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) File.Delete(file); - var map = typeModels.ToDictionary(x => x.Alias, x => x.ClrName); - foreach (var typeModel in typeModels) - { - foreach (var propertyModel in typeModel.Properties) - { - propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); - } - } - - var parseResult = new CodeParser().ParseWithReferencedAssemblies(ourFiles); - var builder = new TextBuilder(typeModels, parseResult, UmbracoConfig.For.ModelsBuilder().ModelsNamespace); + var builder = new TextBuilder(_config, typeModels); var codeBuilder = new StringBuilder(); builder.Generate(codeBuilder, builder.GetModelsToGenerate()); @@ -577,7 +659,7 @@ namespace Umbraco.ModelsBuilder.Umbraco //if (_building && OurFiles.Contains(changed)) //{ - // //_logger.Logger.Info("Ignoring files self-changes."); + // //_logger.Info("Ignoring files self-changes."); // return; //} @@ -585,9 +667,10 @@ namespace Umbraco.ModelsBuilder.Umbraco if (OurFiles.Contains(changed)) return; - _logger.Logger.Info("Detected files changes."); + _logger.Info("Detected files changes."); - ResetModels(); + lock (SyncRoot) // don't reset while being locked + ResetModels(); } public void Stop(bool immediate) diff --git a/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs b/src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs similarity index 54% rename from src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs rename to src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs index 42e8b3b9c9..8886afa1c8 100644 --- a/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs @@ -4,25 +4,19 @@ using System.Linq; using System.Reflection; using System.Web.Compilation; using System.Web.Hosting; -using Microsoft.CodeAnalysis; using Umbraco.Core; -namespace Umbraco.ModelsBuilder +namespace Umbraco.ModelsBuilder.Embedded { internal static class ReferencedAssemblies { private static readonly Lazy> LazyLocations; - private static readonly Lazy> LazyReferences; static ReferencedAssemblies() { LazyLocations = new Lazy>(() => HostingEnvironment.IsHosted ? GetAllReferencedAssembliesLocationFromBuildManager() : GetAllReferencedAssembliesFromDomain()); - - LazyReferences = new Lazy>(() => Locations - .Select(x => MetadataReference.CreateFromFile(x)) - .ToArray()); } /// @@ -31,19 +25,68 @@ namespace Umbraco.ModelsBuilder /// public static IEnumerable Locations => LazyLocations.Value; - /// - /// Gets the metadata reference of all the referenced assemblies. - /// - public static IEnumerable References => LazyReferences.Value; + public static Assembly GetNetStandardAssembly(List assemblies) + { + if (assemblies == null) + assemblies = BuildManager.GetReferencedAssemblies().Cast().ToList(); - // hosted, get referenced assemblies from the BuildManader and filter + // for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of + // the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out + try + { + // so, resorting to an ugly trick + // we should have System.Reflection.Metadata around, and it should reference netstandard + var someAssembly = assemblies.First(x => x.FullName.StartsWith("System.Reflection.Metadata,")); + var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,")); + var netStandard = Assembly.Load(netStandardAssemblyName.FullName); + return netStandard; + } + catch { /* never mind */ } + + return null; + } + + public static Assembly GetNetStandardAssembly() + { + // in PreApplicationStartMethod we cannot get BuildManager.Referenced assemblies, do it differently + try + { + var someAssembly = Assembly.Load("System.Reflection.Metadata"); + var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,")); + var netStandard = Assembly.Load(netStandardAssemblyName.FullName); + return netStandard; + } + catch { /* never mind */ } + + return null; + } + + // hosted, get referenced assemblies from the BuildManager and filter private static IEnumerable GetAllReferencedAssembliesLocationFromBuildManager() { - return BuildManager.GetReferencedAssemblies() - .Cast() + var assemblies = BuildManager.GetReferencedAssemblies().Cast().ToList(); + + assemblies.Add(typeof(ReferencedAssemblies).Assembly); // always include ourselves + + // see https://github.com/aspnet/RoslynCodeDomProvider/blob/master/src/Microsoft.CodeDom.Providers.DotNetCompilerPlatform/CSharpCompiler.cs: + // mentions "Bug 913691: Explicitly add System.Runtime as a reference." + // and explicitly adds System.Runtime to references before invoking csc.exe + // so, doing the same here + try + { + var systemRuntime = Assembly.Load("System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + assemblies.Add(systemRuntime); + } + catch { /* never mind */ } + + // for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of + // the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out + var netStandard = GetNetStandardAssembly(assemblies); + if (netStandard != null) assemblies.Add(netStandard); + + return assemblies .Where(x => !x.IsDynamic && !x.Location.IsNullOrWhiteSpace()) .Select(x => x.Location) - .And(typeof(ReferencedAssemblies).Assembly.Location) // always include ourselves .Distinct() .ToList(); } @@ -99,26 +142,6 @@ namespace Umbraco.ModelsBuilder // ---- - private static IEnumerable GetDeepReferencedAssemblies(Assembly assembly) - { - var visiting = new Stack(); - var visited = new HashSet(); - - visiting.Push(assembly); - visited.Add(assembly); - while (visiting.Count > 0) - { - var visAsm = visiting.Pop(); - foreach (var refAsm in visAsm.GetReferencedAssemblies() - .Select(TryLoad) - .Where(x => x != null && visited.Contains(x) == false)) - { - yield return refAsm; - visiting.Push(refAsm); - visited.Add(refAsm); - } - } - } private static Assembly TryLoad(AssemblyName name) { @@ -132,6 +155,5 @@ namespace Umbraco.ModelsBuilder return null; } } - } } diff --git a/src/Umbraco.ModelsBuilder/TypeExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs similarity index 96% rename from src/Umbraco.ModelsBuilder/TypeExtensions.cs rename to src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs index d3b3ff6b4e..1f270a80a6 100644 --- a/src/Umbraco.ModelsBuilder/TypeExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/TypeExtensions.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.ModelsBuilder +namespace Umbraco.ModelsBuilder.Embedded { internal static class TypeExtensions { diff --git a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj similarity index 54% rename from src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj rename to src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj index b9a5890d57..75121a635d 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj +++ b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj @@ -4,17 +4,19 @@ Debug AnyCPU - {7020A059-C0D1-43A0-8EFD-23591A0C9AF6} + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C} Library Properties - Umbraco.ModelsBuilder - Umbraco.ModelsBuilder + Umbraco.ModelsBuilder.Embedded + Umbraco.ModelsBuilder.Embedded v4.7.2 512 + true + 7.3 true - full + portable false bin\Debug\ DEBUG;TRACE @@ -22,19 +24,20 @@ 4 - pdbonly + portable true bin\Release\ TRACE prompt 4 - bin\Release\Umbraco.ModelsBuilder.xml + bin\Release\Umbraco.ModelsBuilder.Embedded.xml + + - @@ -46,67 +49,58 @@ Properties\SolutionInfo.cs - - - - - - - - + + + + - - - - - - - + + + + - - - - - - + - - - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - 2.8.0 + 2.10.0 + + + 1.0.0-beta2-19324-01 + runtime; build; native; contentfiles; analyzers; buildtransitive + all - {31785bc3-256c-4613-b2f5-a1b0bdded8c1} + {31785BC3-256C-4613-B2F5-A1B0BDDED8C1} Umbraco.Core @@ -114,5 +108,11 @@ Umbraco.Web + + + 5.2.7 + + + \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs b/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs similarity index 68% rename from src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs rename to src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs index f0347d9194..5ede5f45e9 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs @@ -2,17 +2,16 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; -using Umbraco.Core.Configuration; +using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Core.Strings; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Embedded.Building; -namespace Umbraco.ModelsBuilder.Umbraco +namespace Umbraco.ModelsBuilder.Embedded { - public class UmbracoServices + public sealed class UmbracoServices { private readonly IContentTypeService _contentTypeService; private readonly IMediaTypeService _mediaTypeService; @@ -33,6 +32,10 @@ namespace Umbraco.ModelsBuilder.Umbraco { var types = new List(); + // TODO: this will require 3 rather large SQL queries on startup in PureLive. I know that these will be cached after lookup but it will slow + // down startup time ... BUT these queries are also used in NuCache on startup so we can't really avoid them. Maybe one day we can + // load all of these in in one query and still have them cached per service, and/or somehow improve the perf of these since they are used on startup + // in more than one place. types.AddRange(GetTypes(PublishedItemType.Content, _contentTypeService.GetAll().Cast().ToArray())); types.AddRange(GetTypes(PublishedItemType.Media, _mediaTypeService.GetAll().Cast().ToArray())); types.AddRange(GetTypes(PublishedItemType.Member, _memberTypeService.GetAll().Cast().ToArray())); @@ -60,38 +63,8 @@ namespace Umbraco.ModelsBuilder.Umbraco public static string GetClrName(string name, string alias) { - // ideally we should just be able to re-use Umbraco's alias, - // just upper-casing the first letter, however in v7 for backward - // compatibility reasons aliases derive from names via ToSafeAlias which is - // PreFilter = ApplyUrlReplaceCharacters, - // IsTerm = (c, leading) => leading - // ? char.IsLetter(c) // only letters - // : (char.IsLetterOrDigit(c) || c == '_'), // letter, digit or underscore - // StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase, - // BreakTermsOnUpper = false - // - // but that is not ideal with acronyms and casing - // however we CANNOT change Umbraco - // so, adding a way to "do it right" deriving from name, here - - switch (UmbracoConfig.For.ModelsBuilder().ClrNameSource) - { - case ClrNameSource.RawAlias: - // use Umbraco's alias - return alias; - - case ClrNameSource.Alias: - // ModelsBuilder's legacy - but not ideal - return alias.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase); - - case ClrNameSource.Name: - // derive from name - var source = name.TrimStart('_'); // because CleanStringType.ConvertCase accepts them - return source.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase | CleanStringType.Ascii); - - default: - throw new Exception("Invalid ClrNameSource."); - } + // ModelsBuilder's legacy - but not ideal + return alias.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase); } private IList GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) @@ -116,36 +89,26 @@ namespace Umbraco.ModelsBuilder.Umbraco // of course this should never happen, but when it happens, better detect it // else we end up with weird nullrefs everywhere if (uniqueTypes.Contains(typeModel.ClrName)) - throw new Exception($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); + throw new PanicException($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); uniqueTypes.Add(typeModel.ClrName); - // fixme - we need a better way at figuring out what's an element type! - // and then we should not do the alias filtering below - bool IsElement(PublishedContentType x) - { - return x.Alias.InvariantEndsWith("Element"); - } - var publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); switch (itemType) { case PublishedItemType.Content: - if (IsElement(publishedContentType)) - { - typeModel.ItemType = TypeModel.ItemTypes.Element; - if (typeModel.ClrName.InvariantEndsWith("Element")) - typeModel.ClrName = typeModel.ClrName.Substring(0, typeModel.ClrName.Length - "Element".Length); - } - else - { - typeModel.ItemType = TypeModel.ItemTypes.Content; - } + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Content; break; case PublishedItemType.Media: - typeModel.ItemType = TypeModel.ItemTypes.Media; + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Media; break; case PublishedItemType.Member: - typeModel.ItemType = TypeModel.ItemTypes.Member; + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Member; break; default: throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType)); @@ -166,7 +129,7 @@ namespace Umbraco.ModelsBuilder.Umbraco var publishedPropertyType = publishedContentType.GetPropertyType(propertyType.Alias); if (publishedPropertyType == null) - throw new Exception($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); + throw new PanicException($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); propertyModel.ModelClrType = publishedPropertyType.ModelClrType; @@ -188,7 +151,7 @@ namespace Umbraco.ModelsBuilder.Umbraco foreach (var contentType in contentTypes) { var typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id); - if (typeModel == null) throw new Exception("Panic: no type model matching content type."); + if (typeModel == null) throw new PanicException("Panic: no type model matching content type."); IEnumerable compositionTypes; var contentTypeAsMedia = contentType as IMediaType; @@ -197,12 +160,12 @@ namespace Umbraco.ModelsBuilder.Umbraco if (contentTypeAsMedia != null) compositionTypes = contentTypeAsMedia.ContentTypeComposition; else if (contentTypeAsContent != null) compositionTypes = contentTypeAsContent.ContentTypeComposition; else if (contentTypeAsMember != null) compositionTypes = contentTypeAsMember.ContentTypeComposition; - else throw new Exception(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName)); + else throw new PanicException(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName)); foreach (var compositionType in compositionTypes) { var compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id); - if (compositionModel == null) throw new Exception("Panic: composition type does not exist."); + if (compositionModel == null) throw new PanicException("Panic: composition type does not exist."); if (compositionType.Id == contentType.ParentId) continue; @@ -223,11 +186,9 @@ namespace Umbraco.ModelsBuilder.Umbraco { var groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant()); foreach (var group in groups.Where(x => x.Count() > 1)) - { throw new NotSupportedException($"Alias \"{group.Key}\" is used by types" + $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique." + " One of the aliases must be modified in order to use the ModelsBuilder."); - } return typeModels; } diff --git a/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs b/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs deleted file mode 100644 index cc862ff207..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Web.Http.Controllers; -using System.Web.Security; -using Umbraco.Core; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models.Membership; - -namespace Umbraco.ModelsBuilder.Api -{ - - //TODO: This needs to be changed: - // * Authentication cannot happen in a filter, only Authorization - // * The filter must be an AuthorizationFilter, not an ActionFilter - // * Authorization must be done using the Umbraco logic - it is very specific for claim checking for ASP.Net Identity - // * Theoretically this shouldn't be required whatsoever because when we authenticate a request that has Basic Auth (i.e. for - // VS to work, it will add the correct Claims to the Identity and it will automatically be authorized. - // - // we *do* have POC supporting ASP.NET identity, however they require some config on the server - // we'll keep using this quick-and-dirty method for the time being - - public class ApiBasicAuthFilter : System.Web.Http.Filters.ActionFilterAttribute // use the http one, not mvc, with api controllers! - { - private static readonly char[] Separator = ":".ToCharArray(); - private readonly string _section; - - public ApiBasicAuthFilter(string section) - { - _section = section; - } - - public override void OnActionExecuting(HttpActionContext actionContext) - { - try - { - var user = Authenticate(actionContext.Request); - if (user == null || !user.AllowedSections.Contains(_section)) - { - actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized); - } - //else - //{ - // // note - would that be a proper way to pass data to the controller? - // // see http://stevescodingblog.co.uk/basic-authentication-with-asp-net-webapi/ - // actionContext.ControllerContext.RouteData.Values["umbraco-user"] = user; - //} - - base.OnActionExecuting(actionContext); - } - catch - { - actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized); - } - } - - private static IUser Authenticate(HttpRequestMessage request) - { - var ah = request.Headers.Authorization; - if (ah == null || ah.Scheme != "Basic") - return null; - - var token = ah.Parameter; - var credentials = Encoding.ASCII - .GetString(Convert.FromBase64String(token)) - .Split(Separator); - if (credentials.Length != 2) - return null; - - var username = ApiClient.DecodeTokenElement(credentials[0]); - var password = ApiClient.DecodeTokenElement(credentials[1]); - - var providerKey = UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider; - var provider = Membership.Providers[providerKey]; - if (provider == null || !provider.ValidateUser(username, password)) - return null; - var user = Current.Services.UserService.GetByUsername(username); - if (!user.IsApproved || user.IsLockedOut) - return null; - return user; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Api/ApiClient.cs b/src/Umbraco.ModelsBuilder/Api/ApiClient.cs deleted file mode 100644 index dde3641b97..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiClient.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Net.Http.Formatting; -using System.Net.Http.Headers; -using System.Text; - -namespace Umbraco.ModelsBuilder.Api -{ - public class ApiClient - { - private readonly string _url; - private readonly string _user; - private readonly string _password; - - private readonly JsonMediaTypeFormatter _formatter; - private readonly MediaTypeFormatter[] _formatters; - - // fixme hardcoded? - // could be options - but we cannot "discover" them as the API client runs outside of the web app - // in addition, anything that references the controller forces API clients to reference Umbraco.Core - private const string ApiControllerUrl = "/Umbraco/BackOffice/ModelsBuilder/ModelsBuilderApi/"; - - public ApiClient(string url, string user, string password) - { - _url = url.TrimEnd('/'); - _user = user; - _password = password; - - _formatter = new JsonMediaTypeFormatter(); - _formatters = new MediaTypeFormatter[] { _formatter }; - } - - private void SetBaseAddress(HttpClient client, string url) - { - try - { - client.BaseAddress = new Uri(url); - } - catch - { - throw new UriFormatException($"Invalid URI: the format of the URI \"{url}\" could not be determined."); - } - } - - public void ValidateClientVersion() - { - // FIXME - add proxys support - - var hch = new HttpClientHandler(); - - using (var client = new HttpClient(hch)) - { - SetBaseAddress(client, _url); - Authorize(client); - - var data = new ValidateClientVersionData - { - ClientVersion = ApiVersion.Current.Version, - MinServerVersionSupportingClient = ApiVersion.Current.MinServerVersionSupportingClient, - }; - - var result = client.PostAsync(_url + ApiControllerUrl + nameof(ModelsBuilderApiController.ValidateClientVersion), - data, _formatter).Result; - - // this is not providing enough details in case of an error - do our own reporting - //result.EnsureSuccessStatusCode(); - EnsureSuccess(result); - } - } - - public IDictionary GetModels(Dictionary ourFiles, string modelsNamespace) - { - // FIXME - add proxys support - - var hch = new HttpClientHandler(); - - //hch.Proxy = new WebProxy("path.to.proxy", 8888); - //hch.UseProxy = true; - - using (var client = new HttpClient(hch)) - { - SetBaseAddress(client, _url); - Authorize(client); - - var data = new GetModelsData - { - Namespace = modelsNamespace, - ClientVersion = ApiVersion.Current.Version, - MinServerVersionSupportingClient = ApiVersion.Current.MinServerVersionSupportingClient, - Files = ourFiles - }; - - var result = client.PostAsync(_url + ApiControllerUrl + nameof(ModelsBuilderApiController.GetModels), - data, _formatter).Result; - - // this is not providing enough details in case of an error - do our own reporting - //result.EnsureSuccessStatusCode(); - EnsureSuccess(result); - - var genFiles = result.Content.ReadAsAsync>(_formatters).Result; - return genFiles; - } - } - - private static void EnsureSuccess(HttpResponseMessage result) - { - if (result.IsSuccessStatusCode) return; - - var text = result.Content.ReadAsStringAsync().Result; - throw new Exception($"Response status code does not indicate success ({result.StatusCode})\n{text}"); - } - - private void Authorize(HttpClient client) - { - AuthorizeBasic(client); - } - - // fixme - for the time being, we don't cache the token and we auth on each API call - // not used at the moment - /* - private void AuthorizeIdentity(HttpClient client) - { - var formData = new FormUrlEncodedContent(new[] - { - new KeyValuePair("grant_type", "password"), - new KeyValuePair("userName", _user), - new KeyValuePair("password", _password), - }); - - var result = client.PostAsync(_url + UmbracoOAuthTokenUrl, formData).Result; - - EnsureSuccess(result); - - var token = result.Content.ReadAsAsync(_formatters).Result; - if (token.TokenType != "bearer") - throw new Exception($"Received invalid token type \"{token.TokenType}\", expected \"bearer\"."); - - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); - } - */ - - private void AuthorizeBasic(HttpClient client) - { - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", - Convert.ToBase64String(Encoding.UTF8.GetBytes(EncodeTokenElement(_user) + ':' + EncodeTokenElement(_password)))); - } - - public static string EncodeTokenElement(string s) - { - return s.Replace("%", "%a").Replace(":", "%b"); - } - - public static string DecodeTokenElement(string s) - { - return s.Replace("%b", ":").Replace("%a", "%"); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs b/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs deleted file mode 100644 index fa6492fe3f..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Text; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Umbraco; - -namespace Umbraco.ModelsBuilder.Api -{ - internal static class ApiHelper - { - public static Dictionary GetModels(UmbracoServices umbracoServices, string modelsNamespace, IDictionary files) - { - var typeModels = umbracoServices.GetAllTypes(); - - var parseResult = new CodeParser().ParseWithReferencedAssemblies(files); - var builder = new TextBuilder(typeModels, parseResult, modelsNamespace); - - var models = new Dictionary(); - foreach (var typeModel in builder.GetModelsToGenerate()) - { - var sb = new StringBuilder(); - builder.Generate(sb, typeModel); - models[typeModel.ClrName] = sb.ToString(); - } - return models; - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs b/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs deleted file mode 100644 index 2ee64b8c54..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Reflection; - -namespace Umbraco.ModelsBuilder.Api -{ - /// - /// Manages API version handshake between client and server. - /// - public class ApiVersion - { - #region Configure - - // indicate the minimum version of the client API that is supported by this server's API. - // eg our Version = 4.8 but we support connections from VSIX down to version 3.2 - // => as a server, we accept connections from client down to version ... - private static readonly Version MinClientVersionSupportedByServerConst = new Version(3, 0, 0, 0); - - // indicate the minimum version of the server that can support the client API - // eg our Version = 4.8 and we know we're compatible with website server down to version 3.2 - // => as a client, we tell the server down to version ... that it should accept us - private static readonly Version MinServerVersionSupportingClientConst = new Version(3, 0, 0, 0); - - #endregion - - /// - /// Initializes a new instance of the class. - /// - /// The currently executing version. - /// The min client version supported by the server. - /// An opt min server version supporting the client. - internal ApiVersion(Version executingVersion, Version minClientVersionSupportedByServer, Version minServerVersionSupportingClient = null) - { - if (executingVersion == null) throw new ArgumentNullException(nameof(executingVersion)); - if (minClientVersionSupportedByServer == null) throw new ArgumentNullException(nameof(minClientVersionSupportedByServer)); - - Version = executingVersion; - MinClientVersionSupportedByServer = minClientVersionSupportedByServer; - MinServerVersionSupportingClient = minServerVersionSupportingClient; - } - - /// - /// Gets the currently executing API version. - /// - public static ApiVersion Current { get; } - = new ApiVersion(Assembly.GetExecutingAssembly().GetName().Version, - MinClientVersionSupportedByServerConst, MinServerVersionSupportingClientConst); - - /// - /// Gets the executing version of the API. - /// - public Version Version { get; } - - /// - /// Gets the min client version supported by the server. - /// - public Version MinClientVersionSupportedByServer { get; } - - /// - /// Gets the min server version supporting the client. - /// - public Version MinServerVersionSupportingClient { get; } - - /// - /// Gets a value indicating whether the API server is compatible with a client. - /// - /// The client version. - /// An opt min server version supporting the client. - /// - /// A client is compatible with a server if the client version is greater-or-equal _minClientVersionSupportedByServer - /// (ie client can be older than server, up to a point) AND the client version is lower-or-equal the server version - /// (ie client cannot be more recent than server) UNLESS the server . - /// - public bool IsCompatibleWith(Version clientVersion, Version minServerVersionSupportingClient = null) - { - // client cannot be older than server's min supported version - if (clientVersion < MinClientVersionSupportedByServer) - return false; - - // if we know about this client (client is older than server), it is supported - if (clientVersion <= Version) // if we know about this client (client older than server) - return true; - - // if we don't know about this client (client is newer than server), - // give server a chance to tell client it is, indeed, ok to support it - return minServerVersionSupportingClient != null && minServerVersionSupportingClient <= Version; - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs b/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs deleted file mode 100644 index 9a5c55afc2..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.Serialization; - -namespace Umbraco.ModelsBuilder.Api -{ - [DataContract] - public class GetModelsData : ValidateClientVersionData - { - [DataMember] - public string Namespace { get; set; } - - [DataMember] - public IDictionary Files { get; set; } - - public override bool IsValid => base.IsValid && Files != null; - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs b/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs deleted file mode 100644 index 444910b069..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Umbraco; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; - -namespace Umbraco.ModelsBuilder.Api -{ - // read http://umbraco.com/follow-us/blog-archive/2014/1/17/heads-up,-breaking-change-coming-in-702-and-62.aspx - // read http://our.umbraco.org/forum/developers/api-questions/43025-Web-API-authentication - // UmbracoAuthorizedApiController :: /Umbraco/BackOffice/Zbu/ModelsBuilderApi/GetTypeModels - // UmbracoApiController :: /Umbraco/Zbu/ModelsBuilderApi/GetTypeModels ?? UNLESS marked with isbackoffice - // - // BEWARE! the controller url is hard-coded in ModelsBuilderApi and needs to be in sync! - - [PluginController(ControllerArea)] - [IsBackOffice] - //[UmbracoApplicationAuthorize(Constants.Applications.Developer)] // see ApiBasicAuthFilter - that one would be for ASP.NET identity - public class ModelsBuilderApiController : UmbracoApiController // UmbracoAuthorizedApiController - for ASP.NET identity - { - public const string ControllerArea = "ModelsBuilder"; - - private readonly UmbracoServices _umbracoServices; - - public ModelsBuilderApiController(UmbracoServices umbracoServices) - { - _umbracoServices = umbracoServices; - } - - // invoked by the API - [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! - [ApiBasicAuthFilter("developer")] // have to use our own, non-cookie-based, auth - public HttpResponseMessage ValidateClientVersion(ValidateClientVersionData data) - { - if (!UmbracoConfig.For.ModelsBuilder().ApiServer) - return Request.CreateResponse(HttpStatusCode.Forbidden, "API server does not want to talk to you."); - - if (!ModelState.IsValid || data == null || !data.IsValid) - return Request.CreateResponse(HttpStatusCode.BadRequest, "Invalid data."); - - var checkResult = CheckVersion(data.ClientVersion, data.MinServerVersionSupportingClient); - return (checkResult.Success - ? Request.CreateResponse(HttpStatusCode.OK, "OK", Configuration.Formatters.JsonFormatter) - : checkResult.Result); - } - - // invoked by the API - [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! - [ApiBasicAuthFilter("developer")] // have to use our own, non-cookie-based, auth - public HttpResponseMessage GetModels(GetModelsData data) - { - if (!UmbracoConfig.For.ModelsBuilder().ApiServer) - return Request.CreateResponse(HttpStatusCode.Forbidden, "API server does not want to talk to you."); - - if (!ModelState.IsValid || data == null || !data.IsValid) - return Request.CreateResponse(HttpStatusCode.BadRequest, "Invalid data."); - - var checkResult = CheckVersion(data.ClientVersion, data.MinServerVersionSupportingClient); - if (!checkResult.Success) - return checkResult.Result; - - var models = ApiHelper.GetModels(_umbracoServices, data.Namespace, data.Files); - - return Request.CreateResponse(HttpStatusCode.OK, models, Configuration.Formatters.JsonFormatter); - } - - private Attempt CheckVersion(Version clientVersion, Version minServerVersionSupportingClient) - { - if (clientVersion == null) - return Attempt.Fail(Request.CreateResponse(HttpStatusCode.Forbidden, - $"API version conflict: client version () is not compatible with server version({ApiVersion.Current.Version}).")); - - // minServerVersionSupportingClient can be null - var isOk = ApiVersion.Current.IsCompatibleWith(clientVersion, minServerVersionSupportingClient); - var response = isOk ? null : Request.CreateResponse(HttpStatusCode.Forbidden, - $"API version conflict: client version ({clientVersion}) is not compatible with server version({ApiVersion.Current.Version})."); - - return Attempt.If(isOk, response); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/TokenData.cs b/src/Umbraco.ModelsBuilder/Api/TokenData.cs deleted file mode 100644 index c34a6c75c5..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/TokenData.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.ModelsBuilder.Api -{ - [DataContract] - class TokenData - { - [DataMember(Name = "access_token")] - public string AccessToken { get; set; } - - [DataMember(Name = "token_type")] - public string TokenType { get; set; } - - [DataMember(Name = "expires_in")] - public int ExpiresIn { get; set; } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs b/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs deleted file mode 100644 index 39ef08d816..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Runtime.Serialization; - -namespace Umbraco.ModelsBuilder.Api -{ - [DataContract] - public class ValidateClientVersionData - { - // issues 32, 34... problems when serializing versions - // - // make sure System.Version objects are transfered as strings - // depending on the JSON serializer version, it looks like versions are causing issues - // see - // http://stackoverflow.com/questions/13170386/why-system-version-in-json-string-does-not-deserialize-correctly - // - // if the class is marked with [DataContract] then only properties marked with [DataMember] - // are serialized and the rest is ignored, see - // http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization - - [DataMember] - public string ClientVersionString - { - get { return VersionToString(ClientVersion); } - set { ClientVersion = ParseVersion(value, false, "client"); } - } - - [DataMember] - public string MinServerVersionSupportingClientString - { - get { return VersionToString(MinServerVersionSupportingClient); } - set { MinServerVersionSupportingClient = ParseVersion(value, true, "minServer"); } - } - - // not serialized - public Version ClientVersion { get; set; } - public Version MinServerVersionSupportingClient { get; set; } - - private static string VersionToString(Version version) - { - return version?.ToString() ?? "0.0.0.0"; - } - - private static Version ParseVersion(string value, bool canBeNull, string name) - { - if (string.IsNullOrWhiteSpace(value) && canBeNull) - return null; - - Version version; - if (Version.TryParse(value, out version)) - return version; - - throw new ArgumentException($"Failed to parse \"{value}\" as {name} version."); - } - - public virtual bool IsValid => ClientVersion != null; - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs b/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs deleted file mode 100644 index 925337bd1e..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.CodeDom; -using System.Collections.Generic; - -namespace Umbraco.ModelsBuilder.Building -{ - // NOTE - // See nodes in Builder.cs class - that one does not work, is not complete, - // and was just some sort of experiment... - - /// - /// Implements a builder that works by using CodeDom - /// - internal class CodeDomBuilder : Builder - { - /// - /// Initializes a new instance of the class with a list of models to generate. - /// - /// The list of models to generate. - public CodeDomBuilder(IList typeModels) - : base(typeModels, null) - { } - - /// - /// Outputs a generated model to a code namespace. - /// - /// The code namespace. - /// The model to generate. - public void Generate(CodeNamespace ns, TypeModel typeModel) - { - // what about USING? - // what about references? - - if (typeModel.IsMixin) - { - var i = new CodeTypeDeclaration("I" + typeModel.ClrName) - { - IsInterface = true, - IsPartial = true, - Attributes = MemberAttributes.Public - }; - i.BaseTypes.Add(typeModel.BaseType == null ? "IPublishedContent" : "I" + typeModel.BaseType.ClrName); - - foreach (var mixinType in typeModel.DeclaringInterfaces) - i.BaseTypes.Add(mixinType.ClrName); - - i.Comments.Add(new CodeCommentStatement($"Mixin content Type {typeModel.Id} with alias \"{typeModel.Alias}\"")); - - foreach (var propertyModel in typeModel.Properties) - { - var p = new CodeMemberProperty - { - Name = propertyModel.ClrName, - Type = new CodeTypeReference(propertyModel.ModelClrType), - Attributes = MemberAttributes.Public, - HasGet = true, - HasSet = false - }; - i.Members.Add(p); - } - } - - var c = new CodeTypeDeclaration(typeModel.ClrName) - { - IsClass = true, - IsPartial = true, - Attributes = MemberAttributes.Public - }; - - c.BaseTypes.Add(typeModel.BaseType == null ? "PublishedContentModel" : typeModel.BaseType.ClrName); - - // if it's a missing it implements its own interface - if (typeModel.IsMixin) - c.BaseTypes.Add("I" + typeModel.ClrName); - - // write the mixins, if any, as interfaces - // only if not a mixin because otherwise the interface already has them - if (typeModel.IsMixin == false) - foreach (var mixinType in typeModel.DeclaringInterfaces) - c.BaseTypes.Add("I" + mixinType.ClrName); - - foreach (var mixin in typeModel.MixinTypes) - c.BaseTypes.Add("I" + mixin.ClrName); - - c.Comments.Add(new CodeCommentStatement($"Content Type {typeModel.Id} with alias \"{typeModel.Alias}\"")); - - foreach (var propertyModel in typeModel.Properties) - { - var p = new CodeMemberProperty - { - Name = propertyModel.ClrName, - Type = new CodeTypeReference(propertyModel.ModelClrType), - Attributes = MemberAttributes.Public, - HasGet = true, - HasSet = false - }; - p.GetStatements.Add(new CodeMethodReturnStatement( // return - new CodeMethodInvokeExpression( - new CodeMethodReferenceExpression( - new CodeThisReferenceExpression(), // this - "Value", // .Value - new[] // - { - new CodeTypeReference(propertyModel.ModelClrType) - }), - new CodeExpression[] // ("alias") - { - new CodePrimitiveExpression(propertyModel.Alias) - }))); - c.Members.Add(p); - } - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/CodeParser.cs b/src/Umbraco.ModelsBuilder/Building/CodeParser.cs deleted file mode 100644 index 30fcbf1f91..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/CodeParser.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.ModelsBuilder.Building -{ - /// - /// Implements code parsing. - /// - /// Parses user's code and look for generator's instructions. - internal class CodeParser - { - /// - /// Parses a set of file. - /// - /// A set of (filename,content) representing content to parse. - /// The result of the code parsing. - /// The set of files is a dictionary of name, content. - public ParseResult Parse(IDictionary files) - { - return Parse(files, Enumerable.Empty()); - } - - /// - /// Parses a set of file. - /// - /// A set of (filename,content) representing content to parse. - /// Assemblies to reference in compilations. - /// The result of the code parsing. - /// The set of files is a dictionary of name, content. - public ParseResult Parse(IDictionary files, IEnumerable references) - { - SyntaxTree[] trees; - var compiler = new Compiler { References = references }; - var compilation = compiler.GetCompilation("Umbraco.ModelsBuilder.Generated", files, out trees); - - var disco = new ParseResult(); - foreach (var tree in trees) - Parse(disco, compilation, tree); - - return disco; - } - - public ParseResult ParseWithReferencedAssemblies(IDictionary files) - { - return Parse(files, ReferencedAssemblies.References); - } - - private static void Parse(ParseResult disco, CSharpCompilation compilation, SyntaxTree tree) - { - var model = compilation.GetSemanticModel(tree); - - //we quite probably have errors but that is normal - //var diags = model.GetDiagnostics(); - - var classDecls = tree.GetRoot().DescendantNodes().OfType(); - foreach (var classSymbol in classDecls.Select(x => model.GetDeclaredSymbol(x))) - { - ParseClassSymbols(disco, classSymbol); - - var baseClassSymbol = classSymbol.BaseType; - if (baseClassSymbol != null) - //disco.SetContentBaseClass(SymbolDisplay.ToDisplayString(classSymbol), SymbolDisplay.ToDisplayString(baseClassSymbol)); - disco.SetContentBaseClass(classSymbol.Name, baseClassSymbol.Name); - - var interfaceSymbols = classSymbol.Interfaces; - disco.SetContentInterfaces(classSymbol.Name, //SymbolDisplay.ToDisplayString(classSymbol), - interfaceSymbols.Select(x => x.Name)); //SymbolDisplay.ToDisplayString(x))); - - var hasCtor = classSymbol.Constructors - .Any(x => - { - if (x.IsStatic) return false; - if (x.Parameters.Length != 1) return false; - var type1 = x.Parameters[0].Type; - var type2 = typeof (IPublishedContent); - return type1.ToDisplayString() == type2.FullName; - }); - - if (hasCtor) - disco.SetHasCtor(classSymbol.Name); - - foreach (var propertySymbol in classSymbol.GetMembers().Where(x => x is IPropertySymbol)) - ParsePropertySymbols(disco, classSymbol, propertySymbol); - - foreach (var staticMethodSymbol in classSymbol.GetMembers().Where(x => x is IMethodSymbol)) - ParseMethodSymbol(disco, classSymbol, staticMethodSymbol); - } - - var interfaceDecls = tree.GetRoot().DescendantNodes().OfType(); - foreach (var interfaceSymbol in interfaceDecls.Select(x => model.GetDeclaredSymbol(x))) - { - ParseClassSymbols(disco, interfaceSymbol); - - var interfaceSymbols = interfaceSymbol.Interfaces; - disco.SetContentInterfaces(interfaceSymbol.Name, //SymbolDisplay.ToDisplayString(interfaceSymbol), - interfaceSymbols.Select(x => x.Name)); // SymbolDisplay.ToDisplayString(x))); - } - - ParseAssemblySymbols(disco, compilation.Assembly); - } - - private static void ParseClassSymbols(ParseResult disco, ISymbol symbol) - { - foreach (var attrData in symbol.GetAttributes()) - { - var attrClassSymbol = attrData.AttributeClass; - - // handle errors - if (attrClassSymbol is IErrorTypeSymbol) continue; - if (attrData.AttributeConstructor == null) continue; - - var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol); - switch (attrClassName) - { - case "Umbraco.ModelsBuilder.IgnorePropertyTypeAttribute": - var propertyAliasToIgnore = (string)attrData.ConstructorArguments[0].Value; - disco.SetIgnoredProperty(symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/, propertyAliasToIgnore); - break; - case "Umbraco.ModelsBuilder.RenamePropertyTypeAttribute": - var propertyAliasToRename = (string)attrData.ConstructorArguments[0].Value; - var propertyRenamed = (string)attrData.ConstructorArguments[1].Value; - disco.SetRenamedProperty(symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/, propertyAliasToRename, propertyRenamed); - break; - // that one causes all sorts of issues with references to Umbraco.Core in Roslyn - //case "Umbraco.Core.Models.PublishedContent.PublishedContentModelAttribute": - // var contentAliasToRename = (string)attrData.ConstructorArguments[0].Value; - // disco.SetRenamedContent(contentAliasToRename, symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/); - // break; - case "Umbraco.ModelsBuilder.ImplementContentTypeAttribute": - var contentAliasToRename = (string)attrData.ConstructorArguments[0].Value; - disco.SetRenamedContent(contentAliasToRename, symbol.Name, true /*SymbolDisplay.ToDisplayString(symbol)*/); - break; - } - } - } - - private static void ParsePropertySymbols(ParseResult disco, ISymbol classSymbol, ISymbol symbol) - { - foreach (var attrData in symbol.GetAttributes()) - { - var attrClassSymbol = attrData.AttributeClass; - - // handle errors - if (attrClassSymbol is IErrorTypeSymbol) continue; - if (attrData.AttributeConstructor == null) continue; - - var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol); - // ReSharper disable once SwitchStatementMissingSomeCases - switch (attrClassName) - { - case "Umbraco.ModelsBuilder.ImplementPropertyTypeAttribute": - var propertyAliasToIgnore = (string)attrData.ConstructorArguments[0].Value; - disco.SetIgnoredProperty(classSymbol.Name /*SymbolDisplay.ToDisplayString(classSymbol)*/, propertyAliasToIgnore); - break; - } - } - } - - private static void ParseAssemblySymbols(ParseResult disco, ISymbol symbol) - { - foreach (var attrData in symbol.GetAttributes()) - { - var attrClassSymbol = attrData.AttributeClass; - - // handle errors - if (attrClassSymbol is IErrorTypeSymbol) continue; - if (attrData.AttributeConstructor == null) continue; - - var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol); - switch (attrClassName) - { - case "Umbraco.ModelsBuilder.IgnoreContentTypeAttribute": - var contentAliasToIgnore = (string)attrData.ConstructorArguments[0].Value; - // see notes in IgnoreContentTypeAttribute - //var ignoreContent = (bool)attrData.ConstructorArguments[1].Value; - //var ignoreMixin = (bool)attrData.ConstructorArguments[1].Value; - //var ignoreMixinProperties = (bool)attrData.ConstructorArguments[1].Value; - disco.SetIgnoredContent(contentAliasToIgnore /*, ignoreContent, ignoreMixin, ignoreMixinProperties*/); - break; - - case "Umbraco.ModelsBuilder.RenameContentTypeAttribute": - var contentAliasToRename = (string) attrData.ConstructorArguments[0].Value; - var contentRenamed = (string)attrData.ConstructorArguments[1].Value; - disco.SetRenamedContent(contentAliasToRename, contentRenamed, false); - break; - - case "Umbraco.ModelsBuilder.ModelsBaseClassAttribute": - var modelsBaseClass = (INamedTypeSymbol) attrData.ConstructorArguments[0].Value; - if (modelsBaseClass is IErrorTypeSymbol) - throw new Exception($"Invalid base class type \"{modelsBaseClass.Name}\"."); - disco.SetModelsBaseClassName(SymbolDisplay.ToDisplayString(modelsBaseClass)); - break; - - case "Umbraco.ModelsBuilder.ModelsNamespaceAttribute": - var modelsNamespace= (string) attrData.ConstructorArguments[0].Value; - disco.SetModelsNamespace(modelsNamespace); - break; - - case "Umbraco.ModelsBuilder.ModelsUsingAttribute": - var usingNamespace = (string)attrData.ConstructorArguments[0].Value; - disco.SetUsingNamespace(usingNamespace); - break; - } - } - } - - private static void ParseMethodSymbol(ParseResult disco, ISymbol classSymbol, ISymbol symbol) - { - var methodSymbol = symbol as IMethodSymbol; - - if (methodSymbol == null - || !methodSymbol.IsStatic - || methodSymbol.IsGenericMethod - || methodSymbol.ReturnsVoid - || methodSymbol.IsExtensionMethod - || methodSymbol.Parameters.Length != 1) - return; - - var returnType = methodSymbol.ReturnType; - var paramSymbol = methodSymbol.Parameters[0]; - var paramType = paramSymbol.Type; - - // cannot do this because maybe the param type is ISomething and we don't have - // that type yet - will be generated - so cannot put any condition on it really - //const string iPublishedContent = "Umbraco.Core.Models.IPublishedContent"; - //var implements = paramType.AllInterfaces.Any(x => x.ToDisplayString() == iPublishedContent); - //if (!implements) - // return; - - disco.SetStaticMixinMethod(classSymbol.Name, methodSymbol.Name, returnType.Name, paramType.Name); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/Compiler.cs b/src/Umbraco.ModelsBuilder/Building/Compiler.cs deleted file mode 100644 index 66064bef0b..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/Compiler.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Web; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; - -namespace Umbraco.ModelsBuilder.Building -{ - // main Roslyn compiler - internal class Compiler - { - private readonly LanguageVersion _languageVersion; - - public Compiler() - : this(UmbracoConfig.For.ModelsBuilder().LanguageVersion) - { } - - public Compiler(LanguageVersion languageVersion) - { - _languageVersion = languageVersion; - References = ReferencedAssemblies.References; - Debug = HttpContext.Current != null && HttpContext.Current.IsDebuggingEnabled; - } - - // gets or sets the references - public IEnumerable References { get; set; } - - public bool Debug { get; set; } - - // gets a compilation - public CSharpCompilation GetCompilation(string assemblyName, IDictionary files) - { - SyntaxTree[] trees; - return GetCompilation(assemblyName, files, out trees); - } - - // gets a compilation - // used by CodeParser to get a "compilation" of the existing files - public CSharpCompilation GetCompilation(string assemblyName, IDictionary files, out SyntaxTree[] trees) - { - var options = new CSharpParseOptions(_languageVersion); - trees = files.Select(x => - { - var text = x.Value; - var tree = CSharpSyntaxTree.ParseText(text, /*options:*/ options); - var diagnostic = tree.GetDiagnostics().FirstOrDefault(y => y.Severity == DiagnosticSeverity.Error); - if (diagnostic != null) - ThrowExceptionFromDiagnostic(x.Key, x.Value, diagnostic); - return tree; - }).ToArray(); - - var refs = References; - - var compilationOptions = new CSharpCompilationOptions( - OutputKind.DynamicallyLinkedLibrary, - assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default, - optimizationLevel: Debug ? OptimizationLevel.Debug : OptimizationLevel.Release - ); - var compilation = CSharpCompilation.Create( - assemblyName, - /*syntaxTrees:*/ trees, - /*references:*/ refs, - compilationOptions); - - return compilation; - } - - // compile files into a Dll - // used by ModelsBuilderBackOfficeController in [Live]Dll mode, to compile the models to disk - public void Compile(string assemblyName, IDictionary files, string binPath) - { - var assemblyPath = Path.Combine(binPath, assemblyName + ".dll"); - using (var stream = new FileStream(assemblyPath, FileMode.Create)) - { - Compile(assemblyName, files, stream); - } - - // this is how we'd create the pdb: - /* - var pdbPath = Path.Combine(binPath, assemblyName + ".pdb"); - - // create the compilation - var compilation = GetCompilation(assemblyName, files); - - // check diagnostics for errors (not warnings) - foreach (var diag in compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error)) - ThrowExceptionFromDiagnostic(files, diag); - - // emit - var result = compilation.Emit(assemblyPath, pdbPath); - if (result.Success) return; - - // deal with errors - var diagnostic = result.Diagnostics.First(x => x.Severity == DiagnosticSeverity.Error); - ThrowExceptionFromDiagnostic(files, diagnostic); - */ - } - - // compile files into an assembly - public Assembly Compile(string assemblyName, IDictionary files) - { - using (var stream = new MemoryStream()) - { - Compile(assemblyName, files, stream); - return Assembly.Load(stream.GetBuffer()); - } - } - - // compile one file into an assembly - public Assembly Compile(string assemblyName, string path, string code) - { - using (var stream = new MemoryStream()) - { - Compile(assemblyName, new Dictionary { { path, code } }, stream); - return Assembly.Load(stream.GetBuffer()); - } - } - - // compiles files into a stream - public void Compile(string assemblyName, IDictionary files, Stream stream) - { - // create the compilation - var compilation = GetCompilation(assemblyName, files); - - // check diagnostics for errors (not warnings) - foreach (var diag in compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error)) - ThrowExceptionFromDiagnostic(files, diag); - - // emit - var result = compilation.Emit(stream); - if (result.Success) return; - - // deal with errors - var diagnostic = result.Diagnostics.First(x => x.Severity == DiagnosticSeverity.Error); - ThrowExceptionFromDiagnostic(files, diagnostic); - } - - // compiles one file into a stream - public void Compile(string assemblyName, string path, string code, Stream stream) - { - Compile(assemblyName, new Dictionary { { path, code } }, stream); - } - - private static void ThrowExceptionFromDiagnostic(IDictionary files, Diagnostic diagnostic) - { - var message = diagnostic.GetMessage(); - if (diagnostic.Location == Location.None) - throw new CompilerException(message); - - var position = diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1; - var path = diagnostic.Location.SourceTree.FilePath; - var code = files.ContainsKey(path) ? files[path] : string.Empty; - throw new CompilerException(message, path, code, position); - } - - private static void ThrowExceptionFromDiagnostic(string path, string code, Diagnostic diagnostic) - { - var message = diagnostic.GetMessage(); - if (diagnostic.Location == Location.None) - throw new CompilerException(message); - - var position = diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1; - throw new CompilerException(message, path, code, position); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/CompilerException.cs b/src/Umbraco.ModelsBuilder/Building/CompilerException.cs deleted file mode 100644 index e978f67ae5..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/CompilerException.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder.Building -{ - public class CompilerException : Exception - { - public CompilerException(string message) - : base(message) - { } - - public CompilerException(string message, string path, string sourceCode, int line) - : base(message) - { - Path = path; - SourceCode = sourceCode; - Line = line; - } - - public string Path { get; } = string.Empty; - - public string SourceCode { get; } = string.Empty; - - public int Line { get; } = -1; - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/ParseResult.cs b/src/Umbraco.ModelsBuilder/Building/ParseResult.cs deleted file mode 100644 index d1f61363ff..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/ParseResult.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Umbraco.ModelsBuilder.Building -{ - /// - /// Contains the result of a code parsing. - /// - internal class ParseResult - { - // "alias" is the umbraco alias - // content "name" is the complete name eg Foo.Bar.Name - // property "name" is just the local name - - // see notes in IgnoreContentTypeAttribute - - private readonly HashSet _ignoredContent - = new HashSet(StringComparer.InvariantCultureIgnoreCase); - //private readonly HashSet _ignoredMixin - // = new HashSet(StringComparer.InvariantCultureIgnoreCase); - //private readonly HashSet _ignoredMixinProperties - // = new HashSet(StringComparer.InvariantCultureIgnoreCase); - private readonly Dictionary _renamedContent - = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - private readonly HashSet _withImplementContent - = new HashSet(StringComparer.InvariantCultureIgnoreCase); - private readonly Dictionary> _ignoredProperty - = new Dictionary>(); - private readonly Dictionary> _renamedProperty - = new Dictionary>(); - private readonly Dictionary _contentBase - = new Dictionary(); - private readonly Dictionary _contentInterfaces - = new Dictionary(); - private readonly List _usingNamespaces - = new List(); - private readonly Dictionary> _staticMixins - = new Dictionary>(); - private readonly HashSet _withCtor - = new HashSet(StringComparer.InvariantCultureIgnoreCase); - - public static readonly ParseResult Empty = new ParseResult(); - - private class StaticMixinMethodInfo - { - public StaticMixinMethodInfo(string contentName, string methodName, string returnType, string paramType) - { - ContentName = contentName; - MethodName = methodName; - //ReturnType = returnType; - //ParamType = paramType; - } - - // short name eg Type1 - public string ContentName { get; private set; } - - // short name eg GetProp1 - public string MethodName { get; private set; } - - // those types cannot be FQ because when parsing, some of them - // might not exist since we're generating them... and so prob. - // that info is worthless - not using it anyway at the moment... - - //public string ReturnType { get; private set; } - //public string ParamType { get; private set; } - } - - #region Declare - - // content with that alias should not be generated - // alias can end with a * (wildcard) - public void SetIgnoredContent(string contentAlias /*, bool ignoreContent, bool ignoreMixin, bool ignoreMixinProperties*/) - { - //if (ignoreContent) - _ignoredContent.Add(contentAlias); - //if (ignoreMixin) - // _ignoredMixin.Add(contentAlias); - //if (ignoreMixinProperties) - // _ignoredMixinProperties.Add(contentAlias); - } - - // content with that alias should be generated with a different name - public void SetRenamedContent(string contentAlias, string contentName, bool withImplement) - { - _renamedContent[contentAlias] = contentName; - if (withImplement) - _withImplementContent.Add(contentAlias); - } - - // property with that alias should not be generated - // applies to content name and any content that implements it - // here, contentName may be an interface - // alias can end with a * (wildcard) - public void SetIgnoredProperty(string contentName, string propertyAlias) - { - HashSet ignores; - if (!_ignoredProperty.TryGetValue(contentName, out ignores)) - ignores = _ignoredProperty[contentName] = new HashSet(StringComparer.InvariantCultureIgnoreCase); - ignores.Add(propertyAlias); - } - - // property with that alias should be generated with a different name - // applies to content name and any content that implements it - // here, contentName may be an interface - public void SetRenamedProperty(string contentName, string propertyAlias, string propertyName) - { - Dictionary renames; - if (!_renamedProperty.TryGetValue(contentName, out renames)) - renames = _renamedProperty[contentName] = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - renames[propertyAlias] = propertyName; - } - - // content with that name has a base class so no need to generate one - public void SetContentBaseClass(string contentName, string baseName) - { - if (baseName.ToLowerInvariant() != "object") - _contentBase[contentName] = baseName; - } - - // content with that name implements the interfaces - public void SetContentInterfaces(string contentName, IEnumerable interfaceNames) - { - _contentInterfaces[contentName] = interfaceNames.ToArray(); - } - - public void SetModelsBaseClassName(string modelsBaseClassName) - { - ModelsBaseClassName = modelsBaseClassName; - } - - public void SetModelsNamespace(string modelsNamespace) - { - ModelsNamespace = modelsNamespace; - } - - public void SetUsingNamespace(string usingNamespace) - { - _usingNamespaces.Add(usingNamespace); - } - - public void SetStaticMixinMethod(string contentName, string methodName, string returnType, string paramType) - { - if (!_staticMixins.ContainsKey(contentName)) - _staticMixins[contentName] = new List(); - - _staticMixins[contentName].Add(new StaticMixinMethodInfo(contentName, methodName, returnType, paramType)); - } - - public void SetHasCtor(string contentName) - { - _withCtor.Add(contentName); - } - - #endregion - - #region Query - - public bool IsIgnored(string contentAlias) - { - return IsContentOrMixinIgnored(contentAlias, _ignoredContent); - } - - //public bool IsMixinIgnored(string contentAlias) - //{ - // return IsContentOrMixinIgnored(contentAlias, _ignoredMixin); - //} - - //public bool IsMixinPropertiesIgnored(string contentAlias) - //{ - // return IsContentOrMixinIgnored(contentAlias, _ignoredMixinProperties); - //} - - private static bool IsContentOrMixinIgnored(string contentAlias, HashSet ignored) - { - if (ignored.Contains(contentAlias)) return true; - return ignored - .Where(x => x.EndsWith("*")) - .Select(x => x.Substring(0, x.Length - 1)) - .Any(x => contentAlias.StartsWith(x, StringComparison.InvariantCultureIgnoreCase)); - } - - public bool HasContentBase(string contentName) - { - return _contentBase.ContainsKey(contentName); - } - - public bool IsContentRenamed(string contentAlias) - { - return _renamedContent.ContainsKey(contentAlias); - } - - public bool HasContentImplement(string contentAlias) - { - return _withImplementContent.Contains(contentAlias); - } - - public string ContentClrName(string contentAlias) - { - string name; - return (_renamedContent.TryGetValue(contentAlias, out name)) ? name : null; - } - - public bool IsPropertyIgnored(string contentName, string propertyAlias) - { - HashSet ignores; - if (_ignoredProperty.TryGetValue(contentName, out ignores)) - { - if (ignores.Contains(propertyAlias)) return true; - if (ignores - .Where(x => x.EndsWith("*")) - .Select(x => x.Substring(0, x.Length - 1)) - .Any(x => propertyAlias.StartsWith(x, StringComparison.InvariantCultureIgnoreCase))) - return true; - } - string baseName; - if (_contentBase.TryGetValue(contentName, out baseName) - && IsPropertyIgnored(baseName, propertyAlias)) return true; - string[] interfaceNames; - if (_contentInterfaces.TryGetValue(contentName, out interfaceNames) - && interfaceNames.Any(interfaceName => IsPropertyIgnored(interfaceName, propertyAlias))) return true; - return false; - } - - public string PropertyClrName(string contentName, string propertyAlias) - { - Dictionary renames; - string name; - if (_renamedProperty.TryGetValue(contentName, out renames) - && renames.TryGetValue(propertyAlias, out name)) return name; - string baseName; - if (_contentBase.TryGetValue(contentName, out baseName) - && null != (name = PropertyClrName(baseName, propertyAlias))) return name; - string[] interfaceNames; - if (_contentInterfaces.TryGetValue(contentName, out interfaceNames) - && null != (name = interfaceNames - .Select(interfaceName => PropertyClrName(interfaceName, propertyAlias)) - .FirstOrDefault(x => x != null))) return name; - return null; - } - - public bool HasModelsBaseClassName - { - get { return !string.IsNullOrWhiteSpace(ModelsBaseClassName); } - } - - public string ModelsBaseClassName { get; private set; } - - public bool HasModelsNamespace - { - get { return !string.IsNullOrWhiteSpace(ModelsNamespace); } - } - - public string ModelsNamespace { get; private set; } - - public IEnumerable UsingNamespaces - { - get { return _usingNamespaces; } - } - - public IEnumerable StaticMixinMethods(string contentName) - { - return _staticMixins.ContainsKey(contentName) - ? _staticMixins[contentName].Select(x => x.MethodName) - : Enumerable.Empty() ; - } - - public bool HasCtor(string contentName) - { - return _withCtor.Contains(contentName); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs b/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs deleted file mode 100644 index d195846411..0000000000 --- a/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Umbraco.ModelsBuilder.Configuration -{ - /// - /// Defines the CLR name sources. - /// - public enum ClrNameSource - { - /// - /// No source. - /// - Nothing = 0, - - /// - /// Use the name as source. - /// - Name, - - /// - /// Use the alias as source. - /// - Alias, - - /// - /// Use the alias directly. - /// - RawAlias - } -} diff --git a/src/Umbraco.ModelsBuilder/Configuration/Config.cs b/src/Umbraco.ModelsBuilder/Configuration/Config.cs deleted file mode 100644 index ebfe4be709..0000000000 --- a/src/Umbraco.ModelsBuilder/Configuration/Config.cs +++ /dev/null @@ -1,357 +0,0 @@ -using System; -using System.Configuration; -using System.IO; -using System.Reflection; -using System.Web.Configuration; -using System.Web.Hosting; -using Microsoft.CodeAnalysis.CSharp; -using Umbraco.Core; - -namespace Umbraco.ModelsBuilder.Configuration -{ - /// - /// Represents the models builder configuration. - /// - public class Config - { - private static Config _value; - - /// - /// Gets the configuration - internal so that the UmbracoConfig extension - /// can get the value to initialize its own value. Either a value has - /// been provided via the Setup method, or a new instance is created, which - /// will load settings from the config file. - /// - internal static Config Value => _value ?? new Config(); - - /// - /// Sets the configuration programmatically. - /// - /// The configuration. - /// - /// Once the configuration has been accessed via the UmbracoConfig extension, - /// it cannot be changed anymore, and using this method will achieve nothing. - /// For tests, see UmbracoConfigExtensions.ResetConfig(). - /// - public static void Setup(Config config) - { - _value = config; - } - - internal const string DefaultStaticMixinGetterPattern = "Get{0}"; - internal const LanguageVersion DefaultLanguageVersion = LanguageVersion.CSharp6; - internal const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels"; - internal const ClrNameSource DefaultClrNameSource = ClrNameSource.Alias; // for legacy reasons - internal const string DefaultModelsDirectory = "~/App_Data/Models"; - - /// - /// Initializes a new instance of the class. - /// - private Config() - { - const string prefix = "Umbraco.ModelsBuilder."; - - // giant kill switch, default: false - // must be explicitely set to true for anything else to happen - Enable = ConfigurationManager.AppSettings[prefix + "Enable"] == "true"; - - // ensure defaults are initialized for tests - StaticMixinGetterPattern = DefaultStaticMixinGetterPattern; - LanguageVersion = DefaultLanguageVersion; - ModelsNamespace = DefaultModelsNamespace; - ClrNameSource = DefaultClrNameSource; - ModelsDirectory = HostingEnvironment.IsHosted - ? HostingEnvironment.MapPath(DefaultModelsDirectory) - : DefaultModelsDirectory.TrimStart("~/"); - DebugLevel = 0; - - // stop here, everything is false - if (!Enable) return; - - // mode - var modelsMode = ConfigurationManager.AppSettings[prefix + "ModelsMode"]; - if (!string.IsNullOrWhiteSpace(modelsMode)) - { - switch (modelsMode) - { - case nameof(ModelsMode.Nothing): - ModelsMode = ModelsMode.Nothing; - break; - case nameof(ModelsMode.PureLive): - ModelsMode = ModelsMode.PureLive; - break; - case nameof(ModelsMode.Dll): - ModelsMode = ModelsMode.Dll; - break; - case nameof(ModelsMode.LiveDll): - ModelsMode = ModelsMode.LiveDll; - break; - case nameof(ModelsMode.AppData): - ModelsMode = ModelsMode.AppData; - break; - case nameof(ModelsMode.LiveAppData): - ModelsMode = ModelsMode.LiveAppData; - break; - default: - throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." - + " Note that modes are case-sensitive."); - } - } - - // default: false - EnableApi = ConfigurationManager.AppSettings[prefix + "EnableApi"].InvariantEquals("true"); - AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true"); - - // default: true - EnableFactory = !ConfigurationManager.AppSettings[prefix + "EnableFactory"].InvariantEquals("false"); - StaticMixinGetters = !ConfigurationManager.AppSettings[prefix + "StaticMixinGetters"].InvariantEquals("false"); - FlagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false"); - - // default: initialized above with DefaultModelsNamespace const - var value = ConfigurationManager.AppSettings[prefix + "ModelsNamespace"]; - if (!string.IsNullOrWhiteSpace(value)) - ModelsNamespace = value; - - // default: initialized above with DefaultStaticMixinGetterPattern const - value = ConfigurationManager.AppSettings[prefix + "StaticMixinGetterPattern"]; - if (!string.IsNullOrWhiteSpace(value)) - StaticMixinGetterPattern = value; - - // default: initialized above with DefaultLanguageVersion const - value = ConfigurationManager.AppSettings[prefix + "LanguageVersion"]; - if (!string.IsNullOrWhiteSpace(value)) - { - LanguageVersion lv; - if (!Enum.TryParse(value, true, out lv)) - throw new ConfigurationErrorsException($"Invalid language version \"{value}\"."); - LanguageVersion = lv; - } - - // default: initialized above with DefaultClrNameSource const - value = ConfigurationManager.AppSettings[prefix + "ClrNameSource"]; - if (!string.IsNullOrWhiteSpace(value)) - { - switch (value) - { - case nameof(ClrNameSource.Nothing): - ClrNameSource = ClrNameSource.Nothing; - break; - case nameof(ClrNameSource.Alias): - ClrNameSource = ClrNameSource.Alias; - break; - case nameof(ClrNameSource.RawAlias): - ClrNameSource = ClrNameSource.RawAlias; - break; - case nameof(ClrNameSource.Name): - ClrNameSource = ClrNameSource.Name; - break; - default: - throw new ConfigurationErrorsException($"ClrNameSource \"{value}\" is not a valid source." - + " Note that sources are case-sensitive."); - } - } - - // default: initialized above with DefaultModelsDirectory const - value = ConfigurationManager.AppSettings[prefix + "ModelsDirectory"]; - if (!string.IsNullOrWhiteSpace(value)) - { - var root = HostingEnvironment.IsHosted - ? HostingEnvironment.MapPath("~/") - : Directory.GetCurrentDirectory(); - if (root == null) - throw new ConfigurationErrorsException("Could not determine root directory."); - - // GetModelsDirectory will ensure that the path is safe - ModelsDirectory = GetModelsDirectory(root, value, AcceptUnsafeModelsDirectory); - } - - // default: 0 - value = ConfigurationManager.AppSettings[prefix + "DebugLevel"]; - if (!string.IsNullOrWhiteSpace(value)) - { - int debugLevel; - if (!int.TryParse(value, out debugLevel)) - throw new ConfigurationErrorsException($"Invalid debug level \"{value}\"."); - DebugLevel = debugLevel; - } - - // not flagging if not generating, or live (incl. pure) - if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive()) - FlagOutOfDateModels = false; - } - - /// - /// Initializes a new instance of the class. - /// - public Config( - bool enable = false, - ModelsMode modelsMode = ModelsMode.Nothing, - bool enableApi = true, - string modelsNamespace = null, - bool enableFactory = true, - LanguageVersion languageVersion = DefaultLanguageVersion, - bool staticMixinGetters = true, - string staticMixinGetterPattern = null, - bool flagOutOfDateModels = true, - ClrNameSource clrNameSource = DefaultClrNameSource, - string modelsDirectory = null, - bool acceptUnsafeModelsDirectory = false, - int debugLevel = 0) - { - Enable = enable; - ModelsMode = modelsMode; - - EnableApi = enableApi; - ModelsNamespace = string.IsNullOrWhiteSpace(modelsNamespace) ? DefaultModelsNamespace : modelsNamespace; - EnableFactory = enableFactory; - LanguageVersion = languageVersion; - StaticMixinGetters = staticMixinGetters; - StaticMixinGetterPattern = string.IsNullOrWhiteSpace(staticMixinGetterPattern) ? DefaultStaticMixinGetterPattern : staticMixinGetterPattern; - FlagOutOfDateModels = flagOutOfDateModels; - ClrNameSource = clrNameSource; - ModelsDirectory = string.IsNullOrWhiteSpace(modelsDirectory) ? DefaultModelsDirectory : modelsDirectory; - AcceptUnsafeModelsDirectory = acceptUnsafeModelsDirectory; - DebugLevel = debugLevel; - } - - // internal for tests - internal static string GetModelsDirectory(string root, string config, bool acceptUnsafe) - { - // making sure it is safe, ie under the website root, - // unless AcceptUnsafeModelsDirectory and then everything is OK. - - if (!Path.IsPathRooted(root)) - throw new ConfigurationErrorsException($"Root is not rooted \"{root}\"."); - - if (config.StartsWith("~/")) - { - var dir = Path.Combine(root, config.TrimStart("~/")); - - // sanitize - GetFullPath will take care of any relative - // segments in path, eg '../../foo.tmp' - it may throw a SecurityException - // if the combined path reaches illegal parts of the filesystem - dir = Path.GetFullPath(dir); - root = Path.GetFullPath(root); - - if (!dir.StartsWith(root) && !acceptUnsafe) - throw new ConfigurationErrorsException($"Invalid models directory \"{config}\"."); - - return dir; - } - - if (acceptUnsafe) - return Path.GetFullPath(config); - - throw new ConfigurationErrorsException($"Invalid models directory \"{config}\"."); - } - - /// - /// Gets a value indicating whether the whole models experience is enabled. - /// - /// - /// If this is false then absolutely nothing happens. - /// Default value is false which means that unless we have this setting, nothing happens. - /// - public bool Enable { get; } - - /// - /// Gets the models mode. - /// - public ModelsMode ModelsMode { get; } - - /// - /// Gets a value indicating whether to serve the API. - /// - public bool ApiServer => EnableApi && ApiInstalled && IsDebug; - - /// - /// Gets a value indicating whether to enable the API. - /// - /// - /// Default value is true. - /// The API is used by the Visual Studio extension and the console tool to talk to Umbraco - /// and retrieve the content types. It needs to be enabled so the extension & tool can work. - /// - public bool EnableApi { get; } - - /// - /// Gets a value indicating whether the API is installed. - /// - // fixme - this is now always true as the API is part of Core - public bool ApiInstalled => true; - - /// - /// Gets a value indicating whether system.web/compilation/@debug is true. - /// - public bool IsDebug - { - get - { - var section = (CompilationSection) ConfigurationManager.GetSection("system.web/compilation"); - return section != null && section.Debug; - } - } - - /// - /// Gets the models namespace. - /// - /// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied. - public string ModelsNamespace { get; } - - /// - /// Gets a value indicating whether we should enable the models factory. - /// - /// Default value is true because no factory is enabled by default in Umbraco. - public bool EnableFactory { get; } - - /// - /// Gets the Roslyn parser language version. - /// - /// Default value is CSharp6. - public LanguageVersion LanguageVersion { get; } - - /// - /// Gets a value indicating whether to generate static mixin getters. - /// - /// Default value is false for backward compat reaons. - public bool StaticMixinGetters { get; } - - /// - /// Gets the string pattern for mixin properties static getter name. - /// - /// Default value is "GetXxx". Standard string format. - public string StaticMixinGetterPattern { get; } - - /// - /// Gets a value indicating whether we should flag out-of-date models. - /// - /// Models become out-of-date when data types or content types are updated. When this - /// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are - /// generated through the dashboard, the files is cleared. Default value is false. - public bool FlagOutOfDateModels { get; } - - /// - /// Gets the CLR name source. - /// - public ClrNameSource ClrNameSource { get; } - - /// - /// Gets the models directory. - /// - /// Default is ~/App_Data/Models but that can be changed. - public string ModelsDirectory { get; } - - /// - /// Gets a value indicating whether to accept an unsafe value for ModelsDirectory. - /// - /// An unsafe value is an absolute path, or a relative path pointing outside - /// of the website root. - public bool AcceptUnsafeModelsDirectory { get; } - - /// - /// Gets a value indicating the debug log level. - /// - /// 0 means minimal (safe on live site), anything else means more and more details (maybe not safe). - public int DebugLevel { get; } - } -} diff --git a/src/Umbraco.ModelsBuilder/Configuration/UmbracoConfigExtensions.cs b/src/Umbraco.ModelsBuilder/Configuration/UmbracoConfigExtensions.cs deleted file mode 100644 index acc587e779..0000000000 --- a/src/Umbraco.ModelsBuilder/Configuration/UmbracoConfigExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Threading; -using Umbraco.Core.Configuration; - -namespace Umbraco.ModelsBuilder.Configuration -{ - /// - /// Provides extension methods for the class. - /// - public static class UmbracoConfigExtensions - { - private static Config _config; - - /// - /// Gets the models builder configuration. - /// - /// The umbraco configuration. - /// The models builder configuration. - /// Getting the models builder configuration freezes its state, - /// and any attempt at modifying the configuration using the Setup method - /// will be ignored. - public static Config ModelsBuilder(this UmbracoConfig umbracoConfig) - { - // capture the current Config2.Default value, cannot change anymore - LazyInitializer.EnsureInitialized(ref _config, () => Config.Value); - return _config; - } - - // internal for tests - internal static void ResetConfig() - { - _config = null; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs b/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs deleted file mode 100644 index 9e5741805e..0000000000 --- a/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Text; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Umbraco; - -namespace Umbraco.ModelsBuilder.Dashboard -{ - internal static class BuilderDashboardHelper - { - public static bool CanGenerate() - { - return UmbracoConfig.For.ModelsBuilder().ModelsMode.SupportsExplicitGeneration(); - } - - public static bool GenerateCausesRestart() - { - return UmbracoConfig.For.ModelsBuilder().ModelsMode.IsAnyDll(); - } - - public static bool AreModelsOutOfDate() - { - return OutOfDateModelsStatus.IsOutOfDate; - } - - public static string LastError() - { - return ModelsGenerationError.GetLastError(); - } - - public static string Text() - { - var config = UmbracoConfig.For.ModelsBuilder(); - if (!config.Enable) - return "ModelsBuilder is disabled
(the .Enable key is missing, or its value is not 'true')."; - - var sb = new StringBuilder(); - - sb.Append("ModelsBuilder is enabled, with the following configuration:"); - - sb.Append("
    "); - - sb.Append("
  • The models factory is "); - sb.Append(config.EnableFactory || config.ModelsMode == ModelsMode.PureLive - ? "enabled" - : "not enabled. Umbraco will not use models"); - sb.Append(".
  • "); - - sb.Append("
  • The API is "); - if (config.ApiInstalled && config.EnableApi) - { - sb.Append("installed and enabled"); - if (!config.IsDebug) sb.Append(".
    However, the API runs only with debug compilation mode"); - } - else if (config.ApiInstalled || config.EnableApi) - sb.Append(config.ApiInstalled ? "installed but not enabled" : "enabled but not installed"); - else sb.Append("neither installed nor enabled"); - sb.Append(".
    "); - if (!config.ApiServer) - sb.Append("External tools such as Visual Studio cannot use the API"); - else - sb.Append("The API endpoint is open on this server"); - sb.Append(".
  • "); - - sb.Append(config.ModelsMode != ModelsMode.Nothing - ? $"
  • {config.ModelsMode} models are enabled.
  • " - : "
  • No models mode is specified: models will not be generated.
  • "); - - sb.Append($"
  • Models namespace is {config.ModelsNamespace}.
  • "); - - sb.Append("
  • Static mixin getters are "); - sb.Append(config.StaticMixinGetters ? "enabled" : "disabled"); - if (config.StaticMixinGetters) - { - sb.Append(". The pattern for getters is "); - sb.Append(string.IsNullOrWhiteSpace(config.StaticMixinGetterPattern) - ? "not configured (will use default)" - : $"\"{config.StaticMixinGetterPattern}\""); - } - sb.Append(".
  • "); - - sb.Append("
  • Tracking of out-of-date models is "); - sb.Append(config.FlagOutOfDateModels ? "enabled" : "not enabled"); - sb.Append(".
  • "); - - sb.Append("
"); - - return sb.ToString(); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs b/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs deleted file mode 100644 index da77bfa958..0000000000 --- a/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.ModelsBuilder -{ - public static class EnumerableExtensions - { - public static void RemoveAll(this IList list, Func predicate) - { - for (var i = 0; i < list.Count; i++) - { - if (predicate(list[i])) - { - list.RemoveAt(i--); // i-- is important here! - } - } - } - - public static IEnumerable And(this IEnumerable enumerable, T item) - { - foreach (var x in enumerable) yield return x; - yield return item; - } - - public static IEnumerable AndIfNotNull(this IEnumerable enumerable, T item) - where T : class - { - foreach (var x in enumerable) yield return x; - if (item != null) - yield return item; - } - } -} diff --git a/src/Umbraco.ModelsBuilder/IgnoreContentTypeAttribute.cs b/src/Umbraco.ModelsBuilder/IgnoreContentTypeAttribute.cs deleted file mode 100644 index e5ab3a2e35..0000000000 --- a/src/Umbraco.ModelsBuilder/IgnoreContentTypeAttribute.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Policy; -using System.Text; -using System.Threading.Tasks; -using Umbraco.ModelsBuilder; - -namespace Umbraco.ModelsBuilder -{ - // for the time being it's all-or-nothing - // when the content type is ignored then - // - no class is generated for that content type - // - no class is generated for any child of that class - // - no interface is generated for that content type as a mixin - // - and it is ignored as a mixin ie its properties are not generated - // in the future we may way to do - // [assembly:IgnoreContentType("foo", ContentTypeIgnorable.ContentType|ContentTypeIgnorable.Mixin|ContentTypeIgnorable.MixinProperties)] - // so that we can - // - generate a class for that content type, or not - // - if not generated, generate children or not - // - if generate children, include properties or not - // - generate an interface for that content type as a mixin - // - if not generated, still generate properties in content types implementing the mixin or not - // but... I'm not even sure it makes sense - // if we don't want it... we don't want it. - - // about ignoring - // - content (don't generate the content, use as mixin) - // - mixin (don't generate the interface, use the properties) - // - mixin properties (generate the interface, not the properties) - // - mixin: local only or children too... - - /// - /// Indicates that no model should be generated for a specified content type alias. - /// - /// When a content type is ignored, its descendants are also ignored. - /// Supports trailing wildcard eg "foo*". - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public sealed class IgnoreContentTypeAttribute : Attribute - { - public IgnoreContentTypeAttribute(string alias /*, bool ignoreContent = true, bool ignoreMixin = true, bool ignoreMixinProperties = true*/) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/IgnorePropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder/IgnorePropertyTypeAttribute.cs deleted file mode 100644 index 4dce0f9b7f..0000000000 --- a/src/Umbraco.ModelsBuilder/IgnorePropertyTypeAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates that no model should be generated for a specified property type alias. - /// - /// Supports trailing wildcard eg "foo*". - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public sealed class IgnorePropertyTypeAttribute : Attribute - { - public IgnorePropertyTypeAttribute(string alias) - {} - } -} diff --git a/src/Umbraco.ModelsBuilder/ImplementContentTypeAttribute.cs b/src/Umbraco.ModelsBuilder/ImplementContentTypeAttribute.cs deleted file mode 100644 index 142f115b07..0000000000 --- a/src/Umbraco.ModelsBuilder/ImplementContentTypeAttribute.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - // NOTE - // that attribute should inherit from PublishedModelAttribute - // so we do not have different syntaxes - // but... is sealed at the moment. - - /// - /// Indicates that a (partial) class defines the model type for a specific alias. - /// - /// Though a model will be generated - so that is the way to register a rename. - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class ImplementContentTypeAttribute : Attribute - { - public ImplementContentTypeAttribute(string alias) - { } - } -} diff --git a/src/Umbraco.ModelsBuilder/ModelsBaseClassAttribute.cs b/src/Umbraco.ModelsBuilder/ModelsBaseClassAttribute.cs deleted file mode 100644 index 3c401b7fdb..0000000000 --- a/src/Umbraco.ModelsBuilder/ModelsBaseClassAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates the default base class for models. - /// - /// Otherwise it is PublishedContentModel. Would make sense to inherit from PublishedContentModel. - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] - public sealed class ModelsBaseClassAttribute : Attribute - { - public ModelsBaseClassAttribute(Type type) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/ModelsNamespaceAttribute.cs b/src/Umbraco.ModelsBuilder/ModelsNamespaceAttribute.cs deleted file mode 100644 index 1b1d62d9bc..0000000000 --- a/src/Umbraco.ModelsBuilder/ModelsNamespaceAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates the models namespace. - /// - /// Will override anything else that might come from settings. - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] - public sealed class ModelsNamespaceAttribute : Attribute - { - public ModelsNamespaceAttribute(string modelsNamespace) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/ModelsUsingAttribute.cs b/src/Umbraco.ModelsBuilder/ModelsUsingAttribute.cs deleted file mode 100644 index 8fe1335631..0000000000 --- a/src/Umbraco.ModelsBuilder/ModelsUsingAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Policy; -using System.Text; -using System.Threading.Tasks; -using Umbraco.ModelsBuilder; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates namespaces that should be used in generated models (in using clauses). - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public sealed class ModelsUsingAttribute : Attribute - { - public ModelsUsingAttribute(string usingNamespace) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs b/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs deleted file mode 100644 index a2f8d1ae1e..0000000000 --- a/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("Umbraco.ModelsBuilder")] -[assembly: AssemblyDescription("Umbraco ModelsBuilder")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("Umbraco CMS")] - -[assembly: ComVisible(false)] -[assembly: Guid("7020a059-c0d1-43a0-8efd-23591a0c9af6")] - -// code analysis -// IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "~_~")] diff --git a/src/Umbraco.ModelsBuilder/PublishedPropertyTypeExtensions.cs b/src/Umbraco.ModelsBuilder/PublishedPropertyTypeExtensions.cs deleted file mode 100644 index b67ba54432..0000000000 --- a/src/Umbraco.ModelsBuilder/PublishedPropertyTypeExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.ModelsBuilder -{ - public static class PublishedPropertyTypeExtensions - { - // fixme - need to rewrite that one - we don't have prevalues anymore - //public static KeyValuePair[] PreValues(this PublishedPropertyType propertyType) - //{ - // return ApplicationContext.Current.Services.DataTypeService - // .GetPreValuesCollectionByDataTypeId(propertyType.DataType.Id) - // .PreValuesAsArray - // .Select(x => new KeyValuePair(x.Id, x.Value)) - // .ToArray(); - //} - } -} diff --git a/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs b/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs deleted file mode 100644 index dfe369dc21..0000000000 --- a/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates that an Assembly is a PureLive models assembly. - /// - /// Though technically not required, ie models will work without it, the attribute - /// can be used by Umbraco view models binder to figure out whether the model type comes - /// from a PureLive Assembly. - [Obsolete("Should use ModelsBuilderAssemblyAttribute but that requires a change in Umbraco Core.")] - [AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] - public sealed class PureLiveAssemblyAttribute : Attribute - { } -} diff --git a/src/Umbraco.ModelsBuilder/RenameContentTypeAttribute.cs b/src/Umbraco.ModelsBuilder/RenameContentTypeAttribute.cs deleted file mode 100644 index 0f985e70b3..0000000000 --- a/src/Umbraco.ModelsBuilder/RenameContentTypeAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates a model name for a specified content alias. - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public sealed class RenameContentTypeAttribute : Attribute - { - public RenameContentTypeAttribute(string alias, string name) - {} - } -} diff --git a/src/Umbraco.ModelsBuilder/RenamePropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder/RenamePropertyTypeAttribute.cs deleted file mode 100644 index 0d8fd31b63..0000000000 --- a/src/Umbraco.ModelsBuilder/RenamePropertyTypeAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates a model name for a specified property alias. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public sealed class RenamePropertyTypeAttribute : Attribute - { - public RenamePropertyTypeAttribute(string alias, string name) - {} - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs deleted file mode 100644 index b6c37a3558..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Threading; -using System.Web; -using System.Web.Hosting; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Umbraco; -using Umbraco.Web.Cache; - -// will install only if configuration says it needs to be installed -[assembly: PreApplicationStartMethod(typeof(LiveModelsProviderModule), "Install")] - -namespace Umbraco.ModelsBuilder.Umbraco -{ - // supports LiveDll and LiveAppData - but not PureLive - public sealed class LiveModelsProvider - { - private static UmbracoServices _umbracoServices; - private static Mutex _mutex; - private static int _req; - - internal static bool IsEnabled - { - get - { - var config = UmbracoConfig.For.ModelsBuilder(); - return config.ModelsMode.IsLiveNotPure(); - // we do not manage pure live here - } - } - - internal static void Install(UmbracoServices umbracoServices) - { - // just be sure - if (!IsEnabled) - return; - - _umbracoServices = umbracoServices; - - // initialize mutex - // ApplicationId will look like "/LM/W3SVC/1/Root/AppName" - // name is system-wide and must be less than 260 chars - var name = HostingEnvironment.ApplicationID + "/UmbracoLiveModelsProvider"; - _mutex = new Mutex(false, name); - - // anything changes, and we want to re-generate models. - ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; - DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; - - // at the end of a request since we're restarting the pool - // NOTE - this does NOT trigger - see module below - //umbracoApplication.EndRequest += GenerateModelsIfRequested; - } - - // NOTE - // Using HttpContext Items fails because CacheUpdated triggers within - // some asynchronous backend task where we seem to have no HttpContext. - - // So we use a static (non request-bound) var to register that models - // need to be generated. Could be by another request. Anyway. We could - // have collisions but... you know the risk. - - private static void RequestModelsGeneration(object sender, EventArgs args) - { - //HttpContext.Current.Items[this] = true; - Current.Logger.Debug("Requested to generate models."); - Interlocked.Exchange(ref _req, 1); - } - - public static void GenerateModelsIfRequested(object sender, EventArgs args) - { - //if (HttpContext.Current.Items[this] == null) return; - if (Interlocked.Exchange(ref _req, 0) == 0) return; - - // cannot use a simple lock here because we don't want another AppDomain - // to generate while we do... and there could be 2 AppDomains if the app restarts. - - try - { - Current.Logger.Debug("Generate models..."); - const int timeout = 2*60*1000; // 2 mins - _mutex.WaitOne(timeout); // wait until it is safe, and acquire - Current.Logger.Info("Generate models now."); - GenerateModels(); - ModelsGenerationError.Clear(); - Current.Logger.Info("Generated."); - } - catch (TimeoutException) - { - Current.Logger.Warn("Timeout, models were NOT generated."); - } - catch (Exception e) - { - ModelsGenerationError.Report("Failed to build Live models.", e); - Current.Logger.Error("Failed to generate models.", e); - } - finally - { - _mutex.ReleaseMutex(); // release - } - } - - private static void GenerateModels() - { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; - - var bin = HostingEnvironment.MapPath("~/bin"); - if (bin == null) - throw new Exception("Panic: bin is null."); - - var config = UmbracoConfig.For.ModelsBuilder(); - - // EnableDllModels will recycle the app domain - but this request will end properly - ModelsBuilderBackOfficeController.GenerateModels(_umbracoServices, modelsDirectory, config.ModelsMode.IsAnyDll() ? bin : null); - } - } - - // have to do this because it's the only way to subscribe to EndRequest, - // module is installed by assembly attribute at the top of this file - public class LiveModelsProviderModule : IHttpModule - { - public void Init(HttpApplication app) - { - app.EndRequest += LiveModelsProvider.GenerateModelsIfRequested; - } - - public void Dispose() - { - // nothing - } - - public static void Install() - { - if (!LiveModelsProvider.IsEnabled) - return; - - HttpApplication.RegisterModule(typeof(LiveModelsProviderModule)); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs deleted file mode 100644 index 19c9bda5da..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Runtime.Serialization; -using System.Text; -using System.Web.Hosting; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Dashboard; -using Umbraco.Web.Editors; -using Umbraco.Web.WebApi.Filters; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - /// - /// API controller for use in the Umbraco back office with Angular resources - /// - /// - /// We've created a different controller for the backoffice/angular specifically this is to ensure that the - /// correct CSRF security is adhered to for angular and it also ensures that this controller is not subseptipal to - /// global WebApi formatters being changed since this is always forced to only return Angular JSON Specific formats. - /// - [UmbracoApplicationAuthorize(Core.Constants.Applications.Developer)] - public class ModelsBuilderBackOfficeController : UmbracoAuthorizedJsonController - { - private readonly UmbracoServices _umbracoServices; - - public ModelsBuilderBackOfficeController(UmbracoServices umbracoServices) - { - _umbracoServices = umbracoServices; - } - - // invoked by the dashboard - // requires that the user is logged into the backoffice and has access to the developer section - // beware! the name of the method appears in modelsbuilder.controller.js - [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! - public HttpResponseMessage BuildModels() - { - try - { - if (!UmbracoConfig.For.ModelsBuilder().ModelsMode.SupportsExplicitGeneration()) - { - var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." }; - return Request.CreateResponse(HttpStatusCode.OK, result2, Configuration.Formatters.JsonFormatter); - } - - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; - - var bin = HostingEnvironment.MapPath("~/bin"); - if (bin == null) - throw new Exception("Panic: bin is null."); - - // EnableDllModels will recycle the app domain - but this request will end properly - GenerateModels(modelsDirectory, UmbracoConfig.For.ModelsBuilder().ModelsMode.IsAnyDll() ? bin : null); - - ModelsGenerationError.Clear(); - } - catch (Exception e) - { - ModelsGenerationError.Report("Failed to build models.", e); - } - - return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); - } - - // invoked by the back-office - // requires that the user is logged into the backoffice and has access to the developer section - [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! - public HttpResponseMessage GetModelsOutOfDateStatus() - { - var status = OutOfDateModelsStatus.IsEnabled - ? (OutOfDateModelsStatus.IsOutOfDate - ? new OutOfDateStatus { Status = OutOfDateType.OutOfDate } - : new OutOfDateStatus { Status = OutOfDateType.Current }) - : new OutOfDateStatus { Status = OutOfDateType.Unknown }; - - return Request.CreateResponse(HttpStatusCode.OK, status, Configuration.Formatters.JsonFormatter); - } - - // invoked by the back-office - // requires that the user is logged into the backoffice and has access to the developer section - // beware! the name of the method appears in modelsbuilder.controller.js - [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! - public HttpResponseMessage GetDashboard() - { - return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); - } - - private Dashboard GetDashboardResult() - { - return new Dashboard - { - Enable = UmbracoConfig.For.ModelsBuilder().Enable, - Text = BuilderDashboardHelper.Text(), - CanGenerate = BuilderDashboardHelper.CanGenerate(), - GenerateCausesRestart = BuilderDashboardHelper.GenerateCausesRestart(), - OutOfDateModels = BuilderDashboardHelper.AreModelsOutOfDate(), - LastError = BuilderDashboardHelper.LastError(), - }; - } - - private void GenerateModels(string modelsDirectory, string bin) - { - GenerateModels(_umbracoServices, modelsDirectory, bin); - } - - internal static void GenerateModels(UmbracoServices umbracoServices, string modelsDirectory, string bin) - { - if (!Directory.Exists(modelsDirectory)) - Directory.CreateDirectory(modelsDirectory); - - foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) - File.Delete(file); - - var typeModels = umbracoServices.GetAllTypes(); - - var ourFiles = Directory.GetFiles(modelsDirectory, "*.cs").ToDictionary(x => x, File.ReadAllText); - var parseResult = new CodeParser().ParseWithReferencedAssemblies(ourFiles); - var builder = new TextBuilder(typeModels, parseResult, UmbracoConfig.For.ModelsBuilder().ModelsNamespace); - - foreach (var typeModel in builder.GetModelsToGenerate()) - { - var sb = new StringBuilder(); - builder.Generate(sb, typeModel); - var filename = Path.Combine(modelsDirectory, typeModel.ClrName + ".generated.cs"); - File.WriteAllText(filename, sb.ToString()); - } - - // the idea was to calculate the current hash and to add it as an extra file to the compilation, - // in order to be able to detect whether a DLL is consistent with an environment - however the - // environment *might not* contain the local partial files, and thus it could be impossible to - // calculate the hash. So... maybe that's not a good idea after all? - /* - var currentHash = HashHelper.Hash(ourFiles, typeModels); - ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; -[assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")] -"; - */ - - if (bin != null) - { - foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) - ourFiles[file] = File.ReadAllText(file); - var compiler = new Compiler(); - compiler.Compile(builder.GetModelsNamespace(), ourFiles, bin); - } - - OutOfDateModelsStatus.Clear(); - } - - [DataContract] - internal class BuildResult - { - [DataMember(Name = "success")] - public bool Success; - [DataMember(Name = "message")] - public string Message; - } - - [DataContract] - internal class Dashboard - { - [DataMember(Name = "enable")] - public bool Enable; - [DataMember(Name = "text")] - public string Text; - [DataMember(Name = "canGenerate")] - public bool CanGenerate; - [DataMember(Name = "generateCausesRestart")] - public bool GenerateCausesRestart; - [DataMember(Name = "outOfDateModels")] - public bool OutOfDateModels; - [DataMember(Name = "lastError")] - public string LastError; - } - - internal enum OutOfDateType - { - OutOfDate, - Current, - Unknown = 100 - } - - [DataContract] - internal class OutOfDateStatus - { - [DataMember(Name = "status")] - public OutOfDateType Status { get; set; } - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs deleted file mode 100644 index a581319ba5..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web; -using System.Web.Mvc; -using System.Web.Routing; -using LightInject; -using Umbraco.Core; -using Umbraco.Core.Components; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; -using Umbraco.Core.Services.Implement; -using Umbraco.ModelsBuilder.Api; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.Web; -using Umbraco.Web.PublishedCache.NuCache; -using Umbraco.Web.UI.JavaScript; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - [RequiredComponent(typeof(NuCacheComponent))] - [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - public class ModelsBuilderComponent : UmbracoComponentBase, IUmbracoCoreComponent - { - public override void Compose(Composition composition) - { - base.Compose(composition); - - composition.Container.Register(new PerContainerLifetime()); - - var config = UmbracoConfig.For.ModelsBuilder(); - - if (config.ModelsMode == ModelsMode.PureLive) - ComposeForLiveModels(composition.Container); - else if (config.EnableFactory) - ComposeForDefaultModelsFactory(composition.Container); - - // always setup the dashboard - InstallServerVars(composition.Container.GetInstance().Level); - composition.Container.Register(typeof(ModelsBuilderBackOfficeController), new PerRequestLifeTime()); - - // setup the API if enabled (and in debug mode) - if (config.ApiServer) - composition.Container.Register(typeof(ModelsBuilderApiController), new PerRequestLifeTime()); - } - - public void Initialize(UmbracoServices umbracoServices) - { - var config = UmbracoConfig.For.ModelsBuilder(); - - if (config.Enable) - FileService.SavingTemplate += FileService_SavingTemplate; - - // fixme LiveModelsProvider should not be static - if (config.ModelsMode.IsLiveNotPure()) - LiveModelsProvider.Install(umbracoServices); - - // fixme OutOfDateModelsStatus should not be static - if (config.FlagOutOfDateModels) - OutOfDateModelsStatus.Install(); - } - - private void ComposeForDefaultModelsFactory(IServiceContainer container) - { - container.RegisterSingleton(factory - => new PublishedModelFactory(factory.GetInstance().GetTypes())); - } - - private void ComposeForLiveModels(IServiceContainer container) - { - container.RegisterSingleton(); - - // the following would add @using statement in every view so user's don't - // have to do it - however, then noone understands where the @using statement - // comes from, and it cannot be avoided / removed --- DISABLED - // - /* - // no need for @using in views - // note: - // we are NOT using the in-code attribute here, config is required - // because that would require parsing the code... and what if it changes? - // we can AddGlobalImport not sure we can remove one anyways - var modelsNamespace = Configuration.Config.ModelsNamespace; - if (string.IsNullOrWhiteSpace(modelsNamespace)) - modelsNamespace = Configuration.Config.DefaultModelsNamespace; - System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace); - */ - } - - private void InstallServerVars(RuntimeLevel level) - { - // register our url - for the backoffice api - ServerVariablesParser.Parsing += (sender, serverVars) => - { - if (!serverVars.ContainsKey("umbracoUrls")) - throw new Exception("Missing umbracoUrls."); - var umbracoUrlsObject = serverVars["umbracoUrls"]; - if (umbracoUrlsObject == null) - throw new Exception("Null umbracoUrls"); - if (!(umbracoUrlsObject is Dictionary umbracoUrls)) - throw new Exception("Invalid umbracoUrls"); - - if (!serverVars.ContainsKey("umbracoPlugins")) - throw new Exception("Missing umbracoPlugins."); - if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) - throw new Exception("Invalid umbracoPlugins"); - - if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null"); - var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); - - umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); - umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(level); - }; - } - - private Dictionary GetModelsBuilderSettings(RuntimeLevel level) - { - if (level != RuntimeLevel.Run) - return null; - - var settings = new Dictionary - { - {"enabled", UmbracoConfig.For.ModelsBuilder().Enable} - }; - - return settings; - } - - /// - /// Used to check if a template is being created based on a document type, in this case we need to - /// ensure the template markup is correct based on the model name of the document type - /// - /// - /// - private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs e) - { - // don't do anything if the factory is not enabled - // because, no factory = no models (even if generation is enabled) - if (!UmbracoConfig.For.ModelsBuilder().EnableFactory) return; - - // don't do anything if this special key is not found - if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return; - - // ensure we have the content type alias - if (!e.AdditionalData.ContainsKey("ContentTypeAlias")) - throw new InvalidOperationException("The additionalData key: ContentTypeAlias was not found"); - - foreach (var template in e.SavedEntities) - { - // if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key - // is found, then it means a new template is being created based on the creation of a document type - if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content)) - { - // ensure is safe and always pascal cased, per razor standard - // + this is how we get the default model name in Umbraco.ModelsBuilder.Umbraco.Application - var alias = e.AdditionalData["ContentTypeAlias"].ToString(); - var name = template.Name; // will be the name of the content type since we are creating - var className = UmbracoServices.GetClrName(name, alias); - - var modelNamespace = UmbracoConfig.For.ModelsBuilder().ModelsNamespace; - - // we do not support configuring this at the moment, so just let Umbraco use its default value - //var modelNamespaceAlias = ...; - - var markup = ViewHelper.GetDefaultFileContent( - modelClassName: className, - modelNamespace: modelNamespace/*, - modelNamespaceAlias: modelNamespaceAlias*/); - - //set the template content to the new markup - template.Content = markup; - } - } - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs b/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs deleted file mode 100644 index 20f5e94b64..0000000000 --- a/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.Web.Editors; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.ModelsBuilder.Validation -{ - /// - /// Used to validate the aliases for the content type when MB is enabled to ensure that - /// no illegal aliases are used - /// - internal class ContentTypeModelValidator : ContentTypeModelValidatorBase - { - } - - /// - /// Used to validate the aliases for the content type when MB is enabled to ensure that - /// no illegal aliases are used - /// - internal class MediaTypeModelValidator : ContentTypeModelValidatorBase - { - } - - /// - /// Used to validate the aliases for the content type when MB is enabled to ensure that - /// no illegal aliases are used - /// - internal class MemberTypeModelValidator : ContentTypeModelValidatorBase - { - } - - internal abstract class ContentTypeModelValidatorBase : EditorValidator - where TModel: ContentTypeSave - where TProperty: PropertyTypeBasic - { - protected override IEnumerable Validate(TModel model) - { - //don't do anything if we're not enabled - if (UmbracoConfig.For.ModelsBuilder().Enable) - { - var properties = model.Groups.SelectMany(x => x.Properties) - .Where(x => x.Inherited == false) - .ToArray(); - - foreach (var prop in properties) - { - var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop)); - - if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant()) - yield return new ValidationResult(string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), new[] - { - string.Format("Groups[{0}].Properties[{1}].Alias", model.Groups.IndexOf(propertyGroup), propertyGroup.Properties.IndexOf(prop)) - }); - - //we need to return the field name with an index so it's wired up correctly - var groupIndex = model.Groups.IndexOf(propertyGroup); - var propertyIndex = propertyGroup.Properties.IndexOf(prop); - - var validationResult = ValidateProperty(prop, groupIndex, propertyIndex); - if (validationResult != null) - { - yield return validationResult; - } - } - } - } - - private ValidationResult ValidateProperty(PropertyTypeBasic property, int groupIndex, int propertyIndex) - { - //don't let them match any properties or methods in IPublishedContent - //TODO: There are probably more! - var reservedProperties = typeof(IPublishedContent).GetProperties().Select(x => x.Name).ToArray(); - var reservedMethods = typeof(IPublishedContent).GetMethods().Select(x => x.Name).ToArray(); - - var alias = property.Alias; - - if (reservedProperties.InvariantContains(alias) || reservedMethods.InvariantContains(alias)) - { - return new ValidationResult( - string.Format("The alias {0} is a reserved term and cannot be used", alias), new[] - { - string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propertyIndex) - }); - } - - return null; - } - } -} diff --git a/src/Umbraco.TestData/Properties/AssemblyInfo.cs b/src/Umbraco.TestData/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..3c4251cdf6 --- /dev/null +++ b/src/Umbraco.TestData/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Umbraco.TestData")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Umbraco.TestData")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("fb5676ed-7a69-492c-b802-e7b24144c0fc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs new file mode 100644 index 0000000000..650820760e --- /dev/null +++ b/src/Umbraco.TestData/SegmentTestController.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Mvc; + +namespace Umbraco.TestData +{ + public class SegmentTestController : SurfaceController + { + + public ActionResult EnableDocTypeSegments(string alias, string propertyTypeAlias) + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + var ct = Services.ContentTypeService.Get(alias); + if (ct == null) + return Content($"No document type found by alias {alias}"); + + var propType = ct.PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias); + if (propType == null) + return Content($"The document type {alias} does not have a property type {propertyTypeAlias ?? "null"}"); + + if (ct.Variations.VariesBySegment()) + return Content($"The document type {alias} already allows segments, nothing has been changed"); + + ct.SetVariesBy(ContentVariation.Segment); + propType.SetVariesBy(ContentVariation.Segment); + + Services.ContentTypeService.Save(ct); + return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments"); + } + + public ActionResult DisableDocTypeSegments(string alias) + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + var ct = Services.ContentTypeService.Get(alias); + if (ct == null) + return Content($"No document type found by alias {alias}"); + + if (!ct.VariesBySegment()) + return Content($"The document type {alias} does not allow segments, nothing has been changed"); + + ct.SetVariesBy(ContentVariation.Segment, false); + + Services.ContentTypeService.Save(ct); + return Content($"The document type {alias} no longer allows segments"); + } + + public ActionResult AddSegmentData(int contentId, string propertyAlias, string value, string segment, string culture = null) + { + var content = Services.ContentService.GetById(contentId); + if (content == null) + return Content($"No content found by id {contentId}"); + + if (propertyAlias.IsNullOrWhiteSpace() || !content.HasProperty(propertyAlias)) + return Content($"The content by id {contentId} does not contain a property with alias {propertyAlias ?? "null"}"); + + if (content.ContentType.VariesByCulture() && culture.IsNullOrWhiteSpace()) + return Content($"The content by id {contentId} varies by culture but no culture was specified"); + + if (value.IsNullOrWhiteSpace()) + return Content("'value' cannot be null"); + + if (segment.IsNullOrWhiteSpace()) + return Content("'segment' cannot be null"); + + content.SetValue(propertyAlias, value, culture, segment); + Services.ContentService.Save(content); + + return Content($"Segment value has been set on content {contentId} for property {propertyAlias}"); + } + } +} diff --git a/src/Umbraco.TestData/Umbraco.TestData.csproj b/src/Umbraco.TestData/Umbraco.TestData.csproj new file mode 100644 index 0000000000..d61321ebb8 --- /dev/null +++ b/src/Umbraco.TestData/Umbraco.TestData.csproj @@ -0,0 +1,70 @@ + + + + + Debug + AnyCPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC} + Library + Properties + Umbraco.TestData + Umbraco.TestData + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} + Umbraco.Core + + + {651e1350-91b6-44b7-bd60-7207006d7003} + Umbraco.Web + + + + + 28.4.4 + + + 5.2.7 + + + + \ No newline at end of file diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs new file mode 100644 index 0000000000..02949d5345 --- /dev/null +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using System.Web.Mvc; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web; +using Umbraco.Web.Mvc; +using System.Configuration; +using Bogus; +using Umbraco.Core.Scoping; + +namespace Umbraco.TestData +{ + /// + /// Creates test data + /// + public class UmbracoTestDataController : SurfaceController + { + private const string RichTextDataTypeName = "UmbracoTestDataContent.RTE"; + private const string MediaPickerDataTypeName = "UmbracoTestDataContent.MediaPicker"; + private const string TextDataTypeName = "UmbracoTestDataContent.Text"; + private const string TestDataContentTypeAlias = "umbTestDataContent"; + private readonly IScopeProvider _scopeProvider; + private readonly PropertyEditorCollection _propertyEditors; + + public UmbracoTestDataController(IScopeProvider scopeProvider, PropertyEditorCollection propertyEditors, IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, ILogger logger, IProfilingLogger profilingLogger, UmbracoHelper umbracoHelper) : base(umbracoContextAccessor, databaseFactory, services, appCaches, logger, profilingLogger, umbracoHelper) + { + _scopeProvider = scopeProvider; + _propertyEditors = propertyEditors; + } + + /// + /// Creates a content and associated media tree (hierarchy) + /// + /// + /// + /// + /// + /// + /// Each content item created is associated to a media item via a media picker and therefore a relation is created between the two + /// + public ActionResult CreateTree(int count, int depth, string locale = "en") + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + if (!Validate(count, depth, out var message, out var perLevel)) + throw new InvalidOperationException(message); + + var faker = new Faker(locale); + var company = faker.Company.CompanyName(); + + using (var scope = _scopeProvider.CreateScope()) + { + var imageIds = CreateMediaTree(company, faker, count, depth).ToList(); + var contentIds = CreateContentTree(company, faker, count, depth, imageIds, out var root).ToList(); + + Services.ContentService.SaveAndPublishBranch(root, true); + + scope.Complete(); + } + + + return Content("Done"); + } + + private bool Validate(int count, int depth, out string message, out int perLevel) + { + perLevel = 0; + message = null; + + if (count <= 0) + { + message = "Count must be more than 0"; + return false; + } + + perLevel = count / depth; + if (perLevel < 1) + { + message = "Count not high enough for specified for number of levels required"; + return false; + } + + return true; + } + + /// + /// Utility to create a tree hierarchy + /// + /// + /// + /// + /// + /// + /// A callback that returns a tuple of Content and another callback to produce a Container. + /// For media, a container will be another folder, for content the container will be the Content itself. + /// + /// + private IEnumerable CreateHierarchy( + T parent, int count, int depth, + Func container)> create) + where T: class, IContentBase + { + yield return parent.GetUdi(); + + // This will not calculate a balanced tree but it will ensure that there will be enough nodes deep enough to not fill up the tree. + var totalDescendants = count - 1; + var perLevel = Math.Ceiling(totalDescendants / (double)depth); + var perBranch = Math.Ceiling(perLevel / depth); + + var tracked = new Stack<(T parent, int childCount)>(); + + var currChildCount = 0; + + for (int i = 0; i < count; i++) + { + var created = create(parent); + var contentItem = created.content; + + yield return contentItem.GetUdi(); + + currChildCount++; + + if (currChildCount == perBranch) + { + // move back up... + + var prev = tracked.Pop(); + + // restore child count + currChildCount = prev.childCount; + // restore the parent + parent = prev.parent; + + } + else if (contentItem.Level < depth) + { + // track the current parent and it's current child count + tracked.Push((parent, currChildCount)); + + // not at max depth, create below + parent = created.container(); + + currChildCount = 0; + } + + } + } + + /// + /// Creates the media tree hiearachy + /// + /// + /// + /// + /// + /// + private IEnumerable CreateMediaTree(string company, Faker faker, int count, int depth) + { + var parent = Services.MediaService.CreateMediaWithIdentity(company, -1, Constants.Conventions.MediaTypes.Folder); + + return CreateHierarchy(parent, count, depth, currParent => + { + var imageUrl = faker.Image.PicsumUrl(); + + // we are appending a &ext=.jpg to the end of this for a reason. The result of this url will be something like: + // https://picsum.photos/640/480/?image=106 + // and due to the way that we detect images there must be an extension so we'll change it to + // https://picsum.photos/640/480/?image=106&ext=.jpg + // which will trick our app into parsing this and thinking it's an image ... which it is so that's good. + // if we don't do this we don't get thumbnails in the back office. + imageUrl += "&ext=.jpg"; + + var media = Services.MediaService.CreateMedia(faker.Commerce.ProductName(), currParent, Constants.Conventions.MediaTypes.Image); + media.SetValue(Constants.Conventions.Media.File, imageUrl); + Services.MediaService.Save(media); + return (media, () => + { + // create a folder to contain child media + var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), currParent, Constants.Conventions.MediaTypes.Folder); + return container; + }); + }); + } + + /// + /// Creates the content tree hiearachy + /// + /// + /// + /// + /// + /// + /// + private IEnumerable CreateContentTree(string company, Faker faker, int count, int depth, List imageIds, out IContent root) + { + var random = new Random(company.GetHashCode()); + + var docType = GetOrCreateContentType(); + + var parent = Services.ContentService.Create(company, -1, docType.Alias); + parent.SetValue("review", faker.Rant.Review()); + parent.SetValue("desc", company); + parent.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + Services.ContentService.Save(parent); + + root = parent; + + return CreateHierarchy(parent, count, depth, currParent => + { + var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias); + content.SetValue("review", faker.Rant.Review()); + content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); + content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + + Services.ContentService.Save(content); + return (content, () => content); + }); + + } + + private IContentType GetOrCreateContentType() + { + var docType = Services.ContentTypeService.Get(TestDataContentTypeAlias); + if (docType != null) + return docType; + + docType = new ContentType(-1) + { + Alias = TestDataContentTypeAlias, + Name = "Umbraco Test Data Content", + Icon = "icon-science color-green" + }; + docType.AddPropertyGroup("Content"); + docType.AddPropertyType(new PropertyType(GetOrCreateRichText(), "review") + { + Name = "Review" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateMediaPicker(), "media") + { + Name = "Media" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateText(), "desc") + { + Name = "Description" + }); + Services.ContentTypeService.Save(docType); + docType.AllowedContentTypes = new[] { new ContentTypeSort(docType.Id, 0) }; + Services.ContentTypeService.Save(docType); + return docType; + } + + private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce); + + private IDataType GetOrCreateMediaPicker() => GetOrCreateDataType(MediaPickerDataTypeName, Constants.PropertyEditors.Aliases.MediaPicker); + + private IDataType GetOrCreateText() => GetOrCreateDataType(TextDataTypeName, Constants.PropertyEditors.Aliases.TextBox); + + private IDataType GetOrCreateDataType(string name, string editorAlias) + { + var dt = Services.DataTypeService.GetDataType(name); + if (dt != null) return dt; + + var editor = _propertyEditors.FirstOrDefault(x => x.Alias == editorAlias); + if (editor == null) + throw new InvalidOperationException($"No {editorAlias} editor found"); + + dt = new DataType(editor) + { + Name = name, + Configuration = editor.GetConfigurationEditor().DefaultConfigurationObject, + DatabaseType = ValueStorageType.Ntext + }; + + Services.DataTypeService.Save(dt); + return dt; + } + } +} diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md new file mode 100644 index 0000000000..f943326303 --- /dev/null +++ b/src/Umbraco.TestData/readme.md @@ -0,0 +1,51 @@ +## Umbraco Test Data + +This project is a utility to be able to generate large amounts of content and media in an +Umbraco installation for testing. + +Currently this project is referenced in the Umbraco.Web.UI project but only when it's being built +in Debug mode (i.e. when testing within Visual Studio). + +## Usage + +You must use SQL Server for this, using SQLCE will die if you try to bulk create huge amounts of data. + +It has to be enabled by an appSetting: + +```xml + +``` + +Once this is enabled this endpoint can be executed: + +`/umbraco/surface/umbracotestdata/CreateTree?count=100&depth=5` + +The query string options are: + +* `count` = the number of content and media nodes to create +* `depth` = how deep the trees created will be +* `locale` (optional, default = "en") = the language that the data will be generated in + +This creates a content and associated media tree (hierarchy). Each content item created is associated +to a media item via a media picker and therefore a relation is created between the two. Each content and +media tree created have the same root node name so it's easy to know which content branch relates to +which media branch. + +All values are generated using the very handy `Bogus` package. + +## Schema + +This will install some schema items: + +* `umbTestDataContent` Document Type. __TIP__: If you want to delete all of the content data generated with this tool, just delete this content type +* `UmbracoTestDataContent.RTE` Data Type +* `UmbracoTestDataContent.MediaPicker` Data Type +* `UmbracoTestDataContent.Text` Data Type + +For media, the normal folder and image is used + +## Media + +This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value. +This works when viewing the media item in the media section and the image will show up and with recent changes this will also work +when editing content to view the thumbnail for the picked media. diff --git a/src/Umbraco.Tests/Cache/AppCacheTests.cs b/src/Umbraco.Tests/Cache/AppCacheTests.cs index 3a86feb90a..f0ac9a2b0a 100644 --- a/src/Umbraco.Tests/Cache/AppCacheTests.cs +++ b/src/Umbraco.Tests/Cache/AppCacheTests.cs @@ -96,7 +96,7 @@ namespace Umbraco.Tests.Cache return ""; }); - Assert.AreEqual(counter, 1); + Assert.AreEqual(1, counter); } diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs index fbf828ad20..be160a483c 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs @@ -68,9 +68,9 @@ namespace Umbraco.Tests.Cache.PublishedCache var appCache = new DictionaryAppCache(); var domainCache = new DomainCache(ServiceContext.DomainService, DefaultCultureAccessor); var publishedShapshot = new PublishedSnapshot( - new PublishedContentCache(xmlStore, domainCache, appCache, globalSettings, new SiteDomainHelper(), umbracoContextAccessor, ContentTypesCache, null, null), + new PublishedContentCache(xmlStore, domainCache, appCache, globalSettings, ContentTypesCache, null, null), new PublishedMediaCache(xmlStore, ServiceContext.MediaService, ServiceContext.UserService, appCache, ContentTypesCache, Factory.GetInstance(), umbracoContextAccessor), - new PublishedMemberCache(null, appCache, Current.Services.MemberService, ContentTypesCache, umbracoContextAccessor), + new PublishedMemberCache(null, appCache, Current.Services.MemberService, ContentTypesCache), domainCache); var publishedSnapshotService = new Mock(); publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedShapshot); @@ -85,7 +85,7 @@ namespace Umbraco.Tests.Cache.PublishedCache globalSettings, new TestVariationContextAccessor()); - _cache = _umbracoContext.ContentCache; + _cache = _umbracoContext.Content; } [Test] diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs index 08eeb8ef4d..f3d9f895ef 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs @@ -323,8 +323,7 @@ namespace Umbraco.Tests.Cache.PublishedCache // no xpath null, // not from examine - false, - _umbracoContextAccessor), + false), //callback to get the children (dd, n) => children, // callback to get a property @@ -334,8 +333,7 @@ namespace Umbraco.Tests.Cache.PublishedCache // no xpath null, // not from examine - false, - _umbracoContextAccessor); + false); return dicDoc; } diff --git a/src/Umbraco.Tests/Cache/RefresherTests.cs b/src/Umbraco.Tests/Cache/RefresherTests.cs index b1d0ad7b1a..eb8580c9e2 100644 --- a/src/Umbraco.Tests/Cache/RefresherTests.cs +++ b/src/Umbraco.Tests/Cache/RefresherTests.cs @@ -12,20 +12,22 @@ namespace Umbraco.Tests.Cache [Test] public void MediaCacheRefresherCanDeserializeJsonPayload() { - var source = new[] { new MediaCacheRefresher.JsonPayload(1234, TreeChangeTypes.None) }; + var source = new[] { new MediaCacheRefresher.JsonPayload(1234, Guid.NewGuid(), TreeChangeTypes.None) }; var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); Assert.AreEqual(source[0].Id, payload[0].Id); + Assert.AreEqual(source[0].Key, payload[0].Key); Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); } [Test] public void ContentCacheRefresherCanDeserializeJsonPayload() { - var source = new[] { new ContentCacheRefresher.JsonPayload(1234, TreeChangeTypes.None) }; + var source = new[] { new ContentCacheRefresher.JsonPayload(1234, Guid.NewGuid(), TreeChangeTypes.None) }; var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); Assert.AreEqual(source[0].Id, payload[0].Id); + Assert.AreEqual(source[0].Key, payload[0].Key); Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); } diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index b435af9e77..00ba721bdb 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -223,7 +223,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + // gen 1 d.Set(1, "one"); Assert.AreEqual(1, d.Test.GetValues(1).Length); @@ -321,7 +321,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + Assert.AreEqual(0, d.Test.GetValues(1).Length); // gen 1 @@ -416,7 +416,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + // gen 1 d.Set(1, "one"); Assert.AreEqual(1, d.Test.GetValues(1).Length); @@ -578,7 +578,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + d.Set(1, "one"); d.Set(2, "two"); @@ -689,7 +689,7 @@ namespace Umbraco.Tests.Cache { // gen 3 Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.Set(1, "ein"); + d.SetLocked(1, "ein"); Assert.AreEqual(3, d.Test.GetValues(1).Length); Assert.AreEqual(3, d.Test.LiveGen); @@ -727,31 +727,25 @@ namespace Umbraco.Tests.Cache using (var w1 = d.GetScopedWriteLock(scopeProvider)) { Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); Assert.IsTrue(t.NextGen); - using (var w2 = d.GetScopedWriteLock(scopeProvider)) + Assert.Throws(() => { - Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(2, t.WLocked); - Assert.IsTrue(t.NextGen); - - Assert.AreNotSame(w1, w2); // get a new writer each time - - d.Set(1, "one"); - - Assert.AreEqual(0, d.CreateSnapshot().Gen); - } + using (var w2 = d.GetScopedWriteLock(scopeProvider)) + { + } + }); Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); Assert.IsTrue(t.NextGen); Assert.AreEqual(0, d.CreateSnapshot().Gen); } Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(0, t.WLocked); + Assert.IsFalse(t.IsLocked); Assert.IsTrue(t.NextGen); Assert.AreEqual(1, d.CreateSnapshot().Gen); @@ -772,11 +766,14 @@ namespace Umbraco.Tests.Cache using (var w1 = d.GetScopedWriteLock(scopeProvider)) { + // This one is interesting, although we don't allow recursive locks, since this is + // using the same ScopeContext/key, the lock acquisition is only done once + using (var w2 = d.GetScopedWriteLock(scopeProvider)) { Assert.AreSame(w1, w2); - d.Set(1, "one"); + d.SetLocked(1, "one"); } } } @@ -797,19 +794,16 @@ namespace Umbraco.Tests.Cache using (var w1 = d.GetScopedWriteLock(scopeProvider1)) { Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); Assert.IsTrue(t.NextGen); - using (var w2 = d.GetScopedWriteLock(scopeProvider2)) + Assert.Throws(() => { - Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(2, t.WLocked); - Assert.IsTrue(t.NextGen); + using (var w2 = d.GetScopedWriteLock(scopeProvider2)) + { + } + }); - Assert.AreNotSame(w1, w2); - - d.Set(1, "one"); - } } } @@ -848,13 +842,13 @@ namespace Umbraco.Tests.Cache Assert.IsFalse(d.Test.NextGen); Assert.AreEqual("uno", s2.Get(1)); - var scopeProvider = GetScopeProvider(); + var scopeProvider = GetScopeProvider(); using (d.GetScopedWriteLock(scopeProvider)) { // gen 3 Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.Set(1, "ein"); + d.SetLocked(1, "ein"); Assert.AreEqual(3, d.Test.GetValues(1).Length); Assert.AreEqual(3, d.Test.LiveGen); @@ -881,6 +875,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; + // gen 1 d.Set(1, "one"); @@ -894,12 +889,11 @@ namespace Umbraco.Tests.Cache Assert.AreEqual("uno", s2.Get(1)); var scopeProvider = GetScopeProvider(); - using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release - d.Set(1, "ein"); + d.SetLocked(1, "ein"); var s3 = d.CreateSnapshot(); Assert.AreEqual(2, s3.Gen); Assert.AreEqual("uno", s3.Get(1)); @@ -934,12 +928,11 @@ namespace Umbraco.Tests.Cache var scopeContext = new ScopeContext(); var scopeProvider = GetScopeProvider(scopeContext); - using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release - d.Set(1, "ein"); + d.SetLocked(1, "ein"); var s3 = d.CreateSnapshot(); Assert.AreEqual(2, s3.Gen); Assert.AreEqual("uno", s3.Get(1)); @@ -967,7 +960,7 @@ namespace Umbraco.Tests.Cache var d = new SnapDictionary(); var t = d.Test; t.CollectAuto = false; - + // gen 1 d.Set(1, "one"); var s1 = d.CreateSnapshot(); @@ -984,12 +977,11 @@ namespace Umbraco.Tests.Cache var scopeContext = new ScopeContext(); var scopeProvider = GetScopeProvider(scopeContext); - using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release - d.Set(1, "ein"); + d.SetLocked(1, "ein"); var s3 = d.CreateSnapshot(); Assert.AreEqual(2, s3.Gen); Assert.AreEqual("uno", s3.Get(1)); @@ -997,7 +989,7 @@ namespace Umbraco.Tests.Cache // we made some changes, so a next gen is required Assert.AreEqual(3, t.LiveGen); Assert.IsTrue(t.NextGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); // but live snapshot contains changes var ls = t.LiveSnapshot; @@ -1008,7 +1000,7 @@ namespace Umbraco.Tests.Cache // nothing is committed until scope exits Assert.AreEqual(3, t.LiveGen); Assert.IsTrue(t.NextGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); // no changes until exit var s4 = d.CreateSnapshot(); @@ -1020,7 +1012,7 @@ namespace Umbraco.Tests.Cache // now things have changed Assert.AreEqual(2, t.LiveGen); Assert.IsFalse(t.NextGen); - Assert.AreEqual(0, t.WLocked); + Assert.IsFalse(t.IsLocked); // no changes since not completed var s5 = d.CreateSnapshot(); @@ -1097,9 +1089,10 @@ namespace Umbraco.Tests.Cache // writer is scope contextual and scoped // when disposed, nothing happens // when the context exists, the writer is released + using (d.GetScopedWriteLock(scopeProvider)) { - d.Set(1, "ein"); + d.SetLocked(1, "ein"); Assert.IsTrue(d.Test.NextGen); Assert.AreEqual(3, d.Test.LiveGen); Assert.IsNotNull(d.Test.GenObj); @@ -1107,7 +1100,7 @@ namespace Umbraco.Tests.Cache } // writer has not released - Assert.AreEqual(1, d.Test.WLocked); + Assert.IsTrue(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(2, d.Test.GenObj.Gen); @@ -1118,7 +1111,7 @@ namespace Umbraco.Tests.Cache // panic! var s2 = d.CreateSnapshot(); - Assert.AreEqual(1, d.Test.WLocked); + Assert.IsTrue(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(2, d.Test.GenObj.Gen); Assert.AreEqual(3, d.Test.LiveGen); @@ -1127,7 +1120,7 @@ namespace Umbraco.Tests.Cache // release writer scopeContext.ScopeExit(true); - Assert.AreEqual(0, d.Test.WLocked); + Assert.IsFalse(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(2, d.Test.GenObj.Gen); Assert.AreEqual(3, d.Test.LiveGen); @@ -1135,7 +1128,7 @@ namespace Umbraco.Tests.Cache var s3 = d.CreateSnapshot(); - Assert.AreEqual(0, d.Test.WLocked); + Assert.IsFalse(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(3, d.Test.GenObj.Gen); Assert.AreEqual(3, d.Test.LiveGen); @@ -1150,4 +1143,45 @@ namespace Umbraco.Tests.Cache return scopeProvider; } } + + /// + /// Used for tests so that we don't have to wrap every Set/Clear call in locks + /// + public static class SnapDictionaryExtensions + { + internal static void Set(this SnapDictionary d, TKey key, TValue value) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.SetLocked(key, value); + } + } + + internal static void Clear(this SnapDictionary d) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.ClearLocked(); + } + } + + internal static void Clear(this SnapDictionary d, TKey key) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.ClearLocked(key); + } + } + + private static IScopeProvider GetScopeProvider() + { + var scopeProvider = Mock.Of(); + Mock.Get(scopeProvider) + .Setup(x => x.Context).Returns(() => null); + return scopeProvider; + } + } } diff --git a/src/Umbraco.Tests/Components/ComponentTests.cs b/src/Umbraco.Tests/Components/ComponentTests.cs index 2ba94d8c78..042cac1281 100644 --- a/src/Umbraco.Tests/Components/ComponentTests.cs +++ b/src/Umbraco.Tests/Components/ComponentTests.cs @@ -5,7 +5,6 @@ using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Compose; using Umbraco.Core.Composing; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -13,8 +12,6 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Scoping; -[assembly:DisableComposer(typeof(Umbraco.Tests.Components.ComponentTests.Composer26))] - namespace Umbraco.Tests.Components { [TestFixture] @@ -65,10 +62,11 @@ namespace Umbraco.Tests.Components public void Boot1A() { var register = MockRegister(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); + var typeLoader = MockTypeLoader(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); var types = TypeArray(); - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); // 2 is Core and requires 4 // 3 is User - goes away with RuntimeLevel.Unknown @@ -104,10 +102,11 @@ namespace Umbraco.Tests.Components public void Boot1B() { var register = MockRegister(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); + var typeLoader = MockTypeLoader(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); var types = TypeArray(); - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); // 2 is Core and requires 4 // 3 is User - stays with RuntimeLevel.Run @@ -120,10 +119,11 @@ namespace Umbraco.Tests.Components public void Boot2() { var register = MockRegister(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); + var typeLoader = MockTypeLoader(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); var types = TypeArray(); - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); // 21 is required by 20 // => reorder components accordingly @@ -135,10 +135,11 @@ namespace Umbraco.Tests.Components public void Boot3() { var register = MockRegister(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); + var typeLoader = MockTypeLoader(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); var types = TypeArray(); - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); // i23 requires 22 // 24, 25 implement i23 @@ -152,10 +153,11 @@ namespace Umbraco.Tests.Components public void BrokenRequire() { var register = MockRegister(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); + var typeLoader = MockTypeLoader(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); var types = TypeArray(); - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); try { @@ -175,10 +177,11 @@ namespace Umbraco.Tests.Components public void BrokenRequired() { var register = MockRegister(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); + var typeLoader = MockTypeLoader(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); var types = TypeArray(); - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); // 2 is Core and requires 4 // 13 is required by 1 @@ -196,6 +199,7 @@ namespace Umbraco.Tests.Components Terminated.Clear(); var register = MockRegister(); + var typeLoader = MockTypeLoader(); var factory = MockFactory(m => { m.Setup(x => x.TryGetInstance(It.Is(t => t == typeof (ISomeResource)))).Returns(() => new SomeResource()); @@ -210,10 +214,10 @@ namespace Umbraco.Tests.Components throw new NotSupportedException(type.FullName); }); }); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); var types = new[] { typeof(Composer1), typeof(Composer5), typeof(Composer5a) }; - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Assert.IsEmpty(Composed); composers.Compose(); @@ -236,10 +240,11 @@ namespace Umbraco.Tests.Components public void Requires1() { var register = MockRegister(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); + var typeLoader = MockTypeLoader(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); var types = new[] { typeof(Composer6), typeof(Composer7), typeof(Composer8) }; - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); composers.Compose(); Assert.AreEqual(2, Composed.Count); @@ -251,10 +256,11 @@ namespace Umbraco.Tests.Components public void Requires2A() { var register = MockRegister(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); + var typeLoader = MockTypeLoader(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); var types = new[] { typeof(Composer9), typeof(Composer2), typeof(Composer4) }; - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); composers.Compose(); Assert.AreEqual(2, Composed.Count); @@ -267,11 +273,12 @@ namespace Umbraco.Tests.Components public void Requires2B() { var register = MockRegister(); + var typeLoader = MockTypeLoader(); var factory = MockFactory(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); var types = new[] { typeof(Composer9), typeof(Composer2), typeof(Composer4) }; - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); composers.Compose(); var builder = composition.WithCollectionBuilder(); @@ -287,35 +294,36 @@ namespace Umbraco.Tests.Components public void WeakDependencies() { var register = MockRegister(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); + var typeLoader = MockTypeLoader(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); var types = new[] { typeof(Composer10) }; - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); composers.Compose(); Assert.AreEqual(1, Composed.Count); Assert.AreEqual(typeof(Composer10), Composed[0]); types = new[] { typeof(Composer11) }; - composers = new Composers(composition, types, Mock.Of()); + composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); Console.WriteLine("throws:"); - composers = new Composers(composition, types, Mock.Of()); + composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); var requirements = composers.GetRequirements(false); Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer2) }; - composers = new Composers(composition, types, Mock.Of()); + composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); Console.WriteLine("throws:"); - composers = new Composers(composition, types, Mock.Of()); + composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); requirements = composers.GetRequirements(false); Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer12) }; - composers = new Composers(composition, types, Mock.Of()); + composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); composers.Compose(); Assert.AreEqual(1, Composed.Count); @@ -326,10 +334,11 @@ namespace Umbraco.Tests.Components public void DisableMissing() { var register = MockRegister(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); + var typeLoader = MockTypeLoader(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); var types = new[] { typeof(Composer6), typeof(Composer8) }; // 8 disables 7 which is not in the list - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); Composed.Clear(); composers.Compose(); Assert.AreEqual(2, Composed.Count); @@ -341,16 +350,18 @@ namespace Umbraco.Tests.Components public void AttributesPriorities() { var register = MockRegister(); - var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); + var typeLoader = MockTypeLoader(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); - var types = new[] { typeof(Composer26) }; // 26 disabled by assembly attribute - var composers = new Composers(composition, types, Mock.Of()); + var types = new[] { typeof(Composer26) }; + var enableDisableAttributes = new[] { new DisableComposerAttribute(typeof(Composer26)) }; + var composers = new Composers(composition, types, enableDisableAttributes, Mock.Of()); Composed.Clear(); composers.Compose(); Assert.AreEqual(0, Composed.Count); // 26 gone types = new[] { typeof(Composer26), typeof(Composer27) }; // 26 disabled by assembly attribute, enabled by 27 - composers = new Composers(composition, types, Mock.Of()); + composers = new Composers(composition, types, enableDisableAttributes, Mock.Of()); Composed.Clear(); composers.Compose(); Assert.AreEqual(2, Composed.Count); // both @@ -367,7 +378,7 @@ namespace Umbraco.Tests.Components var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); var types = typeLoader.GetTypes().Where(x => x.FullName.StartsWith("Umbraco.Core.") || x.FullName.StartsWith("Umbraco.Web")); - var composers = new Composers(composition, types, Mock.Of()); + var composers = new Composers(composition, types, Enumerable.Empty(), Mock.Of()); var requirements = composers.GetRequirements(); var report = Composers.GetComposersReport(requirements); Console.WriteLine(report); @@ -506,7 +517,6 @@ namespace Umbraco.Tests.Components public class Composer25 : TestComposerBase, IComposer23 { } - // disabled by assembly attribute public class Composer26 : TestComposerBase { } diff --git a/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs index ec757e09f0..1d8390e07e 100644 --- a/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs +++ b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs @@ -354,7 +354,7 @@ namespace Umbraco.Tests.Composing var col2 = factory.GetInstance(); AssertCollection(col2, typeof(Resolved1), typeof(Resolved2)); - AssertSameCollection(col1, col2); + AssertSameCollection(factory, col1, col2); } } @@ -413,11 +413,11 @@ namespace Umbraco.Tests.Composing { col1A = factory.GetInstance(); col1B = factory.GetInstance(); - } - AssertCollection(col1A, typeof(Resolved1), typeof(Resolved2)); - AssertCollection(col1B, typeof(Resolved1), typeof(Resolved2)); - AssertSameCollection(col1A, col1B); + AssertCollection(col1A, typeof(Resolved1), typeof(Resolved2)); + AssertCollection(col1B, typeof(Resolved1), typeof(Resolved2)); + AssertSameCollection(factory, col1A, col1B); + } TestCollection col2; @@ -452,7 +452,7 @@ namespace Umbraco.Tests.Composing Assert.IsInstanceOf(expected[i], colA[i]); } - private static void AssertSameCollection(IEnumerable col1, IEnumerable col2) + private static void AssertSameCollection(IFactory factory, IEnumerable col1, IEnumerable col2) { Assert.AreSame(col1, col2); @@ -460,8 +460,19 @@ namespace Umbraco.Tests.Composing var col2A = col2.ToArray(); Assert.AreEqual(col1A.Length, col2A.Length); + + // Ensure each item in each collection is the same but also + // resolve each item from the factory to ensure it's also the same since + // it should have the same lifespan. for (var i = 0; i < col1A.Length; i++) + { Assert.AreSame(col1A[i], col2A[i]); + + var itemA = factory.GetInstance(col1A[i].GetType()); + var itemB = factory.GetInstance(col2A[i].GetType()); + + Assert.AreSame(itemA, itemB); + } } private static void AssertNotSameCollection(IEnumerable col1, IEnumerable col2) @@ -472,8 +483,11 @@ namespace Umbraco.Tests.Composing var col2A = col2.ToArray(); Assert.AreEqual(col1A.Length, col2A.Length); + for (var i = 0; i < col1A.Length; i++) + { Assert.AreNotSame(col1A[i], col2A[i]); + } } #endregion diff --git a/src/Umbraco.Tests/Composing/CompositionTests.cs b/src/Umbraco.Tests/Composing/CompositionTests.cs index f4478e2add..33855a8bfb 100644 --- a/src/Umbraco.Tests/Composing/CompositionTests.cs +++ b/src/Umbraco.Tests/Composing/CompositionTests.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; +using Umbraco.Core.IO; using Umbraco.Core.Logging; namespace Umbraco.Tests.Composing @@ -35,7 +36,7 @@ namespace Umbraco.Tests.Composing .Returns(() => factoryFactory?.Invoke(mockedFactory)); var logger = new ProfilingLogger(Mock.Of(), Mock.Of()); - var typeLoader = new TypeLoader(Mock.Of(), "", logger); + var typeLoader = new TypeLoader(Mock.Of(), IOHelper.MapPath("~/App_Data/TEMP"), logger); var composition = new Composition(mockedRegister, typeLoader, logger, Mock.Of()); // create the factory, ensure it is the mocked factory diff --git a/src/Umbraco.Tests/Composing/TypeHelperTests.cs b/src/Umbraco.Tests/Composing/TypeHelperTests.cs index 756ca4ca15..1f2477bf98 100644 --- a/src/Umbraco.Tests/Composing/TypeHelperTests.cs +++ b/src/Umbraco.Tests/Composing/TypeHelperTests.cs @@ -165,7 +165,7 @@ namespace Umbraco.Tests.Composing Assert.IsTrue(TypeHelper.MatchType(typeof(int?), typeof(Nullable<>))); - Assert.IsTrue(TypeHelper.MatchType(typeof(Derived), typeof(Object))); + Assert.IsTrue(TypeHelper.MatchType(typeof(Derived), typeof(object))); Assert.IsFalse(TypeHelper.MatchType(typeof(Derived), typeof(List<>))); Assert.IsFalse(TypeHelper.MatchType(typeof(Derived), typeof(IEnumerable<>))); Assert.IsTrue(TypeHelper.MatchType(typeof(Derived), typeof(Base))); diff --git a/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs b/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs index 54080f05de..50ead4b702 100644 --- a/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs +++ b/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs @@ -1,6 +1,4 @@ -using System.Web.Mvc; -using System.Web.Routing; -using Moq; +using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Composing; @@ -10,6 +8,7 @@ using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.Configurations { + [TestFixture] public class GlobalSettingsTests : BaseWebTest { @@ -47,73 +46,18 @@ namespace Umbraco.Tests.Configurations [TestCase("~/some-wacky/nestedPath", "/MyVirtualDir/NestedVDir/", "some-wacky-nestedpath")] public void Umbraco_Mvc_Area(string path, string rootPath, string outcome) { - var globalSettingsMock = Mock.Get(Factory.GetInstance()); //this will modify the IGlobalSettings instance stored in the container - globalSettingsMock.Setup(x => x.Path).Returns(IOHelper.ResolveUrl(path)); + var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); + + var globalSettingsMock = Mock.Get(globalSettings); + globalSettingsMock.Setup(x => x.Path).Returns(() => IOHelper.ResolveUrl(path)); SystemDirectories.Root = rootPath; - Assert.AreEqual(outcome, Current.Configs.Global().GetUmbracoMvcArea()); + Assert.AreEqual(outcome, globalSettings.GetUmbracoMvcAreaNoCache()); } - [TestCase("/umbraco/editContent.aspx")] - [TestCase("/install/default.aspx")] - [TestCase("/install/")] - [TestCase("/install")] - [TestCase("/install/?installStep=asdf")] - [TestCase("/install/test.aspx")] - public void Is_Reserved_Path_Or_Url(string url) - { - var globalSettings = TestObjects.GetGlobalSettings(); - Assert.IsTrue(globalSettings.IsReservedPathOrUrl(url)); - } - - [TestCase("/base/somebasehandler")] - [TestCase("/")] - [TestCase("/home.aspx")] - [TestCase("/umbraco-test")] - [TestCase("/install-test")] - [TestCase("/install.aspx")] - public void Is_Not_Reserved_Path_Or_Url(string url) - { - var globalSettings = TestObjects.GetGlobalSettings(); - Assert.IsFalse(globalSettings.IsReservedPathOrUrl(url)); - } + - [TestCase("/Do/Not/match", false)] - [TestCase("/Umbraco/RenderMvcs", false)] - [TestCase("/Umbraco/RenderMvc", true)] - [TestCase("/Umbraco/RenderMvc/Index", true)] - [TestCase("/Umbraco/RenderMvc/Index/1234", true)] - [TestCase("/Umbraco/RenderMvc/Index/1234/9876", false)] - [TestCase("/api", true)] - [TestCase("/api/WebApiTest", true)] - [TestCase("/api/WebApiTest/1234", true)] - [TestCase("/api/WebApiTest/Index/1234", false)] - public void Is_Reserved_By_Route(string url, bool shouldMatch) - { - //reset the app config, we only want to test routes not the hard coded paths - var globalSettingsMock = Mock.Get(Factory.GetInstance()); //this will modify the IGlobalSettings instance stored in the container - globalSettingsMock.Setup(x => x.ReservedPaths).Returns(""); - globalSettingsMock.Setup(x => x.ReservedUrls).Returns(""); - - var routes = new RouteCollection(); - - routes.MapRoute( - "Umbraco_default", - "Umbraco/RenderMvc/{action}/{id}", - new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional }); - routes.MapRoute( - "WebAPI", - "api/{controller}/{id}", - new { controller = "WebApiTestController", action = "Index", id = UrlParameter.Optional }); - - - var context = new FakeHttpContextFactory(url); - - - Assert.AreEqual( - shouldMatch, - globalSettingsMock.Object.IsReservedPathOrUrl(url, context.HttpContext, routes)); - } + } } diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs index 26e6a1ad8a..33df3caaad 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs @@ -16,7 +16,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public void EmailAddress() { - Assert.AreEqual(SettingsSection.Content.NotificationEmailAddress, "robot@umbraco.dk"); + Assert.AreEqual("robot@umbraco.dk", SettingsSection.Content.NotificationEmailAddress); } [Test] public virtual void DisableHtmlEmail() @@ -27,17 +27,17 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public virtual void Can_Set_Multiple() { - Assert.AreEqual(SettingsSection.Content.Error404Collection.Count(), 3); - Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(0).Culture, "default"); - Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(0).ContentId, 1047); + Assert.AreEqual(3, SettingsSection.Content.Error404Collection.Count()); + Assert.AreEqual("default", SettingsSection.Content.Error404Collection.ElementAt(0).Culture); + Assert.AreEqual(1047, SettingsSection.Content.Error404Collection.ElementAt(0).ContentId); Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(0).HasContentId); Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(0).HasContentKey); - Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(1).Culture, "en-US"); - Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(1).ContentXPath, "$site/error [@name = 'error']"); + Assert.AreEqual("en-US", SettingsSection.Content.Error404Collection.ElementAt(1).Culture); + Assert.AreEqual("$site/error [@name = 'error']", SettingsSection.Content.Error404Collection.ElementAt(1).ContentXPath); Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(1).HasContentId); Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(1).HasContentKey); - Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(2).Culture, "en-UK"); - Assert.AreEqual(SettingsSection.Content.Error404Collection.ElementAt(2).ContentKey, new Guid("8560867F-B88F-4C74-A9A4-679D8E5B3BFC")); + Assert.AreEqual("en-UK", SettingsSection.Content.Error404Collection.ElementAt(2).Culture); + Assert.AreEqual(new Guid("8560867F-B88F-4C74-A9A4-679D8E5B3BFC"), SettingsSection.Content.Error404Collection.ElementAt(2).ContentKey); Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(2).HasContentKey); Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(2).HasContentId); } @@ -47,27 +47,27 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings { Assert.IsTrue(SettingsSection.Content.ImageFileTypes.All(x => "jpeg,jpg,gif,bmp,png,tiff,tif".Split(',').Contains(x))); } - + [Test] public virtual void ImageAutoFillProperties() { - Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.Count(), 2); - Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).Alias, "umbracoFile"); - Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).WidthFieldAlias, "umbracoWidth"); - Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).HeightFieldAlias, "umbracoHeight"); - Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).LengthFieldAlias, "umbracoBytes"); - Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).ExtensionFieldAlias, "umbracoExtension"); - Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).Alias, "umbracoFile2"); - Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).WidthFieldAlias, "umbracoWidth2"); - Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).HeightFieldAlias, "umbracoHeight2"); - Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).LengthFieldAlias, "umbracoBytes2"); - Assert.AreEqual(SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).ExtensionFieldAlias, "umbracoExtension2"); + Assert.AreEqual(2, SettingsSection.Content.ImageAutoFillProperties.Count()); + Assert.AreEqual("umbracoFile", SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).Alias); + Assert.AreEqual("umbracoWidth", SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).WidthFieldAlias); + Assert.AreEqual("umbracoHeight", SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).HeightFieldAlias); + Assert.AreEqual("umbracoBytes", SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).LengthFieldAlias); + Assert.AreEqual("umbracoExtension", SettingsSection.Content.ImageAutoFillProperties.ElementAt(0).ExtensionFieldAlias); + Assert.AreEqual("umbracoFile2", SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).Alias); + Assert.AreEqual("umbracoWidth2", SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).WidthFieldAlias); + Assert.AreEqual("umbracoHeight2", SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).HeightFieldAlias); + Assert.AreEqual("umbracoBytes2", SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).LengthFieldAlias); + Assert.AreEqual("umbracoExtension2", SettingsSection.Content.ImageAutoFillProperties.ElementAt(1).ExtensionFieldAlias); } - + [Test] public void PreviewBadge() { - Assert.AreEqual(SettingsSection.Content.PreviewBadge, @"In Preview Mode - click to end"); + Assert.AreEqual(SettingsSection.Content.PreviewBadge, @"
Preview modeClick to end
"); } [Test] public void ResolveUrlsFromTextString() @@ -116,7 +116,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings Debug.WriteLine("AllowedContainsExtension: {0}", allowedContainsExtension); Debug.WriteLine("DisallowedContainsExtension: {0}", disallowedContainsExtension); - Assert.AreEqual(SettingsSection.Content.IsFileAllowedForUpload(extension), expected); + Assert.AreEqual(expected, SettingsSection.Content.IsFileAllowedForUpload(extension)); } } } diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index ac022a5489..8cbb799d88 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -41,9 +41,7 @@ - In Preview Mode - click to end - ]]> + Preview modeClick to end]]> - - At least {{installer.current.model.minCharLength}} characters long - - - At least {{installer.current.model.minNonAlphaNumericLength}} symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' : ''}} - +
+
+
+
+ +
+ +
+
+ +
+ +
+ + Your email will be used as your login +
+
+ +
+ +
+ + + At least {{installer.current.model.minCharLength}} characters long + + + At least {{installer.current.model.minNonAlphaNumericLength}} symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' : ''}} + +
-
-
-
- -
-
+
+
+ +
+
-
-
- - Customize -
-
+
+
+ + +
+
-
-
- - + + + + diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/version7upgradereport.html b/src/Umbraco.Web.UI.Client/src/installer/steps/version7upgradereport.html index df1e58d737..f30788e858 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/version7upgradereport.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/version7upgradereport.html @@ -2,11 +2,11 @@

Major version upgrade from {{installer.current.model.currentVersion}} to {{installer.current.model.newVersion}}

There were {{installer.current.model.errors.length}} issues detected

- The following compatibility issues were found. If you continue all non-compatible property editors will be converted to a Readonly/Label. + The following compatibility issues were found. If you continue, all non-compatible property editors will be converted to a Readonly/Label. You will be able to change the property editor to a compatible type manually by editing the data type after installation.

- Otherwise if you choose not to proceed you will need to fix the errors listed below. + Otherwise, if you choose not to proceed, you will need to fix the errors listed below. Refer to v{{installer.current.model.newVersion}} upgrade instructions for full details.

@@ -19,4 +19,4 @@

- \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/less/accessibility/sr-only.less b/src/Umbraco.Web.UI.Client/src/less/accessibility/sr-only.less new file mode 100644 index 0000000000..21ca5e6718 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/accessibility/sr-only.less @@ -0,0 +1,24 @@ + +// sr-only - based on the boot strap naming conventions used to remove an element from the view, whilst retaining accessibily for screen readers. More info available at https://getbootstrap.com/docs/4.0/utilities/screenreaders/ +// -------------------------------------------------- +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; + + &--focusable:active, + &--focusable:focus, + &--hoverable:hover { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/application/animations.less b/src/Umbraco.Web.UI.Client/src/less/application/animations.less index 91e2213775..e081c4919a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/animations.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/animations.less @@ -40,10 +40,6 @@ // TREE ANIMATION -.umb-tree-item.ng-animate { - display: none; -} - .umb-tree-item--deleted.ng-leave { animation: leave 600ms cubic-bezier(0.445, 0.050, 0.550, 0.950); display: block; diff --git a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less index 1a04dd10c8..939366d5ac 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less @@ -1,3 +1,7 @@ +*:focus { + outline-color: @ui-outline; +} + .umb-outline { &:focus { outline:none; @@ -10,7 +14,28 @@ left: 0; right: 0; border-radius: 3px; - box-shadow: 0 0 2px @blueMid, inset 0 0 2px 1px @blueMid; + box-shadow: 0 0 2px 0px @ui-outline, inset 0 0 2px 2px @ui-outline; } } + + &.umb-outline--surrounding { + &:focus { + .tabbing-active &::after { + top: -6px; + bottom: -6px; + left: -6px; + right: -6px; + border-radius: 9px; + } + } + } + + &.umb-outline--thin { + &:focus { + .tabbing-active &::after { + box-shadow: 0 0 2px @ui-outline, inset 0 0 2px 1px @ui-outline; + } + } + } + } diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 1e48500bb0..174f9f41d7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -2,6 +2,7 @@ // Core variables and mixins @import "fonts.less"; // Loading fonts @import "variables.less"; // Modify this for custom colors, font-sizes, etc +@import "colors.less"; // Colors from variables but as specific CSS classes @import "mixins.less"; // CSS Reset @@ -74,12 +75,15 @@ @import "sections.less"; @import "helveticons.less"; @import "main.less"; -@import "listview.less"; +@import "listview.less"; @import "gridview.less"; -@import "footer.less"; +@import "filter-toggle.less"; @import "forms/umb-validation-label.less"; +// Umbraco Accessibility +@import "accessibility/sr-only.less"; + // Umbraco Components @import "components/application/umb-app-header.less"; @import "components/application/umb-app-content.less"; @@ -99,17 +103,17 @@ @import "components/tree/umb-actions.less"; @import "components/tree/umb-tree-item.less"; - @import "components/editor.less"; @import "components/overlays.less"; @import "components/card.less"; @import "components/editor/umb-editor.less"; +@import "components/editor/umb-variant-switcher.less"; @import "components/umb-sub-views.less"; @import "components/umb-editor-navigation.less"; @import "components/umb-editor-navigation-item.less"; @import "components/umb-editor-sub-views.less"; @import "components/editor/subheader/umb-editor-sub-header.less"; -@import "components/umb-flatpickr.less"; +@import "components/umb-date-time-picker.less"; @import "components/umb-grid-selector.less"; @import "components/umb-child-selector.less"; @import "components/umb-group-builder.less"; @@ -121,21 +125,28 @@ @import "components/umb-form-check.less"; @import "components/umb-locked-field.less"; @import "components/umb-tabs.less"; +@import "components/umb-loader.less"; @import "components/umb-load-indicator.less"; @import "components/umb-breadcrumbs.less"; @import "components/umb-media-grid.less"; @import "components/umb-folder-grid.less"; @import "components/umb-content-grid.less"; +@import "components/umb-contextmenu.less"; @import "components/umb-layout-selector.less"; +@import "components/umb-mini-search.less"; @import "components/tooltip/umb-tooltip.less"; @import "components/tooltip/umb-tooltip-list.less"; @import "components/overlays/umb-overlay-backdrop.less"; @import "components/overlays/umb-itempicker.less"; +@import "components/overlays/umb-variant-selector-overlay"; @import "components/umb-grid.less"; @import "components/umb-empty-state.less"; @import "components/umb-property-editor.less"; +@import "components/umb-property-actions.less"; +@import "components/umb-code-snippet.less"; @import "components/umb-color-swatches.less"; @import "components/check-circle.less"; +@import "components/umb-file-icon.less"; @import "components/umb-iconpicker.less"; @import "components/umb-insert-code-box.less"; @import "components/umb-packages.less"; @@ -162,6 +173,7 @@ @import "components/umb-textarea.less"; @import "components/umb-dropdown.less"; @import "components/umb-range-slider.less"; +@import "components/umb-number.less"; @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; @@ -182,6 +194,10 @@ @import "components/users/umb-user-preview.less"; @import "components/users/umb-user-picker-list.less"; +@import "components/contextdialogs/umb-dialog-datatype-delete.less"; + +@import "components/umbemailmarketing.less"; + // Utilities @import "utilities/layout/_display.less"; @@ -196,7 +212,6 @@ @import "utilities/_cursor.less"; //page specific styles -@import "pages/document-type-editor.less"; @import "pages/login.less"; @import "pages/welcome-dashboard.less"; @@ -212,6 +227,7 @@ @import "dashboards/umbraco-forms.less"; @import "dashboards/examine-management.less"; @import "dashboards/healthcheck.less"; +@import "dashboards/nucache.less"; @import "typeahead.less"; @import "hacks.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index f21c7f3106..2b50b60ae8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -23,8 +23,7 @@ border-radius: 3px; // Hover/focus state - &:hover, - &:focus { + &:hover { background: @btnBackgroundHighlight; color: @gray-4; background-position: 0 -15px; @@ -35,11 +34,6 @@ .transition(background-position .1s linear); } - // Focus state for keyboard and accessibility - &:focus { - .tab-focus(); - } - // Active state &.active, &:active { @@ -54,7 +48,7 @@ &:disabled:hover { cursor: default; border-color: @btnBorder; - .opacity(65); + .opacity(80); .box-shadow(none); } @@ -67,6 +61,21 @@ border-color: rgba(0,0,0,0.09); } +// Button Reset - remove the default browser styles from the button element +// -------------------------------------------------- + +.btn-reset { + padding: 0; + margin: 0; + border: none; + background: none; + color: currentColor; + font-family: @baseFontFamily; + font-size: @baseFontSize; + line-height: @baseLineHeight; + cursor: pointer; +} + // Button Sizes // -------------------------------------------------- @@ -204,7 +213,7 @@ input[type="button"] { } // Made for Umbraco, 2019, used for buttons that has to stand back. .btn-white { - .buttonBackground(@btnWhiteBackground, @btnWhiteBackgroundHighlight, @btnWhiteType, @btnWhiteTypeHover); + .buttonBackground(@btnWhiteBackground, @btnWhiteBackgroundHighlight, @btnWhiteType, @btnWhiteTypeHover, @gray-10, @gray-7); } // Inverse appears as dark gray .btn-inverse { @@ -215,8 +224,7 @@ input[type="button"] { .buttonBackground(@btnNeutralBackground, @btnNeutralBackgroundHighlight); color: @gray-5; // Hover/focus state - &:hover, - &:focus { + &:hover { color: @gray-5; } @@ -246,18 +254,18 @@ input[type="button"] { .btn-outline { border: 1px solid; border-color: @gray-7; - background: @white; + background: transparent; color: @blueExtraDark; padding: 5px 13px; - transition: all .2s linear; + transition: border-color .12s linear, color .12s linear; + font-weight: 600; } -.btn-outline:hover, -.btn-outline:focus, -.btn-outline:active { +.btn-outline:hover { border-color: @ui-light-type-hover; color: @ui-light-type-hover; - background: @white; + background: transparent; + transition: border-color .12s linear, color .12s linear; } // Cross-browser Jank @@ -294,14 +302,12 @@ input[type="submit"].btn { color: @linkColor; .border-radius(0); } -.btn-link:hover, -.btn-link:focus { +.btn-link:hover { color: @linkColorHover; text-decoration: underline; background-color: transparent; } -.btn-link[disabled]:hover, -.btn-link[disabled]:focus { +.btn-link[disabled]:hover { color: @gray-4; text-decoration: none; } @@ -309,8 +315,7 @@ input[type="submit"].btn { // Make a reverse type of a button link .btn-link-reverse{ text-decoration:underline; - &:hover, - &:focus{ + &:hover { text-decoration:none; } } @@ -323,3 +328,31 @@ input[type="submit"].btn { text-decoration: none; } } + +// Icon buttons +// ------------------------------ + +// 31 July 19, Nathan Woulfe says: Reset styles for cases where button shows an icon only (eg edit/remove property on document type) +// This is lifted from umb-group-builder.less + +.btn-icon { + border: none; + + font-size: 18px; + position: relative; + cursor: pointer; + color: @ui-icon; + + margin: 0; + padding: 5px 10px; + width: auto; + overflow: visible; + background: transparent; + line-height: normal; + outline: 0; + -webkit-appearance: none; + + &:hover { + color: @ui-icon-hover; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less index 7440a5723a..7135692ae8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less +++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less @@ -1,5 +1,7 @@ @import "helveticons.less"; +@import "variables.less"; +@import "application/umb-outline.less"; /******* font-face *******/ @@ -13,711 +15,197 @@ /****************************/ body { - overflow: hidden; - height: 100%; - width: 100%; - width: calc(~"100% - 80px"); // 80px is the fixed left menu for toggling the different browser sizes position: absolute; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + height: calc(~"100% - 40px"); + width: 100%; padding: 0; margin: 0; - font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 20px; - color: #343434; - transition: all 0.2s ease-in-out; - padding-left:80px; + padding-bottom:40px; + background-color: @brownGrayExtraLight; } -h4 { - margin: 0; - font-size: 16px; - background-color: #535353; - color: #b3b3b3; - text-transform: uppercase; - position: initial; - display: block; - padding: 5px 10px; - font-size: 12px; - font-weight: normal; -} - -h5 { - margin: 0 0 6px 0; - font-size: 12px; -} - -ul { - list-style:none; - margin:0; - padding:0; -} - -a, a:hover{ - color:#333; - text-decoration:none; -} - - /****************************/ /* General class */ /****************************/ -.right { - float:right; - display:inline-block; -} - -.left { - float:left; - display:inline-block; -} - -.leftOpen { - padding-left: 330px -} - -.wait { - display: block; - height: 100%; - width: 100%; - background:#fff center center url(../img/loader.gif) no-repeat; -} - -/****************************/ -/* Group button */ -/****************************/ - -.btn-group { - float: right; - margin-right: 10px; -} - -.btn { - display: inline-block; - padding: 4px 12px; - margin-bottom: 0; - font-size: 14px; - line-height: 20px; - color: #000000; - text-align: center; - vertical-align: middle; - cursor: pointer; - background: #f2f2f2; - border: 1px solid #cccccc; - border-radius: 2px; - box-shadow: none; -} - -.btn-group > .btn + .dropdown-toggle { - box-shadow: none; -} - -.btn-group > .btn + .btn { - margin-left: -6px; -} - -.btn-group > .btn + .dropdown-toggle { - padding-right: 8px; - padding-left: 8px; - box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.btn-group > .btn:last-child, .btn-group > .dropdown-toggle { - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; -} - -.caret { - display: inline-block; - width: 0; - height: 0; - vertical-align: top; - border-top: 4px solid #FFFFFF; - border-right: 4px solid transparent; - border-left: 4px solid transparent; - content: ""; - border-top: 0; - border-bottom: 4px solid #ffffff; - margin-top: 8px; - margin-left: 0; -} - -.dropdown-menu { +.menu-bar { position: absolute; - display: block; - top: auto; - right: 0; - z-index: 1000; - display: block; - float: left; - min-width: 160px; - padding: 5px 0; - margin: -96px 10px 0 0; - margin-bottom: 1px; - list-style: none; - background-color: #ffffff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - border-radius: 6px; - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - background-clip: padding-box; -} - -.dropdown-menu > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 20px; - color: black; - white-space: nowrap; - cursor:pointer; -} - -.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus, .dropdown-submenu:hover > a, .dropdown-submenu:focus > a { - color: #000000; - background: #e4e0dd; -} - -/****************************/ -/* Speech bubble */ -/****************************/ - -#speechbubble { - position: absolute; - right: 0; - bottom: 40px; + bottom:0; left: 0; - z-index: 9999; - display: none; - padding: 0; - margin: auto; - margin-left: 300px; - text-align: left; - background: none; - border: none; - border-bottom: none; -} + right: 0; + background-color: @blueExtraDark; + color:@white; -#speechbubble p { - position: relative; - padding: 8px 30px 8px 20px; - margin: auto; - margin-top: 5px; - font-size: 12px; - color: #ffffff; - text-shadow: none; - background-color: #46a546; - border: none; - border-color: transparent; - border-radius: 5px 0 0 5px; -} - - -/****************************/ -/* Main section menu */ -/****************************/ - -.more-options i { - display: inline-block; - width: 5px !important; - height: 5px !important; - margin: 5px 1px 7px 0; - background: #d9d9d9; - border-radius: 20px; -} - -.fix-left-menu { - position: fixed; - top: 0; - left: 0; - width: 80px; - height: 100%; - padding: 0; - margin-left: -80px; font-family: "Lato", Helvetica, Arial, sans-serif; - font-size: 13px; + font-size: 12px; line-height: 16px; - background: #1b264f; - transition: all 0.2s ease-in-out; - z-index: 9999; + + animation: menu-bar-animation 1.2s; + animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); + } -.avatar { - text-align:center; - padding: 27px 0 29px 0; - border-bottom: 1px solid #2E2246; +@keyframes menu-bar-animation { + 0% { + bottom: -50px; + } + 40% { + bottom: -50px; + } + 80% { + bottom: 0px; + } + } + +.menu-bar__right-part { + float: right; + display: flex; + flex: row; + + > div, > button { + border-left: 1px solid rgba(255, 255, 255, .25); + } } -.help { - position: absolute; - bottom: 0; - left: 0; - display: block; - width: 100%; - margin: 0; - font-size: 30px; - text-align: center; - color: #D8D7D9; - opacity: 0.4; - transition: all .3s linear; +.menu-bar__title { + display: inline-block; + padding: 11px 15px; + font-weight: bold; + font-size: 13px; } -ul.sections { - display: block; - background: #1b264f; - height: 100%; - position:absolute; - top: 90px; - width: 80px; - transition: all 0.2s ease-in-out; - list-style:none; - margin:0; - padding:0; - margin-left: -80px; -} +.menu-bar__button { + display: inline-block; + padding: 11px 15px; + height: 40px; + border:none; + background-color: @blueExtraDark; -ul.sections li { - display: block; - border-left: 4px #1b264f solid; - transition: all .3s linear; -} + text-align: left; + font: inherit; + color: inherit; + cursor: pointer; -.fix-left-menu ul.sections li a span, -.fix-left-menu ul.sections li a i { - color: #fff; - opacity: .7; - transition: all .3s linear; -} + transition: color 120ms linear, background-color 120ms linear; + + .icon { + margin-right: 10px; + font-size: 18px; + vertical-align: middle; + } + + span { + vertical-align: middle; + } + + > svg { + display: inline-block; + width: 14px; + height: 14px; + fill: #fff; + margin-right: 10px; + vertical-align: middle; + transition: fill 120ms linear; + } -ul.sections li a { - display: block; - width: 100%; - height: 100%; - padding: 20px 4px 15px 0; - margin: 0 0 0 -4px; - text-align: center; - text-decoration: none; - border-bottom: 1px solid #2E2246; &:hover { - span, i { - opacity: 1; + background-color: lighten(@blueExtraDark, 4%); + } + &.--active { + color: @pinkLight; + > svg { + fill: @pinkLight; } } } -ul.sections li a i { - font-size: 30px; -} +.preview-menu-option { -ul.sections li a span { - display:block; - font-size: 10px; - line-height: 1.4em; - opacity: 0.4; -} - -ul.sections li.current { - background-color: #2E2246; -} - -ul.sections li.current a i { - color: #ffffff; -} - -ul.sections li.current, ul.sections li:hover { - border-left: 4px #f5c1bc solid; -} - -.fix-left-menu:hover ul.sections li a span, -.fix-left-menu:hover ul.sections li a i, -.fix-left-menu:hover .help { - opacity: 1; -} - -.fix-left-menu.selected, -.sections.selected { - margin-left:0px; -} - -/*************************************************/ -/* Main panel */ -/*************************************************/ - -.main-panel { - position: fixed; - top: 0; - left: 0; - margin-left: -330px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - line-height: 16px; - background: #ffffff; - transition: all 0.2s ease-in-out; - width: 250px; - height: 100%; - padding: 0; - z-index: 999; - box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); -} - -.main-panel .header { - padding: 28px 20px 32px 20px; - font-weight: bold; - background: #f8f8f8; - border-bottom: 1px solid #d9d9d9; -} - -.main-panel .header h3 { - color: rgba(179, 179, 179, 0.49); - font-size: 24px; - margin:0; -} - -.main-panel .header h3 i { - position: absolute; - right: 20px; - cursor:pointer; - transition: all 0.2s ease-in-out; -} - -.main-panel .header h3 i:hover { - color:#333; -} - -.main-panel.selected { - margin-left: 80px; - position: absolute; - right: 0; - bottom: 0; - left: 0; - overflow: auto; -} - -.main-panel .content { - padding:20px 0; -} - -/*************************************************/ -/* float-panel */ -/*************************************************/ - -.float-panel { - position: fixed; - top: 0; - z-index: 99; - width: 250px; - height: 100%; - padding: 0; - padding: 20px; - margin-left: -480px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - line-height: 16px; - background: #ffffff; - box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - transition: all 0.2s ease-in-out; -} - -.float-panel.selected{ - margin-left: 0px; -} - -/*************************************************/ -/* sample palette color */ -/*************************************************/ - -.samples h4 { - position: initial; - display: block; - padding: 5px 10px; - margin: 0; - border: 0px solid #6B6B6B; - font-weight: normal; - font-size: 12px; -} - -.samples > li { - padding: 6px 20px; - cursor: pointer; -} - -.samples > li:hover, .samples > li.hover { - background: #f8f8f8; -} - -.samples > li ul { - display:table; - width:100%; -} - -.samples > li ul > li { - display: table-cell; - height: 15px; - padding: 0; -} - -/*************************************************/ -/* canvas designer panel */ -/*************************************************/ - -h4.panel-title { - cursor:pointer; - background-color: #f8f8f8; - color: #767676; -} - -.editor-category{ - margin: 5px 10px; - border: 1px solid #D9D9D9; -} - -.canvasdesigner-panel-container { - padding: 10px 10px; -} - -.canvasdesigner-panel-property { - clear: both; - overflow: hidden; - margin: 0 0 10px 0; -} - -.canvasdesigner .box-slider { - padding: 0px 0px 6px 0px; - overflow: hidden; - clear: both; -} - -.field-title { - float: left; - margin-right: 10px; - font-size: 12px; - color: #d9d9d9; -} - -.div-field { - margin-bottom: 10px; - overflow: hidden; - clear: both; -} - -/*************************************************/ -/* font family picker */ -/*************************************************/ - -.fontFamilyPickerPreview { - float: left; - width: 90%; - padding: 8px; - margin-top: 4px; - clear: both; - font-size: 18px; - color: #CDCDCD; - cursor: pointer; - border: 1px solid #CDCDCD; - text-align: center; position: relative; -} - -.fontFamilyPickerPreview span { - font-size: 32px; - line-height: 32px; -} - -.fontPickerDelete { - position: absolute; - margin: 5px 0 0 -15px; - cursor: pointer; - color:#CDCDCD; - right: 0; - top: 0; -} - -.fontFamilyPickerPreview:hover { - border: 1px solid #979797; - color:#979797; -} - -.fontFamilyPickerPreview:hover .fontPickerDelete { - color:#979797; -} - -.canvasdesigner-fontfamilypicker { - margin-bottom:30px; -} - -.canvasdesigner-fontfamilypicker select { - font-size: 12px; - padding: 2px; -} - -.canvasdesigner-fontfamilypicker select.font-list { - width:170px; -} - -.canvasdesigner-fontfamilypicker select.variant-list { - width:60px; -} - -.canvasdesigner-fontfamilypicker { - font-size: 42px; - line-height: 50px; - margin-bottom:40px; -} - -/*************************************************/ -/* slider */ -/*************************************************/ - -.canvasdesigner .ui-widget-content { - background: rgba(0, 0, 0, 0.27) !important; - border: 0 solid #fff !important; - border-radius: 1px !important; -} - -.canvasdesigner .ui-slider-horizontal { - margin: 14px 11px 0 11px; -} - -.ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default, -.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, -.ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { - border: 1px solid #535353 !important; - background: #535353 !important; - background-color:none !important; - outline:none; -} - -.canvasdesigner .ui-slider .ui-slider-handle:hover, -.canvasdesigner .ui-slider .ui-slider-handle:focus { - color: #d9d9d9 !important; -} - -.canvasdesigner .ui-slider .ui-slider-handle span { - position: absolute !important; - margin: -15px 0 0 -8px !important; - color: #535353 !important; - font-size: 9px !important; - text-align: -webkit-center !important; - display: block !important; - width: 30px !important; -} - -.canvasdesigner .slider-input { - float: right; - width: 25px; - padding: 0; - margin-top: -9px; - margin-right: 1px; - font-size: 12px; - color: #d9d9d9; - text-align: right; - background-color: transparent; - border: none; - -} - -@-moz-document url-prefix() { - .canvasdesigner .slider-input { - margin-top: -6px; - } -} - -.canvasdesigner .sp-replacer { - padding: 0; - margin: 0; - display: block; - border: none; - height: 26px; - border-radius: 1px; - border: 1px solid #CDCDCD; -} - -.canvasdesigner .sp-replacer:hover { - border: 1px solid #979797; -} - -.canvasdesigner .panel-body { - border-top: none !important; -} - -.canvasdesigner select { - font-size: 12px; -} - -.canvasdesigner .sp-dd { - display: none; -} - -.canvasdesigner .sp-preview { - width: 100%; - height: 100%; - margin-right: 0; - border: none; - display: block; -} - -.canvasdesigner .color-picker-preview { - height: 26px; - border: 1px solid #CDCDCD; - border-radius: 1px; - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); - cursor: pointer; -} - -.canvasdesigner .float-panel .sp-container.sp-flat { - background-color: transparent; - border: none; - padding: 10px; - width: 100%; -} - -.canvasdesigner .float-panel .sp-container.sp-flat .sp-picker-container { - padding: 0; - margin: 0px; - margin-bottom: 10px; - border: none; - width: 100%; -} - -.canvasdesigner .float-panel .sp-container.sp-flat .sp-palette-container { - padding: 0; - height: 32px; - width: 100%; - float: left; - margin: 0; - border: none; -} - -.canvasdesigner .float-panel .sp-container.sp-flat .sp-button-container { - display: none; -} - -.colorPickerDelete { - position: absolute; - margin: -23px 0 0 0; - cursor: pointer; -} - -.borderStyleSelect -{ display: inline-block; - float: right; - width: 75px; - height: 28px; + + > .menu-bar__button { + position: relative; + } + + .dropdown-menu { + display:none; + + position: absolute; + right: 0; + bottom: 100%; + min-width: 200px; + + border-radius: 3px 3px 0 3px; + overflow: hidden; + + background-color: @blueExtraDark; + + > button { + position: relative; + display: list-item; + text-align: left; + width: 100%; + + &.--active { + &::before { + content: ''; + position: absolute; + left:0; + width: 3px; + top: 0; + bottom: 0; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + background-color: @pinkLight; + } + } + } + } + + &.--open { + z-index:1; + box-shadow: 0 5px 10px 0 rgba(0,0,0,.26); + > .menu-bar__button { + z-index: @zindexDropdown + 1; + } + .dropdown-menu { + display:block; + z-index: @zindexDropdown; + box-shadow: 0 5px 10px 0 rgba(0,0,0,.26); + } + } + } + + + /*************************************************/ /* IFrame size */ /*************************************************/ -.desktop { +#demo-iframe-wrapper { + transition: all 240ms cubic-bezier(0.165, 0.84, 0.44, 1); +} + +.fullsize { width: 100%; height: 100%; margin: 0 auto; overflow: hidden; } +.desktop { + width: 1920px; + height: 1080px; +} + .laptop { width: 1366px; height: 768px; @@ -743,13 +231,13 @@ h4.panel-title { height: 360px; } -.border { - margin: 75px auto; - background-color: #ffffff; - border-radius: 10px; +.shadow { + margin: 10px auto; + background-color: @white; + border-radius: 3px; + overflow: hidden; opacity: 1.0; - box-shadow: 0 0 0 29px #E9E9EB, 0 0 0 30px #D8D7D9; - transition: all 0.5s ease-in-out; + box-shadow: 0 5px 20px 0 rgba(0,0,0,.26); } iframe { @@ -767,251 +255,3 @@ iframe { .flip:before { transform: rotate(90deg); } - -/*************************************************/ -/* Image picker */ -/*************************************************/ - -.imagePickerPreview { - height: 20px; - text-align: center; - background-color: #fff; - padding: 6px 0 0 0; - cursor: pointer; - background-size: cover; - border-radius:1px; - border: 1px solid #CDCDCD; - color: #CDCDCD; -} - -.sp-clear-display { - background-image: none !important; -} - -.imagePickerPreview:hover { - border: 1px solid #979797; -} - -.imagePickerPreview:hover i { - color: #979797; -} - -.imagePickerPreview i { - font-size:24px; - color: #CDCDCD; -} - -.canvasdesignerImagePicker { - padding: 0; - margin-top: 10px; - overflow: auto; - text-align: left; - list-style: none; -} - -.canvasdesignerImagePicker ul { - padding: 0; - margin: 0; - margin-left: 20px; - list-style: none; -} - -.canvasdesignerImagePicker li { - display: inline-block; - margin-bottom: 10px; -} - -.canvasdesignerImagePicker ul.media-items li { - margin-right: 20px; -} - -.canvasdesignerImagePicker .media-preview { - position: relative; - display: inline-block; - width: 91px; - height: 78px; - cursor: pointer; - background-position: center center; - background-size: cover; - border: 2px solid #F3F2F2; -} - -.canvasdesignerImagePicker .media-preview .folder { - position: absolute; - top: 30px; - left: 0; - width: 100%; - font-size: 40px; - color: gainsboro; - text-align: center; -} - -.canvasdesignerImagePicker .media-preview .folder-name { - margin-top: -5px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - line-height: 0; - color: grey; -} - -.canvasdesignerImagePicker .media-preview:hover, -.media-preview.selected { - border: 2px solid #84B8F0; -} - -.canvasdesignerImagePicker .modal-dialog { - width: 618px; - font-weight: normal; -} - -.bodyCanvasdesignerImagePicker .breadcrumb { - margin-top: 4px; - margin-bottom: 10px; - font-size: 16px; - text-align: left; -} - -.bodyCanvasdesignerImagePicker .breadcrumb > li { - display: inline; -} - -.breadcrumb{ - font-size: 12px; -} - -.bodyCanvasdesignerImagePicker .breadcrumb > li a { - cursor: pointer; -} - -.bodyCanvasdesignerImagePicker .breadcrumb input { - padding: 0; - margin: -4px -4px; -} - -.bodyCanvasdesignerImagePicker .fileinput-button { - position: absolute; - top: 10px; - right: 10px; - font-size: 19px; -} - -.bodyCanvasdesignerImagePicker input.input-media { - position: absolute; - top: 0; - right: 0; - margin: 0; - font-size: 23px; - cursor: pointer; - opacity: 0; - transform: translate(-300px, 0) scale(4); - direction: ltr; -} - -.bodyCanvasdesignerImagePicker .breadcrumb a.disabled, -.bodyCanvasdesignerImagePicker .breadcrumb a.disabled:hover { - color: grey; - text-decoration: none; - cursor: default; -} - -.canvasdesignerImagePicker h3 { - font-size: 18px; - color: #555555; -} - -/*************************************************/ -/* Border editor */ -/*************************************************/ - -.box-preview { - display:inline-block; -} - -.box-preview li { - height: 30px; - width: 30px; - border-radius: 1px; - border: none; - display:inline-block; - background-color:#535353; - cursor:pointer; - margin-right: 6px; - position:relative; -} - -.box-preview li:last-child{ - margin-right: 0px; -} - -.box-preview li.selected { - border-color:#53a93f !important; -} - -.box-preview li.border-all { - border: 6px solid #b3b3b3; - height: 18px; - width: 18px; - margin-left:0px -} - -.box-preview li.border-left { - border-left: 6px solid #b3b3b3; - width: 24px; -} - -.box-preview li.border-right { - border-right: 6px solid #b3b3b3; - width: 24px; -} - -.box-preview li.border-top { - border-top: 6px solid #b3b3b3; - height: 24px; -} - -.box-preview li.border-bottom { - border-bottom: 6px solid #b3b3b3; - height: 24px; -} - -.bordereditor .color-picker-preview { - display: inline-block; - width: 120px; - float: left; -} - -/*************************************************/ -/* Radius editor */ -/*************************************************/ - -.radius-top-left, .radius-top-right, .radius-bottom-left, .radius-bottom-right { - display: block; - position: absolute; - background-color: #b3b3b3; - width: 7px; - height: 7px; -} - -.box-preview li.selected span { - background-color:#53a93f; -} - -.radius-top-left{ - top:0; - left:0; -} - -.radius-top-right{ - top:0; - right:0; -} - -.radius-bottom-left{ - bottom:0; - left:0; -} - -.radius-bottom-right{ - bottom:0; - right:0 -} diff --git a/src/Umbraco.Web.UI.Client/src/less/colors.less b/src/Umbraco.Web.UI.Client/src/less/colors.less new file mode 100644 index 0000000000..4cb70cdf5e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/colors.less @@ -0,0 +1,81 @@ +.red{color: @red;} +.blue{color: @blue;} +.black{color: @black;} +.turquoise{color: @turquoise;} +.turquoise-d1{color: @turquoise-d1;} + +.text-warning { + color: @orange; +} +.text-error { + color: @red; +} +.text-success { + color: @green; +} + + +//icon colors for tree icons +.color-red, .color-red i{color: @red-d1 !important;} +.color-blue, .color-blue i{color: @turquoise-d1 !important;} +.color-orange, .color-orange i{color: @orange !important;} +.color-green, .color-green i{color: @green-d1 !important;} +.color-yellow, .color-yellow i{color: @yellowIcon !important;} + +/* Colors based on https://zavoloklom.github.io/material-design-color-palette/colors.html */ +.btn-color-black {background-color: @black;} +.color-black i { color: @black;} + +.btn-color-blue-grey {background-color: @blueGrey;} +.color-blue-grey, .color-blue-grey i { color: @blueGrey !important;} + +.btn-color-grey{background-color: @grayIcon;} +.color-grey, .color-grey i { color: @grayIcon !important; } + +.btn-color-brown{background-color: @brownIcon;} +.color-brown, .color-brown i { color: @brownIcon !important; } + +.btn-color-blue{background-color: @blueIcon;} +.color-blue, .color-blue i { color: @blueIcon !important; } + +.btn-color-light-blue{background-color: @lightBlueIcon;} +.color-light-blue, .color-light-blue i {color: @lightBlueIcon !important;} + +.btn-color-cyan{background-color: @cyanIcon;} +.color-cyan, .color-cyan i { color: @cyanIcon !important; } + +.btn-color-green{background-color: @greenIcon;} +.color-green, .color-green i { color: @greenIcon !important; } + +.btn-color-light-green{background-color: @lightGreenIcon;} +.color-light-green, .color-light-green i {color: @lightGreenIcon !important; } + +.btn-color-lime{background-color: @limeIcon;} +.color-lime, .color-lime i { color: @limeIcon !important; } + +.btn-color-yellow{background-color: @yellowIcon;} +.color-yellow, .color-yellow i { color: @yellowIcon !important; } + +.btn-color-amber{background-color: @amberIcon;} +.color-amber, .color-amber i { color: @amberIcon !important; } + +.btn-color-orange{background-color: @orangeIcon;} +.color-orange, .color-orange i { color: @orangeIcon !important; } + +.btn-color-deep-orange{background-color: @deepOrangeIcon;} +.color-deep-orange, .color-deep-orange i { color: @deepOrangeIcon !important; } + +.btn-color-red{background-color: @redIcon;} +.color-red, .color-red i { color: @redIcon !important; } + +.btn-color-pink{background-color: @pinkIcon;} +.color-pink, .color-pink i { color: @pinkIcon !important; } + +.btn-color-purple{background-color: @purpleIcon;} +.color-purple, .color-purple i { color: @purpleIcon !important; } + +.btn-color-deep-purple{background-color: @deepPurpleIcon;} +.color-deep-purple, .color-deep-purple i { color: @deepPurpleIcon !important; } + +.btn-color-indigo{background-color: @indigoIcon;} +.color-indigo, .color-indigo i { color: @indigoIcon !important; } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less index d4b21e66f0..bd1b8ab07a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less @@ -16,27 +16,26 @@ margin-right: -10px; } -.umb-app-header__action a { +.umb-app-header__button { padding-left: 10px; padding-right: 10px; text-decoration: none; display: flex; align-items: center; height: @appHeaderHeight; -} - -.umb-app-header__action a { outline: none; + &:focus { .tabbing-active & { .umb-app-header__action-icon::after { content: ''; position: absolute; z-index:10000; - top: -7px; - left: -7px; + top: 50%; + left: 50%; width: 36px; height: 35px; + transform: translate(-50%, -50%); border-radius: 3px; box-shadow: 0 0 2px @pinkLight, inset 0 0 2px 1px @pinkLight; } @@ -51,7 +50,7 @@ font-size: 22px; } -.umb-app-header__action a:hover .umb-app-header__action-icon, -.umb-app-header__action a:focus .umb-app-header__action-icon { +.umb-app-header__button:hover .umb-app-header__action-icon, +.umb-app-header__button:focus .umb-app-header__action-icon { opacity: 1; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-backdrop.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-backdrop.less index 1d9aaabd44..18e331756e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-backdrop.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-backdrop.less @@ -2,7 +2,7 @@ height: 100%; width: 100%; position: fixed; - z-index: @zindexOverlayBackdrop; + z-index: @zindexUmbOverlay; top: 0; left: 0; pointer-events: none; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less index 17fdcc905e..03153973ff 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less @@ -12,7 +12,6 @@ flex: 0 0 @editorHeaderHeight; background: @white; border-bottom: 1px solid @gray-9; - padding: 20px 0 0 0; box-sizing: border-box; display: flex; justify-content: flex-end; @@ -32,6 +31,6 @@ border: none; } -.umb-dashboard__header .umb-tabs-nav .umb-tab > a { +.umb-dashboard__header .umb-tabs-nav .umb-tab > .umb-tab-button { padding-bottom: 25px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less index 8c3b059a94..5c77a15ec7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less @@ -86,6 +86,7 @@ } .umb-help-badge__title { + display: block; font-size: 15px; font-weight: bold; color: @black; @@ -160,6 +161,9 @@ border-radius: 0; border-bottom: 1px solid @gray-9; padding: 10px; + background: transparent; + width:100%; + border: 0 none; } .umb-help-list-item:last-child { @@ -194,7 +198,6 @@ .umb-help-list-item__title { font-size: 14px; display: block; - margin-left: 26px; } .umb-help-list-item__description { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less index 527c742382..4e3741905f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less @@ -12,11 +12,14 @@ align-items: center; justify-content: space-between; padding: 0 20px; - cursor: pointer; + background: transparent; + border: 0 none; border-bottom: 1px solid @gray-9; height: @editorHeaderHeight; box-sizing: border-box; color: @ui-option-type; + width: 100%; + outline-offset: -3px; } .umb-language-picker__expand { @@ -43,20 +46,24 @@ overflow: auto; } -.umb-language-picker__dropdown a { +.umb-language-picker__dropdown-item { + background: transparent; + border: 0 none; padding: 8px 20px; display: block; font-size: 14px; + width: 100%; + text-align: left; + outline-offset: -3px; } -.umb-language-picker__dropdown a:hover, -.umb-language-picker__dropdown a:focus { +.umb-language-picker__dropdown-item:hover { background: @ui-option-hover; text-decoration: none; color:@ui-option-type-hover; } -.umb-language-picker__dropdown a.umb-language-picker__dropdown-item--current { +.umb-language-picker__dropdown .umb-language-picker__dropdown-item.umb-language-picker__dropdown-item--current { padding-left: 16px; border-left: 4px solid @ui-light-active-border; color:@ui-light-active-type; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less index a8fc9c7f8e..2f9430ef41 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less @@ -4,7 +4,7 @@ */ .umb-search { position: fixed; - z-index: @zindexUmbOverlay; + z-index: @zindexSearchBox; width: 660px; max-width: 90%; transform: translate(-50%, 0); @@ -16,6 +16,10 @@ box-shadow: 0 10px 20px rgba(0,0,0,.12),0 6px 6px rgba(0,0,0,.14); } +.umb-search__label{ + margin: 0; +} + /* Search field */ @@ -107,4 +111,4 @@ .umb-search-result__description { color: @gray-5; font-size: 13px; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less index 315cd91dbd..bf2f030cea 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less @@ -1,8 +1,12 @@ -.umb-tour__loader { - background: @white; - z-index: @zindexTourModal; +.umb-loader-wrapper.umb-tour__loader { + margin: 0; position: fixed; - height: 5px; + z-index: @zindexTourModal; + + .umb-loader { + background-color: @white; + height: 5px; + } } .umb-tour__pulse { @@ -108,3 +112,24 @@ .umb-tour-is-visible .umb-backdrop { z-index: @zindexTourBackdrop; } + +.umb-tour__popover .underline{ + font-size: 13px; + background: transparent; + border: none; + padding: 0; +} + +.umb-tour__popover--promotion { + width: 800px; + min-height: 400px; + padding: 40px; + border-radius: @baseBorderRadius * 2; + .umb-tour-step__close { + top: 40px; + right: 40px; + } + a { + text-decoration: underline; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less index e40282cb58..02b67460f6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less @@ -9,6 +9,7 @@ left: auto; } +.umb-button-group__sub-buttons>li>button, .umb-button-group__sub-buttons>li>a { display: flex; } @@ -16,15 +17,17 @@ .umb-button-group { .umb-button__button { - border-radius: @baseBorderRadius; + border-radius: @baseBorderRadius 0 0 @baseBorderRadius; + + &:hover { + z-index: 2; + } } .umb-button-group__toggle { - border-radius: 0px @baseBorderRadius @baseBorderRadius 0; - border-left: 1px solid rgba(0,0,0,0.09); - margin-left: -2px; + border-radius: 0 @baseBorderRadius @baseBorderRadius 0; + margin-left: -1px; padding-left: 10px; padding-right: 10px; } - } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less index 7fc965a8fa..4127c2201c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less @@ -8,21 +8,6 @@ position: relative; } -.umb-button__button:focus { - outline: none; - .tabbing-active &:after { - content: ''; - position: absolute; - z-index: 10000; - top: 0; - bottom: 0; - left: 0; - right: 0; - border-radius: 3px; - box-shadow: 0 0 2px @blueMid, inset 0 0 2px 1px @blueMid; - } -} - .umb-button__content { opacity: 1; transition: opacity 0.25s ease; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less index 6f23677a1c..ff4122b258 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less @@ -21,23 +21,23 @@ background-color: @inputBorder; position: relative; transition: background-color 120ms; - - .umb-toggle:hover &, + + .umb-toggle:hover &, .umb-toggle:focus & { border-color: @inputBorderFocus; } - + .umb-toggle.umb-toggle--checked & { border-color: @ui-btn; background-color: @ui-btn; - + &:hover { background-color: @ui-btn-hover; } } - - .umb-toggle.umb-toggle--checked:focus & { - border-color: black; + + .tabbing-active .umb-toggle:focus & { + box-shadow: 0 0 0 2px highlight; } } @@ -54,12 +54,12 @@ background-color: @white; border-radius: 8px; transition: transform 120ms ease-in-out, background-color 120ms; - + .umb-toggle.umb-toggle--checked & { transform: translateX(20px); - background-color: white; + background-color: @white; } - + } @@ -75,7 +75,7 @@ .umb-toggle__icon--left { left: 5px; - color: white; + color:@white; transition: opacity 120ms; opacity: 0; // .umb-toggle:hover & { @@ -85,7 +85,7 @@ opacity: 1; } .umb-toggle.umb-toggle--checked:hover & { - color: white; + color:@white; } } @@ -103,14 +103,21 @@ - +.umb-toggle.umb-toggle--disabled.umb-toggle--checked, .umb-toggle.umb-toggle--disabled { .umb-toggle__toggle { cursor: not-allowed; - border-color: @gray-5; + background-color: @gray-9; + border-color: @gray-9; + } + .umb-toggle__icon--left { + color: @gray-6; + } + .umb-toggle__icon--right { + color: @gray-6; } .umb-toggle__handler { - background-color: @gray-5; + background-color: @gray-10; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/card.less b/src/Umbraco.Web.UI.Client/src/less/components/card.less index 8324698685..68854e3c53 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/card.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/card.less @@ -2,20 +2,28 @@ Library of card related compoents, like the right-hand icon list on the grid "cards" */ -.umb-card{ +.umb-card { position: relative; padding: 5px 10px 5px 10px; - background: white; + background: @white; width: 100%; - .title{padding: 12px; color: @gray-3; border-bottom: 1px solid @gray-8; font-weight: 400; font-size: 16px; text-transform: none; margin: 0 -10px 10px -10px;} + .title { + padding: 12px; + color: @gray-3; + border-bottom: 1px solid @gray-8; + font-weight: 400; + font-size: 16px; + text-transform: none; + margin: 0 -10px 10px -10px; + } } -.umb-card-thumb{ +.umb-card-thumb { text-align: center; - i{ + i { text-align: center; font-size: 20px; line-height: 40px; @@ -25,18 +33,28 @@ } } -.umb-card-content{ - .item-title{color: @blackLight; font-weight: 400; border: none; font-size: 16px; text-transform: none; margin-bottom: 3px;} - p{color: @gray-3; margin-bottom: 1px;} +.umb-card-content { + .item-title { + color: @blackLight; + font-weight: 400; + border: none; + font-size: 16px; + text-transform: none; + margin-bottom: 3px; + } + p { + color: @gray-3; + margin-bottom: 1px; + } } -.umb-card-actions{ +.umb-card-actions { padding-top: 10px; border-top: @gray-10 1px solid; clear: both; } -.umb-card-icons{ +.umb-card-icons { text-align: center; vertical-align: middle; display: block; @@ -45,7 +63,7 @@ padding: 0; } -.umb-card-icons.vertical{ +.umb-card-icons.vertical { position: absolute; top: 7px; right: 7px; @@ -53,19 +71,19 @@ width: 1px; } -.umb-card-icons li{ +.umb-card-icons li { display: inline-block; margin: 0 2px 0 2px; } -.umb-card-icons.vertical li{ +.umb-card-icons.vertical li { float: right; display: block; margin-bottom: 3px; } //card iocn list -.umb-card-list{ +.umb-card-list { display: block; padding: 0; margin: 0; @@ -81,12 +99,12 @@ //Card icon grid for picking items off a card -.umb-card-grid{ +.umb-card-grid { padding: 0; margin: 0 auto; list-style: none; width: 100%; - + display: flex; flex-flow: row wrap; justify-content: flex-start; @@ -101,14 +119,24 @@ width: 100px; } +.umb-card-grid.-six-in-row li { + flex: 0 0 25%; + max-width: 117px; +} + .umb-card-grid.-four-in-row li { flex: 0 0 25%; max-width: 25%; } .umb-card-grid.-three-in-row li { - flex: 0 0 33.33%; - max-width:33.33%; + flex: 0 0 33.333%; + max-width:33.333%; + + i { + font-size: 36px; + line-height: 28px; + } } .umb-card-grid .umb-card-grid-item { @@ -117,9 +145,9 @@ width: 100%; //height: 100%; padding-top: 100%; - border-radius: 3px; + border-radius: @baseBorderRadius * 2; transition: background-color 120ms; - + > span { position: absolute; top: 10px; @@ -141,6 +169,41 @@ color: @ui-option-type-hover; } +.umb-card-grid .umb-card-grid-item-slot { + position: relative; + display: block; + width: 100%; + padding-top: 100%; + border-radius: @baseBorderRadius * 2; + + box-sizing: border-box; + transition: background-color 120ms; + + &:hover, &:focus { + background-color: @ui-option-hover; + > span { + color:@ui-action-discreet-type-hover; + border-color:@ui-action-discreet-border-hover; + } + } + + > span { + position: absolute; + top: 10px; + bottom: 10px; + left: 10px; + right: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background-color: transparent; + border:1.5px dashed @ui-action-discreet-border; + border-radius: @baseBorderRadius * 2; + } +} + + .umb-card-grid a { color: @ui-option-type; text-decoration: none; @@ -149,6 +212,7 @@ .umb-card-grid i { font-size: 30px; line-height: 20px; + margin-top: 6px; margin-bottom: 10px; display: block; } @@ -164,7 +228,7 @@ //Round icon-like button - this should be somewhere else -.umb-btn-round{ +.umb-btn-round { padding: 4px 6px 4px 6px; display: inline-block; cursor: pointer; @@ -174,7 +238,8 @@ margin: 2px; } -.umb-btn-round:hover, .umb-btn-round:hover *{ +.umb-btn-round:hover, +.umb-btn-round:hover * { background: @blueDark !important; color: @white !important; border-color: @blueDark !important; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/contextdialogs/umb-dialog-datatype-delete.less b/src/Umbraco.Web.UI.Client/src/less/components/contextdialogs/umb-dialog-datatype-delete.less new file mode 100644 index 0000000000..0e0b8f22bd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/contextdialogs/umb-dialog-datatype-delete.less @@ -0,0 +1,33 @@ +.umb-dialog-datatype-delete { + + + .umb-dialog-datatype-delete__table-head-column-name { + width: 140px; + } + + .umb-table-body__icon { + margin-right: 5px; + vertical-align: top; + display: inline-block; + } + + .table tbody td { + vertical-align: top; + } + .table tbody td > span { + margin: 5px 0; + vertical-align: middle; + } + .table tbody p { + line-height: 12px; + margin: 5px 0; + vertical-align: middle; + } + + .table tbody .icon { + vertical-align: top; + margin-right: 5px; + display: inline-block; + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less index 21d459c598..ac55c6ffb1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -74,8 +74,11 @@ } .umb-editor-header__back { - color: @gray-6; - margin-bottom: 1px; + background: transparent; + border: 0; + color: @gray-6; + margin: 0 0 1px 0; + padding: 0; transition: color 0.1s ease-in-out; } @@ -159,117 +162,6 @@ a.umb-editor-header__close-split-view:hover { } } -/* variant switcher */ -.umb-variant-switcher__toggle { - display: flex; - align-items: center; - padding: 0 10px; - margin: 1px 1px; - right: 0; - height: 30px; - text-decoration: none !important; - font-size: 13px; - color: @ui-action-discreet-type; - - max-width: 50%; - white-space: nowrap; - - span { - text-overflow: ellipsis; - overflow: hidden; - } -} - -a.umb-variant-switcher__toggle { - transition: color 0.2s ease-in-out; - &:hover { - //background-color: @gray-10; - color: @ui-action-discreet-type-hover; - .umb-variant-switcher__expand { - color: @ui-action-discreet-type-hover; - } - } -} - -.umb-variant-switcher__expand { - color: @ui-action-discreet-type; - margin-top: 3px; - margin-left: 5px; - margin-right: -5px; - transition: color 0.2s ease-in-out; -} - -.umb-variant-switcher__item { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid @gray-9; - position: relative; - &:hover .umb-variant-switcher__name-wrapper { - - } -} - -.umb-variant-switcher__item:last-child { - border-bottom: none; -} - -.umb-variant-switcher_item--current { - color: @ui-light-active-type; -} -.umb-variant-switcher_item--current .umb-variant-switcher__name-wrapper { - //background-color: @gray-10; - border-left: 4px solid @ui-active; -} - -.umb-variant-switcher__item:hover, -.umb-variant-switcher__item:focus { - outline: none; -} - -.umb-variant-switcher_item--not-allowed:not(.umb-variant-switcher_item--current) .umb-variant-switcher__name-wrapper:hover { - //background-color: @white !important; - cursor: default; -} - -.umb-variant-switcher__item:hover .umb-variant-switcher__split-view { - display: block; - cursor: pointer; -} - -.umb-variant-switcher__name-wrapper { - font-size: 14px; - flex: 1; - cursor: pointer; - padding-top: 6px !important; - padding-bottom: 6px !important; - border-left: 2px solid transparent; -} - -.umb-variant-switcher__name { - display: block; -} - -.umb-variant-switcher__state { - font-size: 13px; - color: @gray-4; -} - -.umb-variant-switcher__split-view { - font-size: 13px; - display: none; - padding: 16px 20px; - position: absolute; - right: 0; - top: 0; - bottom: 0; - background-color: @white; - - &:hover { - background-color: @ui-option-hover; - color: @ui-option-type-hover; - } -} // container @@ -333,14 +225,3 @@ a.umb-variant-switcher__toggle { margin-right: auto; padding-right: 10px; } - -/* Confirm */ -.umb-editor-confirm { - background-color: @white; - padding: 20px; - position: absolute; - left: 0; - bottom: 0; - z-index: 10; - box-shadow: 0 -3px 12px 0px rgba(0,0,0,0.16); -} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 78cccac57a..3c4a037b0b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -1,14 +1,11 @@ .umb-editor-sub-header { padding: 10px 0; - margin-bottom: 10px; background: @brownGrayLight; border-left: 5px solid @brownGrayLight; border-right: 5px solid @brownGrayLight; - margin-left: -5px; - margin-right: -5px; display: flex; justify-content: space-between; - margin-top: -10px; + margin: -10px -1px 10px; position: relative; top: 0; box-sizing: border-box; @@ -20,27 +17,41 @@ } } .umb-editor-sub-header--white { - background-color: white; - border-color: white; + background-color: @white; + border-color: @white; } .umb-editor-sub-header.--state-selection { padding-left: 10px; padding-right: 10px; - background-color: @pinkLight; - border-color: @pinkLight; + background-color: @ui-selected-border; + border-color: @ui-selected-border; + color: @white; border-radius: 3px; } -.umb-editor-sub-header.-umb-sticky-bar { - box-shadow: 0 6px 3px -3px rgba(0,0,0,.16); +[umb-sticky-bar] { transition: box-shadow 240ms; - margin-top: 0; - margin-bottom: 0; - top: calc(@appHeaderHeight + @editorHeaderHeight); - - .umb-editor--infinityMode & { - top: calc(@editorHeaderHeight); + position:sticky; + z-index: 30; + width: calc(100% + 2px); + + &.umb-sticky-bar--active { + box-shadow: 0 6px 3px -3px rgba(0,0,0,.16); + } + + .umb-dashboard__content & { + top:-20px; // umb-dashboard__content has 20px padding - offset here prevents sticky position from firing when page loads + } +} + +.umb-sticky-sentinel { + pointer-events: none; + z-index: 5050; + + &.-top { + height:1px; + transform:translateY(-10px); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less index a9d879ab7f..e81df77772 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less @@ -1,82 +1,87 @@ .umb-editors { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; + .absolute(); overflow: hidden; + z-index: 7500; + + .umb-editor { + box-shadow: 0px 0 30px 0 rgba(0,0,0,.3); + } } .umb-editor { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; + .absolute(); background: @brownGrayLight; z-index: @zIndexEditor; -} -.umb-editor--infinityMode { - transform: none; - will-change: transform; - transition: transform 400ms ease-in-out; - &.moveRight { - transform: translateX(110%); + &--infiniteMode { + transform: none; + will-change: transform; + transition: transform 400ms ease-in-out; + + &.umb-editor--moveRight { + transform: translateX(110%); + } + } + + &--outOfRange { + transform: none; + display: none; + will-change: auto; + transition: display 0s 320ms; + } + + &--level0 { + transform: none; } } -.umb-editor--outOfRange { - transform: none; - display: none; - will-change: auto; - transition: display 0s 320ms; -} -.umb-editor--level0 { - transform: none; -} -.umb-editor--level1 { - transform: translateX(60px); -} -.umb-editor--level2 { - transform: translateX(120px); -} -.umb-editor--level3 { - transform: translateX(180px); -} +// use a loop to build the editor levels +@iterations: 3; +@step: 60px; -.umb-editor--n1 { - right:60px; -} -.umb-editor--n2 { - right:120px; -} -.umb-editor--n3 { - right:180px; -} - -// hide all infinite editors by default -// will be shown through animation -.umb-editors .umb-editor { - box-shadow: 0px 0 30px 0 rgba(0,0,0,.3); -} - -.umb-editor--small { - width: 500px; - will-change: transform; - left: auto; - - .umb-editor-container { - max-width: 500px; +.level-loop (@i) when (@i > 0) { + @x: @i * @step; + .umb-editor--level@{i} { + transform: translateX(@x); } + + .umb-editor--n@{i} { + right:@x; + } + + .level-loop(@i - 1); } +.level-loop(@iterations); + +// and also use a loop to build editor sizes - easily extended with new sizes by adding to the map +@editorSizes: + small 500px, + medium 800px; + +.create-editor-sizes(@iterator:1) when(@iterator <= length(@editorSizes)) { + .umb-editor { + @size: extract(extract(@editorSizes, @iterator), 1); + @value: extract(extract(@editorSizes, @iterator), 2); + + &--@{size} { + width: @value; + will-change: transform; + left: auto; + + .umb-editor--container { + max-width: @value; + } + } + } + + .create-editor-sizes(@iterator + 1); +} + +.create-editor-sizes(); + .umb-editor__overlay { - position: absolute; - top: 0; - bottom: 0; - right: 0; - left: 0; + .absolute(); background: rgba(0,0,0,0.4); z-index: @zIndexEditor; visibility: hidden; @@ -85,8 +90,19 @@ } #contentcolumn > .umb-editor__overlay, -.--notInFront .umb-editor__overlay { +.umb-editor--notInFront .umb-editor__overlay { visibility: visible; opacity: 1; transition: opacity 320ms 20ms, visibility 0s; } + +.umb-editor--trashed-message { + background:@errorBackground; + color:@errorText; + padding:10px; + margin-bottom:20px; + + i { + margin-right:5px; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less new file mode 100644 index 0000000000..8dbc070856 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -0,0 +1,332 @@ +/* variant switcher */ +.umb-variant-switcher__toggle { + position: relative; + display: flex; + align-items: center; + padding: 0 10px; + margin: 1px 1px; + right: 0; + height: 30px; + text-decoration: none !important; + font-size: 13px; + color: @ui-action-discreet-type; + background: transparent; + border: none; + + max-width: 50%; + white-space: nowrap; + + user-select: none; + + span { + text-overflow: ellipsis; + overflow: hidden; + } +} + +button.umb-variant-switcher__toggle { + transition: color 0.2s ease-in-out; + &:hover { + //background-color: @gray-10; + color: @ui-action-discreet-type-hover; + .umb-variant-switcher__expand { + color: @ui-action-discreet-type-hover; + } + } + + &.--error { + &::before { + content: '!'; + position: absolute; + top: -8px; + right: -10px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 10px; + text-align: center; + font-weight: bold; + background-color: @errorBackground; + color: @errorText; + } + } +} + +.umb-variant-switcher__expand { + color: @ui-action-discreet-type; + margin-top: 3px; + margin-left: 5px; + margin-right: -5px; + transition: color 0.2s ease-in-out; +} + + +.umb-variant-switcher { + min-width: 100%; + max-height: 80vh; + overflow-y: auto; + margin-top: 5px; + user-select: none; +} + +.umb-variant-switcher__item { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid @gray-9; + position: relative; + .umb-variant-switcher__name-wrapper:hover { + .umb-variant-switcher__name { + color: @blueMid; + } + .umb-variant-switcher__state { + color: @blueMid; + } + } +} +.umb-variant-switcher__item.--state-notCreated:not(.--active) { + .umb-variant-switcher__name-wrapper::before { + content: "+"; + display: block; + float: left; + font-size: 15px; + font-weight: 900; + padding: 8px 16px 8px 6px; + color: @gray-5; + } + .umb-variant-switcher__item-expand-button + .umb-variant-switcher__name-wrapper::before { + padding: 8px 16px 8px 20px; + } + .umb-variant-switcher__name { + color: @gray-5; + } + .umb-variant-switcher__state { + color: @gray-6; + } + .umb-variant-switcher__name-wrapper::after { + content: ""; + position: absolute; + z-index: 1; + border: 1px dashed @gray-9; + top: 7px; + bottom: 7px; + left: 7px; + right: 7px; + border-radius: 3px; + pointer-events: none; + } + + .umb-variant-switcher__name-wrapper:hover { + &::before { + color: @blueMid; + } + .umb-variant-switcher__name { + color: @blueMid; + } + .umb-variant-switcher__state { + color: @blueMid; + } + } +} +/* +.umb-variant-switcher__item.--state-draft { + .umb-variant-switcher__name { + color: @gray-5; + } + &:hover { + .umb-variant-switcher__name { + color: @blueMid; + } + } +} +*/ + +.umb-variant-switcher.--has-sub-variants { + .umb-variant-switcher__item { + + } +} + +.umb-variant-switcher__item-expand-button { + text-decoration: none; + display: inline-block; + flex: 0; + align-self: stretch; + + padding-left: 22px !important; + padding-right: 14px !important; + + font-size: 12px; + + * { + pointer-events: none; + } +} + +.umb-variant-switcher__item:last-child { + border-bottom: none; +} + +.umb-variant-switcher__item.--current { + //color: @ui-light-active-type; + //background-color: @pinkExtraLight; + .umb-variant-switcher__name { + //color: @ui-light-active-type; + font-weight: 700; + } + &::before { + content: ''; + position: absolute; + border-radius: 0 4px 4px 0; + background-color: @ui-active-border; + width: 4px; + top:8px; + bottom: 8px; + left:0; + z-index:1; + pointer-events: none; + } +} + +.umb-variant-switcher__item:hover { + outline: none; +} + +.umb-variant-switcher__item.--active:not(.--current) .umb-variant-switcher__name-wrapper:hover { + //background-color: @white !important; + cursor: default; +} + +.umb-variant-switcher__item:focus .umb-variant-switcher__split-view, +.umb-variant-switcher__item:focus-within .umb-variant-switcher__split-view, +.umb-variant-switcher__item:hover .umb-variant-switcher__split-view, +.umb-variant-switcher__split-view:focus { + display: block; + cursor: pointer; +} + +.umb-variant-switcher__item.--error { + .umb-variant-switcher__name { + color: @red; + &::after { + content: '!'; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 5px; + top: -3px; + width: 14px; + height: 14px; + border-radius: 7px; + font-size: 8px; + text-align: center; + font-weight: bold; + background-color: @errorBackground; + color: @errorText; + } + } +} + +.umb-variant-switcher__name-wrapper { + font-size: 14px; + text-align: left; + flex: 1; + cursor: pointer; + background-color: transparent; + border: none; +} +.dropdown-menu>li { + > .umb-variant-switcher__name-wrapper { + padding-top: 10px; + padding-bottom: 10px; + } + + > .umb-variant-switcher__item-expand-button + .umb-variant-switcher__name-wrapper { + padding-left: 5px; + } +} + + +.umb-variant-switcher__name { + display: block; + font-weight: 600; + margin-bottom: -2px; +} + +.umb-variant-switcher__state { + font-size: 12px; + color: @gray-4; +} + +.umb-variant-switcher__split-view { + font-size: 12px; + display: none; + padding: 20px 20px; + position: absolute; + right: 0; + top: 0; + bottom: 0; + background-color: @white; + + &:hover { + background-color: @ui-option-hover; + color: @ui-option-type-hover; + } +} + + +.umb-variant-switcher__sub-variants { + + position: relative; + border-bottom: 1px solid @gray-9; + background-color: @gray-13; + /* + &::before { + content: ""; + position: absolute; + z-index: 1; + top: 0px; + left: 20px; + width: 4px; + bottom: 14px; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + background-color: @gray-8; + } + */ + .umb-variant-switcher__item { + border-bottom-color: @gray-10; + } + + .umb-variant-switcher__item.--state-notCreated:not(.--active) { + .umb-variant-switcher__name-wrapper::after { + left: 55px;// overwrite left to achieve same indentation on the dashed border as language. + } + } + + .umb-variant-switcher__name-wrapper { + + margin-left: 48px; + padding-left: 20px; + + padding-top: 10px; + padding-bottom: 10px; + + &:hover { + color: @ui-option-type-hover; + background-color: @ui-option-hover; + } + + .umb-variant-switcher__name { + //margin-right: 20px; + } + .umb-variant-switcher__state { + //flex: 0 0 200px; + } + + + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less b/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less index 7f04fef9a9..67038380ca 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less @@ -1,5 +1,5 @@ .umb-notifications { - z-index: 1000; + z-index: 1100; position: absolute; bottom: @editorFooterHeight; left: 0; @@ -14,7 +14,7 @@ list-style: none; margin: 0; position: relative; -} +} .umb-notifications__notification { padding: 5px 20px; @@ -24,11 +24,14 @@ position: relative; border-radius: 10px; margin: 10px; - + .close { + position: absolute; top: 0; - right: -6px; + bottom: 0; + right: 6px; opacity: 0.4; + margin: auto 0; } } @@ -36,3 +39,8 @@ padding-top: 20px; padding-bottom: 20px; } + + +.emptySection .umb-notifications{ + left:0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index f5050fad85..381157c8bc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -2,38 +2,43 @@ position: fixed; overflow: hidden; background: @white; - z-index: @zindexUmbOverlay; + z-index: 7501; animation: fadeIn 0.2s; box-shadow: 0 10px 50px rgba(0,0,0,0.1), 0 6px 20px rgba(0,0,0,0.16); text-align: left; + + .scoped-view{ + display: none; + } } .umb-overlay__form { display: flex; flex-wrap: nowrap; flex-direction: column; + height: 100%; } .umb-overlay .umb-overlay-header { - border-bottom: 1px solid @purple-l3; + border-bottom: 1px solid @gray-9; margin-top: 0; flex-grow: 0; flex-shrink: 0; - - padding: 20px 30px 0; + padding: 30px 30px 0; } - - .umb-overlay__section-header { width: 100%; margin-top:30px; - margin-bottom: 10px; - + margin-bottom: 20px; + h5 { display: inline; + font-size: 16px; + line-height: 16px; + font-weight: bold; } - + button { display: inline; float: right; @@ -46,10 +51,11 @@ } .umb-overlay__title { - font-size: @fontSizeLarge; + font-size: 16px; color: @black; + line-height: 16px; font-weight: bold; - margin: 7px 0; + margin: 5px 0; } .umb-overlay__subtitle { @@ -63,9 +69,8 @@ flex-shrink: 1; flex-basis: auto; position: relative; - - padding: 0px 30px; - margin-bottom: 10px; + padding: 20px 30px; + background: @white; max-height: calc(100vh - 170px); overflow-y: auto; } @@ -73,12 +78,11 @@ .umb-overlay-drawer { flex-grow: 0; flex-shrink: 0; - flex-basis: 31px; - padding: 10px 20px; + flex-basis: 33px; + padding: 8px 20px; margin: 0; - - background: @gray-10; - border-top: 1px solid @purple-l3; + background: @white; + border-top: 1px solid @gray-9; } .umb-overlay-drawer.-auto-height { @@ -113,10 +117,6 @@ padding: 30px 30px 0; } -.umb-overlay.umb-overlay-center .umb-overlay__form { - -} - .umb-overlay.umb-overlay-center .umb-overlay-drawer { border: none; background: transparent; @@ -129,11 +129,7 @@ max-height: 100vh; box-sizing: border-box; border-radius: @baseBorderRadius; - /* default: - &.umb-overlay--small { - width: 400px; - } - */ + &.umb-overlay--medium { width: 480px; } @@ -147,7 +143,6 @@ .umb-overlay.umb-overlay-target .umb-overlay-drawer { border: none; background: transparent; - padding: 0 30px 20px; } /* ---------- OVERLAY RIGHT ---------- */ @@ -158,10 +153,14 @@ bottom: 0; border: none; box-shadow: 0 0 20px rgba(0,0,0,0.19), 0 0 6px rgba(0,0,0,0.23); + + .umb-drawer-is-visible & { + right:400px; + } } .umb-overlay.umb-overlay-right .umb-overlay-header { - flex-basis: 100px; + flex-basis: 70px; box-sizing: border-box; } @@ -233,12 +232,12 @@ } .umb-overlay__item-details-title { - margin-top: 0; - margin-bottom: 0; + margin: 0; + font-size: 15px; } .umb-overlay__item-details-description { - margin-top: 10px; + margin: 10px 0 0; font-size: 12px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-itempicker.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-itempicker.less index 3727c92251..010ff4636d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-itempicker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-itempicker.less @@ -1,6 +1,3 @@ .umb-itempicker .form-search { margin-top:10px; } -.umb-card-grid { - margin-top: 10px; -} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less new file mode 100644 index 0000000000..b50a622f98 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less @@ -0,0 +1,26 @@ +.umb-variant-selector-overlay { + + + .umb-variant-selector-entry { + .umb-form-check { + .umb-form-check__symbol { + margin-top: 2px; + } + } + } + .umb-variant-selector-entry__title { + font-weight: 600; + font-size: 14px; + .__secondarytitle { + font-weight: normal; + color: @gray-5; + } + } + .umb-variant-selector-entry__description { + display: block; + font-size: 12px; + color: @gray-4; + } + + +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less index 0dbc4c381c..7036d60a63 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less @@ -1,5 +1,5 @@ .umb-prevalues-multivalues { - width: 400px; + width: 425px; max-width: 100%; .umb-overlay & { @@ -44,7 +44,7 @@ display: flex; padding: 6px; margin: 10px 0px !important; - background: #F3F3F5; + background: @gray-10; cursor: move; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less index 14544ded10..0c231830de 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less @@ -27,6 +27,7 @@ } .umb-action-link { + position: relative; white-space: nowrap; font-size: 15px; color: @black; @@ -34,6 +35,7 @@ text-decoration: none; cursor: pointer; display: flex; + width: 100%; align-items: center; body.touch & { @@ -92,6 +94,7 @@ font-size: 14px; color: @black; margin-left: 10px; + text-align: left; } small { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less index b372d41eb4..4a483ce3f0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less @@ -3,57 +3,67 @@ min-width: 100%; width: auto; margin-top:1px; - - .umb-tree-item__label { - user-select: none; - } - - &:hover ins { + + &:hover .umb-tree-item__arrow { visibility: visible; cursor: pointer } } +.umb-tree-item__label { + user-select: none; +} + +.umb-tree-item__arrow { + position: relative; + margin-left: -16px; + width: 16px; + height: 16px; + visibility: hidden; + text-decoration: none; + font-size: 12px; + line-height: 12px; + transition: color 120ms; + + &:hover { + color: @ui-option-type-hover; + } +} + .umb-tree-item > .umb-tree-item__inner { &:hover .umb-tree-item__label { overflow: hidden; margin-right: 6px; } - + // Loading Animation // ------------------------ - .l { + .umb-tree-item__loader { width: 100%; - height: 2px; - overflow: hidden; position: absolute; - left: 0; - bottom: -1px; - - div { - .umb-loader; - } + margin: 0; } .umb-tree-item__label { - padding: 7px 0 5px; + padding: 7px 0 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - flex: 1 0 auto; + flex: 1 1 auto; } } // active is equivilant to selected, its the item that is begin affected by the actions performed in the right-click-dialog. .umb-tree-item.active > .umb-tree-item__inner { + border-color: @ui-selected-border; + box-shadow: 0 0 2px 0 fade(@ui-selected-border, 80%); color: @ui-selected-type; + a { color: @ui-selected-type; } - - border-color: @ui-selected-border; - box-shadow: 0 0 2px 0 fade(@ui-selected-border, 80%); + &::before { content: ""; position: absolute; @@ -61,10 +71,12 @@ left: 0; right: 0; bottom: 0; - border: 2px solid fade(white, 80%); + border: 2px solid fade(@white, 80%); } + &:hover { color: @ui-selected-type-hover; + a { color: @ui-selected-type-hover; } @@ -72,35 +84,22 @@ } .umb-tree-item.current > .umb-tree-item__inner { - background: @ui-active; color:@ui-active-type; - - // override small icon color. TODO => check usage + + // override small icon color. TODO => check usage &:before { color: @blue; } - - .umb-options { - - &:hover i { - opacity: .7; - } - - i { - background: @ui-active-type; - transition: opacity 120ms ease; - } - } a, .umb-tree-icon, - ins { + .umb-tree-item__arrow { color: @ui-active-type !important; } } .umb-tree-item.current-not-active > .umb-tree-item__inner { background: @ui-active-blur; - color:@ui-active-type; + color:@ui-active-type; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index 202c9400da..839e61c5f9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -15,20 +15,6 @@ text-decoration: none; } - ins { - margin: -4px 0 0 -16px; - width: 16px; - height: 16px; - visibility: hidden; - text-decoration: none; - font-size: 12px; - transition: color 120ms; - - &:hover { - color: @ui-option-type-hover; - } - } - i.noSpr { display: inline-block; margin-top: 1px; @@ -47,12 +33,6 @@ display: none; } } - - //loader defaults - .umb-loader { - height: 10px; - margin: 10px 10px 10px 10px; - } .search-subtitle { color: @gray-7; @@ -62,7 +42,7 @@ } body.touch .umb-tree { - ins { + .umb-tree-item__arrow { font-size: 14px; visibility: visible; padding: 7px; @@ -86,9 +66,7 @@ body.touch .umb-tree { overflow: hidden; display: flex; flex-wrap: nowrap; - align-items: center; - - border:2px solid transparent; + align-items: center; color: @ui-option-type; a { @@ -104,7 +82,13 @@ body.touch .umb-tree { } > .umb-options { - visibility: visible; + position: relative; + width: auto; + height: auto; + margin: 0 10px 0 auto; + padding: 9px 5px; + overflow: visible; + clip: auto; } .umb-tree-icon { @@ -113,6 +97,11 @@ body.touch .umb-tree { } } +.umb-tree-item__inner { + border: 2px solid transparent; + overflow: visible; +} + .umb-tree-header { display: flex; padding: 20px 0 20px 20px; @@ -178,7 +167,7 @@ body.touch .umb-tree { } .umb-options { - visibility: hidden; + position: relative; display: flex; flex: 0 0 auto; justify-content: flex-end; @@ -188,22 +177,38 @@ body.touch .umb-tree { cursor: pointer; border-radius: @baseBorderRadius; - &:hover { - background: @btnBackgroundHighlight; - } - i { height: 5px !important; width: 5px !important; border-radius: 20px; - background: @black; display: inline-block; margin: 0 2px 0 0; - + background: @ui-active-type; + &:last-child { margin: 0; } } + &:hover { + background: rgba(255, 255, 255, .5); + i { + background: @ui-active-type-hover; + } + } + + // NOTE - We're having to repeat ourselves here due to an .sr-only class appearing in umbraco/lib/font-awesome/css/font-awesome.min.css + &.sr-only--hoverable:hover, + &.sr-only--focusable:focus { + position: relative; + display: flex; + flex: 0 0 auto; + justify-content: flex-end; + padding: 9px 5px; + text-align: center; + margin: 0 10px 0 auto; + cursor: pointer; + border-radius: 3px; + } .hide-options & { display: none !important; @@ -251,7 +256,7 @@ body.touch .umb-tree { } &.current > .umb-tree-item__inner > .umb-tree-item__annotation { - background-color: @pinkLight; + background-color: @ui-active; } } @@ -288,9 +293,8 @@ body.touch .umb-tree { } .no-access { - .umb-tree-icon, - .root-link, - .umb-tree-item__label { + > .umb-tree-item__inner .umb-tree-icon, + > .umb-tree-item__inner .umb-tree-item__label { color: @gray-7; cursor: not-allowed; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less index 21f4a7bda8..c6b9dc7261 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less @@ -103,7 +103,7 @@ a.umb-avatar-btn:hover { text-decoration: none; } a.umb-avatar-btn .umb-avatar { - border: 2px dashed #A2A1A6; + border: 2px dashed @gray-6; } a.umb-avatar-btn .umb-avatar span { font-size: 50px; @@ -114,4 +114,4 @@ a.umb-avatar-btn .umb-avatar span { font-size: 50px; } -/*border-radius: 50%; width: 100px; height: 100px; font-size: 50px; text-align: center; display: flex; align-items: center; justify-content: center; background-color: #F3F3F5; border: 2px dashed #A2A1A6; color: #A2A1A6;*/ +/*border-radius: 50%; width: 100px; height: 100px; font-size: 50px; text-align: center; display: flex; align-items: center; justify-content: center; background-color: @gray-10; border: 2px dashed @gray-6; color: @gray-6;*/ diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less index c0e91e28c2..1fe59ab9dd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less @@ -1,12 +1,14 @@ +@boxUnit: 10px; + .umb-box { background: @white; border-radius: 3px; - margin-bottom: 20px; + margin-bottom: @boxUnit * 2; box-shadow: 0 1px 1px 0 rgba(0,0,0,.16); } .umb-box-header { - padding: 10px 20px; + padding: @boxUnit @boxUnit * 2; border-bottom: 1px solid @gray-9; display: flex; align-items: center; @@ -27,5 +29,19 @@ } .umb-box-content { - padding: 20px; + padding: @boxUnit * 2; } + +// allow side-by-side boxes +.umb-box-row { + margin-left:-@boxUnit; + margin-right:-@boxUnit; + display:flex; + justify-content: space-around; + + .umb-box { + margin-left:@boxUnit; + margin-right:@boxUnit; + flex:1; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less index ba04069c6b..de678f9798 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less @@ -4,10 +4,32 @@ margin-left: 0; display: flex; flex-wrap: wrap; + user-select: none; } .umb-breadcrumbs__ancestor { display: flex; + align-items: center; +} + +.umb-breadcrumbs__action { + position: relative; + background: transparent; + border: 0 none; + border-radius: 3px; + padding: 0 4px; + color: @ui-option-type; + + &.--current { + font-weight: bold; + pointer-events: none; + } + + &:hover { + color: @ui-option-type-hover; + background-color: @white; + } + } .umb-breadcrumbs__ancestor-link, @@ -18,6 +40,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + padding: 0 4px; } .umb-breadcrumbs__ancestor-link { @@ -31,14 +54,13 @@ .umb-breadcrumbs__separator { position: relative; top: 1px; - margin-left: 5px; - margin-right: 5px; + margin: 0 1px; + margin-top: -3px; color: @gray-7; } input.umb-breadcrumbs__add-ancestor { - height: 25px; - margin-top: -2px; - margin-left: 3px; + height: 24px; + margin: -2px 0 -2px 3px; width: 100px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less index 985e57bea5..11194eeb43 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less @@ -4,6 +4,7 @@ .umb-checkbox-list { list-style: none; margin-left: 0; + margin-top: 6px; } .umb-checkbox-list__item { @@ -12,6 +13,10 @@ margin-bottom: 2px; } +.umb-checkbox-list li:first-child { + font-weight: bold; +} + .umb-checkbox-list__item:last-child { border-bottom: none; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less index 021fc8cc9b..f82e47bf88 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less @@ -2,21 +2,30 @@ border: 2px solid @white; width: 25px; height: 25px; - border: 1px solid @gray-7; + border: 1px solid @ui-action-discreet-border; border-radius: 3px; box-sizing: border-box; display: flex; justify-content: center; align-items: center; - color: @gray-7; + color: @ui-selected-type; cursor: pointer; font-size: 15px; + &:hover { + border-color:@ui-action-discreet-border-hover; + color: @ui-selected-type-hover; + } } .umb-checkmark--checked { - background: @ui-active; - border-color: @ui-active; + background: @ui-selected-border; + border-color: @ui-selected-border; color: @white; + &:hover { + background: @ui-selected-border-hover; + border-color: @ui-selected-border-hover; + color: @white; + } } .umb-checkmark--xs { @@ -45,4 +54,4 @@ width: 50px; height: 50px; font-size: 20px; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less index b6cdc0e8d9..937f746c56 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less @@ -16,20 +16,24 @@ .umb-child-selector__child.-placeholder { border: 1px dashed @gray-8; background: none; - cursor: pointer; text-align: center; justify-content: center; - - color:@ui-action-type; + width: 100%; + color: @ui-action-type; + &:hover { - color:@ui-action-type-hover; - border-color:@ui-action-type-hover; - text-decoration:none; + color: @ui-action-type-hover; + border-color: @ui-action-type-hover; + text-decoration: none; } } .umb-child-selector__children-container { - margin-left: 30px; + margin-left: 30px; + + .umb-child-selector__child.ui-sortable-handle { + cursor: move; + } } .umb-child-selector__child-description { @@ -62,5 +66,6 @@ } .umb-child-selector__child-remove { - cursor: pointer; + background: none; + border: none; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-code-snippet.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-code-snippet.less new file mode 100644 index 0000000000..b372841910 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-code-snippet.less @@ -0,0 +1,43 @@ +.umb-code-snippet { + + .umb-code-snippet__header { + box-sizing: content-box; + background-color: @gray-10; + display: flex; + flex-direction: row; + font-size: .8rem; + border: 1px solid @gray-8; + border-radius: 3px 3px 0 0; + border-bottom: 0; + margin-top: 16px; + min-height: 30px; + + .language { + display: flex; + align-items: center; + justify-content: flex-start; + flex-grow: 1; + padding: 2px 10px; + } + + button { + background-color: transparent; + border: none; + border-left: 1px solid @gray-8; + border-radius: 0; + color: #000; + + &:hover { + background-color: @grayLighter; + } + } + } + + .umb-code-snippet__content { + pre { + border-radius: 0 0 3px 3px; + overflow: auto; + white-space: nowrap; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-confirm-action.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-confirm-action.less index 1f0808a592..f972633bf4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-confirm-action.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-confirm-action.less @@ -102,6 +102,7 @@ border-radius: 40px; box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); font-size: 18px; + cursor: pointer; } .umb_confirm-action__overlay-action:hover { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less index 84cfe04263..47fc8a10b9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less @@ -11,7 +11,7 @@ cursor: pointer; position: relative; user-select: none; - box-shadow: 0 1px 1px 0 rgba(0,0,0,0.16); + box-shadow: 0 1px 2px 0 rgba(0,0,0,0.16); border-radius: 3px; } @@ -29,15 +29,15 @@ border-radius: 5px; box-shadow: 0 0 4px 0 darken(@ui-selected-border, 20), inset 0 0 2px 0 darken(@ui-selected-border, 20); pointer-events: none; + transition: opacity 100ms; } - } } .umb-content-grid__item:hover { &::before { - opacity: .33; + opacity: .2; } } .umb-content-grid__item.-selected:hover { @@ -46,6 +46,7 @@ } } + .umb-content-grid__icon-container { height: 75px; display: flex; @@ -66,8 +67,10 @@ } .umb-content-grid__item-name { + position: relative; + padding: 5px; + margin: -5px -5px 15px -5px; font-weight: bold; - margin-bottom: 15px; line-height: 1.4em; display: inline-flex; @@ -105,6 +108,7 @@ .umb-content-grid__details-value { display: inline-block; word-break: break-word; + margin-left: 3px; } .umb-content-grid__checkmark { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-contextmenu.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-contextmenu.less new file mode 100644 index 0000000000..8512e2020d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-contextmenu.less @@ -0,0 +1,75 @@ +.umb-contextmenu { + margin: 0; + list-style: none; + user-select: none; + + overflow: hidden; + border-radius: 3px; + border: 1px solid @dropdownBorder; + .box-shadow(0 5px 20px rgba(0,0,0,.3)); + border-bottom: 1px solid rgba(0,0,0,.2); + + .sep { + display: block; + border-top: 1px solid @gray-9; + + &:first-child { + border-top: none; + } + } + +} + +.umb-contextmenu-item { + + .icon { + font-size: 18px; + vertical-align: middle; + } + + .menu-label { + display: inline-block; + vertical-align: middle; + margin-left: 5px; + } + + button { + + position: relative; + + display: block; + font-weight: normal; + line-height: @baseLineHeight; + white-space: nowrap; + + background-color: @ui-option; + border: 0; + padding: 7px 12px; + color: @ui-option-type; + width: 100%; + + font-size: 14px; + text-align: left; + + &:hover { + text-decoration: none; + color: @ui-option-type-hover; + background-color: @ui-option-hover; + } + } + + &.-opens-dialog { + .menu-label:after { + // adds an ellipsis (...) after the menu label for actions that open a dialog + content: '\2026'; + } + } + button:disabled { + cursor: not-allowed; + color: @ui-option-disabled-type; + &:hover { + color: @ui-option-disabled-type-hover; + background-color: @ui-option; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less similarity index 82% rename from src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less rename to src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less index 8cdcc8b877..5bcfdd1c71 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less @@ -1,4 +1,4 @@ -.flatpickr-calendar.flatpickr-calendar { +.flatpickr-calendar { border-radius: @baseBorderRadius; box-shadow: 0 5px 10px 0 rgba(0,0,0,0.16); } @@ -6,6 +6,10 @@ span.flatpickr-day { border-radius: @baseBorderRadius; border: none; + + &.today:not(.active) { + border: 1px solid; + } } span.flatpickr-day:hover { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less index e6b3fdbfa9..f3c41dbc33 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less @@ -1,172 +1,188 @@ .umb-sub-views-nav-item { position: relative; display: block; -} -.umb-sub-views-nav-item > a { - text-align: center; - cursor: pointer; - display: block; - padding: 4px 10px 0 10px; - min-width: 70px; - border-right: 1px solid @gray-9; - box-sizing: border-box; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: calc(~'@{editorHeaderHeight}' - ~'1px'); // need to offset the 1px border-bottom on .umb-editor-header - avoids overflowing top of the container - position: relative; - - color: @ui-active-type; - - &:hover { - color: @ui-active-type-hover !important; - } - - &::after { - content: ""; - height: 0px; - left: 8px; - right: 8px; - background-color: @ui-light-active-border; - position: absolute; - bottom: 0; - border-radius: 3px 3px 0 0; - opacity: 0; - transition: all .2s linear; - } -} -.umb-sub-views-nav-item > a:active { - .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); -} -.umb-sub-views-nav-item > a:focus { - outline: none; -} + &__action, + > a { + position: relative; + background: transparent; + text-align: center; + cursor: pointer; + display: block; + padding: 4px 10px 0 10px; + min-width: 70px; + border: 0 none; + border-right: 1px solid @gray-9; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: calc(~'@{editorHeaderHeight}'- ~'1px'); // need to offset the 1px border-bottom on .umb-editor-header - avoids overflowing top of the container + color: @ui-active-type; -.umb-sub-views-nav-item > a:hover, -.umb-sub-views-nav-item > a:focus { - text-decoration: none; -} + &:hover { + color: @ui-active-type-hover !important; + text-decoration: none; + } -.umb-sub-views-nav-item > a.is-active { - - color: @ui-light-active-type; - - &::after { + &::before { + content: ""; + position: absolute; + height: 0px; + left: 8px; + right: 8px; + background-color: @ui-light-active-border; + bottom: 0; + border-radius: 3px 3px 0 0; + opacity: 0; + transition: all .2s linear; + } + + &.is-active { + color: @ui-light-active-type; + + &::before { + opacity: 1; + height: 4px; + } + } + } + + &__action:active, + & > a:active { + .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); + } + + &:focus-within &__anchor_dropdown, + &:hover &__anchor_dropdown { + visibility: visible; opacity: 1; - height: 4px; + transition: opacity 20ms 0; + } + + .icon { + font-size: 24px; + display: block; + text-align: center; + margin-bottom: 7px; + } + + .badge { + position: absolute; + top: 6px; + right: 6px; + min-width: 16px; + color: @white; + background-color: @ui-active-type; + border: 2px solid @white; + border-radius: 50%; + font-size: 10px; + font-weight: bold; + padding: 2px; + line-height: 16px; + display: block; + + &.-type-alert { + background-color: @red; + } + + &.-type-warning { + background-color: @yellow-d2; + } + + &:empty { + height: 12px; + min-width: 12px; + } + } + + &-text { + font-size: 12px; + line-height: 1em; + } + + &__anchor_dropdown { + // inherits from .dropdown-menu + margin: 0; + + // center align horizontal + left: 50%; + transform: translateX(-50%); + opacity: 0; + transition: opacity 250ms 250ms; + visibility: hidden; + + li { + &.is-active a { + border-left-color: @ui-active-border; + } + + a { + border-left: 4px solid transparent; + } + } + } + + // Currently Edge 18 does unfortunately not support :focus-within so for now we will use the "old" behavior - Support is coming with the upcoming release of Edge 76 + // See https://caniuse.com/#search=focus-within + @supports (-ms-ime-align:auto) { + &:hover &__anchor_dropdown { + transition: visibility 0 0, opacity 20ms 0; + } + + &__anchor_dropdown { + visibility: hidden; + transition: visibility 0 500ms, opacity 250ms 250ms; + } + } + + // -------------------------------- + // item__more, appears when there is not enough room for the visible items. + // -------------------------------- + + &-more__icon { + margin-bottom: 10px; + + i { + height: 5px; + width: 5px; + border-radius: 50%; + background: @ui-active-type; // fallback if browser doesnt support currentColor + background: currentColor; + display: inline-block; + margin: 0 5px 0 0; + } + + i:last-of-type { + margin-right: 0; + } + } + + &-more__dropdown { + left: auto; + right: 0; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + min-width: auto; + margin-top: 10px; + + > li { + display: flex; + } + + .umb-sub-views-nav-item:first { + border-left: none; + } } } +// Validation +.show-validation .umb-sub-views-nav-item__action.-has-error, .show-validation .umb-sub-views-nav-item > a.-has-error { - color: @red; - &::after { - background-color: @red; - } -} + color: @red; -.umb-sub-views-nav-item .icon { - font-size: 24px; - display: block; - text-align: center; - margin-bottom: 7px; -} - -.umb-sub-views-nav-item .badge { - position: absolute; - top: 6px; - right: 6px; - min-width: 16px; - color: @white; - background-color: @ui-active-type; - border: 2px solid @white; - border-radius: 50%; - font-size: 10px; - font-weight: bold; - padding: 2px; - line-height: 16px; - display: block; - - &.-type-alert { - background-color: @red; - } - &.-type-warning { - background-color: @yellow-d2; - } - &:empty { - height: 12px; - min-width: 12px; - } -} - -.umb-sub-views-nav-item-text { - font-size: 12px; - line-height: 1em; -} - - -.umb-sub-views-nav-item__anchor_dropdown {// inherits from .dropdown-menu - display: block; - margin: 0; - overflow: hidden; - - // center align horizontal - left: 50%; - transform: translateX(-50%); - - visibility:hidden; - opacity: 0; - transition: visibility 0s 500ms, opacity 250ms 250ms; -} -.umb-sub-views-nav-item__anchor_dropdown li a { - border-left: 4px solid transparent; -} -.umb-sub-views-nav-item__anchor_dropdown li.is-active a { - border-left-color: @ui-selected-border; -} - -.umb-sub-views-nav-item:hover .umb-sub-views-nav-item__anchor_dropdown { - visibility:visible; - opacity: 1; - transition: visibility 0s 0s, opacity 20ms 0s; -} - - - -// -------------------------------- -// item__more, appears when there is not enough room for the visible items. -// -------------------------------- - -.umb-sub-views-nav-item-more__icon { - margin-bottom: 10px; -} - -.umb-sub-views-nav-item-more__icon i { - height: 5px; - width: 5px; - border-radius: 50%; - background: @ui-active-type;// fallback if browser doesnt support currentColor - background: currentColor; - display: inline-block; - margin: 0 5px 0 0; -} - -.umb-sub-views-nav-item-more__icon i:last-of-type { - margin-right: 0; -} - -.umb-sub-views-nav-item-more__dropdown { - left: auto; - right: 0; - display: grid; - grid-template-columns: 1fr 1fr 1fr; - min-width: auto; - margin-top: 10px; -} -.umb-sub-views-nav-item-more__dropdown > li { - display: flex; -} -.umb-sub-views-nav-item-more__dropdown .umb-sub-views-nav-item:first { - border-left: none; + &::before { + background-color: @red; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less index 5071091fcc..b5d8c3cced 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less @@ -14,15 +14,18 @@ margin: 0 0 20px 0; position: relative; transition: height 0.8s; + .illustration { width: 300px; } + &.is-small { height: 100px; .illustration { width: 200px; } } + &.drag-over { border: 1px dashed @gray-1; } @@ -35,15 +38,19 @@ top: 50%; left: 50%; transform: translate(-50%,-50%); + display: flex; + flex-direction: column; } // file select link .file-select { + background: transparent; + border: 0; + padding: 0; font-size: 15px; color: @ui-action-discreet-type; cursor: pointer; - margin-top: 10px; &:hover { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less new file mode 100644 index 0000000000..febee80a97 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less @@ -0,0 +1,60 @@ +.umb-file-icon { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + + .file-icon { + display: flex; + flex-direction: column; + align-items: flex-start; + position: relative; + + > .icon { + font-size: 50px; + line-height: 100%; + color: @gray-4; + display: block; + text-align: center; + } + + > span { + position: absolute; + color: @white; + background: @ui-active; + padding: 1px 3px; + font-size: 10px; + line-height: 130%; + display: block; + margin-bottom: 0.75rem; + min-width: 1.2rem; + bottom: 0; + } + + & + small { + display: block; + margin-top: 0.25rem; + } + } +} + +.umb-file-icon--s { + .file-icon { + > .icon { + + } + } +} + +.umb-file-icon--m { + .file-icon { + > .icon { + font-size: 70px; + } + > span { + font-size: 14px; + margin-bottom: 0.95rem; + min-width: 1.5rem; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index d8a5ebf9d8..a52f81b92a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -1,21 +1,36 @@ -@checkboxWidth: 16px; -@checkboxHeight: 16px; +@checkboxWidth: 18px; +@checkboxHeight: 18px; +label.umb-form-check--checkbox{ + margin:3px 0; +} .umb-form-check { display: flex; - flex-wrap: wrap; - align-items: center; position: relative; - padding: 0; + padding-left: 0px; margin: 0; min-height: 22px; - line-height: 22px; cursor: pointer !important; + .umb-form-check__symbol { + margin-top: 4px; + margin-right: 10px; + } + .umb-form-check__info { + margin-left:20px; + } + + + &.-small-text{ + font-size: 13px; + } + + &.-bold{ + font-weight: 700; + } + &__text { - margin: 0 0 0 26px; position: relative; - top: 0; user-select: none; } @@ -24,7 +39,7 @@ top: 0; left: 0; opacity: 0; - + &:hover ~ .umb-form-check__state .umb-form-check__check { border-color: @inputBorderFocus; } @@ -36,7 +51,7 @@ background: @ui-option-type-hover; } } - + &:checked ~ .umb-form-check__state { .umb-form-check__check { // This only happens if the state has a radiobutton modifier @@ -62,8 +77,7 @@ } } } - - + .tabbing-active &.umb-form-check--radiobutton &__input:focus ~ .umb-form-check__state .umb-form-check__check { //outline: 2px solid @inputBorderTabFocus; border: 2px solid @inputBorderTabFocus; @@ -73,16 +87,21 @@ outline: 2px solid @inputBorderTabFocus; } .tabbing-active &.umb-form-check--checkbox &__input:checked:focus ~ .umb-form-check__state .umb-form-check__check { - border-color: white; + border-color: @white; + } + + // add spacing between when flexed/inline, equal to the width of the input + .flex & + & { + margin-left:@checkboxWidth; } &__state { display: flex; height: 18px; - margin-top: 2px; position: absolute; + margin-top: 2px; top: 0; - left: 0; + left: -1px; } &__check { @@ -90,6 +109,7 @@ position: relative; background: @white; border: 1px solid @inputBorder; + border-radius: @baseBorderRadius; width: @checkboxWidth; height: @checkboxHeight; @@ -149,5 +169,6 @@ &.umb-form-check--disabled { cursor: not-allowed !important; opacity: 0.5; + pointer-events: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid-selector.less index e25349f555..1ae476d584 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid-selector.less @@ -41,12 +41,17 @@ margin-top: 10px; } +button.umb-grid-selector__item { + width: 169px; + height: 194px; +} + .umb-grid-selector__item-icon { - font-size: 50px; - color: @gray-8; - display: block; - line-height: 50px; - margin-bottom: 15px; + font-size: 50px; + color: @gray-8; + display: block; + line-height: 50px; + margin-bottom: 15px; } .umb-grid-selector__item-label { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index 2eafe9b3d7..26d61412ae 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -162,6 +162,8 @@ } .umb-grid .umb-row .umb-cell-placeholder { + display: block; + width: 100%; min-height: 88px; border-width: 1px; border-style: dashed; @@ -226,6 +228,7 @@ .umb-grid .cell-tools-add.-bar { display: block; + width: calc(100% - 20px); text-align: center; padding: 5px; border: 1px dashed @ui-action-discreet-border; @@ -408,7 +411,19 @@ margin-bottom: 10px; } +.umb-grid .umb-editor-preview { + position: relative; + .umb-editor-preview-overlay { + cursor: pointer; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + } +} // Active states // ------------------------- @@ -425,7 +440,7 @@ .umb-grid .umb-row.-active-child { background-color: @gray-10; - + .umb-row-title-bar { cursor: inherit; } @@ -433,7 +448,7 @@ .umb-row-title { color: @gray-3; } - + } @@ -570,10 +585,10 @@ .umb-grid .iconBox.selected { -webkit-appearance: none; - background-image: linear-gradient(to bottom,#e6e6e6,#bfbfbf); + background-image: linear-gradient(to bottom,@gray-9,@gray-7); background-repeat: repeat-x; zoom: 1; - border-color: #bfbfbf #bfbfbf #999; + border-color: @gray-7 @gray-7 @gray-6; border-color: rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25); box-shadow: inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05); border-radius: 3px; @@ -625,9 +640,12 @@ } .umb-grid .mce-toolbar { - border-bottom: 1px solid @gray-8; - background-color: rgba(250, 250, 250, 1); + border-bottom: 1px solid @gray-7; + background-color: @white; display: none; + + left: 0; + right: 0; } .umb-grid .umb-control.-active .mce-toolbar { @@ -642,6 +660,13 @@ overflow-y: hidden!important; } +// had to overwrite defaults from TinyMCE, needed for buttons panel to float to new line in narrow space. +.umb-grid .mce-container > div { + white-space: normal; + left:0; + right:0; +} + // MEDIA EDITOR // ------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index 4c23aef5f0..ac7a2c63ce 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -23,6 +23,7 @@ } .umb-group-builder__group.-placeholder { + width:100%; min-height: 86px; display: flex; justify-content: center; @@ -136,9 +137,10 @@ input.umb-group-builder__group-title-input:disabled:hover { } .umb-group-builder__group-add-property { - min-height: 46px; - margin-right: 45px; + + width: calc(100% - 315px); margin-left: 270px; + min-height: 46px; border-radius: 3px; display: flex; @@ -344,8 +346,9 @@ input.umb-group-builder__group-title-input:disabled:hover { .umb-group-builder__property-actions { flex: 0 0 44px; - text-align: right; - margin-top: 7px; + display: flex; + align-items: center; + justify-content: flex-end; } .umb-group-builder__property-action { @@ -367,7 +370,6 @@ input.umb-group-builder__group-title-input:disabled:hover { overflow: visible; background: transparent; line-height: normal; - outline: 0; -webkit-appearance: none; &:hover, &:focus { @@ -474,7 +476,7 @@ input.umb-group-builder__group-sort-value { font-weight: bold; resize: none; line-height: 1.5em; - padding-left: 0; + padding: 0; border: none; &:focus { @@ -494,50 +496,87 @@ input.umb-group-builder__group-sort-value { font-weight: bold; font-size: 14px; color: @ui-action-type; - - &:hover{ + + &:hover { text-decoration: none; - color:@ui-action-type-hover; - border-color:@ui-action-border-hover; + color: @ui-action-type-hover; + border-color: @ui-action-border-hover; + background-color: @ui-action-discreet-hover; } } - .editor { + .editor-wrapper { margin-bottom: 10px; + } - .editor-icon-wrapper { - border: 1px solid @gray-8; - width: 60px; - height: 60px; - text-align: center; - line-height: 60px; - border-radius: 5px; - float: left; - margin-right: 20px; + .editor { + display: flex; + align-items: center; + align-content: stretch; - .icon { - font-size: 26px; - } + min-height: 80px; + + border: 1px solid @gray-9; + color: @ui-action-discreet-type; + border-radius: @baseBorderRadius; + + } + + .editor-info { + flex: 1 0 auto; + text-align: left; + display: flex; + align-items: center; + max-width: calc(100% - 48px); + min-height: 80px; + color: @ui-action-discreet-type; + + &:hover { + color: @ui-action-discreet-type-hover; + background-color: @ui-action-discreet-hover; + } + } + + .editor-icon-wrapper { + width: 60px; + height: 60px; + text-align: center; + line-height: 60px; + flex: 0 0 60px; + padding-left: 10px; + + .icon { + font-size: 32px; + } + } + + .editor-details { + flex: 1 1 auto; + margin-top: 10px; + margin-bottom: 10px; + + .editor-name { + display: block; + font-weight: bold; } - .editor-details { - float: left; - margin-top: 10px; - - .editor-name { - display: block; - font-weight: bold; - } - - .editor-editor { - display: block; - font-size: 12px; - } + .editor-editor { + display: block; + font-size: 12px; } + } - .editor-settings-icon { - font-size: 18px; - margin-top: 8px; + .editor-remove-icon { + flex: 0 0 48px; + width: 48px; + height: 48px; + font-size: 18px; + + min-height: 80px; + color: @ui-action-discreet-type; + &:hover { + color: @ui-action-discreet-type-hover; + background-color: @ui-action-discreet-hover; } } @@ -545,6 +584,11 @@ input.umb-group-builder__group-sort-value { margin-bottom: 20px; } + .editor-description { + margin-top: 20px; + padding: 0; + } + .editor-description, .editor-validation-pattern { min-width: 100%; @@ -555,7 +599,13 @@ input.umb-group-builder__group-sort-value { overflow: hidden; } - .editor-validation-pattern{ + .editor-validation-message { + min-width: 100%; + min-height: 25px; + margin-top: 4px; + } + + .editor-validation-pattern { border: 1px solid @gray-7; margin: 10px 0 0; padding: 6px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less index 34070256ce..98b2b1d72d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less @@ -15,7 +15,9 @@ overflow: hidden; } -.umb-iconpicker-item a { +.umb-iconpicker-item button { + background: transparent; + border: 0 none; display: flex; justify-content: center; align-items: center; @@ -26,8 +28,8 @@ border-radius: 3px; } -.umb-iconpicker-item a:hover, -.umb-iconpicker-item a:focus { +.umb-iconpicker-item button:hover, +.umb-iconpicker-item button:focus { background: @gray-10; outline: none; } @@ -39,7 +41,7 @@ box-sizing: border-box; } -.umb-iconpicker-item a:active { +.umb-iconpicker-item button:active { background: @gray-10; } @@ -51,7 +53,7 @@ // Color swatch .button { border: none; - color: white; + color: @white; padding: 5px; text-align: center; text-decoration: none; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-insert-code-box.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-insert-code-box.less index a87e7084fb..f3b53f4def 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-insert-code-box.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-insert-code-box.less @@ -11,7 +11,7 @@ cursor: pointer; } -.umb-insert-code-box:hover, +.umb-insert-code-box:hover, .umb-insert-code-box.-selected { background-color: @ui-option-hover; color: @ui-action-type-hover; @@ -32,7 +32,7 @@ .umb-insert-code-box__check { width: 18px; height: 18px; - background: @gray-10;x + background: @gray-10; border-radius: 50%; display: flex; align-items: center; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less index cf407b667f..c0ac89622d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less @@ -4,8 +4,10 @@ } .umb-layout-selector__active-layout { + background: transparent; box-sizing: border-box; - border: 1px solid @inputBorder; + border: 1px solid @ui-action-discreet-border; + color: @ui-action-discreet-type; cursor: pointer; height: 30px; width: 30px; @@ -16,23 +18,26 @@ } .umb-layout-selector__active-layout:hover { - border-color: @inputBorderFocus; + border-color: @ui-action-discreet-border-hover; + color: @ui-action-discreet-type-hover; } .umb-layout-selector__dropdown { position: absolute; padding: 5px; - background: #333; + background: @grayDark; z-index: 999; display: flex; - background: #fff; + background: @white; flex-wrap: wrap; flex-direction: column; transform: translate(-50%,0); left: 50%; + border-radius: 3px; } .umb-layout-selector__dropdown-item { + background: transparent; padding: 5px; margin: 3px 5px; display: flex; @@ -44,11 +49,11 @@ } .umb-layout-selector__dropdown-item:hover { - border: 1px solid @gray-8; + border: 1px solid @ui-action-discreet-border; } .umb-layout-selector__dropdown-item.-active { - border: 1px solid @blue; + border: 1px solid @ui-action-discreet-border-hover; } .umb-layout-selector__dropdown-item-icon, diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less index 733836d85f..ba46c68a57 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less @@ -1,54 +1,76 @@ .umb-list-view-settings__box { - background: @gray-10; - border: 1px solid @gray-7; - display: flex; - animation: fadeIn 0.5s; - padding: 15px; - position: relative; + background: @gray-10; + display: flex; + flex: 1; + padding: 15px; + position: relative; + border-radius: @baseBorderRadius; + + .btn-link { + font-size: 13px; + padding: 0; + } } .umb-list-view-settings__trigger { - margin-bottom: 20px; -} - -.umb-list-view-settings__box.-open { - border-bottom: transparent; -} - -.umb-list-view-settings__content { - display: flex; + margin-bottom: 20px; } .umb-list-view-settings__list-view-icon { - font-size: 20px; - color: @gray-7; - margin-right: 10px; + font-size: 20px; + color: @gray-7; + margin-right: 10px; } .umb-list-view-settings__name { - margin-right: 5px; - font-size: 14px; - font-weight: bold; - float: left; + font-size: 14px; + font-weight: bold; } .umb-list-view-settings__create-new { - font-size: 13px; - color: @ui-action-type; -} - -.umb-list-view-settings__create-new:hover { - color: @ui-action-type-hover; - border-color: @ui-action-type-hover; + color: @ui-action-type; } .umb-list-view-settings__remove-new { - font-size: 13px; - color: @red; + color: @red; } -.umb-list-view-settings__settings { - border: 1px dashed @gray-7; - border-top: none; - padding: 20px; +// display `columns displayed` table as a list-view layout +.umb-list-view-settings__overlay { + + .btn { + vertical-align: top; + } + + .btn-icon { + padding: 0; + } + + table { + width: 100%; + } + + tbody tr { + background: @gray-10; + border-bottom: 1px solid @white; + } + + th { + text-align: left; + } + + td { + padding: 10px 15px 10px 0; + + &:first-child { + padding-left: 15px; + } + + .ui-sortable-handle { + min-height: 37px; + display: flex; + width:0; + align-items: center; + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less index 12d7085b0a..44955e8f8e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less @@ -1,15 +1,18 @@ .umb-list--condensed { .umb-list-item { - padding-top: 5px; - padding-bottom: 5px; + padding-top: 7px; + padding-bottom: 7px; } } .umb-list-item { - border-bottom: 1px solid @gray-9; + border-bottom: 1px solid @gray-11; padding-top: 15px; padding-bottom: 15px; display: flex; + &:last-of-type { + border-bottom: none; + } } a.umb-list-item:hover, @@ -36,6 +39,10 @@ a.umb-list-item:focus { color: @gray-4; } +.umb-list-item__description--checkbox{ + margin: 0 0 0 26px; +} + .umb-list-checkbox { position: absolute; opacity: 0; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less new file mode 100644 index 0000000000..260710ce72 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less @@ -0,0 +1,42 @@ +// Loading Animation +// ------------------------ + +.umb-loader { + background-color: @blue; + margin-top: 0; + margin-left: -100%; + animation-name: bounce_loadingProgressG; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: linear; + width: 100%; + height: 2px; +} + +@keyframes bounce_loadingProgressG { + 0% { + margin-left: -100%; + } + + 100% { + margin-left: 100%; + } +} + +.umb-loader-wrapper { + position: absolute; + right: 0; + left: 0; + margin: 10px 0; + overflow: hidden; +} + +.umb-loader-wrapper.-top { + top: 0; + bottom: auto; +} + +.umb-loader-wrapper.-bottom { + top: auto; + bottom: 0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less index 8d9ae86ce7..2b49348190 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less @@ -6,13 +6,26 @@ } .umb-locked-field__wrapper { - display: flex; - align-items: center; - margin-bottom: 5px; + display: flex; + align-items: center; + margin-bottom: 5px; } .umb-locked-field__toggle { margin-right: 3px; + padding: 0; + background: none; + border: 0; + font-size: inherit; + line-height: inherit; + + &:focus { + outline: none; + + .tabbing-active & { + outline: 2px solid @inputBorderTabFocus; + } + } } .umb-locked-field__toggle:hover, @@ -29,7 +42,8 @@ color: @gray-3; } -input.umb-locked-field__input { +input.umb-locked-field__input, +.umb-locked-field__text { background: rgba(255, 255, 255, 0); // if using transparent it will hide the text in safari border-color: transparent !important; font-size: 13px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less index f7aa0e4558..8beff55b7c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less @@ -1,5 +1,3 @@ -/* PACKAGE DETAILS */ - .umb-logviewer { display: flex; flex-flow: row wrap; @@ -20,6 +18,15 @@ .umb-logviewer__sidebar { flex: 0 0 @sidebarwidth; + + .flatpickr-input { + background-color: @white; + border: 0; + width: 100%; + text-align: center; + font-size: larger; + padding-top: 20px; + } } @media (max-width: 768px) { @@ -32,7 +39,7 @@ flex: 1 1 auto; width: 100%; margin-bottom: 30px; - margin-right: 0; + margin-right: 0; } .umb-logviewer__sidebar { @@ -40,3 +47,99 @@ width: 100%; } } + +.umb-logviewer-search { + .filter-name { + margin-left: 5px; + margin-right: 3px; + max-width: 150px; + } + + .dropdown-item { + padding: 8px 20px 8px 16px; + } + + .filter { + position: relative; + + a.btn-link { + padding-left: 0px; + } + } + + .search-box { + width: 100%; + + .flex-auto { + position: relative; + + .search-input { + width: 100%; + padding-right: 160px; + } + + .icon-rate { + position: absolute; + top: 0; + line-height: 32px; + right: 140px; + color: @yellow-d1; + cursor: pointer; + } + + .icon-wrong { + position: absolute; + top: 0; + line-height: 32px; + right: 120px; + color: @gray-7; + cursor: pointer; + } + + .umb-variant-switcher__toggle { + top: 1px; + right: 0; + position: absolute; + + .icon-navigation-down { + margin-top: 0; + } + } + + .saved-searches { + width: 100%; + max-height: 250px; + overflow-y: scroll; + margin-top: -10px; + } + } + } + + .log-items { + .table { + table-layout: fixed; + + thead th:first-child, thead th:nth-child(3) { + width: 20%; + } + + thead th:nth-child(2) { + width: 15%; + } + + tr td:nth-child(3) { + word-break: break-word; + } + } + + .exception { + border-left: 4px solid @red; + padding: 0 10px 10px 10px; + box-shadow: rgba(0,0,0,0.07) 2px 2px 10px; + + .exception-message { + white-space: pre-wrap; + } + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less index 68973b4c7c..5d6b7ad962 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -21,29 +21,61 @@ margin: 10px; position: relative; - //overflow: hidden; - user-select: none; - - cursor: pointer; - box-shadow: 0 1px 1px 0 rgba(0,0,0,.2); - //border: 2px solid transparent; - transition: box-shadow 150ms ease-in-out; } +.umb-media-grid__item.-unselectable { + &::before { + content: ""; + position: absolute; + z-index: 1; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(230, 230, 230, .5); + pointer-events: none; + } +} + +.umb-media-grid__item.-selectable, +.umb-media-grid__item.-folder {// If folders isnt selectable, they opens if clicked, therefor... + cursor: pointer; +} + .umb-media-grid__item.-file { background-color: @white; } +.umb-media-grid__item.-folder { + + &.-selectable { + .media-grid-item-edit:hover .umb-media-grid__item-name, + .media-grid-item-edit:focus .umb-media-grid__item-name { + text-decoration: underline; + } + } + + &.-unselectable { + &:hover, &:focus { + .umb-media-grid__item-name { + text-decoration: underline; + } + } + } +} + + .umb-media-grid__item.-selected { color:@ui-selected-type; .umb-media-grid__item-overlay { color: @ui-selected-type; } } -.umb-media-grid__item.-selected, .umb-media-grid__item:hover { +.umb-media-grid__item.-selected, +.umb-media-grid__item.-selectable:hover { &::before { content: ""; position: absolute; @@ -58,7 +90,7 @@ pointer-events: none; } } -.umb-media-grid__item:hover { +.umb-media-grid__item.-selectable:hover { &::before { opacity: .33; } @@ -69,16 +101,11 @@ } } -.umb-media-grid__item-file-icon > span { - color: @white; - background: @gray-4; - padding: 1px 3px; - font-size: 10px; - line-height: 130%; - display: block; - margin-top: -30px; - margin-left: -10px; - position: relative; +.umb-media-grid__item-file-icon { + transform: translate(-50%,-50%); + position: absolute; + top: 45%; + left: 50%; } .umb-media-grid__item:hover { @@ -86,38 +113,32 @@ } .umb-media-grid__item-image { - //max-width: 100% !important; - //height: auto; position: relative; - object-fit: contain; height: 100%; } .umb-media-grid__item-image-placeholder { width: 100%; - //max-width: 100%; - //height: auto; position: relative; - object-fit: contain; height: 100%; } .umb-media-grid__image-background { content: ""; - background: url("../img/checkered-background.png"); - background-repeat: repeat; opacity: 0.5; top: 0; left: 0; bottom: 0; right: 0; position: absolute; + .checkeredBackground(); } .umb-media-grid__item-overlay { display: flex; + width: 100%; opacity: 0; position: absolute; right: 0; @@ -130,28 +151,19 @@ overflow: hidden; color: @black; white-space: nowrap; - border-top:1px solid fade(black, 4%); + border-top:1px solid fade(@black, 4%); background: fade(@white, 92%); transition: opacity 150ms; - - &:hover { + + &.-can-open:hover { text-decoration: underline; } + + .tabbing-active &:focus { + opacity: 1; + } } -/* -.umb-media-grid__item.-file .umb-media-grid__item-overlay { - opacity: 1; - color: @gray-4; - background: @white; -} - -.umb-media-grid__item.-file:hover .umb-media-grid__item-overlay, -.umb-media-grid__item.-file.-selected .umb-media-grid__item-overlay { - color: @white; - background: @blueExtraDark; -} -*/ .umb-media-grid__info { margin-right: 5px; } @@ -167,39 +179,17 @@ } } +.umb-media-grid__item-name { + cursor: pointer; +} + .umb-media-grid__item-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.umb-media-grid__item-icon { - color: @gray-4; - position: absolute; - top: 45%; - left: 50%; - font-size: 40px !important; - transform: translate(-50%,-50%); -} -/* -.umb-media-grid__checkmark { - position: absolute; - z-index: 2; - top: 10px; - left: 10px; - width: 26px; - height: 26px; - border: 2px solid @white; - background: @green; - border-radius: 50px; - box-sizing: border-box; - display: flex; - justify-content: center; - align-items: center; - color: @white; - transition: background 100ms; -} -*/ + .umb-media-grid__edit { position: absolute; opacity: 0; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less index 7a2939bd30..8cd08f5045 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less @@ -13,9 +13,15 @@ margin-right: 5px; } +.umb-mini-list-view__breadcrumb { + .flex; + margin-bottom: 10px; + min-height: 25px; +} + .umb-mini-list-view__back { - font-size: 12px; - margin-right: 5px; + font-size: 13px; + margin-right: 5px; color: @gray-4; display: flex; align-items: center; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less new file mode 100644 index 0000000000..ec598c17eb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less @@ -0,0 +1,51 @@ +.umb-mini-search { + position: relative; + display: block; + + .icon { + position: absolute; + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + margin: 1px; + padding: 0; + pointer-events: none; + color: @ui-action-discreet-type; + transition: color .1s linear; + } + + input { + width: 0px; + padding-left: 24px; + margin-bottom: 0px; + background-color: transparent; + border-color: @ui-action-discreet-border; + transition: background-color .1s linear, border-color .1s linear, color .1s linear, width .1s ease-in-out, padding-left .1s ease-in-out; + cursor: pointer; + } + + &:focus-within, &:hover { + .icon { + color: @ui-action-discreet-type-hover; + } + input { + color: @ui-action-discreet-border-hover; + border-color: @ui-action-discreet-border-hover; + } + } + + input:focus, &:focus-within input { + background-color: white; + color: @ui-action-discreet-border-hover; + border-color: @ui-action-discreet-border-hover; + cursor: unset; + } + + input:focus, &:focus-within input, &.--has-value input { + width: 190px; + padding-left:30px; + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less index 6af0641d8c..30a32b8123 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less @@ -1,9 +1,10 @@ .umb-multiple-textbox{ &__confirm{ position: relative; + display: inline-block; &-action{ - margin: 0; + margin: -2px 0 0 0; padding: 2px; background: transparent; border: 0 none; @@ -11,6 +12,10 @@ } } +.umb-multiple-textbox .icon-wrapper { + width: 50px; +} + .umb-multiple-textbox .textbox-wrapper { align-items: center; margin-bottom: 15px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index f1a6300481..716693c778 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -1,5 +1,4 @@ .umb-nested-content { - text-align: center; position: relative; } @@ -15,6 +14,17 @@ pointer-events: none; } +.umb-nested-content--mandatory { + /* + yeah so this is a pain, but we must be super specific in targeting the mandatory property labels, + otherwise all properties within a reqired, nested, nested content property will all appear mandatory + */ + > ng-form > .control-group > .umb-el-wrap > .control-header label:after { + content: '*'; + color: @red; + } +} + .umb-nested-content-overlay { position: absolute; top: 0; @@ -31,11 +41,8 @@ } .umb-nested-content__item.ui-sortable-placeholder { - background: @gray-10; - border: 1px solid @gray-9; + margin-top: 1px; visibility: visible !important; - height: 55px; - margin-top: -1px; } .umb-nested-content__item--single > .umb-nested-content__content { @@ -56,42 +63,55 @@ -webkit-user-select: none; -o-user-select: none; user-select: none; + + &:hover { + .umb-nested-content__heading .umb-nested-content__item-name { + padding-right: 60px; + } + } + } .umb-nested-content__heading { line-height: 20px; position: relative; margin-top:1px; - padding: 15px 20px; + padding: 15px 5px; color:@ui-option-type; border-radius: 3px 3px 0 0; - + &:hover { color:@ui-option-type-hover; } i { - display: inline; - margin-right: 10px; + position: absolute; + margin-top: -1px; } .umb-nested-content__item-name { - display: inline; + display: block; max-height: 20px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + padding-left: 5px; + + &.--has-icon { + padding-left: 30px; + } } - + } .umb-nested-content__icons { opacity: 0; transition: opacity 120ms ease-in-out; position: absolute; - right: 8px; - top: 4px; + right: 0; + top: 3px; padding: 5px; + background-color: @white; } .umb-nested-content__item--active > .umb-nested-content__header-bar { @@ -100,6 +120,9 @@ &:hover { color:@ui-option-type; } + .umb-nested-content__item-name { + padding-right: 60px; + } } .umb-nested-content__icons { background-color: @ui-active; @@ -112,6 +135,8 @@ .umb-nested-content__header-bar:hover .umb-nested-content__icons, +.umb-nested-content__header-bar:focus .umb-nested-content__icons, +.umb-nested-content__header-bar:focus-within .umb-nested-content__icons, .umb-nested-content__item--active > .umb-nested-content__header-bar .umb-nested-content__icons { opacity: 1; } @@ -119,6 +144,8 @@ .umb-nested-content__icon { + background: transparent; + border: 0 none; display: inline-block; padding: 4px; margin: 2px; @@ -138,6 +165,11 @@ .umb-nested-content__icon--disabled { opacity: 0.3; + cursor: default !important; + + &:hover { + color: @ui-option-type; + } } @@ -149,6 +181,7 @@ .umb-nested-content__add-content { display: flex; + width: 100%; align-items: center; justify-content: center; border: 1px dashed @ui-action-discreet-border; @@ -192,19 +225,19 @@ display: none !important; } -.umb-nested-content__doctypepicker table input, +.umb-nested-content__doctypepicker table input, .umb-nested-content__doctypepicker table select { width: 100%; padding-right: 0; } -.umb-nested-content__doctypepicker table td.icon-navigation, +.umb-nested-content__doctypepicker table td.icon-navigation, .umb-nested-content__doctypepicker i.umb-nested-content__help-icon { vertical-align: middle; color: @gray-7; } -.umb-nested-content__doctypepicker table td.icon-navigation:hover, +.umb-nested-content__doctypepicker table td.icon-navigation:hover, .umb-nested-content__doctypepicker i.umb-nested-content__help-icon:hover { color: @gray-2; } @@ -213,8 +246,30 @@ margin-left: 10px; } -.form-horizontal .umb-nested-content--narrow .controls-row -{ +.umb-nested-content__placeholder { + padding: 4px 6px; + border: 1px dashed @gray-8; + background: 0 0; + cursor: pointer; + color: @blueExtraDark; + -webkit-animation: fadeIn .5s; + animation: fadeIn .5s; + text-align: center; + + &--selected { + border: none; + text-align: left; + padding: 0; + } +} + +.umb-nested-content__placeholder:hover { + color: @blueMid; + border-color: @blueMid; + text-decoration: none; +} + +.form-horizontal .umb-nested-content--narrow .controls-row { margin-left: 40% !important; } @@ -232,7 +287,7 @@ // the attribute selector ensures the change only applies to the linkpicker overlay .form-horizontal .umb-nested-content--narrow [ng-controller*="Umbraco.Overlays.LinkPickerController"] .controls-row { margin-left:0!important; - + .umb-textarea, .umb-textstring { width:100%; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less index 1edaffe824..939fd79826 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less @@ -3,6 +3,7 @@ display: flex; box-sizing: border-box; border-bottom: 1px solid @gray-9; + flex-wrap: wrap; } .umb-editor-wrapper .umb-node-preview { @@ -28,7 +29,8 @@ .umb-node-preview__icon { display: flex; width: 25px; - height: 25px; + min-height: 25px; + height: 100%; justify-content: center; align-items: center; font-size: 20px; @@ -64,9 +66,13 @@ flex: 0 0 auto; display: flex; align-items: center; + margin-left: auto; } .umb-node-preview__action { + background: transparent; + padding: 0; + border: 0 none; margin-left: 5px; margin-right: 5px; font-size: 13px; @@ -89,11 +95,13 @@ display: flex; align-items: center; justify-content: center; + background: transparent; border: 1px dashed @ui-action-discreet-border; color: @ui-action-discreet-type; font-weight: bold; padding: 5px 15px; box-sizing: border-box; + width: 100%; } .umb-node-preview-add:hover { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-number.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-number.less new file mode 100644 index 0000000000..c0e3dee8e5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-number.less @@ -0,0 +1,3 @@ +.umb-number { + .umb-property-editor--limit-width(); +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index f704dd48e2..b808e6574a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -67,6 +67,7 @@ height: 100%; width: 100%; border-radius: 3px; + border: 0 none; text-decoration: none !important; transition: border-color 100ms ease; background-color: @white; @@ -193,9 +194,8 @@ color: @black; box-sizing: border-box; justify-content: center; - border-top: 1px solid @gray-8; - border-bottom: 1px solid @gray-8; - border-right: 1px solid @gray-8; + border: 1px solid @gray-8; + border-left: 0; padding: 10px 0; background: @white; } @@ -264,12 +264,16 @@ flex-flow: row wrap; } -a.umb-package-details__back-link { +.umb-package-details__back-action { font-weight: bold; color: @black; + padding: 0; + border: 0; + background-color: transparent; } -.umb-package-details__back-link:hover { +.umb-package-details__back-action:focus, +.umb-package-details__back-action:hover { color: @gray-4; text-decoration: none; } @@ -407,6 +411,7 @@ a.umb-package-details__back-link { } .umb-gallery__thumbnail { + background: transparent; flex: 0 1 100px; border: 1px solid @ui-action-discreet-border; border-radius: 3px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less index 487c1881b1..46d7f04af6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less @@ -107,5 +107,5 @@ .umb-panel-group__details-status-action-description { margin-top: 5px; font-size: 12px; - padding-left: 165px; + padding-left:165px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-progress-circle.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-progress-circle.less index 03816637a7..d8fb3b4d8f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-progress-circle.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-progress-circle.less @@ -5,6 +5,7 @@ .umb-progress-circle__view-box { position: absolute; transform: rotate(-90deg); + right: 0; } // circle highlight on progressbar diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less new file mode 100644 index 0000000000..17c4bf1a55 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less @@ -0,0 +1,97 @@ +.umb-property-actions { + display: inline; +} + +.umb-property-actions__toggle, +.umb-property-actions__menu-open-toggle { + position: relative; + display: flex; + flex: 0 0 auto; + padding: 6px 6px; + text-align: center; + cursor: pointer; + border-radius: 3px; + + background-color: @ui-action-hover; + + i { + height: 3px !important; + width: 3px !important; + border-radius: 3px; + background: @ui-action-type; + display: inline-block; + margin: 0 2px 0 0; + + &:last-child { + margin: 0; + } + } + &:hover { + i { + background: @ui-action-type-hover; + } + } +} +.umb-property-actions__menu-open-toggle { + position: absolute; + z-index:1; + outline: none;// this is not acceccible by keyboard, since we use the .umb-property-actions__toggle for that. + + top: -15px; + border-radius: 3px 3px 0 0; + + border-top-left-radius: 3px; + border-top-right-radius: 3px; + + border: 1px solid @dropdownBorder; + + border-bottom: 1px solid @gray-9; + + .box-shadow(0 5px 20px rgba(0,0,0,.3)); + + background-color: @white; + +} + +.umb-property .umb-property-actions { + float: left; +} +.umb-property .umb-property-actions__toggle { + margin-top: 2px; + opacity: 0; + transition: opacity 120ms; +} +.umb-property:focus-within .umb-property-actions__toggle, +.umb-property:hover .umb-property-actions__toggle, +.umb-property .umb-property-actions__toggle:focus { + opacity: 1; +} +// Revert-style-hack that ensures that we only show property-actions on properties that are directly begin hovered. +.umb-property:hover .umb-property:not(:hover) .umb-property-actions__toggle { + opacity: 0; +} + +.umb-property-actions__menu { + + position: absolute; + z-index: 1000; + + display: block; + + float: left; + min-width: 160px; + list-style: none; + + .umb-contextmenu { + + border-top-left-radius: 0; + margin-top:1px; + + } + + .umb-contextmenu-item > button { + + z-index:2;// need to stay on top of menu-toggle-open shadow. + + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less index 08b1a1b5e1..9550acfb1b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less @@ -1,6 +1,7 @@ .umb-property-file-upload { .umb-upload-button-big { + max-width: (@propertyEditorLimitedWidth - 40); display: block; padding: 20px; opacity: 1; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less index 1461d0f223..6ae92ffa4e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less @@ -1,7 +1,7 @@ .umb-range-slider.noUi-target { - background: linear-gradient(to bottom, #f5f5f5 0%, #f9f9f9 100%); + background: linear-gradient(to bottom, @grayLighter 0%, @grayLighter 100%); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); border-radius: 20px; height: 10px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-sub-views.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-sub-views.less index d3ce368356..cc6be8fa37 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-sub-views.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-sub-views.less @@ -8,7 +8,6 @@ padding-left: 0; padding-right: 0; &:focus { - outline: none; text-decoration: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index f387b6540b..202c488bb4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -27,16 +27,19 @@ } +.umb-table__action, .umb-table a { + background: transparent; + border: 0 none; text-decoration: none; - cursor: pointer; - - &:focus { - outline: none; - text-decoration: none; + padding: 0; + margin-left: 1px; + body:not(.tabbing-active) & { + outline: 0; } } + input.umb-table__input { margin: 0 auto; } @@ -48,25 +51,35 @@ input.umb-table__input { .umb-table-head { font-size: 14px; font-weight: bold; + + color: @ui-disabled-type; } .umb-table-head__link { + background: transparent; + border: 0 none; position: relative; - cursor: default; text-decoration: none; color: @gray-3; + font-size: inherit; + font-weight: inherit; + padding: 0 1px; + &:hover { text-decoration: none; + cursor: default; color: @gray-3; } } -.umb-table-head__link .sortable { +.umb-table-head__link.sortable { + cursor: pointer; + color: @ui-action-discreet-type; &:hover { - text-decoration: none; - cursor: pointer; - color: @black; + color: @ui-action-discreet-type-hover; } + + outline-offset: 1px; } .umb-table-thead__icon { @@ -124,6 +137,9 @@ input.umb-table__input { &::before { opacity:.66; } + .umb-table-body__checkicon { + color: @ui-selected-border; + } } } @@ -136,34 +152,32 @@ input.umb-table__input { } .umb-table-body__link { - + position: relative; color: @ui-option-type; font-size: 14px; font-weight: bold; text-decoration: none; - - &:hover, &:focus { + + &:hover { color: @ui-option-type-hover; text-decoration: underline; } } -.umb-table-body__icon, -.umb-table-body__icon[class^="icon-"], -.umb-table-body__icon[class*=" icon-"] { +.umb-table-body__icon { margin: 0 auto; font-size: 20px; line-height: 20px; color: @ui-option-type; + vertical-align: bottom; + text-decoration: none; } -.umb-table-body__checkicon, -.umb-table-body__checkicon[class^="icon-"], -.umb-table-body__checkicon[class*=" icon-"] { +.umb-table-body__checkicon { display: none; font-size: 18px; line-height: 20px; - color: @green; + color: @ui-selected-border; } .umb-table-body .umb-table__name { @@ -172,7 +186,8 @@ input.umb-table__input { font-weight: bold; a { color: @ui-option-type; - &:hover, &:focus { + outline-offset: 1px; + &:hover { color: @ui-option-type-hover; text-decoration: underline; } @@ -240,10 +255,10 @@ input.umb-table__input { .umb-table-cell { display: flex; flex-flow: row nowrap; - flex: 1 1 1%; //NOTE 1% is a Internet Explore hack, so that cells don't collapse + flex: 1 1 5%; position: relative; - margin: auto 14px; - padding: 6px 2px; + margin: auto 0; + padding: 6px 16px; text-align: left; overflow:hidden; } @@ -253,17 +268,25 @@ input.umb-table__input { white-space: nowrap; //NOTE Disable/Enable this to keep textstring on one line text-overflow: ellipsis; } +.umb-table-cell.--noOverflow > * { + overflow: visible; + white-space: normal; + text-overflow: unset; +} .umb-table-cell:first-of-type:not(.not-fixed) { flex: 0 0 25px; - margin: 0 0 0 15px; - padding: 15px 0; + margin: 0; + padding: 15px 0 15px 15px; } .umb-table-cell--auto-width { flex: 0 0 auto !important; } +.umb-table-cell--nano { + flex: 0 0 50px; +} .umb-table-cell--small { flex: .5 .5 1%; max-width: 12.5%; @@ -280,8 +303,8 @@ input.umb-table__input { // Increases the space for the name cell .umb-table__name { - flex: 1 1 25%; - max-width: 25%; + flex: 1 1 20%; + max-width: 300px; } .umb-table__loading-overlay { @@ -295,7 +318,11 @@ input.umb-table__input { .umb-table__row-expand { font-size: 12px; text-decoration: none; - color: @black; + color: @gray-4; + + &:hover { + color: @black; + } } .umb-table__row-expand--hidden { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less index ee784787fa..15b317aa45 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less @@ -13,25 +13,31 @@ top: 1px; } -.umb-tab > a { - +.umb-tab-button { display: flex; justify-content: center; align-items: center; position: relative; - + cursor: pointer; //border-bottom: 4px solid transparent; color: @ui-light-type; padding: 5px 20px 15px 20px; transition: color 150ms ease-in-out; + &:focus { color: @ui-light-type-hover; + + body:not(.tabbing-active) &{ + outline: none; + } } + &:hover { color: @ui-light-type-hover; text-decoration: none; } + &::after { content: ""; height: 0px; @@ -42,12 +48,21 @@ bottom: 0; border-radius: 3px 3px 0 0; opacity: 0; - transition: all .2s linear; + transition: all 0.2s linear; + } + + &--expand > i { + height: 5px; + width: 5px; + border-radius: 50%; + background: @black; + display: inline-block; + margin: 0 5px 0 0; + opacity: 0.6; } } - -.umb-tab--active > a { +.umb-tab--active > .umb-tab-button { color: @ui-light-active-type; //border-bottom-color: @ui-active; /* @@ -64,19 +79,19 @@ } } -.show-validation .umb-tab--error > a, -.show-validation .umb-tab--error > a:hover, -.show-validation .umb-tab--error > a:focus { - color: @white !important; - background-color: @red !important; - border-color: @errorBorder; +.show-validation .umb-tab--error > .umb-tab-button, +.show-validation .umb-tab--error > .umb-tab-button:hover, +.show-validation .umb-tab--error > .umb-tab-button:focus { + color: @white !important; + background-color: @red !important; + border-color: @errorBorder; } -.show-validation .umb-tab--error a:before { - content: "\e25d"; - font-family: 'icomoon'; - margin-right: 5px; - vertical-align: top; +.show-validation .umb-tab--error .umb-tab-button:before { + content: "\e25d"; + font-family: "icomoon"; + margin-right: 5px; + vertical-align: top; } // tabs tray @@ -86,20 +101,10 @@ left: auto; } -.umb-tabs-tray > a { +.umb-tabs-tray > .umb-tab-button { cursor: pointer; } .umb-tabs-tray-item--active { border-left: 2px solid @ui-active; } - -.umb-tab--expand > a > i { - height: 5px; - width: 5px; - border-radius: 50%; - background: @black; - display: inline-block; - margin: 0 5px 0 0; - opacity: .6; -} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less b/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less new file mode 100644 index 0000000000..f4b3183045 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less @@ -0,0 +1,44 @@ +.umb-email-marketing { + + h2 { + font-weight: 800; + max-width: 26ex; + margin-top: 20px; + } + + .layout { + display: flex; + align-items: center; + align-content: stretch; + + .primary { + flex-basis: 50%; + padding-right: 40px; + padding-top: 20px; + padding-bottom: 20px; + .notice { + color: @gray-5; + font-style: italic; + a { + color: @gray-5; + &:hover { + color: @ui-action-type-hover; + } + } + } + } + + .secondary { + flex-basis: 50%; + svg { + height: 200px; + width: 100%; + margin-top: -60px; + } + } + } + + .cta { + text-align: right; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less index 3bc431e01f..7c327bfb88 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less @@ -63,12 +63,14 @@ .umb-user-card__goToUser { &:hover, &:focus { text-decoration: none; + .umb-user-card__name { text-decoration: underline; color: @ui-option-type-hover; } + .umb-avatar { - border: 1px solid @ui-option-type-hover; + box-shadow: 0px 1px 3px rgba(0, 0, 0, .5); } } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-details.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-details.less index 9ddad03b48..a612af65ef 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-details.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-details.less @@ -1,7 +1,7 @@ .umb-user-details-avatar { margin-bottom: 20px; padding-bottom: 20px; - border-bottom: 1px solid #d8d7d9; + border-bottom: 1px solid @gray-8; } div.umb-user-details-actions > div { @@ -63,6 +63,10 @@ a.umb-user-details-details__back-link { .umb-user-details-details__sidebar { flex: 0 0 @sidebarwidth; + + .umb-button{ + margin-left:0px; + } } @media (max-width: 768px) { @@ -77,7 +81,7 @@ a.umb-user-details-details__back-link { margin-bottom: 30px; margin-right: 0; } - + .umb-user-details-details__sidebar { flex: 1 1 auto; width: 100%; @@ -101,6 +105,7 @@ a.umb-user-details-details__back-link { .umb-user-details-details__information-item { margin-bottom: 10px; font-size: 13px; + margin-top:10px; } .umb-user-details-details__information-item-label { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-picker-list.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-picker-list.less index dff78ce627..0a06120b11 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-picker-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-picker-list.less @@ -7,11 +7,17 @@ display: flex; margin-bottom: 5px; padding: 10px; + position: relative; } -.umb-user-group-picker-list-item:active, -.umb-user-group-picker-list-item:focus { - text-decoration: none; +.umb-user-group-picker__action{ + background: transparent; + border: 0 none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } .umb-user-group-picker-list-item:hover { @@ -35,4 +41,4 @@ .umb-user-group-picker-list-item__permission { font-size: 13px; color: @gray-4; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less index f39096b565..47a2af9231 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less @@ -34,9 +34,13 @@ margin-top: 2px; } -.umb-user-group-preview__permission { +.umb-user-group-preview__permissions { font-size: 13px; color: @gray-3; + + .umb-user-group-preview__permission:not(:last-child):after { + content: ', '; + } } .umb-user-group-preview__actions { @@ -61,4 +65,4 @@ .umb-user-group-preview__action--red:hover { color: @red; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less index 0c61a5d113..31b126e77c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less @@ -3,16 +3,21 @@ .umb-user-table-col-avatar { flex: 0 0 32px; padding: 15px 0; - + + > a { + overflow: visible; + } + .umb-checkmark { margin-left:5px; } } - + .umb-table-cell a { + &:hover, &:focus { .umb-avatar { - border: 1px solid @ui-option-type-hover; + box-shadow: 0px 1px 3px rgba(0, 0, 0, .5); } } } diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/examine-management.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/examine-management.less index 0c219af1e4..7b842c40ad 100644 --- a/src/Umbraco.Web.UI.Client/src/less/dashboards/examine-management.less +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/examine-management.less @@ -12,4 +12,9 @@ border-bottom-left-radius: 0; } } + + .umb-panel-group__details-status-action{ + background-color:transparent; + padding-left:0; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/getstarted.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/getstarted.less index 807d0e863f..d64ffef098 100644 --- a/src/Umbraco.Web.UI.Client/src/less/dashboards/getstarted.less +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/getstarted.less @@ -22,8 +22,8 @@ text-align: center; display: flex; align-items: center; - border: 1px solid #d8d7d9; - background-color: #fff; + border: 1px solid @gray-8; + background-color: @white; margin: 0 0 0.5em; @media (min-width: 500px) { diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less index 449cc4066b..bf01c21dca 100644 --- a/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less @@ -8,7 +8,6 @@ .umb-healthcheck-help-text { line-height: 1.6em; - max-width: 750px; } .umb-healthcheck-action-bar { @@ -22,15 +21,18 @@ .umb-healthcheck-group { display: flex; flex-wrap: wrap; - flex-direction: column; - background: @white; + flex-direction: column; + align-items: center; + background: @white; + border: 0; border-radius: 3px; padding: 20px; box-sizing: border-box; text-align: center; box-shadow: 0 1px 1px 0 rgba(0,0,0,0.16); height: 100%; - box-sizing: border-box; + box-sizing: border-box; + width: 100%; } .umb-healthcheck-group:hover { @@ -117,7 +119,10 @@ /* DETAILS */ .umb-healthcheck-back-link { - font-weight: bold; + background: transparent; + border: 0 none; + padding: 0; + font-weight: bold; color: @black; } diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/nucache.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/nucache.less new file mode 100644 index 0000000000..91922300fb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/nucache.less @@ -0,0 +1,13 @@ +#nuCache { + .no-background { + background-color: transparent; + } + + .top-border { + border-top: 2px solid @gray-10; + } + + .no-left-padding { + padding-left: 0; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/umbraco-forms.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/umbraco-forms.less index b51bfeffa9..21ec047d41 100644 --- a/src/Umbraco.Web.UI.Client/src/less/dashboards/umbraco-forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/umbraco-forms.less @@ -106,7 +106,7 @@ .step-text { font-size: 16px; line-height: 1.5; - color: #4c4c4c; + color: @gray; margin-bottom: 20px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/filter-toggle.less b/src/Umbraco.Web.UI.Client/src/less/filter-toggle.less new file mode 100644 index 0000000000..82f9f3f553 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/filter-toggle.less @@ -0,0 +1,20 @@ +.filter-toggle{ + margin: 0; + padding: 0 8px 0 0; + position: relative; +} + +.filter-toggle__level{ + display: inline-block; + font-weight: 700; + margin: 0 5px; + max-width: 150px; +} + +.filter-toggle__icon{ + position: absolute; + top: 0; + bottom: 0; + right: 0; + margin: auto 0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/footer.less b/src/Umbraco.Web.UI.Client/src/less/footer.less deleted file mode 100644 index 93fdb6ab5e..0000000000 --- a/src/Umbraco.Web.UI.Client/src/less/footer.less +++ /dev/null @@ -1,2 +0,0 @@ -// Footer -// ------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 966fc1fb11..ffc0626981 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -252,7 +252,7 @@ input[type="color"], outline: 0; .tabbing-active & { - outline: 2px solid @inputBorderTabFocus; + outline: 2px solid @ui-outline; } } } @@ -297,11 +297,10 @@ select[size] { } // Focus for select, file, radio, and checkbox -select:focus, -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - .tab-focus(); +input[type="file"], +input[type="radio"], +input[type="checkbox"] { + .umb-outline(); } @@ -528,7 +527,8 @@ input[type="checkbox"][readonly] { .help-inline { display: inline-block; vertical-align: middle; - padding-left: 5px; + padding-top: 4px; + padding-left: 2px; } div.help { @@ -809,6 +809,7 @@ legend + .control-group { // Labels on own row .form-horizontal .control-label { + float:none; width: 100%; } .form-horizontal .controls { diff --git a/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less b/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less index 9b4bac723b..e5b84fc6ca 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less @@ -1,13 +1,17 @@ .umb-validation-label { position: absolute; - top: 27px; - min-width: 100px; - max-width: 200px; - padding: 1px 5px; + z-index: 1; + top: 28px; + min-width: 80px; + max-width: 260px; + padding: 2px 6px; background: @red; color: @white; - font-size: 11px; + font-size: 12px; line-height: 1.5em; + border-radius: @baseBorderRadius; + pointer-events: none; + user-select: none; } .umb-validation-label:after { diff --git a/src/Umbraco.Web.UI.Client/src/less/gridview.less b/src/Umbraco.Web.UI.Client/src/less/gridview.less index bb684cd69b..11ba7b2795 100644 --- a/src/Umbraco.Web.UI.Client/src/less/gridview.less +++ b/src/Umbraco.Web.UI.Client/src/less/gridview.less @@ -101,6 +101,9 @@ position:relative; } + .usky-grid .grid-layout { + max-width: 600px; +} // ROW // ------------------------- @@ -365,12 +368,12 @@ .usky-grid .iconBox.selected { -webkit-appearance: none; - background-image: -webkit-gradient(linear,0 0,0 100%,from(#e6e6e6),to(#bfbfbf)); - background-image: -webkit-linear-gradient(top,#e6e6e6,#bfbfbf); - background-image: linear-gradient(to bottom,#e6e6e6,#bfbfbf); + background-image: -webkit-gradient(linear,0 0,0 100%,from(@gray-9),to(@gray-7)); + background-image: -webkit-linear-gradient(top,@gray-9,@gray-7); + background-image: linear-gradient(to bottom,@gray-9,@gray-7); background-repeat: repeat-x; zoom: 1; - border-color: #bfbfbf #bfbfbf #999; + border-color: @gray-7 @gray-7 @gray-6; border-color: rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25); box-shadow: inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05); border-radius: 3px; @@ -379,7 +382,7 @@ .usky-grid .iconBox i { font-size:16px !important; - color: #5F5F5F; + color: @gray-4; display:block; } @@ -517,6 +520,25 @@ position:relative; } + .usky-grid .uSky-templates .layout { + margin-top: 5px; + margin-bottom: 20px; + float: left; +} + + +.usky-grid .uSky-templates .columns { + margin-top: 5px; + margin-bottom: 25px; + float: left; +} + + +.usky-grid .uSky-templates .columns .preview-cell p { + font-size: 6px; + line-height: 8px; + text-align: center; +} /**************************************************************************************************/ diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index 726b3fa5ed..8d3117febe 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -111,11 +111,10 @@ iframe, .content-column-body { } .pa-select-type label { - padding: 0 20px; + padding: 0 15px; } .pa-access-header { - font-weight: bold; margin: 0 0 3px 0; padding-bottom: 0; } @@ -186,38 +185,45 @@ iframe, .content-column-body { // Inline code // 1: Revert border radius to match look and feel of 7.4+ -code{ - .border-radius(@baseBorderRadius); // 1 +code { + .border-radius(@baseBorderRadius); // 1 } // Blocks of code // 1: Wrapping code is unreadable on small devices. pre { - display: block; - padding: (@baseLineHeight - 1) / 2; - margin: 0 0 @baseLineHeight / 2; - font-family: @sansFontFamily; - //font-size: @baseFontSize - 1; // 14px to 13px - color: @gray-2; - line-height: @baseLineHeight; - white-space: pre-wrap; // 1 - overflow-x: auto; // 1 - background-color: @gray-10; - border: 1px solid @gray-8; - .border-radius(@baseBorderRadius); + display: block; + padding: (@baseLineHeight - 1) / 2; + margin: 0 0 @baseLineHeight / 2; + font-family: @sansFontFamily; + color: @gray-2; + line-height: @baseLineHeight; + white-space: pre-wrap; // 1 + overflow-x: auto; // 1 + background-color: @brownGrayLight; + border: 1px solid @gray-8; + .border-radius(@baseBorderRadius); + // Make prettyprint styles more spaced out for readability + &.prettyprint { + margin-bottom: @baseLineHeight; + } - // Make prettyprint styles more spaced out for readability - &.prettyprint { - margin-bottom: @baseLineHeight; - } - - // Account for some code outputs that place code tags in pre tags - code { - padding: 0; - white-space: pre; // 1 - word-wrap: normal; // 1 - background-color: transparent; - border: 0; - } + // Account for some code outputs that place code tags in pre tags + code { + padding: 0; + white-space: pre; // 1 + word-wrap: normal; // 1 + background-color: transparent; + border: 0; + } +} + +/* Styling for content/media sort order dialog */ +.sort-order { + td.tree-icon { + font-size:20px; + width:20px; + padding-right:0; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/installer.less b/src/Umbraco.Web.UI.Client/src/less/installer.less index 798252c394..4e24161e59 100644 --- a/src/Umbraco.Web.UI.Client/src/less/installer.less +++ b/src/Umbraco.Web.UI.Client/src/less/installer.less @@ -1,7 +1,9 @@ -// Core variables and mixins +// Core variables and mixins @import "fonts.less"; // Loading fonts @import "variables.less"; // Modify this for custom colors, font-sizes, etc +@import "colors.less"; @import "mixins.less"; +@import "application/umb-outline.less"; @import "buttons.less"; @import "forms.less"; @@ -13,241 +15,245 @@ @import "../../lib/bootstrap/less/thumbnails.less"; @import "../../lib/bootstrap/less/media.less"; +// Umbraco Components +@import "components/umb-loader.less"; + [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { - display: none !important; + display: none !important; } html { - background: url('../img/installer.jpg') no-repeat center center fixed; - background-size: cover; + background: url('../img/installer.jpg') no-repeat center center fixed; + background-size: cover; } body { - margin: 0; - padding: 0; - height: 100%; - width: 100%; - - font-family: @baseFontFamily; - font-size: @baseFontSize; - line-height: @baseLineHeight; - color: @textColor; - - vertical-align: middle; - text-align: center; - - // better font rendering - -webkit-font-smoothing: antialiased; - font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - + margin: 0; + padding: 0; + height: 100%; + width: 100%; + font-family: @baseFontFamily; + font-size: @baseFontSize; + line-height: @baseLineHeight; + color: @textColor; + vertical-align: middle; + text-align: center; + // better font rendering + -webkit-font-smoothing: antialiased; + font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -#logo{ - position: absolute; - top: 20px; - left: 20px; - opacity: 0.8; - z-index: 777; +#logo { + position: absolute; + top: 20px; + left: 20px; + opacity: 0.8; + z-index: 777; } -#installer{ - margin: auto; +#installer { + margin: auto; background: @white; - width: 750px; - height: 600px; - text-align: left; - padding: 30px; - overflow:hidden; - z-index: 667; + width: 750px; + height: 600px; + text-align: left; + padding: 30px; + overflow: hidden; + z-index: 667; } -#overlay{ - position: absolute; - top: 0; - right: 0; - left: 0; - bottom: 0; - background: @purple-d2; - z-index: 666; +#overlay { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + background: @purple-d2; + z-index: 666; } -.loading #overlay{ +.loading #overlay { opacity: 0.5; display: block !important; } -#fact{ - color: @white; - text-shadow: 0px 0px 4px @black; - font-size: 25px; - text-align: left; - line-height: 35px; - z-index: 667; - height: 600px; - width: 750px; +#fact { + color: @white; + text-shadow: 0px 0px 4px @black; + font-size: 25px; + text-align: left; + line-height: 35px; + z-index: 667; + height: 600px; + width: 750px; + + h2 { + font-size: 35px; + border-bottom: 1px solid @white; + padding-bottom: 10px; + margin-bottom: 20px; + color: @white; + } + + a { + color: @white; + } } -#fact h2{ - font-size: 35px; - border-bottom: 1px solid @white; - padding-bottom: 10px; - margin-bottom: 20px; - color: @white; -} - -#fact a{color: @white;} - -#feedback{ - color: @white; - text-shadow: 0px 0px 4px @black; - font-size: 14px; - text-align: center; - line-height: 20px; - z-index: 667; - bottom: 20px; - right: 0; - left: 0; - height: 25px; - position: absolute; +#feedback { + color: @white; + text-shadow: 0px 0px 4px @black; + font-size: 14px; + text-align: center; + line-height: 20px; + z-index: 667; + bottom: 20px; + right: 0; + left: 0; + height: 25px; + position: absolute; } -h1{ - border-bottom: 1px solid @gray-10; - padding-bottom: 10px; - color: @gray-2; +h1 { + border-bottom: 1px solid @gray-10; + padding-bottom: 10px; + color: @gray-2; } -.error h1, .error .message, span.error{ color: @red;} +.error h1, .error .message, span.error { + color: @red; +} -legend{font-size: 14px; font-weight: bold} +legend { + font-size: 14px; + font-weight: bold +} -input.ng-dirty.ng-invalid{border-color: #b94a48; color: #b94a48;} -.disabled{ - opacity: 0.6; +input.ng-dirty.ng-invalid { + border-color: @pink; + color: @pink; +} + +.disabled { + opacity: 0.6; } -#installer label.control-label, +#installer label.control-label, #installer .constrol-label { padding-top: 5px !important; } -.controls{ - text-align: left -} +.controls { + text-align: left; -.controls small{display: block; color: @gray-3;} + small { + display: block; + color: @gray-3; + } +} .absolute-center { - margin: auto; - position: absolute; - top: 0; left: 0; bottom: 0; right: 0; + margin: auto; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; } -.fade-hide, .fade-show { - transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; -} -.fade-hide { - opacity:1; -} -.fade-hide.fade-hide-active { - opacity:0; -} +.fade-hide, .fade-show { - opacity:0; + transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; } + +.fade-hide { + opacity: 1; +} + +.fade-hide.fade-hide-active { + opacity: 0; +} + +.fade-show { + opacity: 0; +} + .fade-show.fade-show-active { - opacity:1; + opacity: 1; } +.umb-installer-loader { + margin: 0; + width: 0; + z-index: 777; -.umb-loader{ - background-color: @white; - margin-top:0; - margin-left:-100%; - animation-name:bounce_loadingProgressG; - animation-duration:1s; - animation-iteration-count:infinite; - animation-timing-function:linear; - width:100%; - height: 5px; -} - -@keyframes bounce_loadingProgressG{ - 0%{ - margin-left:-100%; - } - 100%{ - margin-left:100%; + .umb-loader { + background-color: @white; + height: 5px; } } -//loader defaults -.umb-loader-container, .umb-loader-done{ - height: 3px; - position: absolute; - bottom: 0; - left: 0; - overflow: hidden; - width: 0%; - z-index: 777; -} - -.umb-loader-done{ - right: 0%; - background: @white; -} - - .permissions-report { - overflow:auto; - height:320px; - margin:0; - display:block; - padding:0; + overflow: auto; + height: 320px; + margin: 0; + display: block; + padding: 0; } + .permissions-report > li { - list-style:none; + list-style: none; } + .permissions-report h4 { - margin:7px; + margin: 7px; } .upgrade-report { - overflow:auto; - height:280px; - display:block; + overflow: auto; + height: 280px; + display: block; } select { - width:320px; + width: 320px; } #ysod { - height:500px; - width:100%; - overflow:auto; - border:none; + height: 500px; + width: 100%; + overflow: auto; + border: none; } -#starterKits .thumbnails{ - min-height:128px; -} -#starterKits a.thumbnail { - position:relative; - height:98px; -} -#starterKits a.thumbnail small { - position:absolute; - z-index: 50; - top:10px; - left:10px; -} -#starterKits a.thumbnail img { - position:relative; - z-index:100; + +#starterKits { + .thumbnails { + min-height: 128px; + padding-left: 0; + } + + .thumbnail { + position: relative; + cursor: pointer; + + small { + padding: 10px 10px 5px; + display: inline-block; + } + + img { + position: relative; + z-index: 100; + } + } + + .btn-link { + padding-left: 0; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/legacydialog.less b/src/Umbraco.Web.UI.Client/src/less/legacydialog.less index ca920d22c2..f1835a682c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/legacydialog.less +++ b/src/Umbraco.Web.UI.Client/src/less/legacydialog.less @@ -16,8 +16,8 @@ margin-top: 10px; height: 100%; overflow: auto; - border-top: 1px solid #ccc; - border-top: 1px solid #ccc; + border-top: 1px solid @gray-8; + border-top: 1px solid @gray-8; padding: 5px; } @@ -30,11 +30,11 @@ .umb-dialog .diff table th { padding: 5px; width: 25%; - border-bottom: 1px solid #ccc; + border-bottom: 1px solid @gray-8; } .umb-dialog .diff table td { - border-bottom: 1px solid #ccc; + border-bottom: 1px solid @gray-8; padding: 3px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/listview.less b/src/Umbraco.Web.UI.Client/src/less/listview.less index 9bd582a8ad..fe8af6dbc4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/listview.less +++ b/src/Umbraco.Web.UI.Client/src/less/listview.less @@ -1,6 +1,10 @@ // Listview // ------------------------- +.umb-listview { + min-height: 100px; +} + .umb-listview table { border: 1px solid @gray-8; } @@ -43,6 +47,15 @@ /* add padding */ .left-addon input[type="text"] { padding-left: 30px !important; padding-right: 6px; } .right-addon input[type="text"] { padding-right: 30px; padding-left: 6px !important; } + + &__label-icon{ + width: 30px; + height: 30px; + position: absolute; + top: -1px; + left:0; + margin:0 + } } .umb-listview table form { @@ -136,7 +149,36 @@ /* TEMP */ .umb-minilistview { - .umb-table-row.not-allowed { opacity: 0.6; cursor: not-allowed; } + .umb-table-row.not-allowed { + opacity: 0.6; + cursor: not-allowed; + } + + div.umb-mini-list-view__breadcrumb { + margin-bottom: 10px; + } + + div.no-display { + display: none + } + + div.umb-table-cell-padding { + padding-top: 8px; + padding-bottom: 8px; + } + + div.umb-table-cell .form-search { + width: 100%; + margin-right: 0; + + input { + width: 100%; + } + + .icon-search { + font-size: 14px; + } + } } .umb-listview .table-striped tbody td { @@ -203,9 +245,9 @@ .list-view-layout__name-text { margin-right: 3px; } - + .list-view-layout__system { - font-size: 10px; + font-size: 10px; font-weight: normal; } @@ -236,6 +278,8 @@ } .list-view-add-layout { + width:100%; + background:0 0; margin-top: 10px; color: @ui-action-discreet-type; border: 1px dashed @ui-action-discreet-border; diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 9f9bbce310..b34f313435 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -116,24 +116,48 @@ h5.-black { margin: 20px; } .umb-control-group { - border-bottom: 1px solid @gray-11; - padding-bottom: 20px; + position: relative; + + &.umb-control-group__listview { + // position: relative messes up the listview status messages (e.g. "no search results") + position: unset; + } + + &::after { + content: ''; + display:block; + margin-top: 20px; + width: 100%; + height: 1px; + background-color: @gray-11; + } } .umb-control-group.-no-border { - border: none; + &::after { + margin-top: 0px; + height: 0; + background-color: transparent; + } } .umb-property:last-of-type .umb-control-group { - border: none; - margin-bottom: 0 !important; - padding-bottom: 0; + &::after { + margin-top: 0px; + height: 0; + background-color: transparent; + } + margin-bottom: 0 !important; } /* BLOCK MODE */ .block-form .umb-control-group { - border-bottom: none; - padding-bottom: 0; + margin-top: 0px; + &::after { + margin-top: 0px; + height: 0; + background-color: transparent; + } } .block-form .umb-control-group label .help-block, @@ -163,7 +187,44 @@ h5.-black { } .umb-control-group .umb-el-wrap { - padding: 0 + padding: 0; +} +.umb-control-group .control-header { + + .control-label { + float: left; + } + + .control-description { + display: block; + clear: both; + } +} +.form-horizontal .umb-control-group .control-header { + float: left; + width: 160px; + padding-top: 5px; + text-align: left; + + .control-label { + width: auto; + padding-top: 0; + text-align: left; + } + + .control-description { + max-width:480px;// avoiding description becoming too wide when its placed on top of property. + margin-bottom: 10px; + } +} +@media (max-width: 767px) { + + .form-horizontal .umb-control-group .control-header { + float: none; + width: 100%; + } + + } /* LABELS*/ @@ -486,42 +547,6 @@ table thead a { color:@green; } -// Loading Animation -// ------------------------ - -.umb-loader{ - background-color: @blue; - margin-top:0; - margin-left:-100%; - animation-name:bounce_loadingProgressG; - animation-duration:1s; - animation-iteration-count:infinite; - animation-timing-function:linear; - width:100%; - height:2px; -} - -@keyframes bounce_loadingProgressG{ - 0%{ - margin-left:-100%; - } - 100%{ - margin-left:100%; - } -} - -.umb-loader-wrapper { - position: absolute; - right: 0; - left: 0; - margin: 10px 0; - overflow: hidden; -} - -.umb-loader-wrapper.-bottom { - bottom: 0; -} - // Helpers .strong { @@ -592,13 +617,3 @@ input[type=checkbox]:checked + .input-label--small { background-color: @green-l3; text-decoration: none; } - -.visuallyhidden{ - position: absolute !important; - clip: rect(1px, 1px, 1px, 1px); - padding:0 !important; - border:0 !important; - height: 1px !important; - width: 1px !important; - overflow: hidden; -} diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index ce35097658..f5c499cbfc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -30,7 +30,6 @@ outline: thin dotted @gray-3; // Webkit outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; } // Center-align a block level element @@ -308,7 +307,14 @@ opacity: @opacity / 100; } - +// Position +.absolute() { + position:absolute; + top:0; + right:0; + bottom:0; + left:0; +} // BACKGROUNDS // -------------------------------------------------- @@ -325,7 +331,7 @@ } // Gradient Bar Colors for buttons and alerts -.gradientBar(@primaryColor, @secondaryColor, @textColor: #fff) { +.gradientBar(@primaryColor, @secondaryColor, @textColor: @white) { color: @textColor; #gradient > .vertical(@primaryColor, @secondaryColor); border-color: @secondaryColor @secondaryColor darken(@secondaryColor, 15%); @@ -334,7 +340,7 @@ // Gradients #gradient { - .horizontal(@startColor: #555, @endColor: #333) { + .horizontal(@startColor: @gray, @endColor: @grayDark) { background-color: @endColor; background-image: -moz-linear-gradient(left, @startColor, @endColor); // FF 3.6+ background-image: -webkit-gradient(linear, 0 0, 100% 0, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+ @@ -343,7 +349,7 @@ background-image: linear-gradient(to right, @startColor, @endColor); // Standard, IE10 background-repeat: repeat-x; } - .vertical(@startColor: #555, @endColor: #333) { + .vertical(@startColor: @gray, @endColor: @grayDark) { background-color: mix(@startColor, @endColor, 60%); background-image: -moz-linear-gradient(top, @startColor, @endColor); // FF 3.6+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+ @@ -352,7 +358,7 @@ background-image: linear-gradient(to bottom, @startColor, @endColor); // Standard, IE10 background-repeat: repeat-x; } - .directional(@startColor: #555, @endColor: #333, @deg: 45deg) { + .directional(@startColor: @gray, @endColor: @grayDark, @deg: 45deg) { background-color: @endColor; background-repeat: repeat-x; background-image: -moz-linear-gradient(@deg, @startColor, @endColor); // FF 3.6+ @@ -360,7 +366,7 @@ background-image: -o-linear-gradient(@deg, @startColor, @endColor); // Opera 11.10 background-image: linear-gradient(@deg, @startColor, @endColor); // Standard, IE10 } - .horizontal-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 50%, @endColor: #c3325f) { + .horizontal-three-colors(@startColor: @lightBlueIcon, @midColor: @purple-l1, @colorStop: 50%, @endColor: @pink) { background-color: mix(@midColor, @endColor, 80%); background-image: -webkit-gradient(left, linear, 0 0, 0 100%, from(@startColor), color-stop(@colorStop, @midColor), to(@endColor)); background-image: -webkit-linear-gradient(left, @startColor, @midColor @colorStop, @endColor); @@ -370,7 +376,7 @@ background-repeat: no-repeat; } - .vertical-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 50%, @endColor: #c3325f) { + .vertical-three-colors(@startColor: @lightBlueIcon, @midColor: @deepPurpleIcon, @colorStop: 50%, @endColor: @pink) { background-color: mix(@midColor, @endColor, 80%); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), color-stop(@colorStop, @midColor), to(@endColor)); background-image: -webkit-linear-gradient(@startColor, @midColor @colorStop, @endColor); @@ -379,7 +385,7 @@ background-image: linear-gradient(@startColor, @midColor @colorStop, @endColor); background-repeat: no-repeat; } - .radial(@innerColor: #555, @outerColor: #333) { + .radial(@innerColor: @gray, @outerColor: @grayDark) { background-color: @outerColor; background-image: -webkit-gradient(radial, center center, 0, center center, 460, from(@innerColor), to(@outerColor)); background-image: -webkit-radial-gradient(circle, @innerColor, @outerColor); @@ -387,7 +393,7 @@ background-image: -o-radial-gradient(circle, @innerColor, @outerColor); background-repeat: no-repeat; } - .striped(@color: #555, @angle: 45deg) { + .striped(@color: @gray, @angle: 45deg) { background-color: @color; background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(.25, rgba(255,255,255,.15)), color-stop(.25, transparent), color-stop(.5, transparent), color-stop(.5, rgba(255,255,255,.15)), color-stop(.75, rgba(255,255,255,.15)), color-stop(.75, transparent), to(transparent)); background-image: -webkit-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); @@ -397,14 +403,22 @@ } } - +.checkeredBackground(@backgroundColor: @gray-9, @fillColor: @black, @fillOpacity: 0.25) { + background-image: url('data:image/svg+xml;charset=utf-8,\ + \ + \ + \ + '); + background-color: @backgroundColor; + background-size: 10px 10px; + background-repeat: repeat; +} // COMPONENT MIXINS // -------------------------------------------------- - // Limit width of specific property editors .umb-property-editor--limit-width { - max-width: 800px; + max-width: @propertyEditorLimitedWidth; } // Horizontal dividers @@ -420,28 +434,28 @@ // Button backgrounds // ------------------ -.buttonBackground(@startColor, @hoverColor: @startColor, @textColor: #fff, @textColorHover: @textColor) { - +.buttonBackground(@startColor, @hoverColor: @startColor, @textColor: @white, @textColorHover: @textColor, @disabledColor: @sand-1, @disabledTextColor: @white) { + color: @textColor; border-color: @startColor @startColor darken(@startColor, 15%); border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) fadein(rgba(0,0,0,.1), 15%); - + background-color: @startColor; - + .caret { border-top-color: @textColor; border-bottom-color: @textColor; } // in these cases the gradient won't cover the background, so we override - &:hover, &:focus, &:active, &.active { + &:hover { color: @textColorHover; background-color: @hoverColor; } - + &.disabled, &[disabled] { - color: @white; - background-color: @sand-1; + background-color: @disabledColor; + color: @disabledTextColor; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 52a573d8c4..4ce907d06f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -85,12 +85,16 @@ bottom: 0px; position: absolute; padding: 0px; - background: #fff; + background: @white; } -.umb-dialog .umb-btn-toolbar .umb-control-group{ - border: none; - padding: none; +.umb-dialog .abstract{ + margin-bottom:20px; +} + +.umb-dialog .umb-btn-toolbar .umb-control-group { + border: none; + padding: none; } .umb-dialog-body{ @@ -99,7 +103,7 @@ top: 0px; left: 0px; right: 0px; - bottom: 52px; + bottom: 49px; } .umb-dialog-body .umb-pane{margin-top: 15px;} @@ -111,8 +115,12 @@ left: 0px; right: 0px; bottom: 0px; - padding: 20px; + padding: 8px; margin: 0; + + .btn.umb-outline { + position: relative + } } /*we will always make sure to wrap iframe dialogs in proper padding*/ diff --git a/src/Umbraco.Web.UI.Client/src/less/navs.less b/src/Umbraco.Web.UI.Client/src/less/navs.less index a2710fab6c..c347404619 100644 --- a/src/Umbraco.Web.UI.Client/src/less/navs.less +++ b/src/Umbraco.Web.UI.Client/src/less/navs.less @@ -233,11 +233,32 @@ } .dropdown-menu > li > a { + position: relative; padding: 8px 20px; color: @ui-option-type; + text-decoration: none; } -.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus, .dropdown-submenu:hover > a, .dropdown-submenu:focus > a { +.dropdown-menu > li > button { + position: relative; + background: transparent; + border: 0; + padding: 8px 20px; + color: @ui-option-type; + display: block; + clear: both; + font-weight: normal; + line-height: 20px; + white-space: nowrap; + cursor:pointer; + width: 100%; + text-align: left; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > button:hover, +.dropdown-submenu:hover > a, +.dropdown-submenu:hover > button { color: @ui-option-type-hover; background: @ui-option-hover; } @@ -280,8 +301,7 @@ // Active:hover/:focus dropdown links // ------------------------- -.nav > .dropdown.active > a:hover, -.nav > .dropdown.active > a:focus { +.nav > .dropdown.active > a:hover { cursor: pointer; } @@ -289,24 +309,21 @@ // ------------------------- .nav-tabs .open .dropdown-toggle, .nav-pills .open .dropdown-toggle, -.nav > li.dropdown.open.active > a:hover, -.nav > li.dropdown.open.active > a:focus { +.nav > li.dropdown.open.active > a:hover { /*color: @white;*/ background-color: @gray-8; border-color: @gray-8; } .nav li.dropdown.open .caret, .nav li.dropdown.open.active .caret, -.nav li.dropdown.open a:hover .caret, -.nav li.dropdown.open a:focus .caret { +.nav li.dropdown.open a:hover .caret { border-top-color: @white; border-bottom-color: @white; .opacity(100); } // Dropdowns in stacked tabs -.tabs-stacked .open > a:hover, -.tabs-stacked .open > a:focus { +.tabs-stacked .open > a:hover { border-color: @gray-8; } @@ -357,15 +374,13 @@ } .tabs-below > .nav-tabs > li > a { .border-radius(0 0 4px 4px); - &:hover, - &:focus { + &:hover { border-bottom-color: transparent; border-top-color: @gray-8; } } .tabs-below > .nav-tabs > .active > a, -.tabs-below > .nav-tabs > .active > a:hover, -.tabs-below > .nav-tabs > .active > a:focus { +.tabs-below > .nav-tabs > .active > a:hover { border-color: transparent @gray-8 @gray-8 @gray-8; } @@ -394,13 +409,11 @@ margin-right: -1px; .border-radius(4px 0 0 4px); } -.tabs-left > .nav-tabs > li > a:hover, -.tabs-left > .nav-tabs > li > a:focus { +.tabs-left > .nav-tabs > li > a:hover { border-color: @gray-10 @gray-8 @gray-10 @gray-10; } .tabs-left > .nav-tabs .active > a, -.tabs-left > .nav-tabs .active > a:hover, -.tabs-left > .nav-tabs .active > a:focus { +.tabs-left > .nav-tabs .active > a:hover { border-color: @gray-8 transparent @gray-8 @gray-8; *border-right-color: @white; } @@ -415,13 +428,11 @@ margin-left: -1px; .border-radius(0 4px 4px 0); } -.tabs-right > .nav-tabs > li > a:hover, -.tabs-right > .nav-tabs > li > a:focus { +.tabs-right > .nav-tabs > li > a:hover { border-color: @gray-10 @gray-10 @gray-10 @gray-8; } .tabs-right > .nav-tabs .active > a, -.tabs-right > .nav-tabs .active > a:hover, -.tabs-right > .nav-tabs .active > a:focus { +.tabs-right > .nav-tabs .active > a:hover { border-color: @gray-8 @gray-8 @gray-8 transparent; *border-left-color: @white; } @@ -436,8 +447,7 @@ color: @gray-8; } // Nuke hover/focus effects -.nav > .disabled > a:hover, -.nav > .disabled > a:focus { +.nav > .disabled > a:hover { text-decoration: none; background-color: transparent; cursor: default; diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/document-type-editor.less b/src/Umbraco.Web.UI.Client/src/less/pages/document-type-editor.less deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/nonodes.less b/src/Umbraco.Web.UI.Client/src/less/pages/nonodes.less index 868a358d21..7c493ab18e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/nonodes.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/nonodes.less @@ -1,5 +1,6 @@ @import "../fonts.less"; // Loading fonts @import "../variables.less"; // Loading variables +@import "../colors.less"; // Loading colors that were in variables abbr, address, diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index bad0ab9715..40c70f5331 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -341,6 +341,7 @@ .umb-panel-header-icon { cursor: pointer; margin-right: 5px; + margin-top: -6px; height: 50px; display: flex; justify-content: center; diff --git a/src/Umbraco.Web.UI.Client/src/less/properties.less b/src/Umbraco.Web.UI.Client/src/less/properties.less index e14bb5c0d6..9e951feb1a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/properties.less +++ b/src/Umbraco.Web.UI.Client/src/less/properties.less @@ -48,6 +48,10 @@ flex-direction: row; } +.date-wrapper-mini--checkbox{ + margin: 0 0 0 28px; +} + .date-wrapper-mini__date { display: flex; @@ -58,6 +62,10 @@ &:first-of-type { margin-left: 0; } + .flatpickr-input > button:first-of-type { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } } .date-wrapper-mini__date .flatpickr-input > a { @@ -93,7 +101,8 @@ .history-line { width: 2px; - height: 100%; + top: 10px; + bottom: 10px; margin: 0 0 0 14px; background-color: @gray-8; position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 435b64ad34..764b73c593 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -1,5 +1,3 @@ -@checkered-background: url(../img/checkered-background.png); - // // Container styles // -------------------------------------------------- @@ -9,7 +7,7 @@ .umb-property-editor-tiny { width: 60px; - + &.umb-editor-push { width:30%; min-width:0; @@ -32,7 +30,7 @@ } .umb-codeeditor{ - width: 99%; + width: 99%; } // displays property inline with preceeding @@ -141,7 +139,7 @@ margin-bottom: 0; vertical-align: middle; padding: 6px 10px; - background: #f7f7f7; + background: @gray-11; flex: 0 0 auto; } @@ -171,8 +169,8 @@ border: 1px solid @white; padding: 6px 10px; font-family: monospace; - border: 1px solid #dfdfe1; - background: #f7f7f7; + border: 1px solid @gray-8; + background: @gray-11; margin: 0 15px 0 3px; border-radius: 3px; } @@ -234,6 +232,7 @@ } .umb-mediapicker-multi > div { width:100%; + .umb-property-editor--limit-width(); } @@ -250,30 +249,15 @@ transition: all 150ms ease-in-out; - &:focus, + &:active, &:hover { color: @ui-action-discreet-type-hover; border-color: @ui-action-discreet-type-hover; } - - &:focus { - outline: none; - .tabbing-active &:after { - content: ''; - position: absolute; - z-index: 10000; - top: -6px; - bottom: -6px; - left: -6px; - right: -6px; - border-radius: 3px; - box-shadow: 0 0 2px @blueMid, inset 0 0 2px 1px @blueMid; - } - } } -.umb-mediapicker .label{ - &__trashed{ +.umb-mediapicker .label { + &__trashed { background-color: @red; position: absolute; top: 50%; @@ -281,6 +265,7 @@ z-index: 1; transform: translate3d(-50%,-50%,0); margin: 0; + pointer-events: none; } } @@ -356,25 +341,19 @@ padding: 5px; margin: 5px; background: @white; - //border: 1px solid @gray-10; max-width: 100%; } .umb-mediapicker { - + .umb-sortable-thumbnails li { border:none; } - + } - - .umb-mediapicker .umb-sortable-thumbnails li { flex-direction: column; } -/*.umb-mediapicker .umb-sortable-thumbnails li.add-wrapper { - padding: 0px; -}*/ .umb-sortable-thumbnails li:hover a { display: flex; @@ -383,11 +362,11 @@ } .umb-sortable-thumbnails li img { - max-width:100%; - max-height:100%; - margin:auto; - display:block; - background-image: @checkered-background; + max-width: 100%; + max-height: 100%; + margin: auto; + display: block; + .checkeredBackground(); } .umb-sortable-thumbnails li .trashed { @@ -399,34 +378,6 @@ max-height: none !important; } -.umb-sortable-thumbnails .umb-icon-holder { - text-align: center; -} - -.umb-sortable-thumbnails .umb-icon-holder .icon { - font-size: 40px; - line-height: 50px; - color: @gray-3; - display: block; -} - -.umb-sortable-thumbnails .umb-icon-holder .file-icon > span { - color: @white; - background: @gray-4; - padding: 1px 3px; - font-size: 10px; - line-height: 130%; - display: block; - margin-top: -30px; - width: 2em; -} - -.umb-sortable-thumbnails .umb-icon-holder .file-icon + small { - display: block; - margin-top: 1em; -} - - .umb-sortable-thumbnails .umb-sortable-thumbnails__wrapper { width: 124px; height: 124px; @@ -450,8 +401,6 @@ text-decoration: none; display: flex; flex-direction: row; - opacity: 0; - visibility: hidden; } .umb-sortable-thumbnails.ui-sortable:not(.ui-sortable-disabled) { @@ -460,9 +409,8 @@ } } -.umb-sortable-thumbnails li:hover .umb-sortable-thumbnails__actions { +.umb-sortable-thumbnails li:hover .umb-sortable-thumbnails__action { opacity: 1; - visibility: visible; } .umb-sortable-thumbnails .umb-sortable-thumbnails__action { @@ -477,9 +425,13 @@ align-items: center; margin-left: 5px; text-decoration: none; - - //border-color: @inputBorder; .box-shadow(0 1px 2px rgba(0,0,0,0.25)); + opacity: 0; + transition: opacity .1s ease-in-out; + + .tabbing-active &:focus { + opacity: 1; + } } .umb-sortable-thumbnails .umb-sortable-thumbnails__action.-red { @@ -618,7 +570,19 @@ .viewport { max-width: 600px; - background: @checkered-background; + .checkeredBackground(); + + img { + display: block; + margin-left: auto; + margin-right: auto; + } + + img { + display: block; + margin-left: auto; + margin-right: auto; + } &:hover { cursor: pointer; @@ -638,14 +602,14 @@ } .viewport img { - background: @checkered-background; + .checkeredBackground(); } } .imagecropper .umb-cropper__container { position: relative; margin-bottom: 10px; - max-width: 100%; + max-width: 100%; border: 1px solid @gray-10; @media (min-width: 769px) { @@ -708,7 +672,8 @@ } .imagecropper .umb-sortable-thumbnails li .crop-name, - .imagecropper .umb-sortable-thumbnails li .crop-size { + .imagecropper .umb-sortable-thumbnails li .crop-size, + .imagecropper .umb-sortable-thumbnails li .crop-annotation { display: block; text-align: left; font-size: 13px; @@ -720,17 +685,28 @@ margin: 10px 0 5px; } - .imagecropper .umb-sortable-thumbnails li .crop-size { + .imagecropper .umb-sortable-thumbnails li .crop-size, + .imagecropper .umb-sortable-thumbnails li .crop-annotation { font-size: 10px; font-style: italic; margin: 0 0 5px; } + .imagecropper .umb-sortable-thumbnails li .crop-annotation { + color: @gray-6; + } + .btn-crop-delete { display: block; text-align: left; } + .imagecropper .cropList-container { + h5 { + margin-left: 10px; + margin-top: 0; + } + } // // folder-browser // -------------------------------------------------- @@ -746,6 +722,10 @@ // // File upload // -------------------------------------------------- +.umb-fileupload { + display: flex; +} + .umb-fileupload .preview { border-radius: 5px; border: 1px solid @gray-6; @@ -757,13 +737,13 @@ } .umb-fileupload ul { - list-style: none; - vertical-align: middle; - margin-bottom: 0; + list-style: none; + vertical-align: middle; + margin-bottom: 0; - img { - background: @checkered-background; - } + img { + .checkeredBackground(); + } } .umb-fileupload label { @@ -782,30 +762,6 @@ padding-top: 27px; } -.umb-fileupload .file-icon { - display: inline-block; - position: relative; - padding: 5px 0; - - > .icon { - font-size: 70px; - line-height: 110%; - color: @gray-4; - text-align: center; - } - - > span { - color: @white; - background: @gray-4; - padding: 1px 3px; - font-size: 12px; - line-height: 130%; - position: absolute; - top: 45px; - left: 10px; - } -} - .umb-fileupload input { font-size: 12px; line-height: 1; @@ -868,7 +824,7 @@ background: @blueExtraDark; position: relative; user-select: all; - + .icon-trash { position: relative; cursor: pointer; @@ -889,7 +845,7 @@ border: none; background: @white; } - + .twitter-typeahead { margin: 10px; margin-top: 16px; @@ -926,6 +882,21 @@ .umb-datepicker p {margin-top:10px;} .umb-datepicker p a{color: @gray-3;} +// +// Link picker +// -------------------------------------------------- +.umb-linkpicker { + .umb-linkpicker__url { + width: 50%; + padding-right: 5px; + } + + .umb-linkpicker__anchor { + width: 50%; + padding-left: 5px; + } +} + // // Code mirror - even though this isn't a proprety editor right now, it could be so I'm putting the styles here // -------------------------------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/less/rte-content.less b/src/Umbraco.Web.UI.Client/src/less/rte-content.less new file mode 100644 index 0000000000..3fe4e52f92 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/rte-content.less @@ -0,0 +1,45 @@ +@import "variables.less"; +@import "colors.less"; + +.mce-content-body .umb-macro-holder { + border: 3px dotted @pinkLight; + padding: 7px; + display: block; + margin: 3px; +} + + +.umb-rte .mce-content-body .umb-macro-holder.loading { + background: url(assets/img/loader.gif) right no-repeat; + background-size: 18px; background-position-x: 99%; +} + + +.umb-rte .embeditem { + position:relative; + > * { + user-select: none; + pointer-events: none; + } +} + +.umb-rte .embeditem[data-mce-selected] { + outline: 2px solid @pinkLight; +} + +.umb-rte .embeditem::before { + z-index:1000; + width:100%; + height:100%; + position:absolute; + content:' '; +} + +.umb-rte .embeditem[data-mce-selected]::before { + background:rgba(0,0,0,0.025); +} + +.umb-rte *[data-mce-selected="inline-boundary"] { + background:rgba(0,0,0,0.025); + outline: 2px solid @pinkLight; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/rte.less b/src/Umbraco.Web.UI.Client/src/less/rte.less index ccf52acc53..0e43f428ae 100644 --- a/src/Umbraco.Web.UI.Client/src/less/rte.less +++ b/src/Umbraco.Web.UI.Client/src/less/rte.less @@ -3,22 +3,26 @@ .umb-rte { position: relative; - - .umb-property-editor--limit-width(); - .-loading { - position: absolute; - } + .umb-property-editor--limit-width(); } .umb-rte .mce-tinymce { box-shadow: none; - } -.umb-rte .umb-rte-editor{ +.umb-rte .umb-rte-editor-con { + height: 24px; + visibility: hidden; +} +.umb-rte .umb-rte-editor { min-height: 100px; } +.umb-rte.--initialized .umb-rte-editor-con { + height:auto; + min-height: 100px; + visibility: visible; +} .umb-rte .mce-content-body { background-color: @white; @@ -52,12 +56,14 @@ } */ -.mce-ico { +.umb-rte .mce-ico { + text-align: center; font-size: 12px !important; /*color: @gray-1 !important;*/ } /* Special case to support helviticons for the tiny mce button controls */ +// Also used in Prevalue editor. .mce-ico.mce-i-custom[class^="icon-"], .mce-ico.mce-i-custom[class*=" icon-"] { font-family: icomoon; @@ -76,3 +82,95 @@ line-height: 20px; } } + +.umb-rte .mce-toolbar .mce-btn-group { + padding: 0; +} + +.umb-rte .mce-btn { + color: @ui-action-type; + border-radius: 3px; +} + +.umb-rte .mce-btn-group .mce-btn { + margin-top:2px; + margin-bottom:2px; +} + +.umb-rte .mce-btn { + button:hover { + .mce-caret { + border-top-color: @ui-action-type-hover; + } + } +} + +.umb-rte .mce-btn:hover, .umb-rte .mce-btn:active { + background: @ui-action-hover; + border-color: transparent; + button { + color: @ui-action-type-hover; + .mce-ico { + color: @ui-action-type-hover; + } + } +} + +.umb-rte .mce-btn.mce-active, .umb-rte .mce-btn.mce-active:active, +.umb-rte .mce-btn.mce-active:hover, .umb-rte .mce-btn.mce-active:focus { + background: @gray-9; + border-color: transparent; + button { + color: @ui-action-type-hover; + .mce-ico { + color: @ui-action-type-hover; + } + .mce-caret { + border-top-color: @ui-action-type-hover; + } + } +} + + +.mce-menu { + border-radius: 3px; +} + +.mce-menu-item.mce-menu-item-normal.mce-stack-layout-item { + .mce-text, .mce-ico { + color: @ui-action-type; + } + &:hover { + background: @ui-action-hover; + .mce-text, .mce-ico { + color: @ui-action-type-hover; + } + } +} + +.mce-menu-item.mce-menu-item-normal.mce-stack-layout-item.mce-active { + &, &:hover { + background: @gray-9; + } + .mce-text, .mce-ico { + color: @ui-action-type-hover; + } +} + +.umb-grid .umb-rte { + border: 1px solid @gray-8; + max-width: none; +} + +.mce-fullscreen { + position: absolute; + + .mce-in { + position: fixed; + top: 35px !important; + } + + umb-editor__overlay, .umb-editor { + position: fixed; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index 5a1de02617..d4b5cfc998 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -2,128 +2,150 @@ // ------------------------- ul.sections { - margin: 0; - display: flex; - margin-left: -20px; -} - -ul.sections>li { - display: flex; - justify-content: center; - align-items: center; - position: relative; -} - -ul.sections>li>a { - color: @white; - height: @appHeaderHeight; + margin: 0; display: flex; - align-items: center; - justify-content: center; - position: relative; - padding: 0 10px; - text-decoration: none; - outline: none; - cursor: pointer; -} + margin-left: -20px; -ul.sections>li>a .section__name { - border-radius: 3px; - margin-top:1px; - padding: 3px 10px 4px 10px; - opacity: 0.8; - transition: opacity .1s linear, box-shadow .1s; -} + > li { + display: flex; + justify-content: center; + align-items: center; + position: relative; -ul.sections>li>a::after { - content: ""; - left: 10px; - right: 10px; - height: 4px; - bottom: 0; - transform: translateY(4px); - background-color: @pinkLight; - position: absolute; - border-radius: 3px 3px 0 0; - opacity: 0; - padding: 0 2px; - transition: transform 240ms ease-in-out; -} + > a { + color: @white; + height: @appHeaderHeight; + display: flex; + align-items: center; + justify-content: center; + position: relative; + padding: 0 10px; + text-decoration: none; + outline: none; + cursor: pointer; -ul.sections>li.current>a { - color:@pinkLight; -} -ul.sections>li.current>a::after { - opacity: 1; - transform: translateY(0px); -} -ul.sections>li.current>a .section__name, -ul.sections>li>a:hover .section__name, -ul.sections>li>a:focus .section__name { - opacity: 1; - -webkit-font-smoothing: subpixel-antialiased; -} + &::after { + content: ""; + left: 10px; + right: 10px; + height: 4px; + bottom: 0; + transform: translateY(4px); + background-color: @pinkLight; + position: absolute; + border-radius: 3px 3px 0 0; + opacity: 0; + padding: 0 2px; + transition: transform 240ms ease-in-out; + } -ul.sections>li>a:focus .section__name { - .tabbing-active & { - box-shadow: 0 0 2px @pinkLight, inset 0 0 2px 1px @pinkLight; + &:focus .section__name { + .tabbing-active & { + border: 1px solid @gray-9; + } + } + } + + .section__name { + border: 1px solid transparent; + border-radius: 3px; + margin-top: 1px; + padding: 3px 10px 4px 10px; + opacity: 0.8; + transition: opacity .1s linear, box-shadow .1s; + } + + &.current > a { + color: @pinkLight; + + &::after { + opacity: 1; + transform: translateY(0px); + } + } + + &.expand { + i { + height: 5px; + width: 5px; + border-radius: 50%; + background: @white; + display: inline-block; + margin: 0 5px 0 0; + opacity: 0.6; + transition: opacity .1s linear; + } + + &.current { + i { + opacity: 1; + background: @pinkLight; + } + } + + &:hover i { + opacity: 1; + } + } + + &.current .section__name, + a:hover .section__name { + opacity: 1; + -webkit-font-smoothing: subpixel-antialiased; + } } } - - /* Sections tray */ -ul.sections>li.expand i { - height: 5px; - width: 5px; - border-radius: 50%; - background: #fff; - display: inline-block; - margin: 0 5px 0 0; - opacity: 0.6; -} - ul.sections-tray { - position: absolute; - top: @appHeaderHeight; - left: 0; - margin: 0; + position: absolute; + top: @appHeaderHeight; + left: 0; + margin: 0; list-style: none; - background: @purple; - z-index: 10000; - border-radius: 0 0 3px 3px; -} + background: @blueExtraDark; + z-index: 10000; + border-radius: 0 0 3px 3px; -ul.sections-tray>li>a { - padding: 8px 24px; - color: @white; - text-decoration: none; - display: block; - position: relative; -} + li { -ul.sections-tray>li>a::after { - content: ""; - width: 4px; - height: 100%; - background-color: @ui-active; - position: absolute; - border-radius: 0 3px 3px 0; - opacity: 0; - transition: all .2s linear; - top: 0; - left: 0; -} + &.current a { + color: @ui-active-border; + opacity: 1; -ul.sections-tray>li.current>a::after { - opacity: 1; -} + &::after { + opacity: 1; + } + } -ul.sections-tray>li>a .section__name { - opacity: 0.6; -} + a { + padding: 8px 24px; + color: @white; + text-decoration: none; + display: block; + position: relative; + outline: none; -ul.sections-tray>li>a:hover .section__name { - opacity: 1; + &::after { + content: ""; + width: 4px; + height: 100%; + background-color: @ui-active-border; + position: absolute; + border-radius: 0 3px 3px 0; + opacity: 0; + transition: all .2s linear; + top: 0; + left: 0; + } + + &:focus .section__name { + .tabbing-active & { + border: 1px solid; + border-color: @gray-9; + } + } + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/tables.less b/src/Umbraco.Web.UI.Client/src/less/tables.less index cd6304ef49..2ecfa4d04e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tables.less +++ b/src/Umbraco.Web.UI.Client/src/less/tables.less @@ -62,7 +62,7 @@ table { } -.table tr > td:first-child { +.table:not(.table-bordered) tr > td:first-child { border-left: 4px solid transparent; } .table tr.--selected > td:first-child { @@ -263,3 +263,15 @@ table th[class*="span"], .table-sortable tbody tr { cursor: move; } + +.table__action-overlay{ + background: transparent; + border: 0 none; + padding: 0; + font-style: italic; + + &:focus, + &:hover{ + text-decoration: underline; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less b/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less index c0815fa8ac..a3427074cd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less @@ -1,7 +1,5 @@ /* - Flexbox - */ .flex { display: flex; } diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less b/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less index 69bbeef0af..787e50f204 100644 --- a/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less +++ b/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less @@ -57,3 +57,28 @@ .mb5 { margin-bottom: @spacing-extra-large; } .mb6 { margin-bottom: @spacing-extra-extra-large; } .mb7 { margin-bottom: @spacing-extra-extra-extra-large; } + +.ml0 { margin-left: @spacing-none; } +.ml1 { margin-left: @spacing-extra-small; } +.ml2 { margin-left: @spacing-small; } +.ml3 { margin-left: @spacing-medium; } +.ml4 { margin-left: @spacing-large; } +.ml5 { margin-left: @spacing-extra-large; } +.ml6 { margin-left: @spacing-extra-extra-large; } +.ml7 { margin-left: @spacing-extra-extra-extra-large; } + +.mr0 { margin-right: @spacing-none; } +.mr1 { margin-right: @spacing-extra-small; } +.mr2 { margin-right: @spacing-small; } +.mr3 { margin-right: @spacing-medium; } +.mr4 { margin-right: @spacing-large; } +.mr5 { margin-right: @spacing-extra-large; } +.mr6 { margin-right: @spacing-extra-extra-large; } +.mr7 { margin-right: @spacing-extra-extra-extra-large; } + +.p0 { padding: @spacing-none; } + +.pt0 { padding-top: @spacing-none; } +.pb0 { padding-bottom: @spacing-none; } +.pl0 { padding-left: @spacing-none; } +.pr0 { padding-right: @spacing-none; } diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index cc9dced7df..d94e4e1fb1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -75,12 +75,16 @@ @gray-9: #E9E9EB; @gray-10: #F3F3F5; @gray-11: #F6F6F7; +@gray-12: #F9F9FA; +@gray-13: #FBFBFD; -@sand-1: hsl(22, 18%, 84%);// added 2019 -@sand-2: hsl(22, 34%, 88%);// added 2019 -@sand-5: hsl(22, 31%, 93%);// added 2019 -@sand-6: hsl(22, 29%, 95%);// added 2019 -@sand-7: hsl(22, 26%, 97%);// added 2019 +@sand-1: #DED4CF;// added 2019 +@sand-2: #EBDED6;// added 2019 +@sand-5: #F3ECE8;// added 2019 +@sand-6: #F6F1EF;// added 2019 +@sand-7: #F9F7F5;// added 2019 +@sand-8: #fbfaf9;// added 2019 +@sand-9: #fdfcfc;// added 2019 // Additional Icon Colours @@ -108,6 +112,7 @@ //@blueLight: #4f89de; @blue: #2E8AEA; @blueMid: #2152A3;// updated 2019 +@blueMidLight: #6ab4f0; @blueDark: #3544b1;// updated 2019 @blueExtraDark: #1b264f;// added 2019 @blueLight: #ADD8E6; @@ -115,6 +120,7 @@ //@orange: #f79c37;// updated 2019 @pink: #D93F4C;// #C3325F;// update 2019 @pinkLight: #f5c1bc;// added 2019 +@pinkExtraLight: #fee4e1;// added 2020 @pinkRedLight: #ff8a89;// added 2019 @brown: #9d8057;// added 2019 @brownLight: #e4e0dd;// added 2019 @@ -124,7 +130,7 @@ //@u-greyLight: #f2ebe6;// added 2019 @u-white: #f9f7f4;// added 2019 -@u-black: black;// added 2019 +@u-black: @black;// added 2019 // UI colors @@ -132,14 +138,19 @@ @ui-option-type: @blueExtraDark; @ui-option-type-hover: @blueMid; -@ui-option-hover: @sand-7; +@ui-option: @white; +@ui-option-hover: @gray-12; @ui-option-disabled-type: @gray-6; @ui-option-disabled-type-hover: @gray-5; -@ui-option-disabled-hover: @sand-7; +@ui-option-disabled-hover: @gray-12; + +@ui-disabled-type: @gray-6; +@ui-disabled-border: @gray-6; //@ui-active: #346ab3; -@ui-active: @pinkLight; +@ui-active: @pinkExtraLight; +@ui-active-border: @pinkLight; @ui-active-blur: @brownLight; @ui-active-type: @blueExtraDark; @ui-active-type-hover: @blueMid; @@ -148,32 +159,34 @@ @ui-selected-hover: ligthen(@sand-5, 10); @ui-selected-type: @blueExtraDark; @ui-selected-type-hover: @blueMid; -@ui-selected-border: @pinkLight; -@ui-selected-border-hover: darken(@pinkLight, 10); +@ui-selected-border: @blueDark; +@ui-selected-border-hover: darken(@blueDark, 10); @ui-light-border: @pinkLight; @ui-light-type: @gray-4; @ui-light-type-hover: @blueMid; @ui-light-active-border: @pinkLight; -@ui-light-active-type: @blueExtraDark; -@ui-light-active-type-hover: @blueMid; +@ui-light-active-type: @blueMid; +@ui-light-active-type-hover: @blueMidLight; -@ui-action: white; -@ui-action-hover: @sand-7; +@ui-action: @white; +@ui-action-hover: @gray-12; @ui-action-type: @blueExtraDark; @ui-action-type-hover: @blueMid; @ui-action-border: @blueExtraDark; @ui-action-border-hover: @blueMid; -@ui-action-discreet: white; -@ui-action-discreet-hover: @sand-7; +@ui-action-discreet: @white; +@ui-action-discreet-hover: @gray-12; @ui-action-discreet-type: @blueExtraDark; @ui-action-discreet-type-hover: @blueMid; @ui-action-discreet-border: @gray-7; @ui-action-discreet-border-hover: @blueMid; +@ui-outline: @blueMidLight; + @type-white: @white; @type-black: @blueNight; @@ -193,96 +206,11 @@ - -.red{color: @red;} -.blue{color: @blue;} -.black{color: @black;} -.turquoise{color: @turquoise;} -.turquoise-d1{color: @turquoise-d1;} - -.text-warning { - color: @orange; -} -.text-error { - color: @red; -} -.text-success { - color: @green; -} - - -//icon colors for tree icons -.color-red, .color-red i{color: @red-d1 !important;} -.color-blue, .color-blue i{color: @turquoise-d1 !important;} -.color-orange, .color-orange i{color: @orange !important;} -.color-green, .color-green i{color: @green-d1 !important;} -.color-yellow, .color-yellow i{color: @yellowIcon !important;} - -/* Colors based on https://zavoloklom.github.io/material-design-color-palette/colors.html */ -.btn-color-black {background-color: @black;} -.color-black i { color: @black;} - -.btn-color-blue-grey {background-color: @blueGrey;} -.color-blue-grey, .color-blue-grey i { color: @blueGrey !important;} - -.btn-color-grey{background-color: @grayIcon;} -.color-grey, .color-grey i { color: @grayIcon !important; } - -.btn-color-brown{background-color: @brownIcon;} -.color-brown, .color-brown i { color: @brownIcon !important; } - -.btn-color-blue{background-color: @blueIcon;} -.color-blue, .color-blue i { color: @blueIcon !important; } - -.btn-color-light-blue{background-color: @lightBlueIcon;} -.color-light-blue, .color-light-blue i {color: @lightBlueIcon !important;} - -.btn-color-cyan{background-color: @cyanIcon;} -.color-cyan, .color-cyan i { color: @cyanIcon !important; } - -.btn-color-green{background-color: @greenIcon;} -.color-green, .color-green i { color: @greenIcon !important; } - -.btn-color-light-green{background-color: @lightGreenIcon;} -.color-light-green, .color-light-green i {color: @lightGreenIcon !important; } - -.btn-color-lime{background-color: @limeIcon;} -.color-lime, .color-lime i { color: @limeIcon !important; } - -.btn-color-yellow{background-color: @yellowIcon;} -.color-yellow, .color-yellow i { color: @yellowIcon !important; } - -.btn-color-amber{background-color: @amberIcon;} -.color-amber, .color-amber i { color: @amberIcon !important; } - -.btn-color-orange{background-color: @orangeIcon;} -.color-orange, .color-orange i { color: @orangeIcon !important; } - -.btn-color-deep-orange{background-color: @deepOrangeIcon;} -.color-deep-orange, .color-deep-orange i { color: @deepOrangeIcon !important; } - -.btn-color-red{background-color: @redIcon;} -.color-red, .color-red i { color: @redIcon !important; } - -.btn-color-pink{background-color: @pinkIcon;} -.color-pink, .color-pink i { color: @pinkIcon !important; } - -.btn-color-purple{background-color: @purpleIcon;} -.color-purple, .color-purple i { color: @purpleIcon !important; } - -.btn-color-deep-purple{background-color: @deepPurpleIcon;} -.color-deep-purple, .color-deep-purple i { color: @deepPurpleIcon !important; } - -.btn-color-indigo{background-color: @indigoIcon;} -.color-indigo, .color-indigo i { color: @indigoIcon !important; } - - - // Scaffolding // ------------------------- @appHeaderHeight: 55px; @bodyBackground: @gray-10; -@textColor: #000; +@textColor: @black; @editorHeaderHeight: 70px; @editorFooterHeight: 50px; @@ -322,6 +250,7 @@ @paddingSmall: 2px 10px; // 26px @paddingMini: 0 6px; // 22px +@propertyEditorLimitedWidth: 800px; // Disabled this to keep consistency throughout the backoffice UI. Untill a better solution is thought up, this will do. @baseBorderRadius: 3px; // 2px; @@ -339,7 +268,7 @@ // Buttons // ------------------------- @btnBackground: @gray-9; -@btnBackgroundHighlight: @gray-9; +@btnBackgroundHighlight: @gray-10; @btnBorder: @gray-9; @btnPrimaryBackground: @ui-btn-positive; @@ -377,7 +306,7 @@ @inputBackground: @white; @inputBorder: @gray-8; @inputBorderFocus: @gray-7; -@inputBorderTabFocus: @blueExtraDark; +@inputBorderTabFocus: @ui-outline; @inputBorderRadius: 0; @inputDisabledBackground: @gray-10; @formActionsBackground: @gray-9; @@ -426,7 +355,7 @@ @zindexFixedNavbar: 1030; @zindexModalBackdrop: 1040; @zindexModal: 1050; - +@zindexSearchBox: 8000; @zindexUmbOverlay: 7500; @zindexOverlayBackdrop: 2000; @@ -517,7 +446,7 @@ @heroUnitLeadColor: inherit; -// alerts +// Alerts // ------------------------- @warningText: @white; @warningBackground: @yellow-d2; @@ -532,7 +461,7 @@ @successBorder: transparent; @infoText: @white; -@infoBackground: @turquoise-d1; +@infoBackground: @blueDark; @infoBorder: transparent; @alertBorderRadius: 0; diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js similarity index 87% rename from src/Umbraco.Web.UI.Client/src/controllers/main.controller.js rename to src/Umbraco.Web.UI.Client/src/main.controller.js index 83b3c920d7..81eadf150f 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -1,19 +1,18 @@ - -/** +/** * @ngdoc controller - * @name Umbraco.MainController + * @name Umbraco.MainController * @function * - * @description + * @description * The main application controller * */ function MainController($scope, $location, appState, treeService, notificationsService, - userService, historyService, updateChecker, assetsService, eventsService, - tmhDynamicLocale, localStorageService, editorService, overlayService, focusService) { - + userService, historyService, updateChecker, navigationService, eventsService, + tmhDynamicLocale, localStorageService, editorService, overlayService, assetsService, tinyMceAssets) { + //the null is important because we do an explicit bool check on this in the view - $scope.authenticated = null; + $scope.authenticated = null; $scope.touchDevice = appState.getGlobalState("touchDevice"); $scope.infiniteMode = false; $scope.overlay = {}; @@ -21,7 +20,13 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.search = {}; $scope.login = {}; $scope.tabbingActive = false; - + + // Load TinyMCE assets ahead of time in the background for the user + // To help with first load of the RTE + tinyMceAssets.forEach(function (tinyJsAsset) { + assetsService.loadJs(tinyJsAsset, $scope); + }); + // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { @@ -61,13 +66,19 @@ function MainController($scope, $location, appState, treeService, notificationsS }; var evts = []; - + //when a user logs out or timesout evts.push(eventsService.on("app.notAuthenticated", function (evt, data) { $scope.authenticated = null; $scope.user = null; const isTimedOut = data && data.isTimedOut ? true : false; + $scope.showLoginScreen(isTimedOut); + + // Remove the localstorage items for tours shown + // Means that when next logged in they can be re-shown if not already dismissed etc + localStorageService.remove("emailMarketingTourShown"); + localStorageService.remove("introTourShown"); })); evts.push(eventsService.on("app.userRefresh", function(evt) { @@ -105,6 +116,13 @@ function MainController($scope, $location, appState, treeService, notificationsS //if the user has changed we need to redirect to the root so they don't try to continue editing the //last item in the URL (NOTE: the user id can equal zero, so we cannot just do !data.lastUserId since that will resolve to true) if (data.lastUserId !== undefined && data.lastUserId !== null && data.lastUserId !== data.user.id) { + + var section = appState.getSectionState("currentSection"); + if (section) { + //if there's a section already assigned, reload it so the tree is cleared + navigationService.reloadSection(section); + } + $location.path("/").search(""); historyService.removeAll(); treeService.clearCache(); diff --git a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/navigation.controller.js similarity index 85% rename from src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js rename to src/Umbraco.Web.UI.Client/src/navigation.controller.js index c426d0d955..a383c2d44a 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/navigation.controller.js @@ -68,7 +68,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar args.event.stopPropagation(); args.event.preventDefault(); - if (n.metaData && n.metaData["jsClickCallback"] && angular.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { + if (n.metaData && n.metaData["jsClickCallback"] && Utilities.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { //this is a legacy tree node! var jsPrefix = "javascript:"; var js; @@ -140,6 +140,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //// TODO: remove this it's not a thing //$scope.selectedId = navigationService.currentId; + var isInit = false; var evts = []; //Listen for global state changes @@ -199,7 +200,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar $scope.treeApi.load({ section: $scope.currentSection, customTreeParams: $scope.customTreeParams, cacheKey: $scope.treeCacheKey }); }); } - + //show/hide search results if (args.key === "showSearchResults") { $scope.showSearchResults = args.value; @@ -211,18 +212,34 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar evts.push(eventsService.on("editors.languages.languageDeleted", function (e, args) { loadLanguages().then(function (languages) { $scope.languages = languages; + const defaultCulture = $scope.languages[0].culture; + + if (args.language.culture === $scope.selectedLanguage.culture) { + $scope.selectedLanguage = defaultCulture; + + if ($scope.languages.length > 1) { + $location.search("mculture", defaultCulture); + } else { + $location.search("mculture", null); + } + + var currentEditorState = editorState.getCurrent(); + if (currentEditorState && currentEditorState.path) { + $scope.treeApi.syncTree({ path: currentEditorState.path, activate: true }); + } + } }); })); //Emitted when a language is created or an existing one saved/edited evts.push(eventsService.on("editors.languages.languageSaved", function (e, args) { - if(args.isNew){ + if (args.isNew) { //A new language has been created - reload languages for tree loadLanguages().then(function (languages) { $scope.languages = languages; }); } - else if(args.language.isDefault){ + else if (args.language.isDefault) { //A language was saved and was set to be the new default (refresh the tree, so its at the top) loadLanguages().then(function (languages) { $scope.languages = languages; @@ -236,8 +253,11 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar })); //when the application is ready and the user is authorized, setup the data + //this will occur anytime a new user logs in! evts.push(eventsService.on("app.ready", function (evt, data) { - init(); + $scope.authenticated = true; + ensureInit(); + ensureMainCulture(); })); // event for infinite editors @@ -252,7 +272,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar evts.push(eventsService.on("treeService.removeNode", function (e, args) { //check to see if the current page has been removed - var currentEditorState = editorState.getCurrent() + var currentEditorState = editorState.getCurrent(); if (currentEditorState && currentEditorState.id.toString() === args.node.id.toString()) { //current page is loaded, so navigate to root var section = appState.getSectionState("currentSection"); @@ -260,8 +280,22 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar } })); - - + /** + * For multi language sites, this ensures that mculture is set to either the last selected language or the default one + */ + function ensureMainCulture() { + if ($location.search().mculture) { + return; + } + var language = lastLanguageOrDefault(); + if (!language) { + return; + } + // trigger a language selection in the next digest cycle + $timeout(function () { + $scope.selectLanguage(language); + }); + } /** * Based on the current state of the application, this configures the scope variables that control the main tree and language drop down @@ -276,6 +310,9 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //select the current language if set in the query string if (mainCulture && $scope.languages && $scope.languages.length > 1) { var found = _.find($scope.languages, function (l) { + if (mainCulture === true) { + return false; + } return l.culture.toLowerCase() === mainCulture.toLowerCase(); }); if (found) { @@ -305,9 +342,14 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar /** * Called when the app is ready and sets up the navigation (should only be called once) */ - function init() { + function ensureInit() { - $scope.authenticated = true; + //only run once ever! + if (isInit) { + return; + } + + isInit = true; var navInit = false; @@ -340,7 +382,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar return contentResource.allowsCultureVariation().then(function (b) { if (b === true) { - return languageResource.getAll() + return languageResource.getAll(); } else { return $q.when([]); //resolve an empty collection } @@ -358,28 +400,19 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar if ($scope.languages.length > 1) { //if there's already one set, check if it exists - var currCulture = null; + var language = null; var mainCulture = $location.search().mculture; if (mainCulture) { - currCulture = _.find($scope.languages, function (l) { + language = _.find($scope.languages, function (l) { return l.culture.toLowerCase() === mainCulture.toLowerCase(); }); } - if (!currCulture) { - // no culture in the request, let's look for one in the cookie that's set when changing language - var defaultCulture = $cookies.get("UMB_MCULTURE"); - if (!defaultCulture || !_.find($scope.languages, function (l) { - return l.culture.toLowerCase() === defaultCulture.toLowerCase(); - })) { - // no luck either, look for the default language - var defaultLang = _.find($scope.languages, function (l) { - return l.isDefault; - }); - if (defaultLang) { - defaultCulture = defaultLang.culture; - } + if (!language) { + language = lastLanguageOrDefault(); + + if (language) { + $location.search("mculture", language.culture); } - $location.search("mculture", defaultCulture ? defaultCulture : null); } } @@ -399,11 +432,30 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //the nav is ready, let the app know eventsService.emit("app.navigationReady", { treeApi: $scope.treeApi }); - + } }); }); } + + function lastLanguageOrDefault() { + if (!$scope.languages || $scope.languages.length <= 1) { + return null; + } + // see if we can find a culture in the cookie set when changing language + var lastCulture = $cookies.get("UMB_MCULTURE"); + var language = lastCulture ? _.find($scope.languages, function (l) { + return l.culture.toLowerCase() === lastCulture.toLowerCase(); + }) : null; + if (!language) { + // no luck, look for the default language + language = _.find($scope.languages, function (l) { + return l.isDefault; + }); + } + return language; + } + function nodeExpandedHandler(args) { //store the reference to the expanded node path if (args.node) { @@ -417,7 +469,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar // add the selected culture to a cookie so the user will log back into the same culture later on (cookie lifetime = one year) var expireDate = new Date(); expireDate.setDate(expireDate.getDate() + 365); - $cookies.put("UMB_MCULTURE", language.culture, {path: "/", expires: expireDate}); + $cookies.put("UMB_MCULTURE", language.culture, { path: "/", expires: expireDate }); // close the language selector $scope.page.languageSelectorIsOpen = false; @@ -443,9 +495,10 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //execute them sequentially // set selected language to active - angular.forEach($scope.languages, function(language){ + Utilities.forEach($scope.languages, language => { language.active = false; }); + language.active = true; angularHelper.executeSequentialPromises(promises); @@ -483,6 +536,14 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar if (!event) { return; } + closeTree(); + }; + + $scope.onOutsideClick = function () { + closeTree(); + }; + + function closeTree() { if (!appState.getGlobalState("touchDevice")) { treeActive = false; $timeout(function () { @@ -491,7 +552,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar } }, 300); } - }; + } $scope.toggleLanguageSelector = function () { $scope.page.languageSelectorIsOpen = !$scope.page.languageSelectorIsOpen; diff --git a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js index 7d6584d2f1..5ff8dd3633 100644 --- a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js @@ -6,7 +6,32 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.services']) .controller("previewController", function ($scope, $window, $location) { - + + $scope.currentCulture = { iso: '', title: '...', icon: 'icon-loading' } + var cultures = []; + + $scope.tabbingActive = false; + // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. + // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 + function handleFirstTab(evt) { + if (evt.keyCode === 9) { + $scope.tabbingActive = true; + $scope.$digest(); + window.removeEventListener('keydown', handleFirstTab); + window.addEventListener('mousedown', disableTabbingActive); + } + } + + function disableTabbingActive(evt) { + $scope.tabbingActive = false; + $scope.$digest(); + window.removeEventListener('mousedown', disableTabbingActive); + window.addEventListener("keydown", handleFirstTab); + } + + window.addEventListener("keydown", handleFirstTab); + + //gets a real query string value function getParameterByName(name, url) { if (!url) url = $window.location.href; @@ -75,17 +100,57 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.valueAreLoaded = false; $scope.devices = [ - { name: "desktop", css: "desktop", icon: "icon-display", title: "Desktop" }, - { name: "laptop - 1366px", css: "laptop border", icon: "icon-laptop", title: "Laptop" }, - { name: "iPad portrait - 768px", css: "iPad-portrait border", icon: "icon-ipad", title: "Tablet portrait" }, - { name: "iPad landscape - 1024px", css: "iPad-landscape border", icon: "icon-ipad flip", title: "Tablet landscape" }, - { name: "smartphone portrait - 480px", css: "smartphone-portrait border", icon: "icon-iphone", title: "Smartphone portrait" }, - { name: "smartphone landscape - 320px", css: "smartphone-landscape border", icon: "icon-iphone flip", title: "Smartphone landscape" } + { name: "fullsize", css: "fullsize", icon: "icon-application-window-alt", title: "Browser" }, + { name: "desktop", css: "desktop shadow", icon: "icon-display", title: "Desktop" }, + { name: "laptop - 1366px", css: "laptop shadow", icon: "icon-laptop", title: "Laptop" }, + { name: "iPad portrait - 768px", css: "iPad-portrait shadow", icon: "icon-ipad", title: "Tablet portrait" }, + { name: "iPad landscape - 1024px", css: "iPad-landscape shadow", icon: "icon-ipad flip", title: "Tablet landscape" }, + { name: "smartphone portrait - 480px", css: "smartphone-portrait shadow", icon: "icon-iphone", title: "Smartphone portrait" }, + { name: "smartphone landscape - 320px", css: "smartphone-landscape shadow", icon: "icon-iphone flip", title: "Smartphone landscape" } ]; $scope.previewDevice = $scope.devices[0]; - - function setPageUrl(){ + $scope.sizeOpen = false; + $scope.cultureOpen = false; + + $scope.toggleSizeOpen = function () { + $scope.sizeOpen = toggleMenu($scope.sizeOpen); + } + $scope.toggleCultureOpen = function () { + $scope.cultureOpen = toggleMenu($scope.cultureOpen); + } + + function toggleMenu(isCurrentlyOpen) { + if (isCurrentlyOpen === false) { + closeOthers(); + return true; + } else { + return false; + } + } + function closeOthers() { + $scope.sizeOpen = false; + $scope.cultureOpen = false; + } + + $scope.windowClickHandler = function () { + closeOthers(); + } + function windowBlurHandler() { + closeOthers(); + $scope.$digest(); + } + + var win = $($window); + + win.on("blur", windowBlurHandler); + + $scope.$on("$destroy", function () { + win.off("blur", handleBlwindowBlurHandlerur); + }); + + + function setPageUrl() { $scope.pageId = $location.search().id || getParameterByName("id"); var culture = $location.search().culture || getParameterByName("culture"); @@ -113,13 +178,18 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.exitPreview = function () { var culture = $location.search().culture || getParameterByName("culture"); - var relativeUrl = "/" + $scope.pageId +'?culture='+ culture; + var relativeUrl = "/" + $scope.pageId; + if (culture) { + relativeUrl += '?culture=' + culture; + } window.top.location.href = "../preview/end?redir=" + encodeURIComponent(relativeUrl); }; $scope.onFrameLoaded = function (iframe) { $scope.frameLoaded = true; configureSignalR(iframe); + + $scope.currentCultureIso = $location.search().culture || null; }; /*****************************************************************************/ @@ -133,14 +203,32 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi /*****************************************************************************/ /* Change culture */ /*****************************************************************************/ - $scope.changeCulture = function (culture) { - if($location.search().culture !== culture){ + $scope.changeCulture = function (iso) { + if ($location.search().culture !== iso) { $scope.frameLoaded = false; - $location.search("culture", culture); + $scope.currentCultureIso = iso; + $location.search("culture", iso); setPageUrl(); } }; - + $scope.registerCulture = function (iso, title, isDefault) { + var cultureObject = { iso: iso, title: title, isDefault: isDefault }; + cultures.push(cultureObject); + } + + $scope.$watch("currentCultureIso", function (oldIso, newIso) { + // if no culture is selected, we will pick the default one: + if ($scope.currentCultureIso === null) { + $scope.currentCulture = cultures.find(function (culture) { + return culture.isDefault === true; + }) + return; + } + $scope.currentCulture = cultures.find(function (culture) { + return culture.iso === $scope.currentCultureIso; + }) + }); + }) @@ -164,7 +252,7 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi }); } - function hideUmbracoPreviewBadge (iframe) { + function hideUmbracoPreviewBadge(iframe) { if (iframe && iframe.contentDocument && iframe.contentDocument.getElementById("umbracoPreviewBadge")) { iframe.contentDocument.getElementById("umbracoPreviewBadge").style.display = "none"; } diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index 001888f3ca..93122792d3 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -39,10 +39,14 @@ app.config(function ($routeProvider) { $route.current.params.section = "content"; } + var found = _.find(user.allowedSections, function (s) { + return s.localeCompare($route.current.params.section, undefined, { sensitivity: 'accent' }) === 0; + }) + // U4-5430, Benjamin Howarth // We need to change the current route params if the user only has access to a single section // To do this we need to grab the current user's allowed sections, then reject the promise with the correct path. - if (user.allowedSections.indexOf($route.current.params.section) > -1) { + if (found) { //this will resolve successfully so the route will continue return $q.when(true); } else { @@ -119,7 +123,7 @@ app.config(function ($routeProvider) { sectionService.getSectionsForUser().then(function(sections) { //find the one we're requesting var found = _.find(sections, function(s) { - return s.alias === $routeParams.section; + return s.alias.localeCompare($routeParams.section, undefined, { sensitivity: 'accent' }) === 0; }) if (found && found.routePath) { //there's a custom route path so redirect @@ -150,7 +154,7 @@ app.config(function ($routeProvider) { //This allows us to dynamically change the template for this route since you cannot inject services into the templateUrl method. template: "
", //This controller will execute for this route, then we replace the template dynamically based on the current tree. - controller: function ($scope, $routeParams, treeService) { + controller: function ($scope, $routeParams, navigationService) { if (!$routeParams.method) { $scope.templateUrl = "views/common/dashboard.html"; @@ -172,24 +176,7 @@ app.config(function ($routeProvider) { $scope.templateUrl = "views/users/overview.html"; return; } - - // Here we need to figure out if this route is for a user's package tree and if so then we need - // to change it's convention view path to: - // /App_Plugins/{mypackage}/backoffice/{treetype}/{method}.html - - // otherwise if it is a core tree we use the core paths: - // views/{treetype}/{method}.html - - var packageTreeFolder = treeService.getTreePackageFolder($routeParams.tree); - - if (packageTreeFolder) { - $scope.templateUrl = (Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + - "/" + packageTreeFolder + - "/backoffice/" + $routeParams.tree + "/" + $routeParams.method + ".html"); - } - else { - $scope.templateUrl = ('views/' + $routeParams.tree + '/' + $routeParams.method + '.html'); - } + $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method); }, reloadOnSearch: false, resolve: canRoute(true) @@ -198,32 +185,16 @@ app.config(function ($routeProvider) { //This allows us to dynamically change the template for this route since you cannot inject services into the templateUrl method. template: "
", //This controller will execute for this route, then we replace the template dynamically based on the current tree. - controller: function ($scope, $route, $routeParams, treeService) { + controller: function ($scope, $routeParams, navigationService) { if (!$routeParams.tree || !$routeParams.method) { $scope.templateUrl = "views/common/dashboard.html"; + return; } - - // Here we need to figure out if this route is for a package tree and if so then we need - // to change it's convention view path to: - // /App_Plugins/{mypackage}/backoffice/{treetype}/{method}.html - - // otherwise if it is a core tree we use the core paths: - // views/{treetype}/{method}.html - - var packageTreeFolder = treeService.getTreePackageFolder($routeParams.tree); - - if (packageTreeFolder) { - $scope.templateUrl = (Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + - "/" + packageTreeFolder + - "/backoffice/" + $routeParams.tree + "/" + $routeParams.method + ".html"); - } - else { - $scope.templateUrl = ('views/' + $routeParams.tree + '/' + $routeParams.method + '.html'); - } - + $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method); }, reloadOnSearch: false, + reloadOnUrl: false, resolve: canRoute(true) }) .otherwise({ redirectTo: '/login' }); diff --git a/src/Umbraco.Web.UI.Client/src/utilities.js b/src/Umbraco.Web.UI.Client/src/utilities.js new file mode 100644 index 0000000000..9121ba0e25 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/utilities.js @@ -0,0 +1,137 @@ +/** + * A friendly utility collection to replace AngularJs' ng-functions + * If it doesn't exist here, it's probably available as vanilla JS + * + * Still carries a dependency on underscore, but if usages of underscore from + * elsewhere in the codebase can instead use these methods, the underscore + * dependency will be nicely abstracted and can be removed/swapped later + * + * This collection is open to extension... + */ +(function (window) { + + /** + * Equivalent to angular.noop + */ + const noop = () => { }; + + /** + * Facade to angular.copy + */ + const copy = val => angular.copy(val); + + /** + * Equivalent to angular.isArray + */ + const isArray = val => Array.isArray(val) || val instanceof Array; + + /** + * Facade to angular.equals + */ + const equals = (a, b) => angular.equals(a, b); + + /** + * Facade to angular.extend + * Use this with Angular objects, for vanilla JS objects, use Object.assign() + */ + const extend = (dst, src) => angular.extend(dst, src); + + /** + * Equivalent to angular.isFunction + */ + const isFunction = val => typeof val === 'function'; + + /** + * Equivalent to angular.isUndefined + */ + const isUndefined = val => typeof val === 'undefined'; + + /** + * Equivalent to angular.isDefined. Inverts result of const isUndefined + */ + const isDefined = val => !isUndefined(val); + + /** + * Equivalent to angular.isString + */ + const isString = val => typeof val === 'string'; + + /** + * Equivalent to angular.isNumber + */ + const isNumber = val => typeof val === 'number'; + + /** + * Equivalent to angular.isObject + */ + const isObject = val => val !== null && typeof val === 'object'; + + const isWindow = obj => obj && obj.window === obj; + + const isScope = obj => obj && obj.$evalAsync && obj.$watch; + + const toJsonReplacer = (key, value) => { + var val = value; + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { + val = undefined; + } else if (isWindow(value)) { + val = '$WINDOW'; + } else if (value && window.document === value) { + val = '$DOCUMENT'; + } else if (isScope(value)) { + val = '$SCOPE'; + } + return val; + } + /** + * Equivalent to angular.toJson + */ + const toJson = (obj, pretty) => { + if (isUndefined(obj)) return undefined; + if (!isNumber(pretty)) { + pretty = pretty ? 2 : null; + } + return JSON.stringify(obj, toJsonReplacer, pretty); + } + + /** + * Equivalent to angular.fromJson + */ + const fromJson = (val) => { + if (!isString(val)) { + return val; + } + return JSON.parse(val); + } + + /** + * Not equivalent to angular.forEach. But like the angularJS method this does not fail on null or undefined. + */ + const forEach = (obj, iterator) => { + if (obj) { + return obj.forEach(iterator); + } + return obj; + } + + let _utilities = { + noop: noop, + copy: copy, + isArray: isArray, + equals: equals, + extend: extend, + isFunction: isFunction, + isUndefined: isUndefined, + isDefined: isDefined, + isString: isString, + isNumber: isNumber, + isObject: isObject, + fromJson: fromJson, + toJson: toJson, + forEach: forEach + }; + + if (typeof (window.Utilities) === 'undefined') { + window.Utilities = _utilities; + } +})(window); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js index 3323f2bfb3..268bfb3a8c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter) { + function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter, editorState) { var vm = this; var evts = []; @@ -18,6 +18,10 @@ vm.startTour = startTour; vm.getTourGroupCompletedPercentage = getTourGroupCompletedPercentage; vm.showTourButton = showTourButton; + + vm.showDocTypeTour = false; + vm.docTypeTours = []; + vm.nodeName = ''; function startTour(tour) { tourService.startTour(tour); @@ -58,9 +62,16 @@ handleSectionChange(); })); + evts.push(eventsService.on("editorState.changed", + function (e, args) { + setDocTypeTour(args.entity); + })); + findHelp(vm.section, vm.tree, vm.userType, vm.userLang); }); + + setDocTypeTour(editorState.getCurrent()); // check if a tour is running - if it is open the matching group var currentTour = tourService.getCurrentTour(); @@ -84,7 +95,7 @@ setSectionName(); findHelp(vm.section, vm.tree, vm.userType, vm.userLang); - + setDocTypeTour(); } }); } @@ -168,6 +179,26 @@ }); } + function setDocTypeTour(node) { + vm.showDocTypeTour = false; + vm.docTypeTours = []; + vm.nodeName = ''; + + if (vm.section === 'content' && vm.tree === 'content') { + + if (node) { + tourService.getToursForDoctype(node.contentTypeAlias).then(function (data) { + if (data && data.length > 0) { + vm.docTypeTours = data; + var currentVariant = _.find(node.variants, (x) => x.active); + vm.nodeName = currentVariant.name; + vm.showDocTypeTour = true; + } + }); + } + } + } + evts.push(eventsService.on("appState.tour.complete", function (event, tour) { tourService.getGroupedTours().then(function(groupedTours) { vm.tours = groupedTours; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html index 56db1fd88a..aa6126e73e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -8,26 +8,56 @@ - -
+ +
+
Need help editing current item '{{vm.nodeName}}' ?
-
Tours
+
-
+ +
+
+
+
+ {{ tour.name }} +
+
+ +
+
+
+
+
+
+ + +
+ +
+ Tours +
+ +
- -
- +
+ + Other + + -
+
@@ -38,8 +68,8 @@ {{ tour.name }}
- - + +
@@ -47,33 +77,35 @@
-
+
- -
-
-
{{dashboard.label}}
-
-
-
+ +
+
+
{{dashboard.label}}
+
+
+
+
-
- -
-
Articles
-
-
+
    -
  • +
  • {{group.containerPath}}
  • @@ -77,7 +77,8 @@ checklist-model="model.compositeContentTypes" checklist-value="compositeContentType.contentType.alias" ng-change="model.selectCompositeContentType(compositeContentType.contentType)" - ng-disabled="compositeContentType.allowed===false || compositeContentType.inherited" /> + ng-disabled="compositeContentType.allowed===false || compositeContentType.inherited" /> +
+ +

+ Choose where to copy the selected item(s) +

+
-
- -
+ + + + + + + + + + @@ -83,7 +91,8 @@ button-style="success" label-key="general_submit" state="vm.saveButtonState" - action="vm.submit(model)"> + action="vm.submit(model)" + disabled="!model.target"> diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.controller.js new file mode 100644 index 0000000000..9ee80488af --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.controller.js @@ -0,0 +1,106 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DataTypeConfigurationPickerController + * @function + * + * @description + * The controller for the content type editor data type configuration picker dialog + */ + +(function() { + "use strict"; + + function DataTypeConfigurationPicker($scope, $filter, dataTypeResource, dataTypeHelper, contentTypeResource, localizationService, editorService) { + + var vm = this; + + vm.configs = []; + + vm.loading = true; + + vm.newDataType = newDataType; + vm.pickDataType = pickDataType; + vm.close = close; + + function activate() { + setTitle(); + load(); + } + + function setTitle() { + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectEditorConfiguration") + .then(function(data){ + $scope.model.title = data; + }); + } + } + + function load() { + + dataTypeResource.getGroupedDataTypes().then(function(configs) { + + var filteredConfigs = []; + + _.each(configs, function(configGroup) { + for(var i = 0; i + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js index c86f55b255..d622ccea52 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js @@ -10,33 +10,26 @@ (function() { "use strict"; - function DataTypePicker($scope, $filter, dataTypeResource, dataTypeHelper, contentTypeResource, localizationService, editorService) { + function DataTypePicker($scope, $filter, dataTypeResource, contentTypeResource, localizationService, editorService) { var vm = this; - + + vm.showDataTypes = true; + vm.dataTypes = []; + vm.loading = true; + vm.loadingConfigs = false; vm.searchTerm = ""; - vm.showTabs = false; - vm.tabsLoaded = 0; - vm.typesAndEditors = []; - vm.userConfigured = []; - vm.loading = false; - vm.tabs = []; - vm.labels = {}; - - vm.onTabChange = onTabChange; - vm.filterItems = filterItems; - vm.showDetailsOverlay = showDetailsOverlay; - vm.hideDetailsOverlay = hideDetailsOverlay; - vm.pickEditor = pickEditor; + vm.searchResult = null; + + vm.viewOptionsForEditor = viewOptionsForEditor; vm.pickDataType = pickDataType; + vm.pickEditor = pickEditor; vm.close = close; + vm.searchTermChanged = searchTermChanged; function activate() { setTitle(); - loadTabs(); - getGroupedDataTypes(); - getGroupedPropertyEditors(); - + loadTypes(); } function setTitle() { @@ -44,117 +37,104 @@ localizationService.localize("defaultdialogs_selectEditor") .then(function(data){ $scope.model.title = data; - }); + } + ); } } - - function loadTabs() { - - var labels = ["contentTypeEditor_availableEditors", "contentTypeEditor_reuse"]; - - localizationService.localizeMany(labels) - .then(function(data){ - vm.labels.availableDataTypes = data[0]; - vm.labels.reuse = data[1]; - - vm.tabs = [{ - active: true, - id: 1, - label: vm.labels.availableDataTypes, - alias: "Default", - typesAndEditors: [] - }, { - active: false, - id: 2, - label: vm.labels.reuse, - alias: "Reuse", - userConfigured: [] - }]; - - }); - } - - function getGroupedPropertyEditors() { - - vm.loading = true; - - dataTypeResource.getGroupedPropertyEditors().then(function(data) { - vm.tabs[0].typesAndEditors = data; - vm.typesAndEditors = data; - vm.tabsLoaded = vm.tabsLoaded + 1; - checkIfTabContentIsLoaded(); - }); - - } - - function getGroupedDataTypes() { - - vm.loading = true; - - dataTypeResource.getGroupedDataTypes().then(function(data) { - vm.tabs[1].userConfigured = data; - vm.userConfigured = data; - vm.tabsLoaded = vm.tabsLoaded + 1; - checkIfTabContentIsLoaded(); - }); - - } - - function checkIfTabContentIsLoaded() { - if (vm.tabsLoaded === 2) { + + function loadTypes() { + + dataTypeResource.getGroupedPropertyEditors().then(function(dataTypes) { + vm.dataTypes = dataTypes; vm.loading = false; - vm.showTabs = true; - } - } - - function onTabChange(selectedTab) { - vm.tabs.forEach(function(tab) { - tab.active = false; }); - selectedTab.active = true; + + } + + function loadConfigurations() { + + vm.loading = true; + vm.loadingConfigs = true; + + dataTypeResource.getGroupedDataTypes().then(function(configs) { + vm.configs = configs; + vm.loading = false; + performeSearch(); + }); + } - function filterItems() { - // clear item details - $scope.model.itemDetails = null; - - if (vm.searchTerm) { - vm.showTabs = false; - - var regex = new RegExp(vm.searchTerm, "i"); - vm.filterResult = { - userConfigured: filterCollection(vm.userConfigured, regex), - typesAndEditors: filterCollection(vm.typesAndEditors, regex) - }; + + function searchTermChanged() { + + vm.showDataTypes = (vm.searchTerm === ""); + + if(vm.loadingConfigs !== true) { + loadConfigurations() } else { - vm.filterResult = null; - vm.showTabs = true; + performeSearch(); } + } - + + function performeSearch() { + + if (vm.searchTerm) { + if (vm.configs) { + + var regex = new RegExp(vm.searchTerm, "i"); + vm.searchResult = { + configs: filterCollection(vm.configs, regex), + dataTypes: filterCollection(vm.dataTypes, regex) + }; + } + } else { + vm.searchResult = null; + } + + } + function filterCollection(collection, regex) { return _.map(_.keys(collection), function (key) { return { group: key, - dataTypes: $filter('filter')(collection[key], function (dataType) { + entries: $filter('filter')(collection[key], function (dataType) { return regex.test(dataType.name) || regex.test(dataType.alias); }) } }); } - function showDetailsOverlay(property) { + + function viewOptionsForEditor(editor) { + + var dataTypeConfigurationPicker = { + editor: editor, + property: $scope.model.property, + contentTypeName: $scope.model.contentTypeName, + view: "views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html", + size: "small", + submit: function(dataType, propertyType, isNew) { + submit(dataType, propertyType, isNew); + editorService.close(); + }, + close: function() { + editorService.close(); + } + }; - var propertyDetails = {}; - propertyDetails.icon = property.icon; - propertyDetails.title = property.name; - - $scope.model.itemDetails = propertyDetails; + editorService.open(dataTypeConfigurationPicker); } - function hideDetailsOverlay() { - $scope.model.itemDetails = null; + function pickDataType(selectedDataType) { + selectedDataType.loading = true; + dataTypeResource.getById(selectedDataType.id).then(function(dataType) { + contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function(propertyType) { + selectedDataType.loading = false; + submit(dataType, propertyType, false); + }); + }); } function pickEditor(propertyEditor) { @@ -180,16 +160,6 @@ } - function pickDataType(selectedDataType) { - selectedDataType.loading = true; - dataTypeResource.getById(selectedDataType.id).then(function(dataType) { - contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function(propertyType) { - selectedDataType.loading = false; - submit(dataType, propertyType, false); - }); - }); - } - function submit(dataType, propertyType, isNew) { // update property $scope.model.property.config = propertyType.config; @@ -203,7 +173,7 @@ $scope.model.submit($scope.model); } - + function close() { if($scope.model.close) { $scope.model.close(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html index fd859b9e2e..b31a396c2d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html @@ -15,79 +15,49 @@ -
- - -
- - - -
- -
-
-
-
{{key}}
- -
-
-
+ + + +
+
+
{{key | umbCmsTitleCase}}
+ +
- -
-
-
-
-
{{result.group}}
-
    -
  • +
    +
    +
    +
    +
    {{result.group | umbCmsTitleCase}}
    +
      +
    • @@ -103,18 +73,17 @@
    -
    -
    -
    -
    {{result.group}}
    -
      -
    • - +
      +
      +
      +
      {{result.group | umbCmsTitleCase}}
      +
        +
      • + - - {{ systemDataType.name }} + + {{ dataType.name }}
      • diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html index 8acaa544c1..428905020b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html @@ -21,7 +21,7 @@ -
        +
        using this editor will get updated with the new settings.
        diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js index 06a5f028ef..515f54e3d7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js @@ -7,6 +7,7 @@ var origWidth = 500; var origHeight = 300; + vm.loading = false; vm.trustedPreview = null; $scope.model.embed = { @@ -17,9 +18,16 @@ preview: "", success: false, info: "", - supportsDimensions: "" + supportsDimensions: false }; + if ($scope.model.modify) { + angular.extend($scope.model.embed, $scope.model.modify); + + showPreview(); + } + + vm.toggleConstrain = toggleConstrain; vm.showPreview = showPreview; vm.changeSize = changeSize; vm.submit = submit; @@ -37,10 +45,10 @@ if ($scope.model.embed.url) { $scope.model.embed.show = true; - $scope.model.embed.preview = "
        "; $scope.model.embed.info = ""; $scope.model.embed.success = false; + vm.loading = true; $http({ method: 'GET', @@ -54,29 +62,41 @@ $scope.model.embed.preview = ""; - switch (response.data.OEmbedStatus) { case 0: //not supported + $scope.model.embed.preview = ""; $scope.model.embed.info = "Not supported"; + $scope.model.embed.success = false; + $scope.model.embed.supportsDimensions = false; + vm.trustedPreview = null; break; case 1: //error + $scope.model.embed.preview = ""; $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; + $scope.model.embed.success = false; + $scope.model.embed.supportsDimensions = false; + vm.trustedPreview = null; break; case 2: + $scope.model.embed.success = true; + $scope.model.embed.supportsDimensions = response.data.SupportsDimensions; $scope.model.embed.preview = response.data.Markup; vm.trustedPreview = $sce.trustAsHtml(response.data.Markup); - $scope.model.embed.supportsDimensions = response.data.SupportsDimensions; - $scope.model.embed.success = true; break; } + + vm.loading = false; + }, function() { + $scope.model.embed.success = false; $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ""; $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; - }); + vm.loading = false; + }); } else { $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ""; @@ -105,6 +125,10 @@ } + function toggleConstrain() { + $scope.model.embed.constrain = !$scope.model.embed.constrain; + } + function submit() { if($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html index e48ec84b25..5862ca7059 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html @@ -16,7 +16,7 @@ - + - - -

        -
        -
        - -
        - - + + + +
        + + +

        +
        + +
        + + + - - - + + + - - - + + + +
        +
        @@ -62,6 +68,7 @@ button-style="success" label-key="general_submit" state="vm.saveButtonState" + disabled="!model.embed.url.length || !model.embed.preview.length" action="vm.submit(model)"> diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js index 471d23ae84..5bfee22c4e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js @@ -16,7 +16,7 @@ function IconPickerController($scope, iconHelper, localizationService) { vm.close = close; vm.colors = [ - { name: "Black", value: "color-black" }, + { name: "Black", value: "color-black", default: true }, { name: "Blue Grey", value: "color-blue-grey" }, { name: "Grey", value: "color-grey" }, { name: "Brown", value: "color-brown" }, @@ -49,7 +49,7 @@ function IconPickerController($scope, iconHelper, localizationService) { }); // set a default color if nothing is passed in - vm.color = $scope.model.color ? findColor($scope.model.color) : vm.colors[0]; + vm.color = $scope.model.color ? findColor($scope.model.color) : vm.colors.find(x => x.default); // if an icon is passed in - preselect it vm.icon = $scope.model.icon ? $scope.model.icon : undefined; @@ -71,12 +71,13 @@ function IconPickerController($scope, iconHelper, localizationService) { } function findColor(value) { - return _.findWhere(vm.colors, {value: value}); + return vm.colors.find(x => x.value === value); } - function selectColor(color, $index, $event) { - $scope.model.color = color.value; - vm.color = color; + function selectColor(color) { + let newColor = (color || vm.colors.find(x => x.default)); + $scope.model.color = newColor.value; + vm.color = newColor; } function close() { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html index 3caa6ae03d..c2b0d0e458 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html @@ -46,9 +46,10 @@
        • - - - +
        diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.controller.js index f2cc0dbecb..3de26ba99c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.controller.js @@ -5,8 +5,8 @@ var vm = this; - vm.field; - vm.defaultValue; + vm.field = null; + vm.defaultValue = null; vm.recursive = false; vm.showDefaultValue = false; @@ -16,10 +16,14 @@ function onInit() { + var labelKeys = [ + "template_insertPageField" + ]; + // set default title if(!$scope.model.title) { - localizationService.localize("template_insertPageField").then(function(value){ - $scope.model.title = value; + localizationService.localizeMany(labelKeys).then(function (data) { + $scope.model.title = data[0]; }); } @@ -37,42 +41,40 @@ function generateOutputSample() { - var fallback; + var fallback = null; - if(vm.recursive !== false && vm.defaultValue !== undefined){ + if (vm.recursive !== false && vm.defaultValue !== null) { fallback = "Fallback.To(Fallback.Ancestors, Fallback.DefaultValue)"; - }else if(vm.recursive !== false){ + } else if (vm.recursive !== false) { fallback = "Fallback.ToAncestors"; - }else if(vm.defaultValue !== undefined){ + } else if (vm.defaultValue !== null) { fallback = "Fallback.ToDefaultValue"; } - var pageField = (vm.field !== undefined ? '@Model.Value("' + vm.field + '"' : "") - + (fallback !== undefined? ', fallback: ' + fallback : "") - + (vm.defaultValue !== undefined ? ', defaultValue: new HtmlString("' + vm.defaultValue + '")' : "") + var pageField = (vm.field !== null ? '@Model.Value("' + vm.field + '"' : "") + + (fallback !== null? ', fallback: ' + fallback : "") + + (vm.defaultValue !== null ? ', defaultValue: new HtmlString("' + vm.defaultValue + '")' : "") + (vm.field ? ')' : ""); $scope.model.umbracoField = pageField; return pageField; - } function submit(model) { - if($scope.model.submit) { + if ($scope.model.submit) { $scope.model.submit(model); } } function close() { - if($scope.model.close) { + if ($scope.model.close) { $scope.model.close(); } } onInit(); - } angular.module("umbraco").controller("Umbraco.Editors.InsertFieldController", InsertFieldController); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.html index b2c6382b98..bbb2e8c798 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.html @@ -33,10 +33,9 @@
        - +
        @@ -52,13 +51,17 @@
        -
        diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js index 029f765985..a9803b70f9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js @@ -1,7 +1,7 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", - function ($scope, eventsService, entityResource, contentResource, mediaResource, mediaHelper, udiParser, userService, localizationService, tinyMceService, editorService) { - + function ($scope, eventsService, entityResource, mediaResource, mediaHelper, udiParser, userService, localizationService, editorService) { + var vm = this; var dialogOptions = $scope.model; @@ -16,24 +16,24 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", if (!$scope.model.title) { localizationService.localize("defaultdialogs_selectLink") - .then(function(value) { + .then(function (value) { $scope.model.title = value; }); } - + $scope.customTreeParams = dialogOptions.dataTypeKey ? "dataTypeKey=" + dialogOptions.dataTypeKey : ""; $scope.dialogTreeApi = {}; $scope.model.target = {}; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, + dataTypeKey: dialogOptions.dataTypeKey, results: [], - selectedSearchResults: [], - ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes + selectedSearchResults: [] }; - $scope.customTreeParams = dialogOptions.ignoreUserStartNodes ? "ignoreUserStartNodes=" + dialogOptions.ignoreUserStartNodes : ""; $scope.showTarget = $scope.model.hideTarget !== true; + $scope.showAnchor = $scope.model.hideAnchor !== true; // this ensures that we only sync the tree once and only when it's ready var oneTimeTreeSync = { @@ -59,11 +59,11 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", if (dialogOptions.currentTarget) { // clone the current target so we don't accidentally update the caller's model while manipulating $scope.model.target - $scope.model.target = angular.copy(dialogOptions.currentTarget); - //if we have a node ID, we fetch the current node to build the form data + $scope.model.target = Utilities.copy(dialogOptions.currentTarget); + // if we have a node ID, we fetch the current node to build the form data if ($scope.model.target.id || $scope.model.target.udi) { - //will be either a udi or an int + // will be either a udi or an int var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id; if ($scope.model.target.udi) { @@ -88,17 +88,13 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", oneTimeTreeSync.sync(); }); - // get the content properties to build the anchor name list - - var options = {}; - options.ignoreUserStartNodes = dialogOptions.ignoreUserStartNodes; - - contentResource.getById(id, options).then(function (resp) { - $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); - $scope.model.target.url = resp.urls[0].text; + entityResource.getUrlAndAnchors(id).then(function (resp) { + $scope.anchorValues = resp.anchorValues; + $scope.model.target.url = resp.url; }); + } - } else if ($scope.model.target.url.length) { + } else if ($scope.model.target.url && $scope.model.target.url.length) { // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs // only do the substring if there's a # or a ? var indexOfAnchor = $scope.model.target.url.search(/(#|\?)/); @@ -143,13 +139,11 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", if (args.node.id < 0) { $scope.model.target.url = "/"; - } else { - var options = {}; - options.ignoreUserStartNodes = dialogOptions.ignoreUserStartNodes; - - contentResource.getById(args.node.id, options).then(function (resp) { - $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); - $scope.model.target.url = resp.urls[0].text; + } + else { + entityResource.getUrlAndAnchors(args.node.id).then(function (resp) { + $scope.anchorValues = resp.anchorValues; + $scope.model.target.url = resp.url; }); } @@ -158,6 +152,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", } } + function nodeExpandedHandler(args) { // open mini list view for list views if (args.node.metaData.isContainer) { @@ -167,17 +162,21 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", $scope.switchToMediaPicker = function () { userService.getCurrentUser().then(function (userData) { - var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; - var startNodeIsVirtual = userData.startMediaIds.length !== 1; - if (dialogOptions.ignoreUserStartNodes) { + + var startNodeId, startNodeIsVirtual; + if (dialogOptions.ignoreUserStartNodes === true) { startNodeId = -1; startNodeIsVirtual = true; } + else { + startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + startNodeIsVirtual = userData.startMediaIds.length !== 1; + } var mediaPicker = { startNodeId: startNodeId, startNodeIsVirtual: startNodeIsVirtual, - ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes, + dataTypeKey: dialogOptions.dataTypeKey, submit: function (model) { var media = model.selection[0]; @@ -185,7 +184,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", $scope.model.target.udi = media.udi; $scope.model.target.isMedia = true; $scope.model.target.name = media.name; - $scope.model.target.url = mediaHelper.resolveFile(media); + $scope.model.target.url = media.image; editorService.close(); @@ -195,7 +194,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", tree: "content" }); }, - close: function() { + close: function () { editorService.close(); } }; @@ -252,13 +251,13 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", } function close() { - if($scope.model && $scope.model.close) { + if ($scope.model && $scope.model.close) { $scope.model.close(); } } function submit() { - if($scope.model && $scope.model.submit) { + if ($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index b7f63cfe22..ad0aaab57c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html @@ -1,4 +1,4 @@ -
        +
        @@ -14,32 +14,33 @@ -
        +
        - + - - + + - +
        - + - +
        Link to page
        - +
        - - +
        - - - +
        - + enablecheckboxes="true" + customtreeparams="{{customTreeParams}}">
        - - - +
        - +
        Link to media
        diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.controller.js new file mode 100644 index 0000000000..5b81cb947d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.controller.js @@ -0,0 +1,127 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.MacroParameterPickerController + * @function + * + * @description + * The controller for the content type editor macro parameter dialog + */ + +(function() { + "use strict"; + + function MacroParameterController($scope, $filter, macroResource, localizationService, editorService) { + + var vm = this; + + vm.searchTerm = ""; + vm.parameterEditors = []; + vm.loading = false; + vm.labels = {}; + + vm.filterItems = filterItems; + vm.showDetailsOverlay = showDetailsOverlay; + vm.hideDetailsOverlay = hideDetailsOverlay; + vm.pickParameterEditor = pickParameterEditor; + vm.close = close; + + function init() { + setTitle(); + getGroupedParameterEditors(); + } + + function setTitle() { + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectEditor") + .then(function(data){ + $scope.model.title = data; + }); + } + } + + function getGroupedParameterEditors() { + + vm.loading = true; + + macroResource.getGroupedParameterEditors().then(function (data) { + vm.parameterEditors = data; + vm.loading = false; + }, function () { + vm.loading = false; + }); + + } + + function filterItems() { + // clear item details + $scope.model.itemDetails = null; + + if (vm.searchTerm) { + + var regex = new RegExp(vm.searchTerm, "i"); + + var parameterEditors = filterCollection(vm.parameterEditors, regex); + + var totalResults = _.reduce(_.pluck(parameterEditors, 'count'), (m, n) => m + n, 0); + + vm.filterResult = { + parameterEditors: parameterEditors, + totalResults: totalResults + }; + } else { + vm.filterResult = null; + } + } + + function filterCollection(collection, regex) { + return _.map(_.keys(collection), function (key) { + + var filteredEditors = $filter('filter')(collection[key], function (editor) { + return regex.test(editor.name) || regex.test(editor.alias); + }); + + return { + group: key, + count: filteredEditors.length, + parameterEditors: filteredEditors + } + }); + } + + function showDetailsOverlay(property) { + + var propertyDetails = {}; + propertyDetails.icon = property.icon; + propertyDetails.title = property.name; + + $scope.model.itemDetails = propertyDetails; + } + + function hideDetailsOverlay() { + $scope.model.itemDetails = null; + } + + function pickParameterEditor(selectedParameterEditor) { + + console.log("pickParameterEditor", selectedParameterEditor); + console.log("$scope.model", $scope.model); + + $scope.model.parameter.editor = selectedParameterEditor.alias; + $scope.model.parameter.dataTypeName = selectedParameterEditor.name; + $scope.model.parameter.dataTypeIcon = selectedParameterEditor.icon; + + $scope.model.submit($scope.model); + } + + function close() { + if ($scope.model.close) { + $scope.model.close(); + } + } + + init(); + } + + angular.module("umbraco").controller("Umbraco.Editors.MacroParameterPickerController", MacroParameterController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html new file mode 100644 index 0000000000..9f2b56401d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html @@ -0,0 +1,106 @@ +
        + + + +
        + + + + + + + + + +
        + +
        + + + +
        + + +
        +
        +
        +
        +
        {{result.group}}
        + +
        +
        +
        + + + + +
        + + + + + + + + + + + + +
        + + + +
        diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js index aa63f2d6d6..ba577045aa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js @@ -8,9 +8,9 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi $scope.noMacroParams = false; function onInit() { - if(!$scope.model.title) { - localizationService.localize("defaultdialogs_selectMacro").then(function(value){ - $scope.model.title = value; + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectMacro").then(function (value) { + $scope.model.title = value; }); } } @@ -26,8 +26,8 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi } }; - $scope.close = function() { - if($scope.model.close) { + $scope.close = function () { + if ($scope.model.close) { $scope.model.close(); } } @@ -41,10 +41,8 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi macroResource.getMacroParameters($scope.model.selectedMacro.id) .then(function (data) { - - //go to next page if there are params otherwise we can just exit - if (!angular.isArray(data) || data.length === 0) { + if (!Utilities.isArray(data) || data.length === 0) { if (insertIfNoParameters) { $scope.model.submit($scope.model); @@ -53,7 +51,7 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi } } else { - + $scope.wizardStep = "paramSelect"; $scope.model.macroParams = data; @@ -72,8 +70,8 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi //detect if it is a json string if (val.detectIsJson()) { try { - //Parse it to json - prop.value = angular.fromJson(val); + //Parse it from json + prop.value = Utilities.fromJson(val); } catch (e) { // not json @@ -106,13 +104,13 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi entityResource.getAll("Macro", ($scope.model.dialogData && $scope.model.dialogData.richTextEditor && $scope.model.dialogData.richTextEditor === true) ? "UseInEditor=true" : null) .then(function (data) { - if (angular.isArray(data) && data.length == 0) { + if (Utilities.isArray(data) && data.length == 0) { $scope.nomacros = true; } //if 'allowedMacros' is specified, we need to filter - if (angular.isArray($scope.model.dialogData.allowedMacros) && $scope.model.dialogData.allowedMacros.length > 0) { - $scope.macros = _.filter(data, function(d) { + if (Utilities.isArray($scope.model.dialogData.allowedMacros) && $scope.model.dialogData.allowedMacros.length > 0) { + $scope.macros = _.filter(data, function (d) { return _.contains($scope.model.dialogData.allowedMacros, d.alias); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html index 66c64657a9..fc1bec4ec1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html @@ -32,10 +32,10 @@
        • - + - {{ availableItem.name }} + {{availableItem.name}}
        • diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index f37e1156a8..7a4db7cbc5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -1,78 +1,115 @@ //used for the media picker dialog angular.module("umbraco") .controller("Umbraco.Editors.MediaPickerController", - function ($scope, mediaResource, entityResource, mediaHelper, mediaTypeHelper, eventsService, userService, treeService, localStorageService, localizationService, editorService) { + function ($scope, $timeout, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService, umbSessionStorage) { - if (!$scope.model.title) { - localizationService.localizeMany(["defaultdialogs_selectMedia", "general_includeFromsubFolders"]) - .then(function (data) { - $scope.labels = { - title: data[0], - includeSubFolders: data[1] - } - }); - } + var vm = this; + + vm.submit = submit; + vm.close = close; + + vm.toggle = toggle; + vm.upload = upload; + vm.dragLeave = dragLeave; + vm.dragEnter = dragEnter; + vm.onUploadComplete = onUploadComplete; + vm.onFilesQueue = onFilesQueue; + vm.changeSearch = changeSearch; + vm.submitFolder = submitFolder; + vm.enterSubmitFolder = enterSubmitFolder; + vm.focalPointChanged = focalPointChanged; + vm.changePagination = changePagination; + + vm.clickHandler = clickHandler; + vm.clickItemName = clickItemName; + vm.gotoFolder = gotoFolder; + vm.shouldShowUrl = shouldShowUrl; var dialogOptions = $scope.model; - $scope.disableFolderSelect = dialogOptions.disableFolderSelect; - $scope.onlyImages = dialogOptions.onlyImages; - $scope.showDetails = dialogOptions.showDetails; + $scope.disableFolderSelect = (dialogOptions.disableFolderSelect && dialogOptions.disableFolderSelect !== "0") ? true : false; + $scope.disableFocalPoint = (dialogOptions.disableFocalPoint && dialogOptions.disableFocalPoint !== "0") ? true : false; + $scope.onlyImages = (dialogOptions.onlyImages && dialogOptions.onlyImages !== "0") ? true : false; + $scope.onlyFolders = (dialogOptions.onlyFolders && dialogOptions.onlyFolders !== "0") ? true : false; + $scope.showDetails = (dialogOptions.showDetails && dialogOptions.showDetails !== "0") ? true : false; $scope.multiPicker = (dialogOptions.multiPicker && dialogOptions.multiPicker !== "0") ? true : false; $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; - $scope.ignoreUserStartNodes = Object.toBoolean(dialogOptions.ignoreUserStartNodes); $scope.cropSize = dialogOptions.cropSize; $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); $scope.lockedFolder = true; $scope.allowMediaEdit = dialogOptions.allowMediaEdit ? dialogOptions.allowMediaEdit : false; - var userStartNodes = []; + $scope.filterOptions = { + excludeSubFolders: umbSessionStorage.get("mediaPickerExcludeSubFolders") || false + }; + + var userStartNodes = []; + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); + if ($scope.onlyImages) { - $scope.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); + vm.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); } else { // Use whitelist of allowed file types if provided if (allowedUploadFiles !== '') { - $scope.acceptedFileTypes = allowedUploadFiles; + vm.acceptedFileTypes = allowedUploadFiles; } else { // If no whitelist, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles - $scope.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); + vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); } } - $scope.maxFileSize = umbracoSettings.maxFileSize + "KB"; + vm.maxFileSize = umbracoSettings.maxFileSize + "KB"; $scope.model.selection = []; - $scope.acceptedMediatypes = []; + vm.acceptedMediatypes = []; mediaTypeHelper.getAllowedImagetypes($scope.startNodeId) - .then(function(types) { - $scope.acceptedMediatypes = types; + .then(function (types) { + vm.acceptedMediatypes = types; }); - $scope.searchOptions = { + var dataTypeKey = null; + if ($scope.model && $scope.model.dataTypeKey) { + dataTypeKey = $scope.model.dataTypeKey; + } + + vm.searchOptions = { pageNumber: 1, pageSize: 100, totalItems: 0, totalPages: 0, - filter: "", - ignoreUserStartNodes: $scope.model.ignoreUserStartNodes + filter: '', + dataTypeKey: dataTypeKey }; - //preload selected item - $scope.target = undefined; + // preload selected item + $scope.target = null; + if (dialogOptions.currentTarget) { $scope.target = dialogOptions.currentTarget; } + function setTitle() { + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectMedia") + .then(function (data) { + $scope.model.title = data; + }); + } + } + function onInit() { - userService.getCurrentUser().then(function(userData) { + + setTitle(); + + userService.getCurrentUser().then(function (userData) { userStartNodes = userData.startMediaIds; if ($scope.startNodeId !== -1) { entityResource.getById($scope.startNodeId, "media") - .then(function(ent) { + .then(function (ent) { $scope.startNodeId = ent.id; run(); }); @@ -92,101 +129,207 @@ angular.module("umbraco") gotoStartNode(); } } else { - //if a target is specified, go look it up - generally this target will just contain ids not the actual full - //media object so we need to look it up + // if a target is specified, go look it up - generally this target will just contain ids not the actual full + // media object so we need to look it up var id = $scope.target.udi ? $scope.target.udi : $scope.target.id; var altText = $scope.target.altText; - mediaResource.getById(id) - .then(function (node) { - $scope.target = node; - if (ensureWithinStartNode(node)) { - selectImage(node); - $scope.target.url = mediaHelper.resolveFile(node); - $scope.target.altText = altText; - $scope.openDetailsDialog(); - } - }, - gotoStartNode); + + // ID of a UDI or legacy int ID still could be null/undefinied here + // As user may dragged in an image that has not been saved to media section yet + if (id) { + entityResource.getById(id, "Media") + .then(function (node) { + $scope.target = node; + if (ensureWithinStartNode(node)) { + selectMedia(node); + $scope.target.url = mediaHelper.resolveFileFromEntity(node); + $scope.target.thumbnail = mediaHelper.resolveFileFromEntity(node, true); + $scope.target.altText = altText; + openDetailsDialog(); + } + }, gotoStartNode); + } else { + // No ID set - then this is going to be a tmpimg that has not been uploaded + // User editing this will want to be changing the ALT text + openDetailsDialog(); + } } } - $scope.upload = function(v) { - angular.element(".umb-file-dropzone .file-select").trigger("click"); - }; + function upload() { + $(".umb-file-dropzone .file-select").trigger("click"); + } - $scope.dragLeave = function(el, event) { + function dragLeave() { $scope.activeDrag = false; - }; + } - $scope.dragEnter = function(el, event) { + function dragEnter() { $scope.activeDrag = true; - }; + } - $scope.submitFolder = function() { + function submitFolder() { if ($scope.model.newFolderName) { $scope.model.creatingFolder = true; mediaResource .addFolder($scope.model.newFolderName, $scope.currentFolder.id) - .then(function(data) { + .then(function (data) { //we've added a new folder so lets clear the tree cache for that specific item treeService.clearCache({ cacheKey: "__media", //this is the main media tree cache key childrenOf: data.parentId //clear the children of the parent }); $scope.model.creatingFolder = false; - $scope.gotoFolder(data); + gotoFolder(data); $scope.model.showFolderInput = false; $scope.model.newFolderName = ""; }); } else { $scope.model.showFolderInput = false; } - }; + } - $scope.enterSubmitFolder = function(event) { + function enterSubmitFolder(event) { if (event.keyCode === 13) { - $scope.submitFolder(); + submitFolder(); event.stopPropagation(); } - }; + } - $scope.gotoFolder = function (folder) { + function gotoFolder(folder) { if (!$scope.multiPicker) { - deselectAllImages($scope.model.selection); + deselectAllMedia($scope.model.selection); } if (!folder) { folder = { id: -1, name: "Media", icon: "icon-folder" }; } - var options = {}; if (folder.id > 0) { - options.ignoreUserStartNodes = $scope.model.ignoreUserStartNodes; - entityResource.getAncestors(folder.id, "media", options) - .then(function(anc) { + entityResource.getAncestors(folder.id, "media", null, { dataTypeKey: dataTypeKey }) + .then(function (anc) { $scope.path = _.filter(anc, - function(f) { + function (f) { return f.path.indexOf($scope.startNodeId) !== -1; }); }); mediaTypeHelper.getAllowedImagetypes(folder.id) - .then(function(types) { - $scope.acceptedMediatypes = types; + .then(function (types) { + vm.acceptedMediatypes = types; }); } else { $scope.path = []; } $scope.lockedFolder = (folder.id === -1 && $scope.model.startNodeIsVirtual) || hasFolderAccess(folder) === false; - - $scope.currentFolder = folder; + localStorageService.set("umbLastOpenedMediaNodeId", folder.id); - options.ignoreUserStartNodes = $scope.ignoreUserStartNodes; - return getChildren(folder.id, options); + + return getChildren(folder.id); + } + + function clickHandler(media, event, index) { + + if (media.isFolder) { + if ($scope.disableFolderSelect) { + gotoFolder(media); + } else { + selectMedia(media); + } + } else { + if ($scope.showDetails) { + + $scope.target = media; + + // handle both entity and full media object + if (media.image) { + $scope.target.url = media.image; + } else { + $scope.target.url = mediaHelper.resolveFile(media); + } + + openDetailsDialog(); + } else { + selectMedia(media); + } + } + } + + function clickItemName(item, event, index) { + if (item.isFolder) { + gotoFolder(item); + } + else { + $scope.clickHandler(item, event, index); + } }; + function selectMedia(media) { + if (!media.selectable) { + return; + } + if (media.selected) { + for (var i = 0; $scope.model.selection.length > i; i++) { + var imageInSelection = $scope.model.selection[i]; + if (media.key === imageInSelection.key) { + media.selected = false; + $scope.model.selection.splice(i, 1); + } + } + } else { + if (!$scope.multiPicker) { + deselectAllMedia($scope.model.selection); + } + eventsService.emit("dialogs.mediaPicker.select", media); + media.selected = true; + $scope.model.selection.push(media); + } + } + + function deselectAllMedia(medias) { + for (var i = 0; i < medias.length; i++) { + var media = medias[i]; + media.selected = false; + } + medias.length = 0; + } + + function onUploadComplete(files) { + gotoFolder($scope.currentFolder).then(function () { + $timeout(function () { + if ($scope.multiPicker) { + var images = _.rest($scope.images, $scope.images.length - files.length); + _.each(images, function (image) { + selectMedia(image); + }); + } else { + var image = $scope.images[$scope.images.length - 1]; + clickHandler(image); + } + }); + }); + } + + function onFilesQueue() { + $scope.activeDrag = false; + } + + function ensureWithinStartNode(node) { + // make sure that last opened node is on the same path as start node + var nodePath = node.path.split(","); + + // also make sure the node is not trashed + if (nodePath.indexOf($scope.startNodeId.toString()) !== -1 && node.trashed === false) { + gotoFolder({ id: $scope.lastOpenedNode || $scope.startNodeId, name: "Media", icon: "icon-folder", path: node.path }); + return true; + } else { + gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); + return false; + } + } + function hasFolderAccess(node) { var nodePath = node.path ? node.path.split(',') : [node.id]; @@ -198,173 +341,95 @@ angular.module("umbraco") return false; } - $scope.clickHandler = function(image, event, index) { - if (image.isFolder) { - if ($scope.disableFolderSelect) { - $scope.gotoFolder(image); - } else { - eventsService.emit("dialogs.mediaPicker.select", image); - selectImage(image); - } - } else { - eventsService.emit("dialogs.mediaPicker.select", image); - if ($scope.showDetails) { - - $scope.target = image; - - // handle both entity and full media object - if (image.image) { - $scope.target.url = image.image; - } else { - $scope.target.url = mediaHelper.resolveFile(image); - } - - $scope.openDetailsDialog(); - } else { - selectImage(image); - } - } - }; - - $scope.clickItemName = function(item) { - if (item.isFolder) { - $scope.gotoFolder(item); - } - }; - - function selectImage(image) { - if (image.selected) { - for (var i = 0; $scope.model.selection.length > i; i++) { - var imageInSelection = $scope.model.selection[i]; - if (image.key === imageInSelection.key) { - image.selected = false; - $scope.model.selection.splice(i, 1); - } - } - } else { - if (!$scope.multiPicker) { - deselectAllImages($scope.model.selection); - } - image.selected = true; - $scope.model.selection.push(image); - } + function gotoStartNode() { + gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); } - function deselectAllImages(images) { - for (var i = 0; i < images.length; i++) { - var image = images[i]; - image.selected = false; - } - images.length = 0; - } + function openDetailsDialog() { + localizationService.localize("defaultdialogs_editSelectedMedia").then(function (data) { + vm.mediaPickerDetailsOverlay = { + show: true, + title: data, + disableFocalPoint: $scope.disableFocalPoint, + submit: function (model) { + $scope.model.selection.push($scope.target); + $scope.model.submit($scope.model); - $scope.onUploadComplete = function(files) { - $scope.gotoFolder($scope.currentFolder).then(function() { - if (files.length === 1 && $scope.model.selection.length === 0) { - var image = $scope.images[$scope.images.length - 1]; - $scope.target = image; - $scope.target.url = mediaHelper.resolveFile(image); - selectImage(image); - } + vm.mediaPickerDetailsOverlay.show = false; + vm.mediaPickerDetailsOverlay = null; + }, + close: function (oldModel) { + vm.mediaPickerDetailsOverlay.show = false; + vm.mediaPickerDetailsOverlay = null; + + close(); + } + }; }); }; - $scope.onFilesQueue = function() { - $scope.activeDrag = false; - }; + var debounceSearchMedia = _.debounce(function () { + $scope.$apply(function () { + if (vm.searchOptions.filter) { + searchMedia(); + } else { - function ensureWithinStartNode(node) { - // make sure that last opened node is on the same path as start node - var nodePath = node.path.split(","); + // reset pagination + vm.searchOptions = { + pageNumber: 1, + pageSize: 100, + totalItems: 0, + totalPages: 0, + filter: '', + dataTypeKey: dataTypeKey + }; - // also make sure the node is not trashed - if (nodePath.indexOf($scope.startNodeId.toString()) !== -1 && node.trashed === false) { - $scope.gotoFolder({ id: $scope.lastOpenedNode, name: "Media", icon: "icon-folder" }); - return true; - } else { - $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); - return false; - } - } + getChildren($scope.currentFolder.id); + } + }); + }, 500); - function gotoStartNode(err) { - $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); - } - - $scope.openDetailsDialog = function() { - - $scope.mediaPickerDetailsOverlay = {}; - $scope.mediaPickerDetailsOverlay.show = true; - - $scope.mediaPickerDetailsOverlay.submit = function(model) { - $scope.model.selection.push($scope.target); - $scope.model.submit($scope.model); - - $scope.mediaPickerDetailsOverlay.show = false; - $scope.mediaPickerDetailsOverlay = null; - }; - - $scope.mediaPickerDetailsOverlay.close = function(oldModel) { - $scope.mediaPickerDetailsOverlay.show = false; - $scope.mediaPickerDetailsOverlay = null; - }; - }; - - var debounceSearchMedia = _.debounce(function() { - $scope.$apply(function() { - if ($scope.searchOptions.filter) { - searchMedia(); - } else { - // reset pagination - $scope.searchOptions = { - pageNumber: 1, - pageSize: 100, - totalItems: 0, - totalPages: 0, - filter: "", - ignoreUserStartNodes: $scope.model.ignoreUserStartNodes - }; - getChildren($scope.currentFolder.id); - } - }); - }, 500); - - $scope.changeSearch = function() { - $scope.loading = true; + function changeSearch() { + vm.loading = true; debounceSearchMedia(); - }; - - $scope.toggle = function() { - // Make sure to activate the changeSearch function everytime the toggle is clicked - $scope.changeSearch(); } - $scope.changePagination = function(pageNumber) { - $scope.loading = true; - $scope.searchOptions.pageNumber = pageNumber; + function toggle() { + umbSessionStorage.set("mediaPickerExcludeSubFolders", $scope.filterOptions.excludeSubFolders); + // Make sure to activate the changeSearch function everytime the toggle is clicked + changeSearch(); + } + + function changePagination(pageNumber) { + vm.loading = true; + vm.searchOptions.pageNumber = pageNumber; searchMedia(); }; function searchMedia() { - $scope.loading = true; - entityResource.getPagedDescendants($scope.currentFolder.id, "Media", $scope.searchOptions) - .then(function(data) { + vm.loading = true; + entityResource.getPagedDescendants($scope.filterOptions.excludeSubFolders ? $scope.currentFolder.id : $scope.startNodeId, "Media", vm.searchOptions) + .then(function (data) { // update image data to work with image grid - angular.forEach(data.items, function(mediaItem) { - setMediaMetaData(mediaItem); - }); + if (data.items) { + data.items.forEach(mediaItem => setMediaMetaData(mediaItem)); + } + // update images $scope.images = data.items ? data.items : []; + // update pagination if (data.pageNumber > 0) - $scope.searchOptions.pageNumber = data.pageNumber; + vm.searchOptions.pageNumber = data.pageNumber; if (data.pageSize > 0) - $scope.searchOptions.pageSize = data.pageSize; - $scope.searchOptions.totalItems = data.totalItems; - $scope.searchOptions.totalPages = data.totalPages; - // set already selected images to selected - preSelectImages(); - $scope.loading = false; + vm.searchOptions.pageSize = data.pageSize; + + vm.searchOptions.totalItems = data.totalItems; + vm.searchOptions.totalPages = data.totalPages; + + // set already selected medias to selected + preSelectMedia(); + vm.loading = false; }); } @@ -372,46 +437,71 @@ angular.module("umbraco") // set thumbnail and src mediaItem.thumbnail = mediaHelper.resolveFileFromEntity(mediaItem, true); mediaItem.image = mediaHelper.resolveFileFromEntity(mediaItem, false); - // set properties to match a media object - if (mediaItem.metaData && - mediaItem.metaData.umbracoWidth && - mediaItem.metaData.umbracoHeight) { - mediaItem.properties = [ - { - alias: "umbracoWidth", - value: mediaItem.metaData.umbracoWidth.Value - }, - { - alias: "umbracoHeight", - value: mediaItem.metaData.umbracoHeight.Value - } - ]; + // set properties to match a media object + if (mediaItem.metaData) { + mediaItem.properties = []; + if (mediaItem.metaData.umbracoWidth && mediaItem.metaData.umbracoHeight) { + mediaItem.properties.push( + { + alias: "umbracoWidth", + editor: mediaItem.metaData.umbracoWidth.PropertyEditorAlias, + value: mediaItem.metaData.umbracoWidth.Value + }, + { + alias: "umbracoHeight", + editor: mediaItem.metaData.umbracoHeight.PropertyEditorAlias, + value: mediaItem.metaData.umbracoHeight.Value + } + ); + } + if (mediaItem.metaData.umbracoFile) { + // this is required for resolving files through the mediahelper + mediaItem.properties.push( + { + alias: "umbracoFile", + editor: mediaItem.metaData.umbracoFile.PropertyEditorAlias, + value: mediaItem.metaData.umbracoFile.Value + } + ); + } } } - function getChildren(id, options) { - $scope.loading = true; - return mediaResource.getChildren(id, options) - .then(function(data) { - $scope.searchOptions.filter = ""; - $scope.images = data.items ? data.items : []; - // set already selected images to selected - preSelectImages(); - $scope.loading = false; - }); + function getChildren(id) { + vm.loading = true; + return entityResource.getChildren(id, "Media", vm.searchOptions).then(function (data) { + + var allowedTypes = dialogOptions.filter ? dialogOptions.filter.split(",") : null; + + for (var i = 0; i < data.length; i++) { + if (data[i].metaData.MediaPath !== null) { + data[i].thumbnail = mediaHelper.resolveFileFromEntity(data[i], true); + data[i].image = mediaHelper.resolveFileFromEntity(data[i], false); + } + + data[i].filtered = allowedTypes && allowedTypes.indexOf(data[i].metaData.ContentTypeAlias) < 0; + } + + vm.searchOptions.filter = ""; + $scope.images = data ? data : []; + + // set already selected medias to selected + preSelectMedia(); + vm.loading = false; + }); } - function preSelectImages() { - for (var folderImageIndex = 0; folderImageIndex < $scope.images.length; folderImageIndex++) { - var folderImage = $scope.images[folderImageIndex]; + function preSelectMedia() { + for (var folderIndex = 0; folderIndex < $scope.images.length; folderIndex++) { + var folderImage = $scope.images[folderIndex]; var imageIsSelected = false; - if ($scope.model && angular.isArray($scope.model.selection)) { - for (var selectedImageIndex = 0; - selectedImageIndex < $scope.model.selection.length; - selectedImageIndex++) { - var selectedImage = $scope.model.selection[selectedImageIndex]; + if ($scope.model && Utilities.isArray($scope.model.selection)) { + for (var selectedIndex = 0; + selectedIndex < $scope.model.selection.length; + selectedIndex++) { + var selectedImage = $scope.model.selection[selectedIndex]; if (folderImage.key === selectedImage.key) { imageIsSelected = true; @@ -425,49 +515,51 @@ angular.module("umbraco") } } - $scope.editMediaItem = function(item) { - var mediaEditor = { - id: item.id, - submit: function(model) { - editorService.close() - // update the media picker item in the picker so it matched the saved media item - // the media picker is using media entities so we get the - // entity so we easily can format it for use in the media grid - if(model && model.mediaNode) { - entityResource.getById(model.mediaNode.id, "media") - .then(function (mediaEntity) { - angular.extend(item, mediaEntity); - setMediaMetaData(item); - setUpdatedMediaNodes(item); - }); - } - }, - close: function(model) { - setUpdatedMediaNodes(item); - editorService.close(); - } + + /** + * Called when the umbImageGravity component updates the focal point value + * @param {any} left + * @param {any} top + */ + function focalPointChanged(left, top) { + // update the model focalpoint value + $scope.target.focalPoint = { + left: left, + top: top }; - editorService.mediaEditor(mediaEditor); - }; + } function setUpdatedMediaNodes(item) { // add udi to list of updated media items so we easily can update them in other editors - if($scope.model.updatedMediaNodes.indexOf(item.udi) === -1) { + if ($scope.model.updatedMediaNodes.indexOf(item.udi) === -1) { $scope.model.updatedMediaNodes.push(item.udi); } } - $scope.submit = function() { - if($scope.model.submit) { + function shouldShowUrl() { + if (!$scope.target) { + return false; + } + if ($scope.target.id) { + return false; + } + if ($scope.target.url && $scope.target.url.toLower().indexOf("blob:") === 0) { + return false; + } + return true; + } + + function submit() { + if ($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); } - }; + } - $scope.close = function() { - if($scope.model.close) { + function close() { + if ($scope.model && $scope.model.close) { $scope.model.close($scope.model); } - }; + } onInit(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html index 6eee269cee..e1a89061b6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html @@ -1,192 +1,225 @@
          - + - - + + - + -
          + -
          +
          + +
          + +
          + + +
          + + +
          +
          + +
          + +
          +
            +
          • + + +
          • + +
          • + + +
          • + +
          • + + + +
          • +
          +
          +
          + + + + + + -
          + ng-if="vm.loading"> - -
          - -
          + + +
          +
          + +
          +
          - - - - - - -
          - - +
          +
          + +
          +
          - - - - -
          - - -
          -
          - - -
          - -
          - +
          Preview
          - - + {{target.name}} +
          + +
          +
          + Focal point +
          + +
          + + +
          + +
          + +
          + Preview +
          + + + + +
          +
          -
          +
          -
          - - -
          + -
          - - -
          + + + -
          + + - + + - + + - - + - - - - - - - - - - - -
          +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html index 8424bcefbe..1c9fd5b822 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html @@ -13,6 +13,9 @@ +

          + Choose where to move the selected item(s) +

          @@ -75,7 +78,8 @@ button-style="success" label-key="general_submit" state="vm.saveButtonState" - action="vm.submit(model)"> + action="vm.submit(model)" + disabled="!model.target"> diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js index b8581d28d0..a6d1383640 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js @@ -30,6 +30,7 @@ vm.close = close; vm.toggleAllowCultureVariants = toggleAllowCultureVariants; + vm.toggleAllowSegmentVariants = toggleAllowSegmentVariants; vm.toggleValidation = toggleValidation; vm.toggleShowOnMemberProfile = toggleShowOnMemberProfile; vm.toggleMemberCanEdit = toggleMemberCanEdit; @@ -56,7 +57,8 @@ "validation_validateAsEmail", "validation_validateAsNumber", "validation_validateAsUrl", - "validation_enterCustomValidation" + "validation_enterCustomValidation", + "validation_fieldIsMandatory" ]; localizationService.localizeMany(labels) @@ -66,6 +68,7 @@ vm.labels.validateAsNumber = data[1]; vm.labels.validateAsUrl = data[2]; vm.labels.customValidation = data[3]; + vm.labels.fieldIsMandatory = data[4]; vm.validationTypes = [ { @@ -112,7 +115,7 @@ property: $scope.model.property, contentTypeName: $scope.model.contentTypeName, view: "views/common/infiniteeditors/datatypepicker/datatypepicker.html", - size: "small", + size: "medium", submit: function(model) { $scope.model.updateSameDataTypes = model.updateSameDataTypes; @@ -246,6 +249,10 @@ $scope.model.property.allowCultureVariant = toggleValue($scope.model.property.allowCultureVariant); } + function toggleAllowSegmentVariants() { + $scope.model.property.allowSegmentVariant = toggleValue($scope.model.property.allowSegmentVariant); + } + function toggleValidation() { $scope.model.property.validation.mandatory = toggleValue($scope.model.property.validation.mandatory); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 93d7936326..1b28e84607 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -18,8 +18,8 @@
          -
          -
          +
          +
          - +
          - + - - - -
          - - - - - -
          - {{ model.property.dataTypeName }} - {{ model.property.editor }} + +
          + Required editor
          - - - - - + + +
          + + + +
          - +
          - +
          - +
          - - + label-on="{{vm.labels.fieldIsMandatory}}" + label-off="{{vm.labels.fieldIsMandatory}}" + show-labels="true" + label-position="right" + focus-when="{{vm.focusOnMandatoryField}}" + class="mb1"> -
          -
          - -
          - - - + localize="placeholder" + placeholder="@validation_validationRegExp" + ng-model="model.property.validation.pattern" + ng-change="vm.changeValidationPattern()" + ng-if="vm.showValidationPattern" + umb-auto-resize + focus-when="{{vm.focusOnPatternField}}" + ng-keypress="vm.submitOnEnter($event)"> + + +
          - -
          +
          + +
          + + + +
          +
          + +
          + + + + +
          + +
          @@ -159,7 +194,7 @@ checked="model.property.isSensitiveData" on-click="vm.toggleIsSensitiveData()"> - +
          @@ -189,7 +224,7 @@ label-key="general_submit" action="vm.submit(model)"> - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html index 8376f50713..37a0d41207 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html @@ -15,164 +15,157 @@ -
          +
          -
          -
          +
          +
          - I want + I want - - from + from - - + + -
          +
          -
          +
          - - where - - - and - + + where + + + and + -
          + +
          -
          +
          - - + + - - - - {{term.name}} - - - + + + + {{term.name}} + + + -
          +
          - + - - - + + + - - - - + + + + - + - - - + + + - - - + + + -
          +
          -
          +
          - order by + order by -
          + +
          - - + + -
          -
          +
          +
          {{model.result.resultCount}} items, returned in {{model.result.executionTime}} ms
          - + -
          {{model.result.queryExpression}}
          + {{model.result.queryExpression}} -
          +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js index afec0ae120..24ed971de0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function RollbackController($scope, contentResource, localizationService, assetsService) { + function RollbackController($scope, contentResource, localizationService, assetsService, dateHelper, userService) { var vm = this; @@ -90,11 +90,15 @@ const culture = $scope.model.node.variants.length > 1 ? vm.currentVersion.language.culture : null; return contentResource.getRollbackVersions(nodeId, culture) - .then(function(data){ - vm.previousVersions = data.map(version => { - version.displayValue = version.versionDate + " - " + version.versionAuthorName; - return version; - }); + .then(function (data) { + // get current backoffice user and format dates + userService.getCurrentUser().then(function (currentUser) { + vm.previousVersions = data.map(version => { + var timestampFormatted = dateHelper.getLocalDate(version.versionDate, currentUser.locale, 'LLL'); + version.displayValue = timestampFormatted + ' - ' + version.versionAuthorName; + return version; + }); + }); }); } @@ -126,8 +130,8 @@ } // diff requires a string - property.value = property.value ? property.value : ""; - oldProperty.value = oldProperty.value ? oldProperty.value : ""; + property.value = property.value ? property.value + "" : ""; + oldProperty.value = oldProperty.value ? oldProperty.value + "" : ""; var diffProperty = { "alias": property.alias, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html index 9b230410b0..e292a94606 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html @@ -9,9 +9,9 @@ hide-icon="true" hide-description="true"> - + - + @@ -21,21 +21,21 @@
          -
          - +
          - +

          {{vm.currentVersion.name}} (Created: {{vm.currentVersion.createDate}})

          - +
          - - - +
          + +
          +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index 247f694470..387dd71da8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -28,15 +28,16 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", vm.treeAlias = $scope.model.treeAlias; vm.multiPicker = $scope.model.multiPicker; vm.hideHeader = (typeof $scope.model.hideHeader) === "boolean" ? $scope.model.hideHeader : true; + vm.dataTypeKey = $scope.model.dataTypeKey; vm.searchInfo = { searchFromId: $scope.model.startNodeId, searchFromName: null, showSearch: false, + dataTypeKey: vm.dataTypeKey, results: [], selectedSearchResults: [] } vm.startNodeId = $scope.model.startNodeId; - vm.ignoreUserStartNodes = $scope.model.ignoreUserStartNodes; //Used for toggling an empty-state message //Some trees can have no items (dictionary & forms email templates) vm.hasItems = true; @@ -57,9 +58,14 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", vm.hideSearch = hideSearch; vm.closeMiniListView = closeMiniListView; vm.selectListViewNode = selectListViewNode; + vm.listViewItemsLoaded = listViewItemsLoaded; vm.submit = submit; vm.close = close; + var currentNode = $scope.model.currentNode; + + var previouslyFocusedElement = null; + function initDialogTree() { vm.dialogTreeApi.callbacks.treeLoaded(treeLoadedHandler); // TODO: Also deal with unexpanding!! @@ -70,25 +76,35 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", /** * Performs the initialization of this component */ - function onInit () { + function onInit() { - // load languages - languageResource.getAll().then(function (languages) { - vm.languages = languages; + if (vm.showLanguageSelector) { + // load languages + languageResource.getAll().then(function (languages) { + vm.languages = languages; - // set the default language - vm.languages.forEach(function (language) { - if (language.isDefault) { - vm.selectedLanguage = language; - vm.languageSelectorIsOpen = false; - } + // set the default language + vm.languages.forEach(function (language) { + if (language.isDefault) { + vm.selectedLanguage = language; + vm.languageSelectorIsOpen = false; + } + }); }); - }); + } if (vm.treeAlias === "content") { vm.entityType = "Document"; if (!$scope.model.title) { - localizationService.localize("defaultdialogs_selectContent").then(function(value){ + localizationService.localize("defaultdialogs_selectContent").then(function (value) { + $scope.model.title = value; + }); + } + } + else if (vm.treeAlias === "documentTypes") { + vm.entityType = "DocumentType"; + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectContentType").then(function (value) { $scope.model.title = value; }); } @@ -96,15 +112,31 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", else if (vm.treeAlias === "member" || vm.section === "member") { vm.entityType = "Member"; if (!$scope.model.title) { - localizationService.localize("defaultdialogs_selectMember").then(function(value){ + localizationService.localize("defaultdialogs_selectMember").then(function (value) { $scope.model.title = value; - }) + }); + } + } + else if (vm.treeAlias === "memberTypes") { + vm.entityType = "MemberType"; + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectMemberType").then(function (value) { + $scope.model.title = value; + }); } } else if (vm.treeAlias === "media" || vm.section === "media") { vm.entityType = "Media"; if (!$scope.model.title) { - localizationService.localize("defaultdialogs_selectMedia").then(function(value){ + localizationService.localize("defaultdialogs_selectMedia").then(function (value) { + $scope.model.title = value; + }); + } + } + else if (vm.treeAlias === "mediaTypes") { + vm.entityType = "MediaType"; + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectMediaType").then(function (value) { $scope.model.title = value; }); } @@ -144,7 +176,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", if (angular.isFunction($scope.model.filter)) { $scope.model.filterAdvanced = true; } - else if (angular.isObject($scope.model.filter)) { + else if (Utilities.isObject($scope.model.filter)) { $scope.model.filterAdvanced = true; } else { @@ -157,10 +189,16 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", if ($scope.model.filter.startsWith("{")) { $scope.model.filterAdvanced = true; //convert to object - $scope.model.filter = angular.fromJson($scope.model.filter); + $scope.model.filter = Utilities.fromJson($scope.model.filter); } } } + + vm.filter = { + filterAdvanced: $scope.model.filterAdvanced, + filterExclude: $scope.model.filterExclude, + filter: $scope.model.filter + }; } /** @@ -172,14 +210,15 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", if (vm.startNodeId) { queryParams["startNodeId"] = $scope.model.startNodeId; } - if (vm.ignoreUserStartNodes) { - queryParams["ignoreUserStartNodes"] = $scope.model.ignoreUserStartNodes; - } if (vm.selectedLanguage && vm.selectedLanguage.id) { queryParams["culture"] = vm.selectedLanguage.culture; } + if (vm.dataTypeKey) { + queryParams["dataTypeKey"] = vm.dataTypeKey; + } + var queryString = $.param(queryParams); //create the query string from the params object - + if (!queryString) { vm.customTreeParams = $scope.model.customTreeParams; } @@ -204,7 +243,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", $timeout(function () { //reload the tree with it's updated querystring args vm.dialogTreeApi.load(vm.section).then(function () { - + //create the list of promises var promises = []; for (var i = 0; i < expandedPaths.length; i++) { @@ -219,7 +258,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", function toggleLanguageSelector() { vm.languageSelectorIsOpen = !vm.languageSelectorIsOpen; }; - + function nodeExpandedHandler(args) { //store the reference to the expanded node path @@ -232,7 +271,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", openMiniListView(args.node); } - if (angular.isArray(args.children)) { + if (Utilities.isArray(args.children)) { //iterate children _.each(args.children, @@ -260,6 +299,12 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", vm.hasItems = args.tree.root.children.length > 0; tree = args.tree; + + var nodeHasPath = currentNode && currentNode.path; + var startNodeNotDefined = !vm.startNodeId; + if (startNodeNotDefined && nodeHasPath) { + vm.dialogTreeApi.syncTree({ path: currentNode.path, activate: true }); + } } //wires up selection @@ -375,7 +420,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", if ($scope.model.selection.length > 0) { for (var i = 0; $scope.model.selection.length > i; i++) { var selectedItem = $scope.model.selection[i]; - if (selectedItem.id === item.id) { + if (selectedItem.id === parseInt(item.id)) { found = true; foundIndex = i; } @@ -401,7 +446,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", // be allowed to be clicked on nodes = _.filter(nodes, function (n) { - return !angular.isObject(n.metaData.listViewNode); + return !Utilities.isObject(n.metaData.listViewNode); }); if ($scope.model.filterAdvanced) { @@ -446,6 +491,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", } function openMiniListView(node) { + previouslyFocusedElement = document.activeElement; vm.miniListView = node; } @@ -590,8 +636,8 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", _.each(vm.searchInfo.results, function (result) { var exists = _.find($scope.model.selection, - function (selectedId) { - return result.id == selectedId; + function (item) { + return result.id == item.id; }); if (exists) { result.selected = true; @@ -609,20 +655,35 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", function closeMiniListView() { vm.miniListView = undefined; + if (previouslyFocusedElement) { + $timeout(function () { + previouslyFocusedElement.focus(); + previouslyFocusedElement = null; + }); + } + } + + function listViewItemsLoaded(items) { + var selectedIds = _.pluck($scope.model.selection, "id"); + _.each(items, function (item) { + if (_.contains(selectedIds, item.id)) { + item.selected = true; + } + }); } function submit(model) { - if($scope.model.submit) { + if ($scope.model.submit) { $scope.model.submit(model); } } function close() { - if($scope.model.close) { + if ($scope.model.close) { $scope.model.close(); } } - + //initialize onInit(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html index acd838f7bf..0c10d94136 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html @@ -36,22 +36,23 @@ search-from-id="{{vm.searchInfo.searchFromId}}" search-from-name="{{vm.searchInfo.searchFromName}}" show-search="{{vm.searchInfo.showSearch}}" - ignore-user-start-nodes="{{vm.ignoreUserStartNodes}}" - section="{{vm.section}}"> + datatype-key="{{vm.searchInfo.dataTypeKey}}" + section="{{vm.section}}" + auto-focus="true">
          - + - + {{ vm.emptyStateMessage }} - +
          -
          - +
          - - + on-items-loaded="vm.listViewItemsLoaded(items)" + entity-type-filter="vm.filter"> @@ -93,7 +95,7 @@ shortcut="esc" action="vm.close()"> - + - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html index e2ae1ab524..b82b919f9f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html @@ -13,10 +13,10 @@ - + - + - + - + - No user groups have been added + No user groups have been added - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/changepassword/changepassword.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/changepassword/changepassword.html new file mode 100644 index 0000000000..db69a32f7f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/changepassword/changepassword.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/confirm/confirm.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/confirm/confirm.html new file mode 100644 index 0000000000..79dd6bd4f6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/confirm/confirm.html @@ -0,0 +1,13 @@ +
          + +
          + {{model.confirmMessage}} +
          + +

          {{model.content}}

          + +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html index 0fdf303c6b..715dc12a07 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html @@ -19,31 +19,28 @@
          - diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js index 5ed87f073b..8248d11863 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js @@ -91,6 +91,10 @@ angular.module("umbraco") }); } + $scope.linkProvider = function (e) { + e.target.submit(); + } + $scope.unlink = function (e, loginProvider, providerKey) { var result = confirm("Are you sure you want to unlink this account?"); if (!result) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html index 4af8c83983..8b12d061f3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html @@ -26,8 +26,7 @@ action="togglePasswordFields()" button-style="action" label="Change password" - label-key="general_changePassword" - button-style="success"> + label-key="general_changePassword"> -
          + - + + Link your {{login.caption}} account +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js new file mode 100644 index 0000000000..8ecc737278 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js @@ -0,0 +1,24 @@ +(function () { + "use strict"; + + function EmailsController($scope, userService) { + + var vm = this; + + vm.optIn = function() { + // Get the current user in backoffice + userService.getCurrentUser().then(function(user){ + // Send this user along to opt in + // It's a fire & forget - not sure we need to check the response + userService.addUserToEmailMarketing(user); + }); + + // Mark Tour as complete + // This is also can help us indicate that the user accepted + // Where disabled is set if user closes modal or chooses NO + $scope.model.completeTour(); + } + } + + angular.module("umbraco").controller("Umbraco.Tours.UmbEmailMarketing.EmailsController", EmailsController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html new file mode 100644 index 0000000000..887624ed05 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html @@ -0,0 +1,26 @@ +
          + + + +

          {{ model.currentStep.title }}

          + +
          + +
          +
          +
          + + +
          + paperplane +
          +
          + +
          + + +
          + +
          + +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatecontent/nodename/nodename.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatecontent/nodename/nodename.controller.js index bdd7eb65d0..eca0c4582b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatecontent/nodename/nodename.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatecontent/nodename/nodename.controller.js @@ -2,16 +2,16 @@ "use strict"; function NodeNameController($scope) { - + var vm = this; - var element = angular.element($scope.model.currentStep.element); + var element = $($scope.model.currentStep.element); vm.error = false; - + vm.initNextStep = initNextStep; function initNextStep() { - if(element.val().toLowerCase() === 'home') { + if (element.val().toLowerCase() === 'home') { $scope.model.nextStep(); } else { vm.error = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/doctypename/doctypename.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/doctypename/doctypename.controller.js index fee52eb506..ed5a0b93e3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/doctypename/doctypename.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/doctypename/doctypename.controller.js @@ -2,16 +2,16 @@ "use strict"; function DocTypeNameController($scope) { - + var vm = this; - var element = angular.element($scope.model.currentStep.element); + var element = $($scope.model.currentStep.element); vm.error = false; - + vm.initNextStep = initNextStep; function initNextStep() { - if(element.val().toLowerCase() === 'home page') { + if (element.val().toLowerCase() === 'home page') { $scope.model.nextStep(); } else { vm.error = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/propertyname/propertyname.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/propertyname/propertyname.controller.js index 1ebe878928..5ceae2f2f8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/propertyname/propertyname.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/propertyname/propertyname.controller.js @@ -2,12 +2,12 @@ "use strict"; function PropertyNameController($scope) { - + var vm = this; - var element = angular.element($scope.model.currentStep.element); + var element = $($scope.model.currentStep.element); vm.error = false; - + vm.initNextStep = initNextStep; function initNextStep() { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/tabname/tabname.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/tabname/tabname.controller.js index c477204cb7..c134e50a34 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/tabname/tabname.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/tabname/tabname.controller.js @@ -2,12 +2,12 @@ "use strict"; function TabNameController($scope) { - + var vm = this; - var element = angular.element($scope.model.currentStep.element); + var element = $($scope.model.currentStep.element); vm.error = false; - + vm.initNextStep = initNextStep; function initNextStep() { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/foldername/foldername.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/foldername/foldername.controller.js index ebda50481d..bad9718251 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/foldername/foldername.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/foldername/foldername.controller.js @@ -2,16 +2,16 @@ "use strict"; function FolderNameController($scope) { - + var vm = this; - var element = angular.element($scope.model.currentStep.element); + var element = $($scope.model.currentStep.element); vm.error = false; - + vm.initNextStep = initNextStep; function initNextStep() { - if(element.val().toLowerCase() === "my images") { + if (element.val().toLowerCase() === "my images") { $scope.model.nextStep(); } else { vm.error = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/uploadimages/uploadimages.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/uploadimages/uploadimages.controller.js index 0989076e95..08f0751510 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/uploadimages/uploadimages.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/uploadimages/uploadimages.controller.js @@ -2,12 +2,11 @@ "use strict"; function UploadImagesController($scope, editorState, mediaResource) { - + var vm = this; - var element = angular.element($scope.model.currentStep.element); vm.error = false; - + vm.initNextStep = initNextStep; function initNextStep() { @@ -23,7 +22,7 @@ var children = data; - if(children.items && children.items.length > 0) { + if (children.items && children.items.length > 0) { $scope.model.nextStep(); } else { vm.error = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrorenderintemplate/templatetree/templatetree.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrorenderintemplate/templatetree/templatetree.controller.js index 3fdeb8d3c0..4edd9c9837 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrorenderintemplate/templatetree/templatetree.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrorenderintemplate/templatetree/templatetree.controller.js @@ -2,13 +2,12 @@ "use strict"; function TemplatesTreeController($scope) { - - var vm = this; - var eventElement = angular.element($scope.model.currentStep.eventElement); - + + var eventElement = $($scope.model.currentStep.eventElement); + function onInit() { // check if tree is already open - if it is - go to next step - if(eventElement.hasClass("icon-navigation-down")) { + if (eventElement.hasClass("icon-navigation-down")) { $scope.model.nextStep(); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html index a14e930b8a..c6c4f98e25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html @@ -1,7 +1,7 @@ +
          - @@ -10,25 +10,35 @@
          diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html index 92da12c423..a6d42d923b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html @@ -6,10 +6,14 @@
          diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index baf9af916c..2e81395643 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -18,11 +18,11 @@

          -
          - - + + Required The confirmed password doesn't match the new password! @@ -138,7 +138,7 @@ id="{{login.authType}}" name="provider" value="{{login.authType}}" title="Log in using your {{login.caption}} account"> - Sign in with {{login.caption}} + Sign in with {{login.caption}}
          @@ -148,18 +148,17 @@
          -
          -
          {{vm.errorMsg}}
          +
          +
          -
          - - + +
          - - + +
          - +
          Forgotten password? @@ -185,14 +183,14 @@
          -

          +

          An email will be sent to the address specified with a link to reset your password

          - - + +
          @@ -200,9 +198,11 @@
          -
          + +

          +
          @@ -222,13 +222,13 @@
          - - + +
          - - + +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html index 829582329f..c133d5d38b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html @@ -1,4 +1,4 @@ -
          +
          + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html index 79c9a8bbe9..3b6d82f73c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -5,42 +5,46 @@
          diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html index 6c73122464..9b4e60a413 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html @@ -1,6 +1,7 @@ 
          +
          -
          +
          {{file.fileName}} @@ -26,30 +27,27 @@
          -
          +
          - - - - .{{file.extension}} - - -
          {{file.fileName}}
          + + +
          {{file.fileName}}
          - - - - .{{file.extension}} - - -
          {{file.fileName}}
          + + +
          {{file.fileName}}
          -
          - Remove file + + +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html index 7b6c7ca0e1..38ea13eebb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html @@ -1,70 +1,67 @@
          -
          +
          Password has been reset to:
          - {{passwordValues.generatedPassword}} + {{vm.passwordValues.generatedPassword}}
          -
          +
          - - - - - {{passwordForm.resetPassword.errorMsg}} + + + + + + {{changePasswordForm.resetPassword.errorMsg}} - + - - + - + Required - {{passwordForm.oldPassword.errorMsg}} + {{changePasswordForm.oldPassword.errorMsg}} - - + - + Required - Minimum {{config.minPasswordLength}} characters - {{passwordForm.password.errorMsg}} + Minimum {{vm.config.minPasswordLength}} characters + {{changePasswordForm.password.errorMsg}} - - + - + The confirmed password doesn't match the new password! - +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html index 8617f94e5e..20718cf804 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html @@ -5,15 +5,15 @@
          {{ name }}
          -
          +
          Sections: - {{ section.name }}, + {{ section.name }} All sections
          -
          +
          Content start node: No start node selected @@ -21,7 +21,7 @@
          -
          +
          Media start node: No start node selected @@ -29,18 +29,18 @@
          -
          +
          Permissions: - {{ permission.name }}, + {{ permission.name }}
          - Edit - Remove + +
          -
          \ No newline at end of file +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html index 32aa5ed1a2..014297ae51 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html @@ -1,12 +1,11 @@
          - +
          @@ -15,7 +14,6 @@
          - Remove -
          - -
          \ No newline at end of file + +
          +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js index 2a3f67a7e3..66747a8697 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js @@ -7,35 +7,62 @@ //if we make the viewModel the variant itself, we end up with a circular reference in the models which isn't ideal // (i.e. variant.apps[contentApp].viewModel = variant) //so instead since we already have access to the content, we can just get the variant directly by the index. + + var unbindLanguageWatcher = function() {}; + var unbindSegmentWatcher = function() {}; + var timeout = null; var vm = this; vm.loading = true; function onInit() { - //get the variant by index (see notes above) - vm.content = $scope.content.variants[$scope.model.viewModel]; + serverValidationManager.notify(); vm.loading = false; + timeout = null;// ensure timeout is set to null, so we know that its not running anymore. //if this variant has a culture/language assigned, then we need to watch it since it will change //if the language drop down changes and we need to re-init - if (vm.content.language) { - $scope.$watch(function () { - return vm.content.language.culture; + if ($scope.variantContent) { + if ($scope.variantContent.language) { + unbindLanguageWatcher = $scope.$watch(function () { + return $scope.variantContent.language.culture; + }, function (newVal, oldVal) { + if (newVal !== oldVal) { + requestUpdate(); + } + }); + } + + unbindSegmentWatcher = $scope.$watch(function () { + return $scope.variantContent.segment; }, function (newVal, oldVal) { if (newVal !== oldVal) { - vm.loading = true; - - // TODO: Can we minimize the flicker? - $timeout(function () { - onInit(); - }, 100); + requestUpdate(); } }); } + + } + + function requestUpdate() { + if (timeout === null) { + vm.loading = true; + + // TODO: Can we minimize the flicker? + timeout = $timeout(function () { + onInit(); + }, 100); + } } onInit(); + + $scope.$on("$destroy", function() { + unbindLanguageWatcher(); + unbindSegmentWatcher(); + $timeout.cancel(timeout); + }); } angular.module("umbraco").controller("Umbraco.Editors.Content.Apps.ContentController", ContentAppContentController); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html index d391f2cd95..7023a4187a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html @@ -2,7 +2,8 @@ + content-node-model="content" + content="variantContent"> - +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html b/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html index ecb1e34e0c..6238d11a69 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html @@ -2,7 +2,7 @@ -
          +
          @@ -24,11 +24,11 @@
          Domains
          - +
          - +
          @@ -198,7 +193,7 @@ - Back to overview + @@ -225,7 +220,7 @@
          -
          @@ -234,7 +229,7 @@
          {{vm.selectedIndex.healthStatus}}
          - The index cannot be read and will need to be rebuilt + The index cannot be read and will need to be rebuilt
          @@ -298,10 +293,9 @@
          @@ -375,7 +369,7 @@ @@ -387,13 +381,14 @@
          - The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation + The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation
          - This index cannot be rebuilt because it has no assigned IIndexPopulator + This index cannot be rebuilt because it has no assigned + IIndexPopulator
          @@ -413,10 +408,4 @@ - - - diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagementresults.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagementresults.html index bb8a29fca5..26fa0cb72f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagementresults.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagementresults.html @@ -1,18 +1,41 @@
          - -
          @@ -45,7 +45,9 @@
          - + + + @@ -53,9 +55,8 @@ - - +
          + +
          -
          +
          -
          +

          -
          - - - - -
          - -
          - +
          + +
          -
          +
          -
          +
          - + server-validation-field="{{variant.htmlId}}" + > + + + * + + + + — {{variant.language.name}} + * + + + + - + + + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + + -
          - -
          +
          -
          diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js index aacea9fe0e..2d40005618 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js @@ -9,8 +9,6 @@ vm.isNew = true; vm.changeSelection = changeSelection; - vm.dirtyVariantFilter = dirtyVariantFilter; - vm.pristineVariantFilter = pristineVariantFilter; function changeSelection(variant) { var firstSelected = _.find(vm.variants, function (v) { @@ -19,16 +17,18 @@ $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected } - function dirtyVariantFilter(variant) { + function saveableVariantFilter(variant) { //determine a variant is 'dirty' (meaning it will show up as save-able) if it's // * the active one // * it's editor is in a $dirty state - // * it is in NotCreated state return (variant.active || variant.isDirty); } - function pristineVariantFilter(variant) { - return !(dirtyVariantFilter(variant)); + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return (variant.language && variant.language.isMandatory === true && variant.segment == null); } function hasAnyData(variant) { @@ -57,6 +57,7 @@ function onInit() { vm.variants = $scope.model.variants; + vm.availableVariants = vm.variants.filter(saveableVariantFilter); if(!$scope.model.title) { localizationService.localize("content_readyToSave").then(function(value){ @@ -64,10 +65,15 @@ }); } - vm.hasPristineVariants = false; - _.each(vm.variants, function (variant) { + + //reset state: + variant.save = false; + variant.publish = false; + + variant.isMandatory = isMandatoryFilter(variant); + if(variant.state !== "NotCreated"){ vm.isNew = false; } @@ -75,34 +81,40 @@ _.each(vm.variants, function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = "_content_variant_" + variant.compositeId; - - //check for pristine variants - if (!vm.hasPristineVariants) { - vm.hasPristineVariants = pristineVariantFilter(variant); - } - if(vm.isNew && hasAnyData(variant)){ variant.save = true; } }); if (vm.variants.length !== 0) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + + _.find(vm.variants, function (v) { + if(v.active) { + //ensure that the current one is selected + v.save = true; + } }); - var active = _.find(vm.variants, function (v) { - return v.active; + vm.availableVariants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); - if (active) { - //ensure that the current one is selected - active.save = true; - } - } else { //disable save button if we have nothing to save $scope.model.disableSubmitButton = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html index 018a1b7add..dd6e96df95 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html @@ -1,81 +1,56 @@ -
          - - +
          -
          +

          - - +

          + ng-repeat="variant in vm.availableVariants track by variant.compositeId"> -
          +
          + server-validation-field="{{variant.htmlId}}" + > + + + * + + + + — {{variant.language.name}} + * + + + + - + + + + {{saveVariantSelectorForm.saveVariantSelector.errorMsg}} + + -
          - -

          - -
          -
          -

          - - -

          -
          - -
          -
          -
          - {{ variant.language.name }} - * -
          - -
          - -
          - -
          -
          {{notification.message}}
          -
          -
          -
          -
          diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js index 5aa7eff1ef..e7f31ac707 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js @@ -12,7 +12,6 @@ vm.clearPublishDate = clearPublishDate; vm.clearUnpublishDate = clearUnpublishDate; vm.dirtyVariantFilter = dirtyVariantFilter; - vm.pristineVariantFilter = pristineVariantFilter; vm.changeSelection = changeSelection; vm.firstSelectedDates = {}; @@ -24,7 +23,7 @@ function onInit() { vm.variants = $scope.model.variants; - vm.hasPristineVariants = false; + vm.displayVariants = vm.variants.slice(0);// shallow copy, we dont want to share the array-object(because we will be performing a sort method) but each entry should be shared (because we need validation and notifications). for (let i = 0; i < vm.variants.length; i++) { origDates.push({ @@ -38,36 +37,42 @@ $scope.model.title = value; }); } + + _.each(vm.variants, function (variant) { + variant.isMandatory = isMandatoryFilter(variant); + + }); // Check for variants: if a node is invariant it will still have the default language in variants // so we have to check for length > 1 if (vm.variants.length > 1) { - _.each(vm.variants, - function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = "_content_variant_" + variant.compositeId; - - //check for pristine variants - if (!vm.hasPristineVariants) { - vm.hasPristineVariants = pristineVariantFilter(variant); + vm.displayVariants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; } - }); - - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); - var active = _.find(vm.variants, function (v) { - return v.active; + _.each(vm.variants, function (v) { + if (v.active) { + v.save = true; + } }); - - if (active) { - //ensure that the current one is selected - active.save = true; - } - + $scope.model.disableSubmitButton = !canSchedule(); } @@ -127,6 +132,7 @@ * @param {any} type publish or unpublish */ function datePickerChange(variant, dateStr, type) { + console.log("datePickerChange", variant, dateStr, type) if (type === 'publish') { setPublishDate(variant, dateStr); } else if (type === 'unpublish') { @@ -241,6 +247,7 @@ * @param {any} variant */ function clearPublishDate(variant) { + console.log("clearPublishDate", variant, variant.releaseDate) if(variant && variant.releaseDate) { variant.releaseDate = null; // we don't have a publish date anymore so we can clear the min date for unpublish @@ -256,6 +263,7 @@ * @param {any} variant */ function clearUnpublishDate(variant) { + console.log("clearUnpublishDate", variant) if(variant && variant.expireDate) { variant.expireDate = null; // we don't have a unpublish date anymore so we can clear the max date for publish @@ -297,8 +305,11 @@ return (variant.active || variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges" || variant.state === "NotCreated"); } - function pristineVariantFilter(variant) { - return !(dirtyVariantFilter(variant)); + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return (variant.language && variant.language.isMandatory === true && variant.segment == null); } /** Returns true if publishing is possible based on if there are un-published mandatory languages */ @@ -321,7 +332,7 @@ return true; } - var isMandatory = variant.language && variant.language.isMandatory; + var isMandatory = variant.segment == null && variant.language && variant.language.isMandatory; //if this variant will show up in the publish-able list var publishable = dirtyVariantFilter(variant); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html index b573acaf52..8f515a10e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html @@ -1,9 +1,9 @@ -
          +
          -
          +

          @@ -15,7 +15,7 @@
          -
          - + @@ -47,7 +47,7 @@
          -
          - +
          @@ -78,48 +78,57 @@
          -
          +

          -
          +
          -
          +
          + on-change="vm.changeSelection(variant)" + server-validation-field="{{variant.htmlId}}" + > + + + * + + + + — {{variant.language.name}} + * + + + + - + +
          - -
          -
          +
          + @@ -138,14 +147,14 @@
          Unpublish:  {{variant.expireDateFormatted}}
          -
          - @@ -153,8 +162,8 @@ Set date
          -
          - + +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js index c85e8d7013..f425dce4a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js @@ -6,8 +6,6 @@ var vm = this; vm.loading = true; - vm.modifiedVariantFilter = modifiedVariantFilter; - vm.unmodifiedVariantFilter = unmodifiedVariantFilter; vm.changeSelection = changeSelection; function onInit() { @@ -21,28 +19,40 @@ }); } + _.each(vm.variants, function (variant) { + variant.isMandatory = isMandatoryFilter(variant); + }); + + vm.availableVariants = vm.variants.filter(publishableVariantFilter); + + if (vm.availableVariants.length !== 0) { - if (vm.variants.length !== 0) { - _.each(vm.variants, - function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = "_content_variant_" + variant.compositeId; - }); - - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + vm.availableVariants = vm.availableVariants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); - var active = _.find(vm.variants, function (v) { - return v.active; + _.each(vm.availableVariants, function (v) { + if(v.active) { + v.save = true; + } }); - if (active) { - //ensure that the current one is selected - active.save = true; - } - } else { //disable save button if we have nothing to save $scope.model.disableSubmitButton = true; @@ -59,20 +69,20 @@ $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected } - function modifiedVariantFilter(variant) { - //determine a variant is 'modified' (meaning it will show up as able to send for approval) - // * it's editor is in a $dirty state - // * it is in Draft state - // * it is published with pending changes - return (variant.active || variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges"); + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return (variant.language && variant.language.isMandatory === true && variant.segment == null); } - function unmodifiedVariantFilter(variant) { - //determine a variant is 'unmodified' (meaning it will NOT show up as able to send for approval) + function publishableVariantFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * variant is active // * it's editor is in a $dirty state - // * it has been published - // * it is not created for that specific language - return (variant.state === "Published" && !variant.isDirty && !variant.active || variant.state === "NotCreated" && !variant.isDirty && !variant.active); + // * it has pending saves + // * it is unpublished + return (variant.active || variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges"); } //when this dialog is closed, reset all 'save' flags diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html index b1da239b1f..91fa1799fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html @@ -1,6 +1,6 @@ -
          +
          -
          +

          @@ -10,34 +10,36 @@
          -
          +
          -
          +
          - + server-validation-field="{{variant.htmlId}}" + > + + + * + + + + — {{variant.language.name}} + * + + + + - + + + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + + -
          - -
          +
          @@ -45,20 +47,4 @@
          -
          -
          -

          -
          - -
          -
          -
          - {{ variant.language.name }} - * -
          - -
          -
          -
          -
          diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js index 3011bf49ee..3db76ee49f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js @@ -7,12 +7,11 @@ var autoSelectedVariants = []; vm.changeSelection = changeSelection; - vm.publishedVariantFilter = publishedVariantFilter; - vm.unpublishedVariantFilter = unpublishedVariantFilter; function onInit() { vm.variants = $scope.model.variants; + vm.unpublishableVariants = vm.variants.filter(publishedVariantFilter) // set dialog title if (!$scope.model.title) { @@ -21,24 +20,38 @@ }); } - _.each(vm.variants, - function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = "_content_variant_" + variant.compositeId; - }); + _.each(vm.variants, function (variant) { + variant.isMandatory = isMandatoryFilter(variant); + }); // node has variants if (vm.variants.length !== 1) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + + vm.unpublishableVariants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); var active = _.find(vm.variants, function (v) { return v.active; }); - if (active) { + if (active && publishedVariantFilter(active)) { //ensure that the current one is selected active.save = true; } @@ -51,21 +64,15 @@ function changeSelection(selectedVariant) { - // disable submit button if nothing is selected - var firstSelected = _.find(vm.variants, function (v) { - return v.save; - }); - $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected - - // if a mandatory variant is selected we want to select all other variants + // if a mandatory variant is selected we want to select all other variants, we cant have anything published if a mandatory variants gets unpublished. // and disable selection for the others - if(selectedVariant.save && selectedVariant.language.isMandatory) { + if(selectedVariant.save && selectedVariant.segment == null && selectedVariant.language && selectedVariant.language.isMandatory) { - angular.forEach(vm.variants, function(variant){ - if(!variant.save && publishedVariantFilter(variant)) { + vm.variants.forEach(function(variant) { + if(!variant.save) { // keep track of the variants we automaically select // so we can remove the selection again - autoSelectedVariants.push(variant.language.culture); + autoSelectedVariants.push(variant); variant.save = true; } variant.disabled = true; @@ -79,12 +86,12 @@ // if a mandatory variant is deselected we want to deselet all the variants // that was automatically selected so it goes back to the state before the mandatory language was selected. // We also want to enable all checkboxes again - if(!selectedVariant.save && selectedVariant.language.isMandatory) { + if(!selectedVariant.save && selectedVariant.segment == null && selectedVariant.language && selectedVariant.language.isMandatory) { - angular.forEach(vm.variants, function(variant){ + vm.variants.forEach( function(variant){ // check if variant was auto selected, then deselect - if(_.contains(autoSelectedVariants, variant.language.culture)) { + if(_.contains(autoSelectedVariants, variant)) { variant.save = false; }; @@ -93,6 +100,19 @@ autoSelectedVariants = []; } + // disable submit button if nothing is selected + var firstSelected = _.find(vm.variants, function (v) { + return v.save; + }); + $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected + + } + + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return (variant.language && variant.language.isMandatory === true && variant.segment == null); } function publishedVariantFilter(variant) { @@ -102,13 +122,6 @@ return (variant.state === "Published" || variant.state === "PublishedPendingChanges"); } - function unpublishedVariantFilter(variant) { - //determine a variant is 'modified' (meaning it will NOT show up as able to unpublish) - // * it's editor is in a $dirty state - // * it is published with pending changes - return (variant.state !== "Published" && variant.state !== "PublishedPendingChanges"); - } - //when this dialog is closed, remove all unpublish and disabled flags $scope.$on('$destroy', function () { for (var i = 0; i < vm.variants.length; i++) { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html index 2c6dea3a6f..94dd9fdac5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html @@ -1,6 +1,6 @@ -
          +
          - +

          @@ -10,12 +10,12 @@

          - +
          - -
          + +
          -
          +
          + server-validation-field="{{variant.htmlId}}" + > + + + * + + + + — {{variant.language.name}} + * + + + + - + + + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + + -
          - -
          - +

          - -
          -
          -

          -
          - -
          -
          -
          - {{ variant.language.name }} - * -
          - -
          - -
          -
          -
          -
          - +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html index f7f1e36fcd..6377fd7c65 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/protect.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -18,31 +18,29 @@ Choose how to restrict access to this page

          - -
          - +
          + - -
          + +
          -
          - +
          + - -
          - + +

          Select the members that should have access to this page

          - +
          Add - +
          @@ -61,7 +59,7 @@

          Select the groups that should have access to this page

          - +
          Add - +

          Select the pages that contain login form and error messages

          - +
          - +
          @@ -119,8 +117,6 @@

          Are you sure you want to remove the protection from this page?

          - - diff --git a/src/Umbraco.Web.UI.Client/src/views/content/restore.html b/src/Umbraco.Web.UI.Client/src/views/content/restore.html index ead6ed91ab..2f7bacca82 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/restore.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/restore.html @@ -39,7 +39,7 @@
          -
          diff --git a/src/Umbraco.Web.UI.Client/src/views/content/rights.html b/src/Umbraco.Web.UI.Client/src/views/content/rights.html index 292db6f105..3395468cb0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/rights.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/rights.html @@ -4,9 +4,9 @@
          - + -
          +
          -
          Set permissions for {{ currentNode.name }}
          -

          +
          Set permissions for {{ currentNode.name }}
          +

          - - - + + - - + @@ -61,24 +62,6 @@ on-edit="vm.editPermissions(group)">
          - -
          -
          -

          -

          - - - - -
          -
          diff --git a/src/Umbraco.Web.UI.Client/src/views/content/sort.html b/src/Umbraco.Web.UI.Client/src/views/content/sort.html index 789f7fe6b5..471f3de956 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/sort.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/sort.html @@ -1,6 +1,6 @@
          -
          {{result.score}} {{result.id}} - {{result.values['nodeName']}}  - - ({{result.fieldCount}} fields) - + {{result.values['nodeName']}} + {{result.values['nodeName']}} +
          {{result.score}} {{result.id}} - {{result.values['nodeName']}}  - - ({{result.fieldCount}} fields) - + {{result.values['nodeName']}} + {{result.values['nodeName']}} +
          - - - - - - - - - - - - -
          FieldValue
          {{key}}{{val}}
          - + + + + + + + + + + + + + + + + + + + +
          FieldValue
          {{key}}{{val}}
          +
          +
          +
          + + + + + + +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js index 8631b09a45..991a8132cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js @@ -1,4 +1,4 @@ -(function() { +(function () { "use strict"; function HealthCheckController($scope, healthCheckResource) { @@ -22,7 +22,7 @@ // Get a (grouped) list of all health checks healthCheckResource.getAllChecks() - .then(function(response) { + .then(function (response) { vm.groups = response; }); @@ -33,11 +33,11 @@ var totalInfo = 0; // count total number of statusses - angular.forEach(group.checks, - function(check) { - angular.forEach(check.status, - function(status) { - switch (status.resultType) { + Utilities.forEach(group.checks, check => { + + if (check.status) { + check.status.forEach(status => { + switch (status.resultType) { case SUCCESS: totalSuccess = totalSuccess + 1; break; @@ -50,9 +50,10 @@ case INFO: totalInfo = totalInfo + 1; break; - } - }); - }); + } + }); + } + }); group.totalSuccess = totalSuccess; group.totalError = totalError; @@ -66,7 +67,7 @@ check.loading = true; check.status = null; healthCheckResource.getStatus(check.id) - .then(function(response) { + .then(function (response) { check.loading = false; check.status = response; }); @@ -75,7 +76,7 @@ function executeAction(check, index, action) { check.loading = true; healthCheckResource.executeAction(action) - .then(function(response) { + .then(function (response) { check.status[index] = response; check.loading = false; }); @@ -94,13 +95,12 @@ group.checkCounter = 0; group.loading = true; - angular.forEach(checks, - function(check) { - + if (checks) { + checks.forEach(check => { check.loading = true; healthCheckResource.getStatus(check.id) - .then(function(response) { + .then(function (response) { check.status = response; group.checkCounter = group.checkCounter + 1; check.loading = false; @@ -112,6 +112,7 @@ } }); }); + } } function openGroup(group) { diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html index 1f81bfc6e2..a73b5eccd7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html @@ -1,30 +1,28 @@
          - + -
          -

          Health Check

          - - -
          -
          -

          The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. - You can add your own health checks, have a look at the documentation for more information about custom health checks.

          + +

          The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. + You can add your own health checks, have a look at the documentation for more information about custom health checks.

          +
          +
          +
          + +
          - -
          -
          +
          +
          -
          +
          - + {{ group.totalSuccess }} + + passed +
          - + {{ group.totalWarning }} + + warning +
          - + {{ group.totalError }} + + failed +
          - + {{ group.totalInfo }} + + suggestion +
          -
          - -
          +
          -
          -
          - ← Back to overview + -
          -
          {{ vm.selectedGroup.name }}
          + label="Check group" + label-key="healthcheck_checkGroup">
          -
          -
          {{ check.name }}
          {{ check.description }}
          - -
          +
          -
          + + Check passed: + + + warning: + + + Check failed: + + + suggestion: +
          @@ -116,16 +133,15 @@
          - +
          - + @@ -133,9 +149,7 @@
          -
          -
          @@ -144,13 +158,8 @@
          -
          -
          -
          -
          -
          diff --git a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.controller.js similarity index 59% rename from src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.controller.js rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.controller.js index b0e0c303cf..423a20d864 100644 --- a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.controller.js @@ -1,4 +1,4 @@ -function modelsBuilderController($scope, $http, umbRequestHelper, modelsBuilderResource) { +function modelsBuilderManagementController($scope, $http, umbRequestHelper, modelsBuilderManagementResource) { var vm = this; @@ -9,8 +9,8 @@ function generate() { vm.generating = true; umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("modelsBuilderBaseUrl", "BuildModels")), - 'Failed to generate.') + $http.post(umbRequestHelper.getApiUrl("modelsBuilderBaseUrl", "BuildModels")), + 'Failed to generate.') .then(function (result) { vm.generating = false; vm.dashboard = result; @@ -19,7 +19,7 @@ function reload() { vm.loading = true; - modelsBuilderResource.getDashboard().then(function (result) { + modelsBuilderManagementResource.getDashboard().then(function (result) { vm.dashboard = result; vm.loading = false; }); @@ -27,7 +27,7 @@ function init() { vm.loading = true; - modelsBuilderResource.getDashboard().then(function (result) { + modelsBuilderManagementResource.getDashboard().then(function (result) { vm.dashboard = result; vm.loading = false; }); @@ -35,4 +35,4 @@ init(); } -angular.module("umbraco").controller("Umbraco.Dashboard.ModelsBuilderController", modelsBuilderController); \ No newline at end of file +angular.module("umbraco").controller("Umbraco.Dashboard.ModelsBuilderManagementController", modelsBuilderManagementController); diff --git a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.html similarity index 94% rename from src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.html rename to src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.html index 0c10c33e39..1ba86bf72b 100644 --- a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/modelsbuildermanagement.html @@ -1,4 +1,4 @@ -
          +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.html index 689bcf8fa2..0b14e9c3ff 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.html @@ -3,52 +3,112 @@
          - -

          - (wait) - {{vm.status}} -

          - -
          - -
          - -

          Memory Cache

          -

          - This button lets you reload the in-memory cache, by entirely reloading it from the database - cache (but it does not rebuild that database cache). This is relatively fast. - Use it when you think that the memory cache has not been properly refreshed, after some events - triggered—which would indicate a minor Umbraco issue. - (note: triggers the reload on all servers in an LB environment). + (wait)

          -
          - +
          +
          +
          + Published Cache Status +
          +
          +
          +
          +
          +
          +
          +

          {{vm.status}}

          +
          +
          +
          + +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          + Caches +
          +
          +
          +
          +
          +
          + Memory Cache +
          +
          + + This button lets you reload the in-memory cache, by entirely reloading it from the database + cache (but it does not rebuild that database cache). This is relatively fast. + Use it when you think that the memory cache has not been properly refreshed, after some events + triggered—which would indicate a minor Umbraco issue. + (note: triggers the reload on all servers in an LB environment). + -

          Database Cache

          +
          -

          - This button lets you rebuild the database cache, ie the content of the cmsContentNu table. - Rebuilding can be expensive. - Use it when reloading is not enough, and you think that the database cache has not been - properly generated—which would indicate some critical Umbraco issue. -

          +
          +
          + +
          +
          +
          -
          - -
          -

          Internals

          +
          +
          + Database Cache +
          +
          + + This button lets you rebuild the database cache, ie the content of the cmsContentNu table. + Rebuilding can be expensive. + Use it when reloading is not enough, and you think that the database cache has not been + properly generated—which would indicate some critical Umbraco issue. + +
          +
          +
          + +
          +
          +
          -

          - This button lets you trigger a NuCache snapshots collection (after running a fullCLR GC). - Unless you know what that means, you probably do not need to use it. -

          -
          - +
          +
          + Internals +
          +
          + + This button lets you trigger a NuCache snapshots collection (after running a fullCLR GC). + Unless you know what that means, you probably do not need to use it. + + +
          +
          +
          + +
          +
          +
          +
          +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.controller.js new file mode 100644 index 0000000000..295263a47c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.controller.js @@ -0,0 +1,38 @@ +function ProfilerController($scope, $cookies, $http, umbRequestHelper) { + var vm = this; + + vm.loading = true; + vm.toggle = toggle; + + function toggle() { + if (vm.alwaysOn === true) { + $cookies.remove("UMB-DEBUG", { + path: "/" + }); + vm.alwaysOn = false; + } + else { + $cookies.put("UMB-DEBUG", "true", { + path: "/", + expires: "Tue, 01 Jan 2100 00:00:01 GMT" + }); + vm.alwaysOn = true; + } + } + + function init() { + vm.alwaysOn = $cookies.get("UMB-DEBUG") === "true"; + + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("webProfilingBaseUrl", "GetStatus")), + "Failed to retrieve status for web profiling" + ).then(function(status) { + vm.loading = false; + vm.profilerEnabled = status.Enabled; + }); + } + + init(); +} + +angular.module("umbraco").controller("Umbraco.Dashboard.ProfilerController", ProfilerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html new file mode 100644 index 0000000000..95c6af9f79 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html @@ -0,0 +1,57 @@ +
          + +
          + +
          + + + +

          + Performance profiling +

          +
          +
          + +

          + Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. +

          +

          + If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. +

          +

          + If you want the profiler to be activated by default for all page renderings, you can use the toggle below. + It will set a cookie in your browser, which then activates the profiler automatically. + In other words, the profiler will only be active by default in your browser - not everyone else's. +

          +
          +
          +
          +
          + + +
          +
          +

          + Friendly reminder +

          + +

          + You should never let a production site run in debug mode. Debug mode is turned off by setting debug="false" on the <compilation /> element in web.config. +

          +
          +
          +
          + +

          + Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site. +

          +

          + Debug mode is turned on by setting debug="true" on the <compilation /> element in web.config. +

          +
          +
          +
          +
          +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedstatus.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedstatus.html index 458f1e5148..3a8a99ff9b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedstatus.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedstatus.html @@ -1,11 +1,8 @@ 
          - - -

          Published Cache

          +
          + +
          - +
          -
          -
          -
          diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html index cc3e57bdc4..0ad88b9894 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html @@ -1,14 +1,30 @@ -

          Start here

          -

          This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section:

          -
          Find out more:
          +

          + Start here +

          + +

          This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section:

          +
          : +
          + Find out more: +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardvideos.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardvideos.html index 64a9c819ab..96d015f758 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardvideos.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardvideos.html @@ -1,12 +1,18 @@ -

          Hours of Umbraco training videos are only a click away

          -

          Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

          +

          + Hours of Umbraco training videos are only a click away +

          + +

          Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

          +
          -

          To get you started:

          +

          + To get you started: +

          • diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html index c359e7bfd1..55039831ac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html @@ -6,20 +6,20 @@
            diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.delete.controller.js index 2a1fd5255a..4542cde343 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.delete.controller.js @@ -8,7 +8,13 @@ */ function DataTypeDeleteController($scope, dataTypeResource, treeService, navigationService, localizationService) { - $scope.performDelete = function() { + var vm = this; + + vm.propertyJoinSeparator = ', '; + vm.hasReferences = false; + vm.references = []; + + vm.performDelete = function() { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; @@ -24,7 +30,7 @@ function DataTypeDeleteController($scope, dataTypeResource, treeService, navigat }); }; - $scope.performContainerDelete = function () { + vm.performContainerDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; @@ -41,16 +47,42 @@ function DataTypeDeleteController($scope, dataTypeResource, treeService, navigat }; - $scope.cancel = function() { + vm.cancel = function() { navigationService.hideDialog(); }; - $scope.labels = {}; + vm.onReferenceClicked = function(event) { + if (event.metaKey !== true) { + navigationService.hideDialog(); + } + }; + + vm.labels = {}; localizationService - .format(["editdatatype_yesDelete", "editdatatype_andAllRelated"], "%0% " + $scope.currentNode.name + " %1%") + .localize("editdatatype_acceptDeleteConsequence", [$scope.currentNode.name]) .then(function (data) { - $scope.labels.deleteConfirm = data; + vm.labels.deleteConfirm = data; }); + + var init = function() { + + if($scope.currentNode.nodeType === "dataTypes") { + + vm.loading = true; + + dataTypeResource.getReferences($scope.currentNode.id) + .then(function (data) { + vm.loading = false; + vm.references = data; + + vm.hasReferences = data.documentTypes.length > 0 || data.mediaTypes.length > 0 || data.memberTypes.length > 0; + }); + + } + + } + + init(); } angular.module("umbraco").controller("Umbraco.Editors.DataType.DeleteController", DataTypeDeleteController); diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js index ead73beab8..66983bbc05 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js @@ -6,22 +6,40 @@ * @description * The controller for the content editor */ -function DataTypeEditController($scope, $routeParams, appState, navigationService, dataTypeResource, serverValidationManager, contentEditingHelper, formHelper, editorState, dataTypeHelper, eventsService) { +function DataTypeEditController($scope, $routeParams, appState, navigationService, dataTypeResource, serverValidationManager, contentEditingHelper, formHelper, editorState, dataTypeHelper, eventsService, localizationService) { + + var evts = []; + var vm = this; + + vm.header = {}; + vm.header.editorfor = "visuallyHiddenTexts_newDataType"; + vm.header.setPageTitle = true; //setup scope vars - $scope.page = {}; - $scope.page.loading = false; - $scope.page.nameLocked = false; - $scope.page.menu = {}; - $scope.page.menu.currentSection = appState.getSectionState("currentSection"); - $scope.page.menu.currentNode = null; - var evts = []; + vm.page = {}; + vm.page.loading = false; + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; + //set up the standard data type props + vm.properties = { + selectedEditor: { + alias: "selectedEditor", + description: "Select a property editor", + label: "Property editor" + } + }; + + //setup the pre-values as props + vm.preValues = []; + + //method used to configure the pre-values when we retrieve them from the server function createPreValueProps(preVals) { - $scope.preValues = []; + vm.preValues = []; for (var i = 0; i < preVals.length; i++) { - $scope.preValues.push({ + vm.preValues.push({ hideLabel: preVals[i].hideLabel, alias: preVals[i].key, description: preVals[i].description, @@ -32,67 +50,33 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic }); } } - - //set up the standard data type props - $scope.properties = { - selectedEditor: { - alias: "selectedEditor", - description: "Select a property editor", - label: "Property editor" - }, - selectedEditorId: { - alias: "selectedEditorId", - label: "Property editor alias" + + + function setHeaderNameState(content) { + if(content.isSystem == 1) { + vm.page.nameLocked = true; } - }; - - //setup the pre-values as props - $scope.preValues = []; - - if ($routeParams.create) { - - $scope.page.loading = true; - $scope.showIdentifier = false; - - //we are creating so get an empty data type item - dataTypeResource.getScaffold($routeParams.id) - .then(function(data) { - - $scope.preValuesLoaded = true; - $scope.content = data; - - setHeaderNameState($scope.content); - - //set a shared state - editorState.set($scope.content); - - $scope.page.loading = false; - - }); } - else { - loadDataType(); - } - + + function loadDataType() { - $scope.page.loading = true; - - $scope.showIdentifier = true; + vm.page.loading = true; + vm.showIdentifier = true; //we are editing so get the content item from the server dataTypeResource.getById($routeParams.id) .then(function(data) { - $scope.preValuesLoaded = true; - $scope.content = data; + vm.preValuesLoaded = true; + vm.content = data; - createPreValueProps($scope.content.preValues); + createPreValueProps(vm.content.preValues); - setHeaderNameState($scope.content); + setHeaderNameState(vm.content); //share state - editorState.set($scope.content); + editorState.set(vm.content); //in one particular special case, after we've created a new item we redirect back to the edit // route but there might be server validation errors in the collection which we need to display @@ -101,49 +85,22 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic serverValidationManager.notifyAndClearAllSubscriptions(); navigationService.syncTree({ tree: "datatypes", path: data.path }).then(function (syncArgs) { - $scope.page.menu.currentNode = syncArgs.node; + vm.page.menu.currentNode = syncArgs.node; }); - $scope.page.loading = false; + vm.page.loading = false; }); - } - - $scope.$watch("content.selectedEditor", function (newVal, oldVal) { - - //when the value changes, we need to dynamically load in the new editor - if (newVal && (newVal != oldVal && (oldVal || $routeParams.create))) { - //we are editing so get the content item from the server - var currDataTypeId = $routeParams.create ? undefined : $routeParams.id; - dataTypeResource.getPreValues(newVal, currDataTypeId) - .then(function (data) { - $scope.preValuesLoaded = true; - $scope.content.preValues = data; - createPreValueProps($scope.content.preValues); - - setHeaderNameState($scope.content); - - //share state - editorState.set($scope.content); - }); - } - }); - - function setHeaderNameState(content) { - - if(content.isSystem == 1) { - $scope.page.nameLocked = true; - } } - $scope.save = function() { + function saveDataType() { if (formHelper.submitForm({ scope: $scope })) { - $scope.page.saveButtonState = "busy"; + vm.page.saveButtonState = "busy"; - dataTypeResource.save($scope.content, $scope.preValues, $routeParams.create) + dataTypeResource.save(vm.content, vm.preValues, $routeParams.create) .then(function(data) { formHelper.resetForm({ scope: $scope }); @@ -156,41 +113,42 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic } }); - setHeaderNameState($scope.content); + setHeaderNameState(vm.content); //share state - editorState.set($scope.content); + editorState.set(vm.content); navigationService.syncTree({ tree: "datatypes", path: data.path, forceReload: true }).then(function (syncArgs) { - $scope.page.menu.currentNode = syncArgs.node; + vm.page.menu.currentNode = syncArgs.node; }); - $scope.page.saveButtonState = "success"; + vm.page.saveButtonState = "success"; - dataTypeHelper.rebindChangedProperties($scope.content, data); + dataTypeHelper.rebindChangedProperties(vm.content, data); }, function(err) { //NOTE: in the case of data type values we are setting the orig/new props // to be the same thing since that only really matters for content/media. contentEditingHelper.handleSaveError({ - redirectOnFailure: false, err: err }); - $scope.page.saveButtonState = "error"; + vm.page.saveButtonState = "error"; //share state - editorState.set($scope.content); + editorState.set(vm.content); }); } }; - + + vm.save = saveDataType; + evts.push(eventsService.on("app.refreshEditor", function(name, error) { loadDataType(); })); - + //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { @@ -198,6 +156,80 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic } }); + function init() { + + $scope.$watch("vm.content.selectedEditor", function (newVal, oldVal) { + + //when the value changes, we need to dynamically load in the new editor + if (newVal && (newVal != oldVal && (oldVal || $routeParams.create))) { + //we are editing so get the content item from the server + var currDataTypeId = $routeParams.create ? undefined : $routeParams.id; + dataTypeResource.getPreValues(newVal, currDataTypeId) + .then(function (data) { + vm.preValuesLoaded = true; + vm.content.preValues = data; + createPreValueProps(vm.content.preValues); + + setHeaderNameState(vm.content); + + //share state + editorState.set(vm.content); + }); + } + }); + + if ($routeParams.create) { + + vm.page.loading = true; + vm.showIdentifier = false; + + //we are creating so get an empty data type item + dataTypeResource.getScaffold($routeParams.id) + .then(function(data) { + + vm.preValuesLoaded = true; + vm.content = data; + + setHeaderNameState(vm.content); + + //set a shared state + editorState.set(vm.content); + + vm.page.loading = false; + + }); + } + else { + loadDataType(); + } + + var labelKeys = [ + "general_settings", + "general_info" + ]; + + localizationService.localizeMany(labelKeys).then(function (values) { + + vm.page.navigation = [ + { + "name": values[0], + "alias": "settings", + "icon": "icon-settings", + "view": "views/datatypes/views/datatype.settings.html", + "active": true + }, + { + "name": values[1], + "alias": "info", + "icon": "icon-info", + "view": "views/datatypes/views/datatype.info.html" + } + ]; + }); + } + + init(); + } angular.module("umbraco").controller("Umbraco.Editors.DataType.EditController", DataTypeEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/delete.html index 5db72deaa5..bdce1723a0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/delete.html @@ -1,29 +1,122 @@ -
            +
            - -

            - Are you sure you want to delete {{currentNode.name}}? -

            - + +

            + Are you sure you want to delete {{currentNode.name}}? +

            +
            -

            - All property types & property data - using this data type will be deleted permanently, please confirm you want to delete these as well. -

            -
            - - + - +
            +

            + Are you sure you want to delete {{currentNode.name}}? +

            +
            + +
            + +

            + Deleting {{currentNode.name}} will have the following consequence +

            + +
            + + + +
            + +
            + +
            + + + + + + + + + + + + + + +
            NameProperties
            {{::relation.name}}

            {{::property.name}}

            +
            + + + +
            + +
            + +
            + + + + + + + + + + + + + + +
            NameProperties
            {{::relation.name}}

            {{::property.name}}

            +
            + + + +
            + +
            + +
            + + + + + + + + + + + + + + +
            NameProperties
            {{::relation.name}}

            {{::property.name}}

            + +
            + + + + +
            + + +
            diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html index 2f061de83a..2d481de852 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html @@ -1,81 +1,35 @@ -
            +
            - +
            + + + + - + + + + - + + + + + + + + - - - - - - - - - - - -
            {{content.id}}
            - {{content.key}} -
            - - -
            - - - Required - -
            - -
            - - -
            {{content.selectedEditor}}
            -
            - - - - - -
            - -
            - -
            - - - - - - - - - - - - - -
            - -
            diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html index 2723dd305d..7e7e70d650 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html @@ -7,9 +7,7 @@ Select the folder to move {{source.name}} to in the tree structure below

            -
            -
            -
            +
            @@ -45,9 +43,9 @@
            diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js index 596f848abe..ea1ca00b21 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js @@ -12,6 +12,10 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes //setup scope vars vm.nameDirty = false; + vm.header = {}; + vm.header.editorfor = "template_insertDictionaryItem"; + vm.header.setPageTitle = true; + vm.page = {}; vm.page.loading = false; vm.page.nameLocked = false; @@ -91,7 +95,6 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes function (err) { contentEditingHelper.handleSaveError({ - redirectOnFailure: false, err: err }); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html index 41320108e7..c2b0adec6f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html @@ -13,7 +13,9 @@ hide-description="true" hide-alias="true" on-back="vm.back()" - show-back-button="vm.showBackButton"> + show-back-button="vm.showBackButton" + editorfor="vm.header.editorfor" + setpagetitle="vm.header.setPageTitle"> diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html index 4b90c244e8..8b81462ad5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html @@ -7,9 +7,7 @@ Select the folder to copy {{source.name}} to in the tree structure below

            -
            -
            -
            +
            @@ -45,9 +43,9 @@
            @@ -54,7 +54,7 @@
            - + @@ -79,23 +79,29 @@
            - - - - - - + + + + + - - - - - - + + + + + - +
            + +
            diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.controller.js index 919239a95d..f0c8a18251 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.controller.js @@ -6,7 +6,7 @@ * @description * The controller for deleting content */ -function DocumentTypesDeleteController($scope, dataTypeResource, contentTypeResource, treeService, navigationService) { +function DocumentTypesDeleteController($scope, dataTypeResource, contentTypeResource, treeService, navigationService, localizationService) { $scope.performDelete = function() { @@ -47,6 +47,13 @@ function DocumentTypesDeleteController($scope, dataTypeResource, contentTypeReso $scope.cancel = function() { navigationService.hideDialog(); }; + + $scope.labels = {}; + localizationService + .format(["contentTypeEditor_yesDelete", "contentTypeEditor_andAllDocuments"], "%0% " + $scope.currentNode.name + " %1%") + .then(function (data) { + $scope.labels.deleteConfirm = data; + }); } angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.DeleteController", DocumentTypesDeleteController); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.html index 2a0185e186..e7fdf4f96e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.html @@ -11,7 +11,7 @@
            + on-cancel="cancel" confirm-button-style="danger">
            @@ -28,12 +28,10 @@
            - + + - +
            diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index 62cf9d3e88..42dba5f580 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js @@ -18,6 +18,8 @@ var documentTypeId = $routeParams.id; var create = $routeParams.create; var noTemplate = $routeParams.notemplate; + var isElement = $routeParams.iselement; + var allowVaryByCulture = $routeParams.culturevary; var infiniteMode = $scope.model && $scope.model.infiniteMode; vm.save = save; @@ -25,6 +27,9 @@ vm.currentNode = null; vm.contentType = {}; + vm.header = {}; + vm.header.editorfor = "content_documentType"; + vm.header.setPageTitle = true; vm.labels = {}; vm.submitButtonKey = "buttons_save"; vm.generateModelsKey = "buttons_saveAndGenerateModels"; @@ -33,7 +38,7 @@ vm.page.loading = false; vm.page.saveButtonState = "init"; vm.page.navigation = []; - + var labelKeys = [ "general_design", "general_listView", @@ -43,7 +48,7 @@ "shortcuts_navigateSections", "shortcuts_addGroup", "shortcuts_addProperty", - "shortcuts_addEditor", + "defaultdialogs_selectEditor", "shortcuts_editDataType", "shortcuts_toggleListView", "shortcuts_toggleAllowAsRoot", @@ -56,10 +61,13 @@ function onInit() { // get init values from model when in infinite mode - if(infiniteMode) { + if (infiniteMode) { documentTypeId = $scope.model.id; create = $scope.model.create; - noTemplate = $scope.model.notemplate; + if (create && !documentTypeId) documentTypeId = -1; + noTemplate = $scope.model.notemplate || $scope.model.noTemplate; + isElement = $scope.model.isElement; + allowVaryByCulture = $scope.model.allowVaryByCulture; vm.submitButtonKey = "buttons_saveAndClose"; vm.generateModelsKey = "buttons_generateModelsAndClose"; } @@ -89,8 +97,7 @@ "name": vm.labels.design, "alias": "design", "icon": "icon-document-dashed-line", - "view": "views/documenttypes/views/design/design.html", - "active": true + "view": "views/documenttypes/views/design/design.html" }, { "name": vm.labels.listview, @@ -291,12 +298,34 @@ }); vm.page.navigation = buttons; + initializeActiveNavigationPanel(); + } + + function initializeActiveNavigationPanel() { + // Initialise first loaded panel based on page route paramater + // i.e. ?view=design|listview|permissions + var initialViewSetFromRouteParams = false; + var view = $routeParams.view; + if (view) { + var viewPath = "views/documenttypes/views/" + view + "/" + view + ".html"; + for (var i = 0; i < vm.page.navigation.length; i++) { + if (vm.page.navigation[i].view === viewPath) { + vm.page.navigation[i].active = true; + initialViewSetFromRouteParams = true; + break; + } + } + } + + if (initialViewSetFromRouteParams === false) { + vm.page.navigation[0].active = true; + } } /* ---------- SAVE ---------- */ function save() { - saveInternal().then(angular.noop, angular.noop); + saveInternal().then(Utilities.noop, Utilities.noop); } /** This internal save method performs the actual saving and returns a promise, not to be bound to any buttons but used by other bound methods */ @@ -317,10 +346,7 @@ saveMethod: contentTypeResource.save, scope: $scope, content: vm.contentType, - //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc - // type when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, + infiniteMode: infiniteMode, // we need to rebind... the IDs that have been created! rebindCallback: function (origContentType, savedContentType) { vm.contentType.id = savedContentType.id; @@ -409,7 +435,14 @@ contentType.defaultTemplate = contentTypeHelper.insertDefaultTemplatePlaceholder(contentType.defaultTemplate); contentType.allowedTemplates = contentTypeHelper.insertTemplatePlaceholder(contentType.allowedTemplates); } - + // set isElement checkbox by default + if (isElement) { + contentType.isElement = true; + } + // set vary by culture checkbox by default + if (allowVaryByCulture) { + contentType.allowCultureVariant = true; + } // convert icons for content type convertLegacyIcons(contentType); @@ -480,15 +513,30 @@ loadDocumentType(); })); - evts.push(eventsService.on("editors.documentType.saved", function(name, args) { - if(args.documentType.allowedTemplates.length > 0){ - navigationService.syncTree({ tree: "templates", path: [], forceReload: true }) - .then(function (syncArgs) { - navigationService.reloadNode(syncArgs.node) - }); + evts.push(eventsService.on("editors.documentType.reload", function (name, args) { + if (args && args.node && vm.contentType.id === args.node.id) { + loadDocumentType(); } })); + evts.push(eventsService.on("editors.documentType.saved", function(name, args) { + if(args.documentType.allowedTemplates.length > 0) { + navigationService.hasTree("templates").then(function (treeExists) { + if (treeExists) { + navigationService.syncTree({ tree: "templates", path: [], forceReload: true }) + .then(function (syncArgs) { + navigationService.reloadNode(syncArgs.node) + } + ); + } + }); + } + })); + + evts.push(eventsService.on("editors.groupsBuilder.changed", function(name, args) { + angularHelper.getCurrentForm($scope).$setDirty(); + })); + //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.html index 52f4d710ba..cfbee358eb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.html @@ -12,7 +12,9 @@ key="vm.contentType.key" description="vm.contentType.description" navigation="vm.page.navigation" - icon="vm.contentType.icon"> + icon="vm.contentType.icon" + editorfor="vm.header.editorfor" + setpagetitle="vm.header.setPageTitle"> diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/importdocumenttype.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/importdocumenttype.html index 5fd453bcd7..8f84a0cceb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/importdocumenttype.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/importdocumenttype.html @@ -9,7 +9,7 @@
            - diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js index a3b422e4f3..da8dee8e03 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js @@ -9,7 +9,7 @@ (function() { 'use strict'; - function PermissionsController($scope, contentTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) { + function PermissionsController($scope, $timeout, contentTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) { /* ----------- SCOPE VARIABLES ----------- */ @@ -20,11 +20,15 @@ vm.selectedChildren = []; vm.overlayTitle = ""; + vm.showAllowSegmentationOption = Umbraco.Sys.ServerVariables.umbracoSettings.showAllowSegmentationForDocumentTypes || false; vm.addChild = addChild; vm.removeChild = removeChild; + vm.sortChildren = sortChildren; vm.toggleAllowAsRoot = toggleAllowAsRoot; vm.toggleAllowCultureVariants = toggleAllowCultureVariants; + vm.toggleAllowSegmentVariants = toggleAllowSegmentVariants; + vm.canToggleIsElement = false; vm.toggleIsElement = toggleIsElement; /* ---------- INIT ---------- */ @@ -48,9 +52,16 @@ if($scope.model.id === 0) { contentTypeHelper.insertChildNodePlaceholder(vm.contentTypes, $scope.model.name, $scope.model.icon, $scope.model.id); } - }); + // Can only switch to an element type if there are no content nodes already created from the type. + if ($scope.model.id > 0 && !$scope.model.isElement ) { + contentTypeResource.hasContentNodes($scope.model.id).then(function (result) { + vm.canToggleIsElement = !result; + }); + } else { + vm.canToggleIsElement = true; + } } function addChild($event) { @@ -84,6 +95,13 @@ $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); } + function sortChildren() { + // we need to wait until the next digest cycle for vm.selectedChildren to be updated + $timeout(function () { + $scope.model.allowedContentTypes = _.pluck(vm.selectedChildren, "id"); + }); + } + // note: "safe toggling" here ie handling cases where the value is undefined, etc function toggleAllowAsRoot() { @@ -94,6 +112,10 @@ $scope.model.allowCultureVariant = $scope.model.allowCultureVariant ? false : true; } + function toggleAllowSegmentVariants() { + $scope.model.allowSegmentVariant = $scope.model.allowSegmentVariant ? false : true; + } + function toggleIsElement() { $scope.model.isElement = $scope.model.isElement ? false : true; } diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html index e890921ad4..b44ce0ff7e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html @@ -33,7 +33,8 @@ parent-icon="model.icon" parent-id="model.id" on-add="vm.addChild" - on-remove="vm.removeChild"> + on-remove="vm.removeChild" + on-sort="vm.sortChildren">
          @@ -42,8 +43,8 @@
          -
          - +
          +
          @@ -56,16 +57,35 @@
          + +
          + +
          +
          + +
          + +
          + + +
          + +
          +
          +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index a11aa8bff8..96441e6101 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function LanguagesEditController($scope, $timeout, $location, $routeParams, navigationService, notificationsService, localizationService, languageResource, contentEditingHelper, formHelper, eventsService) { + function LanguagesEditController($scope, $q, $timeout, $location, $routeParams, overlayService, navigationService, notificationsService, localizationService, languageResource, contentEditingHelper, formHelper, eventsService) { var vm = this; @@ -20,6 +20,8 @@ vm.toggleMandatory = toggleMandatory; vm.toggleDefault = toggleDefault; + var currCulture = null; + function init() { // localize labels @@ -32,7 +34,8 @@ "languages_addLanguage", "languages_noFallbackLanguageOption", "languages_fallbackLanguageDescription", - "languages_fallbackLanguage" + "languages_fallbackLanguage", + "defaultdialogs_confirmSure" ]; localizationService.localizeMany(labelKeys).then(function (values) { @@ -43,6 +46,7 @@ vm.labels.defaultLanguageHelp = values[4]; vm.labels.addLanguage = values[5]; vm.labels.noFallbackLanguageOption = values[6]; + vm.labels.areYouSure = values[9]; $scope.properties = { fallbackLanguage: { @@ -54,45 +58,55 @@ if ($routeParams.create) { vm.page.name = vm.labels.addLanguage; - languageResource.getCultures().then(function (culturesDictionary) { - var cultures = []; - angular.forEach(culturesDictionary, function (value, key) { - cultures.push({ - name: key, - displayName: value - }); - }); - vm.availableCultures = cultures; - }); } }); vm.loading = true; - languageResource.getAll().then(function (languages) { + + var promises = []; + + //load all culture/languages + promises.push(languageResource.getCultures().then(function (culturesDictionary) { + var cultures = []; + angular.forEach(culturesDictionary, function (value, key) { + cultures.push({ + name: key, + displayName: value + }); + }); + vm.availableCultures = cultures; + })); + + //load all possible fallback languages + promises.push(languageResource.getAll().then(function (languages) { vm.availableLanguages = languages.filter(function (l) { return $routeParams.id != l.id; }); vm.loading = false; - }); + })); if (!$routeParams.create) { - vm.loading = true; - - languageResource.getById($routeParams.id).then(function(lang) { + promises.push(languageResource.getById($routeParams.id).then(function (lang) { vm.language = lang; vm.page.name = vm.language.name; - /* we need to store the initial default state so we can disabel the toggle if it is the default. + /* we need to store the initial default state so we can disable the toggle if it is the default. we need to prevent from not having a default language. */ - vm.initIsDefault = angular.copy(vm.language.isDefault); + vm.initIsDefault = Utilities.copy(vm.language.isDefault); - vm.loading = false; makeBreadcrumbs(); - }); + + //store to check if we are changing the lang culture + currCulture = vm.language.culture; + })); } + $q.all(promises, function () { + vm.loading = false; + }); + $timeout(function () { navigationService.syncTree({ tree: "languages", path: "-1" }); }); @@ -103,31 +117,54 @@ if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; - languageResource.save(vm.language).then(function (lang) { + //check if the culture is being changed + if (currCulture && vm.language.culture !== currCulture) { - formHelper.resetForm({ scope: $scope }); + const changeCultureAlert = { + title: vm.labels.areYouSure, + view: "views/languages/overlays/change.html", + submitButtonLabelKey: "general_continue", + submit: function (model) { + saveLanguage(); + overlayService.close(); + }, + close: function () { + overlayService.close(); + vm.page.saveButtonState = "init"; + } + }; - vm.language = lang; - vm.page.saveButtonState = "success"; - localizationService.localize("speechBubbles_languageSaved").then(function(value){ - notificationsService.success(value); - }); - - // emit event when language is created or updated/saved - var args = { language: lang, isNew: $routeParams.create ? true : false }; - eventsService.emit("editors.languages.languageSaved", args); - - back(); - - }, function (err) { - vm.page.saveButtonState = "error"; - - formHelper.handleError(err); - - }); + overlayService.open(changeCultureAlert); + } + else { + saveLanguage(); + } } + } + function saveLanguage() { + languageResource.save(vm.language).then(function (lang) { + formHelper.resetForm({ scope: $scope }); + + vm.language = lang; + vm.page.saveButtonState = "success"; + localizationService.localize("speechBubbles_languageSaved").then(function (value) { + notificationsService.success(value); + }); + + // emit event when language is created or updated/saved + var args = { language: lang, isNew: $routeParams.create ? true : false }; + eventsService.emit("editors.languages.languageSaved", args); + + back(); + + }, function (err) { + vm.page.saveButtonState = "error"; + + formHelper.handleError(err); + + }); } function back() { @@ -145,12 +182,12 @@ function toggleDefault() { // it shouldn't be possible to uncheck the default language - if(vm.initIsDefault) { + if (vm.initIsDefault) { return; } vm.language.isDefault = !vm.language.isDefault; - if(vm.language.isDefault) { + if (vm.language.isDefault) { vm.showDefaultLanguageInfo = true; } else { vm.showDefaultLanguageInfo = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overlays/change.html b/src/Umbraco.Web.UI.Client/src/views/languages/overlays/change.html new file mode 100644 index 0000000000..c5e813fa51 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overlays/change.html @@ -0,0 +1,7 @@ +
          + +
          + Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt. +
          + +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js index c1972ec067..90d418cf77 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js @@ -70,6 +70,7 @@ view: "views/languages/overlays/delete.html", language: language, submitButtonLabelKey: "contentTypeEditor_yesDelete", + submitButtonStyle: "danger", submit: function (model) { performDelete(model.language); overlayService.close(); @@ -84,7 +85,7 @@ overlayService.open(dialog); }); - event.preventDefault() + event.preventDefault(); event.stopPropagation(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html index 15a9c84367..f6ae77ebc4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html @@ -16,7 +16,7 @@ - @@ -39,7 +39,7 @@ - {{ language.name }} + {{ language.name }} {{ language.culture }} @@ -58,7 +58,9 @@ {{vm.getLanguageById(language.fallbackLanguageId).name}} - (none) + + (none) + 1000" + { + "name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", + "query": "Has(Duration) and Duration > 1000" }, - { - "name": "Find all logs that are from the namespace 'Umbraco.Core'", - "query": "StartsWith(SourceContext, 'Umbraco.Core')" + { + "name": "Find all logs that are from the namespace 'Umbraco.Core'", + "query": "StartsWith(SourceContext, 'Umbraco.Core')" }, - { - "name": "Find all logs that use a specific log message template", - "query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" + { + "name": "Find all logs that use a specific log message template", + "query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" } ] - }); + }); - var numOfErrors = logViewerResource.getNumberOfErrors().then(function (data) { + var numOfErrors = logViewerResource.getNumberOfErrors(vm.startDate, vm.endDate).then(function (data) { vm.numberOfErrors = data; }); - var logCounts = logViewerResource.getLogLevelCounts().then(function (data) { + var logCounts = logViewerResource.getLogLevelCounts(vm.startDate, vm.endDate).then(function (data) { vm.logTypeData = []; - vm.logTypeData.push(data.Information); - vm.logTypeData.push(data.Debug); - vm.logTypeData.push(data.Warning); - vm.logTypeData.push(data.Error); - vm.logTypeData.push(data.Fatal); + + for (let [key, value] of Object.entries(data)) { + const index = vm.logTypeLabels.findIndex(x => key.startsWith(x)); + if (index > -1) { + vm.logTypeData[index] = value; + } + } }); - var commonMsgs = logViewerResource.getMessageTemplates().then(function(data){ + var commonMsgs = logViewerResource.getMessageTemplates(vm.startDate, vm.endDate).then(function (data) { vm.commonLogMessages = data; }); + + var logLevel = logViewerResource.getLogLevel().then(function(data) { + vm.logLevel = data; + const index = vm.logTypeLabels.findIndex(x => vm.logLevel.startsWith(x)); + vm.logLevelColor = index > -1 ? vm.logTypeColors[index] : '#000'; + }); - //Set loading indicatior to false when these 3 queries complete - $q.all([savedSearches, numOfErrors, logCounts, commonMsgs]).then(function(data) { + //Set loading indicator to false when these 3 queries complete + $q.all([savedSearches, numOfErrors, logCounts, commonMsgs, logLevel]).then(function () { vm.loading = false; }); $timeout(function () { - navigationService.syncTree({ tree: "logViewer", path: "-1" }); + navigationService.syncTree({ + tree: "logViewer", + path: "-1" + }); }); } - function searchLogQuery(logQuery){ - $location.path("/settings/logViewer/search").search({lq: logQuery}); + function searchLogQuery(logQuery) { + $location.path("/settings/logViewer/search").search({ + lq: logQuery, + startDate: vm.startDate, + endDate: vm.endDate + }); } - function findMessageTemplate(template){ + function findMessageTemplate(template) { var logQuery = "@MessageTemplate='" + template.MessageTemplate + "'"; searchLogQuery(logQuery); } + function getDateRangeLabel(suffix) { + return "Log Overview for " + suffix; + } + + function searchErrors(){ + var logQuery = "@Level='Fatal' or @Level='Error' or Has(@Exception)"; + searchLogQuery(logQuery); + } + preFlightCheck(); + ///////////////////// + + vm.config = { + enableTime: false, + dateFormat: "Y-m-d", + time_24hr: false, + mode: "range", + maxDate: "today", + conjunction: " to " + }; + + vm.dateRangeChange = function (selectedDates, dateStr, instance) { + + if (selectedDates.length > 0) { + + // Update view by re-requesting route with updated querystring. + // By doing this we make sure the URL matches the selected time period, aiding sharing the link. + // Also resolves a minor layout issue where the " to " conjunction between the selected dates + // is collapsed to a comma. + const startDate = selectedDates[0].toIsoDateString(); + const endDate = selectedDates[selectedDates.length - 1].toIsoDateString(); // Take the last date as end + $location.path("/settings/logViewer/overview").search({ + startDate: startDate, + endDate: endDate + }); + } + + } } angular.module("umbraco").controller("Umbraco.Editors.LogViewer.OverviewController", LogViewerOverviewController); diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html index a46853f97e..b58d3c035e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html @@ -3,7 +3,7 @@ -
          - - - -

          Today's log file is too large to be viewed and would cause performance problems.

          -

          If you need to view the log files, try opening them manually

          -
          -
          -
          - -
          +
          - - - - - - - - - - - - -
          - All Logs -
          - {{search.name}} -
          -
          -
          - - - - - - Total Unique Message types: {{ vm.commonLogMessages.length }} - - - +
          + + + +

          Today's log file is too large to be viewed and would cause performance problems.

          +

          If you need to view the log files, try opening them manually

          +
          +
          +
          +
          + + + + +
          + - - -
          - {{ template.MessageTemplate }} - - {{ template.Count }} + All Logs
          - Show More -
          -
          + + + + {{search.name}} + + + + + + + + + + + Total Unique Message types: {{ vm.commonLogMessages.length }} + + + + + + + +
          + {{ template.MessageTemplate }} + + {{ template.Count }} +
          + + +
          +
          +
          -
          - - - - - {{ vm.numberOfErrors }} - - - + - - + - - - - + + + +
          +
          + + + + + {{ vm.numberOfErrors }} + + + + + + + {{ vm.logLevel }} + + +
          + + + + + + + + + + + + +
          +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/search.controller.js b/src/Umbraco.Web.UI.Client/src/views/logviewer/search.controller.js index 74395ffb25..d03f292717 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/search.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/search.controller.js @@ -11,26 +11,28 @@ vm.showBackButton = true; vm.page = {}; + // this array is also used to map the logTypeColor param onto the log items + // in setLogTypeColors() vm.logLevels = [ { name: 'Verbose', - logTypeColor: 'gray' + logTypeColor: '' }, { name: 'Debug', - logTypeColor: 'secondary' + logTypeColor: 'gray' }, { name: 'Information', - logTypeColor: 'primary' + logTypeColor: 'success' }, { name: 'Warning', - logTypeColor: 'warning' + logTypeColor: 'primary' }, { name: 'Error', - logTypeColor: 'danger' + logTypeColor: 'warning' }, { name: 'Fatal', @@ -86,7 +88,6 @@ vm.deleteSavedSearch = deleteSavedSearch; vm.back = back; - function init() { //If we have a Querystring set for lq (log query) @@ -96,6 +97,14 @@ vm.logOptions.filterExpression = querystring.lq; } + if(querystring.startDate){ + vm.logOptions.startDate = querystring.startDate; + } + + if(querystring.endDate){ + vm.logOptions.endDate = querystring.endDate; + } + vm.loading = true; logViewerResource.getSavedSearches().then(function (data) { @@ -110,7 +119,7 @@ "query": "Not(@Level='Verbose') and Not(@Level='Debug')" }, { - "name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)", + "name": "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", "query": "Has(@Exception)" }, { @@ -165,25 +174,8 @@ } function setLogTypeColor(logItems) { - angular.forEach(logItems, function (log) { - switch (log.Level) { - case "Information": - log.logTypeColor = "primary"; - break; - case "Debug": - log.logTypeColor = "secondary"; - break; - case "Warning": - log.logTypeColor = "warning"; - break; - case "Fatal": - case "Error": - log.logTypeColor = "danger"; - break; - default: - log.logTypeColor = "gray"; - } - }); + logItems.forEach(logItem => + logItem.logTypeColor = vm.logLevels.find(x => x.name === logItem.Level).logTypeColor); } function getFilterName(array) { @@ -290,10 +282,11 @@ function deleteSavedSearch(searchItem) { var overlay = { - title: "Delete Search", - subtitle: "Are you sure you wish to delete", + title: "Delete Saved Search", + subtitle: "Are you sure you wish to delete?", closeButtonLabel: "Cancel", - submitButtonLabel: "Delete Search", + submitButtonLabel: "Delete Saved Search", + submitButtonStyle: "danger", view: "default", submit: function (model) { //Resource call with two params (name & query) diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/search.html b/src/Umbraco.Web.UI.Client/src/views/logviewer/search.html index 5981b41598..b4f910fd7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/search.html +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/search.html @@ -1,4 +1,4 @@ -
          + -
          +
          @@ -84,9 +76,9 @@
          + ng-class="{'-selected': group.selected, '-selectable': group.hasAccess && !group.isSystemUserGroup}">
          @@ -119,4 +111,9 @@
          + + + + +
          diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index fdae716b39..bb3efaede2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -71,18 +71,15 @@ sections="userGroup.sections" content-start-node="userGroup.contentStartNode" media-start-node="userGroup.mediaStartNode" - allow-remove="!model.user.isCurrentUser" + allow-remove="true" on-remove="model.removeSelectedItem($index, model.user.userGroups)"> - + @@ -92,7 +89,7 @@ ng-repeat="node in model.user.startContentIds" icon="node.icon" name="node.name" - allow-remove="!model.user.isCurrentUser" + allow-remove="true" on-remove="model.removeSelectedItem($index, model.user.startContentIds)"> @@ -101,14 +98,12 @@ name="model.labels.noStartNodes"> - + @@ -118,7 +113,7 @@ ng-repeat="node in model.user.startMediaIds" icon="node.icon" name="node.name" - allow-remove="!model.user.isCurrentUser" + allow-remove="true" on-remove="model.removeSelectedItem($index, model.user.startMediaIds)"> @@ -127,14 +122,12 @@ name="model.labels.noStartNodes"> - + @@ -199,23 +192,22 @@ @@ -252,7 +244,7 @@
          -
          - - - - - - - - - -


          Password reset to value: {{model.user.resetPasswordValue}}

          @@ -324,7 +302,7 @@ rows="4"> 0 && filterCollection && filterCollection.length > 0) { + for (var i = 0; i < selectedCollection.length; i++) { + for (var j = 0; j < filterCollection.length; j++) { + if (filterCollection[j][keyField] === selectedCollection[i]) { + filterCollection[j].selected = true; + } + } + } + } + } + function getSortLabel(sortKey, sortDirection) { var found = _.find(vm.userSortData, function (i) { @@ -200,19 +247,19 @@ function selectLayout(selectedLayout) { // save the selected layout for "users" so it's applied next time the user visits this section - vm.activeLayout = listViewHelper.setLayout("users", selectedLayout, vm.layouts); + vm.activeLayout = listViewHelper.setLayout("users", selectedLayout, vm.layouts); } - + function isSelectable(user) { return !user.isCurrentUser; } - + function selectUser(user) { - + if (!isSelectable(user)) { return; } - + if (user.selected) { var index = vm.selection.indexOf(user.id); vm.selection.splice(index, 1); @@ -221,9 +268,9 @@ user.selected = true; vm.selection.push(user.id); } - + setBulkActions(vm.users); - + } function clearSelection() { @@ -232,14 +279,14 @@ }); vm.selection = []; } - + function clickUser(user, $event) { - + $event.stopPropagation(); - + if ($event) { // targeting a new tab/window? - if ($event.ctrlKey || + if ($event.ctrlKey || $event.shiftKey || $event.metaKey || // apple ($event.button && $event.button === 1) // middle click, >IE9 + everyone else @@ -248,7 +295,7 @@ return; } } - + goToUser(user); $event.preventDefault(); @@ -339,7 +386,7 @@ vm.selectedBulkUserGroups = []; editorService.close(); clearSelection(); - }, angular.noop); + }, Utilities.noop); }, close: function () { vm.selectedBulkUserGroups = []; @@ -350,9 +397,10 @@ } function openUserGroupPicker() { - var oldSelection = angular.copy(vm.newUser.userGroups); + var currentSelection = []; + Utilities.copy(vm.newUser.userGroups, currentSelection); var userGroupPicker = { - selection: vm.newUser.userGroups, + selection: currentSelection, submit: function (model) { // apply changes if (model.selection) { @@ -362,7 +410,6 @@ }, close: function () { // rollback on close - vm.newUser.userGroups = oldSelection; editorService.close(); } }; @@ -407,6 +454,7 @@ var search = _.debounce(function () { $scope.$apply(function () { + vm.usersOptions.pageNumber = 1; getUsers(); }); }, 500); @@ -415,6 +463,10 @@ search(); } + function onBlurSearch() { + updateLocation("filter", vm.usersOptions.filter); + } + function getFilterName(array) { var name = vm.labels.all; var found = false; @@ -467,7 +519,8 @@ vm.usersOptions.userStates.splice(index, 1); } - getUsers(); + updateLocation("userStates", vm.usersOptions.userStates.join(",")); + changePageNumber(1); } function setUserGroupFilter(userGroup) { @@ -483,20 +536,29 @@ vm.usersOptions.userGroups.splice(index, 1); } - getUsers(); + updateLocation("userGroups", vm.usersOptions.userGroups.join(",")); + changePageNumber(1); } function setOrderByFilter(value, direction) { vm.usersOptions.orderBy = value; vm.usersOptions.orderDirection = direction; + updateLocation("orderBy", value); + updateLocation("orderDirection", direction); getUsers(); } function changePageNumber(pageNumber) { vm.usersOptions.pageNumber = pageNumber; + updateLocation("pageNumber", pageNumber); getUsers(); } + function updateLocation(key, value) { + $location.search("filter", vm.usersOptions.filter);// update filter, but first when something else requests a url update. + $location.search(key, value); + } + function createUser(addUserForm) { if (formHelper.submitForm({ formCtrl: addUserForm, scope: $scope })) { @@ -549,7 +611,7 @@ // copy to clip board success function copySuccess() { if (vm.page.copyPasswordButtonState !== "success") { - $timeout(function(){ + $timeout(function () { vm.page.copyPasswordButtonState = "success"; }); $timeout(function () { @@ -561,7 +623,7 @@ // copy to clip board error function copyError() { if (vm.page.copyPasswordButtonState !== "error") { - $timeout(function() { + $timeout(function () { vm.page.copyPasswordButtonState = "error"; }); $timeout(function () { @@ -575,17 +637,54 @@ } function goToUser(user) { - $location.path(pathToUser(user)).search("create", null).search("invite", null); + $location.path(pathToUser(user)) + .search("orderBy", vm.usersOptions.orderBy) + .search("orderDirection", vm.usersOptions.orderDirection) + .search("pageNumber", vm.usersOptions.pageNumber) + .search("userStates", getUsersOptionsFilterCollectionAsDelimitedStringOrNull(vm.usersOptions.userStates)) + .search("userGroups", getUsersOptionsFilterCollectionAsDelimitedStringOrNull(vm.usersOptions.userGroups)) + .search("create", null) + .search("invite", null); } - + + function getUsersOptionsFilterCollectionAsDelimitedStringOrNull(collection) { + if (collection && collection.length > 0) { + return collection.join(","); + } + + return null; + } + function getEditPath(user) { - return pathToUser(user) + "?mculture=" + $location.search().mculture; + return pathToUser(user) + usersOptionsAsQueryString(); } - + function pathToUser(user) { return "/users/users/user/" + user.id; } + function usersOptionsAsQueryString() { + var qs = "?orderBy=" + vm.usersOptions.orderBy + + "&orderDirection=" + vm.usersOptions.orderDirection + + "&pageNumber=" + vm.usersOptions.pageNumber + + "&filter=" + vm.usersOptions.filter; + + qs += addUsersOptionsFilterCollectionToQueryString("userStates", vm.usersOptions.userStates); + qs += addUsersOptionsFilterCollectionToQueryString("userGroups", vm.usersOptions.userGroups); + + qs += "&mculture=" + $location.search().mculture; + + return qs; + } + + function addUsersOptionsFilterCollectionToQueryString(name, collection) { + if (collection && collection.length > 0) { + return "&" + name + "=" + collection.join(","); + } + + return ""; + } + // helpers function getUsers() { @@ -600,10 +699,11 @@ vm.usersOptions.pageSize = data.pageSize; vm.usersOptions.totalItems = data.totalItems; vm.usersOptions.totalPages = data.totalPages; - + formatDates(vm.users); setUserDisplayState(vm.users); vm.userStatesFilter = usersHelper.getUserStatesFilter(data.userStates); + initUserStateSelections(); vm.loading = false; @@ -653,19 +753,19 @@ var firstSelectedUserGroups; angular.forEach(users, function (user) { - + if (!user.selected) { return; } - - + + // if the current user is selected prevent any bulk actions with the user included if (user.isCurrentUser) { vm.allowDisableUser = false; vm.allowEnableUser = false; vm.allowUnlockUser = false; vm.allowSetUserGroup = false; - + return false; } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 24f504be63..a0098d411c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -11,7 +11,7 @@ @@ -27,18 +27,10 @@ - + + + + @@ -127,7 +119,7 @@ - + @@ -173,14 +165,14 @@
          -
          +
          {{ user.userDisplayState.name }} @@ -233,7 +225,7 @@
          + class="umb-table-row umb-user-table-row umb-outline umb-outline--surrounding">
          - Back to users + @@ -359,6 +351,8 @@ Add + + @@ -408,7 +402,7 @@ - Back to users + @@ -490,7 +484,7 @@
          - Back to users + @@ -503,6 +497,7 @@

          diff --git a/src/Umbraco.Web.UI.Client/test/config/app.unit.js b/src/Umbraco.Web.UI.Client/test/config/app.unit.js index 32b05ab0d4..1f49d237e6 100644 --- a/src/Umbraco.Web.UI.Client/test/config/app.unit.js +++ b/src/Umbraco.Web.UI.Client/test/config/app.unit.js @@ -13,7 +13,7 @@ var app = angular.module('umbraco', [ 'ngSanitize', //'ngMessages', - 'tmh.dynamicLocale', + 'tmh.dynamicLocale' //'ngFileUpload', //'LocalStorageModule', //'chart.js' diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js index 08223de092..4e3a78144b 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -15,6 +15,7 @@ module.exports = function (config) { 'node_modules/angular/angular.js', 'node_modules/angular-animate/angular-animate.js', 'node_modules/angular-cookies/angular-cookies.js', + 'node_modules/angular-aria/angular-aria.min.js', 'node_modules/angular-local-storage/dist/angular-local-storage.min.js', 'node_modules/angular-route/angular-route.js', 'node_modules/angular-sanitize/angular-sanitize.js', @@ -35,7 +36,6 @@ module.exports = function (config) { '../Umbraco.Web.UI/Umbraco/js/*.filters.js', '../Umbraco.Web.UI/Umbraco/js/*.services.js', '../Umbraco.Web.UI/Umbraco/js/*.interceptors.js', - '../Umbraco.Web.UI/Umbraco/js/*.security.js', '../Umbraco.Web.UI/Umbraco/js/*.resources.js', //mocked data and routing @@ -50,9 +50,23 @@ module.exports = function (config) { exclude: [], // use dolts reporter, as travis terminal does not support escaping sequences - // possible values: 'dots', 'progress', 'junit', 'teamcity' + // possible values: 'dots', 'progress', 'junit', 'spec' + // *** + // progress: Outputs a simple list like: "Executed 128 of 144 SUCCESS (0 secs / 0.814 secs)" + // spec: Outputs a more verbose report which is more useful for debugging if one of the tests fails. + // *** // CLI --reporters progress - reporters: ['progress', 'junit'], + + reporters: ['spec', 'junit'], + specReporter: { + maxLogLines: 5, // limit number of lines logged per test + suppressErrorSummary: true, // do not print error summary + suppressFailed: false, // do not print information about failed tests + suppressPassed: false, // do not print information about passed tests + suppressSkipped: true, // do not print information about skipped tests + showSpecTiming: false // print the time elapsed for each spec + }, + // web server port // CLI --port 9876 @@ -101,7 +115,9 @@ module.exports = function (config) { plugins: [ require('karma-jasmine'), require('karma-phantomjs-launcher'), - require('karma-junit-reporter') + require('karma-junit-reporter'), + require('karma-spec-reporter') + ], // the default configuration diff --git a/src/Umbraco.Web.UI.Client/test/lib/angular/angular-mocks.js b/src/Umbraco.Web.UI.Client/test/lib/angular/angular-mocks.js index b4a3fb9ed6..31310e1c46 100644 --- a/src/Umbraco.Web.UI.Client/test/lib/angular/angular-mocks.js +++ b/src/Umbraco.Web.UI.Client/test/lib/angular/angular-mocks.js @@ -51,12 +51,12 @@ angular.mock.$Browser = function () { self.onUrlChange = function (listener) { self.pollFns.push( - function () { - if (self.$$lastUrl != self.$$url) { - self.$$lastUrl = self.$$url; - listener(self.$$url); - } - } + function () { + if (self.$$lastUrl != self.$$url) { + self.$$lastUrl = self.$$url; + listener(self.$$url); + } + } ); return listener; @@ -104,7 +104,7 @@ angular.mock.$Browser = function () { * @param {number=} number of milliseconds to flush. See {@link #defer.now} */ self.defer.flush = function (delay) { - if (angular.isDefined(delay)) { + if (Utilities.isDefined(delay)) { self.defer.now += delay; } else { if (self.deferredFns.length) { @@ -165,15 +165,15 @@ angular.mock.$Browser.prototype = { if (value == undefined) { delete this.cookieHash[name]; } else { - if (angular.isString(value) && //strings only + if (Utilities.isString(value) && //strings only value.length <= 4096) { //strict cookie storage limits this.cookieHash[name] = value; } } } else { if (!angular.equals(this.cookieHash, this.lastCookieHash)) { - this.lastCookieHash = angular.copy(this.cookieHash); - this.cookieHash = angular.copy(this.cookieHash); + this.lastCookieHash = Utilities.copy(this.cookieHash); + this.cookieHash = Utilities.copy(this.cookieHash); } return this.cookieHash; } @@ -397,7 +397,7 @@ angular.mock.$LogProvider = function () { }); if (errors.length) { errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or an expected " + - "log message was not checked and removed:"); + "log message was not checked and removed:"); errors.push(''); throw new Error(errors.join('\n---------\n')); } @@ -486,7 +486,7 @@ angular.mock.$LogProvider = function () { */ angular.mock.TzDate = function (offset, timestamp) { var self = new Date(0); - if (angular.isString(timestamp)) { + if (Utilities.isString(timestamp)) { var tsStr = timestamp; self.origDate = jsonStringToDate(timestamp); @@ -581,12 +581,12 @@ angular.mock.$LogProvider = function () { if (self.toISOString) { self.toISOString = function () { return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + - padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + - padNumber(self.origDate.getUTCDate(), 2) + 'T' + - padNumber(self.origDate.getUTCHours(), 2) + ':' + - padNumber(self.origDate.getUTCMinutes(), 2) + ':' + - padNumber(self.origDate.getUTCSeconds(), 2) + '.' + - padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' + padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + + padNumber(self.origDate.getUTCDate(), 2) + 'T' + + padNumber(self.origDate.getUTCHours(), 2) + ':' + + padNumber(self.origDate.getUTCMinutes(), 2) + ':' + + padNumber(self.origDate.getUTCSeconds(), 2) + '.' + + padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' } } @@ -686,19 +686,19 @@ angular.mock.dump = function (object) { var out; if (angular.isElement(object)) { - object = angular.element(object); - out = angular.element('
          '); + object = $(object); + out = $('
          '); angular.forEach(object, function (element) { - out.append(angular.element(element).clone()); + out.append($(element).clone()); }); out = out.html(); - } else if (angular.isArray(object)) { + } else if (Utilities.isArray(object)) { out = []; angular.forEach(object, function (o) { out.push(serialize(o)); }); out = '[ ' + out.join(', ') + ' ]'; - } else if (angular.isObject(object)) { + } else if (Utilities.isObject(object)) { if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { out = serializeScope(object); } else if (object instanceof Error) { @@ -943,7 +943,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { wasExpected = false; function prettyPrint(data) { - return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) + return (Utilities.isString(data) || angular.isFunction(data) || data instanceof RegExp) ? data : angular.toJson(data); } @@ -1004,7 +1004,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { throw wasExpected ? Error('No response defined !') : Error('Unexpected request: ' + method + ' ' + url + '\n' + - (expectation ? 'Expected ' + expectation : 'No more request expected')); + (expectation ? 'Expected ' + expectation : 'No more request expected')); } /** @@ -1267,7 +1267,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { $rootScope.$digest(); if (!responses.length) throw Error('No pending request to flush !'); - if (angular.isDefined(count)) { + if (Utilities.isDefined(count)) { while (count--) { if (!responses.length) throw Error('No more pending request to flush !'); responses.shift()(); @@ -1365,8 +1365,8 @@ function MockHttpExpectation(method, url, data, headers) { this.match = function (m, u, d, h) { if (method != m) return false; if (!this.matchUrl(u)) return false; - if (angular.isDefined(d) && !this.matchData(d)) return false; - if (angular.isDefined(h) && !this.matchHeaders(h)) return false; + if (Utilities.isDefined(d) && !this.matchData(d)) return false; + if (Utilities.isDefined(h) && !this.matchHeaders(h)) return false; return true; }; @@ -1385,7 +1385,7 @@ function MockHttpExpectation(method, url, data, headers) { this.matchData = function (d) { if (angular.isUndefined(data)) return true; if (data && angular.isFunction(data.test)) return data.test(d); - if (data && !angular.isString(data)) return angular.toJson(data) == d; + if (data && !Utilities.isString(data)) return angular.toJson(data) == d; return data == d; }; @@ -1499,7 +1499,7 @@ angular.mock.$TimeoutDecorator = function ($delegate, $browser) { */ angular.mock.$RootElementProvider = function () { this.$get = function () { - return angular.element('
          '); + return $('
          '); } }; @@ -1572,7 +1572,7 @@ angular.module('ngMockE2E', ['ng']).config(function ($provide) { * * // adds a new phone to the phones array * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { - * phones.push(angular.fromJSON(data)); + * phones.push(JSON.parse(data)); * }); * $httpBackend.whenGET(/^\/templates\//).passThrough(); * //... @@ -1710,7 +1710,7 @@ angular.mock.clearDataCache = function () { if (cache.hasOwnProperty(key)) { var handle = cache[key].handle; - handle && angular.element(handle.elem).unbind(); + handle && $(handle.elem).unbind(); delete cache[key]; } } diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js index 278d52c9a2..5954a2f984 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js @@ -33,7 +33,10 @@ } - beforeEach(inject(function ($controller, $rootScope, $q, $location) { + beforeEach(inject(function ($controller, $rootScope, $q, $location, authMocks) { + + authMocks.register(); + contentTypeResource = { getAllowedTypes: function () { var def = $q.defer(); diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/dropdown-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/dropdown-controller.spec.js index dba3373ec2..a82a926a6d 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/dropdown-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/dropdown-controller.spec.js @@ -37,7 +37,7 @@ describe('Drop down controller tests', function () { $routeParams: routeParams }); - //this should be the expected format based on the changes made to the sortable prevalues + // this should be the expected format based on the changes made to the sortable prevalues expect(scope.model.config.items[0].value).toBe("value0"); expect(scope.model.config.items[1].value).toBe("value1"); expect(scope.model.config.items[2].value).toBe("value2"); diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js index 9668a03580..715861de0b 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js @@ -1,7 +1,23 @@ describe('RTE controller tests', function () { var scope, controllerFactory; - - beforeEach(module('umbraco')); + + //mock tinymce globals + if ((typeof tinymce) === "undefined") { + tinymce = { + DOM: { + events: { + domLoaded: false + } + }, + baseUrl: "" + } + } + + beforeEach(module('LocalStorageModule')); + + beforeEach(module('umbraco', function ($provide) { + $provide.value('tinyMceAssets', []); + })); beforeEach(inject(function ($rootScope, $controller) { controllerFactory = $controller; @@ -20,4 +36,5 @@ describe('RTE controller tests', function () { }); }); -}); \ No newline at end of file +}); + diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js new file mode 100644 index 0000000000..1e9ea2ea46 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js @@ -0,0 +1,69 @@ +(function () { + + describe('truncate filter', function() { + var $truncate; + + var testCases = [ + {input:'test', noOfChars:5, appendDots:true, expectedResult: 'test'}, + {input:'test ', noOfChars:4, appendDots:true, expectedResult: 'test…'}, + {input:'test a long text with space', noOfChars:5, appendDots:true, expectedResult: 'test …'}, + {input:'scenarios is a long word', noOfChars:5, appendDots:true, expectedResult: 'scena…'}, + {input:'scenarios is a long word', noOfChars:10, appendDots:true, expectedResult: 'scenarios …'}, + {input:'test', noOfChars:5, appendDots:false, expectedResult: 'test'}, + {input:'test a long text with space', noOfChars:5, appendDots:false, expectedResult: 'test '}, + {input:'scenarios is a long word', noOfChars:5, appendDots:false, expectedResult: 'scena'}, + {input:'scenarios is a long word', noOfChars:10, appendDots:false, expectedResult: 'scenarios '} + ]; + + var testCasesNew = [ + {value:'test', wordwise:false, max:20, tail:'...', expectedResult: 'test'}, + {value:'LoremIpsumLoremIpsumLoremIpsum',wordwise:false, max:20, tail:null, expectedResult: 'LoremIpsumLoremIpsum…'} + ]; + + beforeEach(module('umbraco')); + + beforeEach(inject(function($filter) { + $truncate = $filter('truncate'); + })); + + it('empty string as input is expected to give an empty string', function() { + expect($truncate('', 5, true)).toBe(''); + }); + + it('null as input is expected to give an empty string', function() { + expect($truncate(null, 5, true)).toBe(''); + }); + + it('undefined as input is expected to give an empty string', function() { + expect($truncate(undefined, 5, true)).toBe(''); + }); + + it('null as noOfChars to result in \'test\'', function() { + expect($truncate('test', null, true)).toBe('test'); + }); + + it('undefined as noOfChars to result in \'test\'', function() { + expect($truncate('test', undefined, true)).toBe('test'); + }); + + it('null as appendDots to behave as false', function() { + expect($truncate('test', 5, null)).toBe('test'); + }); + + testCases.forEach(function(test){ + + it('Expects \'' + test.input + '\' to be truncated as \''+ test.expectedResult + '\', when noOfChars=' + test.noOfChars + ', and appendDots=' + test.appendDots, function() { + console.log($truncate(test.input, test.noOfChars, test.appendDots)); + expect($truncate(test.input, test.noOfChars, test.appendDots)).toBe(test.expectedResult); + }); + }); + + testCasesNew.forEach(function(test){ + it('Expects \'' + test.value + '\' to be truncated as \''+ test.expectedResult + '\', when wordwise=' + test.wordwise + ', and max=' + test.max + ', and tail=' + test.tail, function() { + expect($truncate(test.value, test.wordwise, test.max, test.tail)).toBe(test.expectedResult); + }); + }); + + }); + +}()); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/filters/umbCmsJoinArray.filter.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/filters/umbCmsJoinArray.filter.spec.js new file mode 100644 index 0000000000..4a44b17bff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/common/filters/umbCmsJoinArray.filter.spec.js @@ -0,0 +1,43 @@ +(function () { + + describe('umbCmsJoinArray filter', function() { + var $umbCmsJoinArray; + + var testCases = [ + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:', ', prop:'param' , expectedResult: 'a, b, c'}, + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:' ', prop:'param' , expectedResult: 'a b c'}, + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:'', prop:'param' , expectedResult: 'abc'}, + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:'', prop:'wrong' , expectedResult: ''}, + {input:[], separator:', ', prop:'param' , expectedResult: ''}, + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:', ', prop:null , expectedResult: ', , '}, + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:null, prop:'param' , expectedResult: 'abc'}, + ]; + + var testCasesWithExpectedError = [ + {input:'test', separator:', ', prop:'param'}, + {input:null, separator:', ', prop:'param'}, + {input:undefined, separator:', ', prop:'param'}, + ]; + + beforeEach(module('umbraco')); + + beforeEach(inject(function($filter) { + $umbCmsJoinArray = $filter('umbCmsJoinArray'); + })); + + + testCases.forEach(function(test){ + it('Blackbox tests with expected result=\''+test.expectedResult+'\'', function() { + expect($umbCmsJoinArray(test.input, test.separator, test.prop)).toBe(test.expectedResult); + }); + }); + + testCasesWithExpectedError.forEach(function(test){ + it('Blackbox tests with expected error. Input=\''+test.input+'\'', function() { + expect(function() { $umbCmsJoinArray(test.input, test.separator, test.prop)}).toThrow(); + }); + }); + + }); + +}()); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js index f3d2f8d759..227a359d1d 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js @@ -30,7 +30,6 @@ describe('contentEditingHelper tests', function () { //act var handled = contentEditingHelper.handleSaveError({ - redirectOnFailure: true, err: err, allNewProps: contentEditingHelper.getAllProps(content), allOrigProps: contentEditingHelper.getAllProps(content) @@ -49,7 +48,6 @@ describe('contentEditingHelper tests', function () { //act var handled = contentEditingHelper.handleSaveError({ - redirectOnFailure: true, err: err, allNewProps: [], allOrigProps: [] @@ -70,7 +68,6 @@ describe('contentEditingHelper tests', function () { //act var handled = contentEditingHelper.handleSaveError({ - redirectOnFailure: true, err: err, allNewProps: contentEditingHelper.getAllProps(content), allOrigProps: contentEditingHelper.getAllProps(content) @@ -124,13 +121,15 @@ describe('contentEditingHelper tests', function () { //act //note the null, that's because culture is null - formHelper.handleServerValidation({ "_Properties.bodyText.null.value": ["Required"] }); + formHelper.handleServerValidation({ "_Properties.bodyText.null.null.value": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); expect(serverValidationManager.items[0].fieldName).toBe("value"); expect(serverValidationManager.items[0].errorMsg).toBe("Required"); expect(serverValidationManager.items[0].propertyAlias).toBe("bodyText"); + expect(serverValidationManager.items[0].culture).toBe("invariant"); + expect(serverValidationManager.items[0].segment).toBeNull(); }); it('adds a multiple property and field level server validation errors when they are invalid', function () { @@ -145,7 +144,7 @@ describe('contentEditingHelper tests', function () { "Name": ["Required"], "UpdateDate": ["Invalid date"], //note the null, that's because culture is null - "_Properties.bodyText.null.value": ["Required field"], + "_Properties.bodyText.en-US.mySegment.value": ["Required field"], "_Properties.textarea": ["Invalid format"] }); @@ -160,6 +159,8 @@ describe('contentEditingHelper tests', function () { expect(serverValidationManager.items[2].fieldName).toBe("value"); expect(serverValidationManager.items[2].errorMsg).toBe("Required field"); expect(serverValidationManager.items[2].propertyAlias).toBe("bodyText"); + expect(serverValidationManager.items[2].culture).toBe("en-US"); + expect(serverValidationManager.items[2].segment).toBe("mySegment"); expect(serverValidationManager.items[3].fieldName).toBe(""); expect(serverValidationManager.items[3].errorMsg).toBe("Invalid format"); expect(serverValidationManager.items[3].propertyAlias).toBe("textarea"); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/notifications.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/notifications.spec.js index 28f6f80802..071c2158b8 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/notifications.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/notifications.spec.js @@ -14,21 +14,23 @@ describe('notification tests', function () { var not1 = notifications.success("success", "something great happened"); var not2 = notifications.error("error", "something great happened"); var not3 = notifications.warning("warning", "something great happened"); - var not4 = notifications.warning("warning", ""); + var not4 = notifications.warning("warning", ""); + var not5 = notifications.info("info", "something informative happened"); - expect(notifications.getCurrent().length).toBe(4); + expect(notifications.getCurrent().length).toBe(5); //remove at index 0 notifications.remove(0); - expect(notifications.getCurrent().length).toEqual(3); + expect(notifications.getCurrent().length).toEqual(4); expect(notifications.getCurrent()[0].headline).toBe("error: "); expect(notifications.getCurrent()[1].headline).toBe("warning: "); - expect(notifications.getCurrent()[2].headline).toBe("warning"); + expect(notifications.getCurrent()[2].headline).toBe("warning"); + expect(notifications.getCurrent()[3].headline).toBe("info: "); notifications.removeAll(); expect(notifications.getCurrent().length).toEqual(0); }); }); -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index 3f953fffcd..5d2a618f46 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -22,6 +22,7 @@ expect(err.propertyAlias).toBeNull(); expect(err.fieldName).toEqual("Name"); expect(err.errorMsg).toEqual("Required"); + expect(err.culture).toEqual("invariant"); }); it('will return null for a non-existing field error', function () { @@ -60,30 +61,127 @@ it('can retrieve property validation errors for a sub field', function () { //arrange - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); //act - var err1 = serverValidationManager.getPropertyError("myProperty", null, "value1"); - var err2 = serverValidationManager.getPropertyError("myProperty", null, "value2"); + var err1 = serverValidationManager.getPropertyError("myProperty", null, "value1", null); + var err2 = serverValidationManager.getPropertyError("myProperty", null, "value2", null); //assert expect(err1).not.toBeUndefined(); expect(err1.propertyAlias).toEqual("myProperty"); expect(err1.fieldName).toEqual("value1"); expect(err1.errorMsg).toEqual("Some value 1"); + expect(err1.culture).toEqual("invariant"); expect(err2).not.toBeUndefined(); expect(err2.propertyAlias).toEqual("myProperty"); expect(err2.fieldName).toEqual("value2"); expect(err2.errorMsg).toEqual("Another value 2"); + expect(err2.culture).toEqual("invariant"); + }); + + it('can retrieve property validation errors for a sub field for culture', function () { + + //arrange + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 2", null); + + //act + var err1 = serverValidationManager.getPropertyError("myProperty", "en-US", "value1", null); + var err1NotFound = serverValidationManager.getPropertyError("myProperty", null, "value1", null); + var err2 = serverValidationManager.getPropertyError("myProperty", "fr-FR", "value2", null); + var err2NotFound = serverValidationManager.getPropertyError("myProperty", null, "value2", null); + + + //assert + expect(err1NotFound).toBeUndefined(); + expect(err2NotFound).toBeUndefined(); + + expect(err1).not.toBeUndefined(); + expect(err1.propertyAlias).toEqual("myProperty"); + expect(err1.fieldName).toEqual("value1"); + expect(err1.errorMsg).toEqual("Some value 1"); + expect(err1.culture).toEqual("en-US"); + + expect(err2).not.toBeUndefined(); + expect(err2.propertyAlias).toEqual("myProperty"); + expect(err2.fieldName).toEqual("value2"); + expect(err2.errorMsg).toEqual("Another value 2"); + expect(err2.culture).toEqual("fr-FR"); + }); + + it('can retrieve property validation errors for a sub field for segments', function () { + + //arrange + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", "segment1"); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", "segment2"); + + //act + var err1 = serverValidationManager.getPropertyError("myProperty", null, "value1", "segment1"); + var err1NotFound = serverValidationManager.getPropertyError("myProperty", null, "value1", null); + var err2 = serverValidationManager.getPropertyError("myProperty", null, "value2", "segment2"); + var err2NotFound = serverValidationManager.getPropertyError("myProperty", null, "value2", null); + + + //assert + expect(err1NotFound).toBeUndefined(); + expect(err2NotFound).toBeUndefined(); + + expect(err1).not.toBeUndefined(); + expect(err1.propertyAlias).toEqual("myProperty"); + expect(err1.fieldName).toEqual("value1"); + expect(err1.errorMsg).toEqual("Some value 1"); + expect(err1.segment).toEqual("segment1"); + + expect(err2).not.toBeUndefined(); + expect(err2.propertyAlias).toEqual("myProperty"); + expect(err2.fieldName).toEqual("value2"); + expect(err2.errorMsg).toEqual("Another value 2"); + expect(err2.segment).toEqual("segment2"); + }); + + + it('can retrieve property validation errors for a sub field for culture with segments', function () { + + //arrange + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1", "segment1"); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 2", "segment2"); + + //act + var err1 = serverValidationManager.getPropertyError("myProperty", "en-US", "value1", "segment1"); + expect(serverValidationManager.getPropertyError("myProperty", null, "value1", null)).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", "en-US", "value1", null)).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", null, "value1", "segment1")).toBeUndefined(); + var err2 = serverValidationManager.getPropertyError("myProperty", "fr-FR", "value2", "segment2"); + expect(serverValidationManager.getPropertyError("myProperty", null, "value2", null)).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", "fr-FR", "value2", null)).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", null, "value2", "segment2")).toBeUndefined(); + + + //assert + + expect(err1).not.toBeUndefined(); + expect(err1.propertyAlias).toEqual("myProperty"); + expect(err1.fieldName).toEqual("value1"); + expect(err1.errorMsg).toEqual("Some value 1"); + expect(err1.culture).toEqual("en-US"); + expect(err1.segment).toEqual("segment1"); + + expect(err2).not.toBeUndefined(); + expect(err2.propertyAlias).toEqual("myProperty"); + expect(err2.fieldName).toEqual("value2"); + expect(err2.errorMsg).toEqual("Another value 2"); + expect(err2.culture).toEqual("fr-FR"); + expect(err2.segment).toEqual("segment2"); }); it('can add a property errors with multiple sub fields and it the first will be retreived with only the property alias', function () { //arrange - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", null); //act var err = serverValidationManager.getPropertyError("myProperty"); @@ -93,15 +191,16 @@ expect(err.propertyAlias).toEqual("myProperty"); expect(err.fieldName).toEqual("value1"); expect(err.errorMsg).toEqual("Some value 1"); + expect(err.culture).toEqual("invariant"); }); it('will return null for a non-existing property error', function () { //arrage - serverValidationManager.addPropertyError("myProperty", null, "value", "Required"); + serverValidationManager.addPropertyError("myProperty", null, "value", "Required", null); //act - var err = serverValidationManager.getPropertyError("DoesntExist", null, "value"); + var err = serverValidationManager.getPropertyError("DoesntExist", null, "value", null); //assert expect(err).toBeUndefined(); @@ -111,15 +210,15 @@ it('detects if a property error exists', function () { //arrange - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", null); //act var err1 = serverValidationManager.hasPropertyError("myProperty"); - var err2 = serverValidationManager.hasPropertyError("myProperty", null, "value1"); - var err3 = serverValidationManager.hasPropertyError("myProperty", null, "value2"); + var err2 = serverValidationManager.hasPropertyError("myProperty", null, "value1", null); + var err3 = serverValidationManager.hasPropertyError("myProperty", null, "value2", null); var err4 = serverValidationManager.hasPropertyError("notFound"); - var err5 = serverValidationManager.hasPropertyError("myProperty", null, "notFound"); + var err5 = serverValidationManager.hasPropertyError("myProperty", null, "notFound", null); //assert expect(err1).toBe(true); @@ -133,15 +232,15 @@ it('can remove a property error with a sub field specified', function () { //arrage - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", null); //act - serverValidationManager.removePropertyError("myProperty", null, "value1"); + serverValidationManager.removePropertyError("myProperty", null, "value1", null); //assert - expect(serverValidationManager.hasPropertyError("myProperty", null, "value1")).toBe(false); - expect(serverValidationManager.hasPropertyError("myProperty", null, "value2")).toBe(true); + expect(serverValidationManager.hasPropertyError("myProperty", null, "value1", null)).toBe(false); + expect(serverValidationManager.hasPropertyError("myProperty", null, "value2", null)).toBe(true); }); @@ -155,8 +254,61 @@ serverValidationManager.removePropertyError("myProperty"); //assert - expect(serverValidationManager.hasPropertyError("myProperty", null, "value1")).toBe(false); - expect(serverValidationManager.hasPropertyError("myProperty", null, "value2")).toBe(false); + expect(serverValidationManager.hasPropertyError("myProperty", null, "value1", null)).toBe(false); + expect(serverValidationManager.hasPropertyError("myProperty", null, "value2", null)).toBe(false); + + }); + + }); + + describe('managing culture validation errors', function () { + + it('can retrieve culture validation errors', function () { + + //arrange + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 2", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", null); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 3", null); + + //assert + expect(serverValidationManager.hasCultureError(null)).toBe(true); + expect(serverValidationManager.hasCultureError("en-US")).toBe(true); + expect(serverValidationManager.hasCultureError("fr-FR")).toBe(true); + expect(serverValidationManager.hasCultureError("es-ES")).toBe(false); + + }); + + }); + + describe('managing variant validation errors', function () { + + it('can retrieve variant validation errors', function () { + + //arrange + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 2", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", null); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 3", null); + + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", "MySegment"); + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 2", "MySegment"); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", "MySegment"); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 3", "MySegment"); + + //assert + expect(serverValidationManager.hasVariantError(null, null)).toBe(true); + expect(serverValidationManager.hasVariantError("en-US", null)).toBe(true); + expect(serverValidationManager.hasVariantError("fr-FR", null)).toBe(true); + + expect(serverValidationManager.hasVariantError(null, "MySegment")).toBe(true); + expect(serverValidationManager.hasVariantError("en-US", "MySegment")).toBe(true); + expect(serverValidationManager.hasVariantError("fr-FR", "MySegment")).toBe(true); + + expect(serverValidationManager.hasVariantError("es-ES", null)).toBe(false); + expect(serverValidationManager.hasVariantError("es-ES", "MySegment")).toBe(false); + expect(serverValidationManager.hasVariantError("fr-FR", "MySegmentNotRight")).toBe(false); + expect(serverValidationManager.hasVariantError(null, "MySegmentNotRight")).toBe(false); }); @@ -174,11 +326,11 @@ propertyErrors: propertyErrors, allErrors: allErrors }; - }); + }, null); //act serverValidationManager.addFieldError("Name", "Required"); - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); //assert expect(args).not.toBeUndefined(); @@ -195,8 +347,8 @@ }; var cb2 = function () { }; - serverValidationManager.subscribe(null, null, "Name", cb1); - serverValidationManager.subscribe(null, null, "Title", cb2); + serverValidationManager.subscribe(null, null, "Name", cb1, null); + serverValidationManager.subscribe(null, null, "Title", cb2, null); //act serverValidationManager.addFieldError("Name", "Required"); @@ -230,7 +382,7 @@ propertyErrors: propertyErrors, allErrors: allErrors }; - }); + }, null); serverValidationManager.subscribe("myProperty", null, "", function (isValid, propertyErrors, allErrors) { numCalled++; @@ -239,12 +391,12 @@ propertyErrors: propertyErrors, allErrors: allErrors }; - }); + }, null); //act - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Some value 2"); - serverValidationManager.addPropertyError("myProperty", null, "", "Some value 3"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Some value 2", null); + serverValidationManager.addPropertyError("myProperty", null, "", "Some value 3", null); //assert expect(args1).not.toBeUndefined(); @@ -267,6 +419,45 @@ // if the property has errors existing. expect(numCalled).toEqual(3); }); + + it('can subscribe to a culture error for both a property and its sub field', function () { + var args1; + var args2; + var numCalled = 0; + + //arrange + serverValidationManager.subscribe(null, "en-US", null, function (isValid, propertyErrors, allErrors) { + numCalled++; + args1 = { + isValid: isValid, + propertyErrors: propertyErrors, + allErrors: allErrors + }; + }, null); + + serverValidationManager.subscribe(null, "es-ES", null, function (isValid, propertyErrors, allErrors) { + numCalled++; + args2 = { + isValid: isValid, + propertyErrors: propertyErrors, + allErrors: allErrors + }; + }, null); + + //act + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", "en-US", "value2", "Some value 2", null); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "", "Some value 3", null); + + //assert + expect(args1).not.toBeUndefined(); + expect(args1.isValid).toBe(false); + + expect(args2).toBeUndefined(); + + expect(numCalled).toEqual(2); + }); // TODO: Finish testing the rest! diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/umb-request-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/umb-request-helper.spec.js index 4e6a6de295..ba8d651b56 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/umb-request-helper.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/umb-request-helper.spec.js @@ -1,6 +1,7 @@ describe('umbRequestHelper tests', function () { var umbRequestHelper; + beforeEach(module('umbraco')); beforeEach(module('umbraco.services')); beforeEach(inject(function ($injector) { @@ -33,4 +34,4 @@ describe('umbRequestHelper tests', function () { }); }); -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/test/unit/utilities.spec.js b/src/Umbraco.Web.UI.Client/test/unit/utilities.spec.js new file mode 100644 index 0000000000..db642fde9f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/utilities.spec.js @@ -0,0 +1,63 @@ +(function () { + describe("Utilities", function () { + describe("fromJson", function () { + it("should deserialize json as object", function () { + expect(Utilities.fromJson('{"a":1,"b":2}')).toEqual({ a: 1, b: 2 }); + }); + it("should return object as object", function () { + expect(Utilities.fromJson({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 }); + }); + }), + describe("toJson", function () { + it("should delegate to JSON.stringify", function () { + var spy = spyOn(JSON, "stringify").and.callThrough(); + + expect(Utilities.toJson({})).toEqual("{}"); + expect(spy).toHaveBeenCalled(); + }); + + it("should format objects pretty", function () { + expect(Utilities.toJson({ a: 1, b: 2 }, true)).toBe( + '{\n "a": 1,\n "b": 2\n}' + ); + expect(Utilities.toJson({ a: { b: 2 } }, true)).toBe( + '{\n "a": {\n "b": 2\n }\n}' + ); + expect(Utilities.toJson({ a: 1, b: 2 }, false)).toBe('{"a":1,"b":2}'); + expect(Utilities.toJson({ a: 1, b: 2 }, 0)).toBe('{"a":1,"b":2}'); + expect(Utilities.toJson({ a: 1, b: 2 }, 1)).toBe( + '{\n "a": 1,\n "b": 2\n}' + ); + expect(Utilities.toJson({ a: 1, b: 2 }, {})).toBe( + '{\n "a": 1,\n "b": 2\n}' + ); + }); + + it("should not serialize properties starting with $$", function () { + expect(Utilities.toJson({ $$some: "value" }, false)).toEqual("{}"); + }); + + it("should serialize properties starting with $", function () { + expect(Utilities.toJson({ $few: "v" }, false)).toEqual('{"$few":"v"}'); + }); + + it("should not serialize $window object", function () { + expect(Utilities.toJson(window)).toEqual('"$WINDOW"'); + }); + + it("should not serialize $document object", function () { + expect(Utilities.toJson(document)).toEqual('"$DOCUMENT"'); + }); + + it("should not serialize scope instances", inject(function ( + $rootScope + ) { + expect(Utilities.toJson({ key: $rootScope })).toEqual('{"key":"$SCOPE"}'); + })); + + it("should serialize undefined as undefined", function () { + expect(Utilities.toJson(undefined)).toEqual(undefined); + }); + }); + }); +})(); diff --git a/src/Umbraco.Web.UI.Docs/README.md b/src/Umbraco.Web.UI.Docs/README.md new file mode 100644 index 0000000000..ad9f631241 --- /dev/null +++ b/src/Umbraco.Web.UI.Docs/README.md @@ -0,0 +1,14 @@ +# Umbraco Backoffice UI API Documentation + +This project builds the documentation for the UI of the Umbraco backoffice, it is published on Our Umbraco in the "Reference" section of the documentation. + +In order to build the documentation, please follow the following two steps: + +``` +npm install +npx gulp docs +``` + +After this, you should have an `api` directory which contains index.html. + +In order to check if the documentation works properly, you would need to run the `api` directory in a webserver. On Windows, this can be accomplished by opening the `api` directory in [Visual Studio Code](https://code.visualstudio.com/) and running it with the [IIS Express plugin](https://marketplace.visualstudio.com/items?itemName=warren-buckley.iis-express). \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/docs.js b/src/Umbraco.Web.UI.Docs/gulpfile.js similarity index 72% rename from src/Umbraco.Web.UI.Client/gulp/tasks/docs.js rename to src/Umbraco.Web.UI.Docs/gulpfile.js index 34271a018c..789e2b7845 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/docs.js +++ b/src/Umbraco.Web.UI.Docs/gulpfile.js @@ -1,6 +1,7 @@ 'use strict'; -var config = require('../config'); + +var config = require('../Umbraco.Web.UI.Client/gulp/config'); var gulp = require('gulp'); var connect = require('gulp-connect'); @@ -16,28 +17,28 @@ gulp.task('docs', [], function (cb) { html5Mode: false, startPage: '/api', title: "Umbraco Backoffice UI API Documentation", - dest: 'docs/api', - styles: ['docs/umb-docs.css'], + dest: './api', + styles: ['./umb-docs.css'], image: "https://our.umbraco.com/assets/images/logo.svg" } return gulpDocs.sections({ api: { - glob: ['src/common/**/*.js', 'docs/src/api/**/*.ngdoc'], + glob: ['../Umbraco.Web.UI.Client/src/common/**/*.js', './src/api/**/*.ngdoc'], api: true, - title: 'API Documentation' + title: 'UI API Documentation' } }) .pipe(gulpDocs.process(options)) - .pipe(gulp.dest('docs/api')); + .pipe(gulp.dest('./api')); cb(); }); gulp.task('connect:docs', function (cb) { connect.server({ - root: 'docs/api', + root: './api', livereload: true, - fallback: 'docs/api/index.html', + fallback: './api/index.html', port: 8880 }); cb(); @@ -52,4 +53,4 @@ gulp.task('open:docs', function (cb) { gulp.src(__filename) .pipe(open(options)); cb(); -}); +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Docs/package.json b/src/Umbraco.Web.UI.Docs/package.json new file mode 100644 index 0000000000..843740482e --- /dev/null +++ b/src/Umbraco.Web.UI.Docs/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "scripts": { + "docs": "gulp docs", + "start": "gulp docs", + "default": "gulp docs" + }, + "devDependencies": { + "gulp": "^3.9.1", + "gulp-connect": "5.6.1", + "gulp-ngdocs": "0.3.0", + "gulp-open": "3.0.1" + } +} diff --git a/src/Umbraco.Web.UI.Client/docs/umb-docs.css b/src/Umbraco.Web.UI.Docs/umb-docs.css similarity index 76% rename from src/Umbraco.Web.UI.Client/docs/umb-docs.css rename to src/Umbraco.Web.UI.Docs/umb-docs.css index eef0fcee39..0f2e3e7f74 100644 --- a/src/Umbraco.Web.UI.Client/docs/umb-docs.css +++ b/src/Umbraco.Web.UI.Docs/umb-docs.css @@ -7,6 +7,21 @@ body { } +.container, .navbar-static-top .container, .navbar-fixed-top .container, .navbar-fixed-bottom .container { + max-width: 1500px; + width: 95%; +} + +.span3 { + width: 220px; + width: calc(90% / 12 * 3); +} + +.span9 { + width: 700px; + width: calc(90% / 12 * 9); +} + .h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { font-family: inherit; font-weight: 400; @@ -76,3 +91,12 @@ a:hover { width: 50px; margin-top: 5px; } + +.content .methods code { + border: 1px solid #e1e1e8; + background-color: #f7f7f9; + padding: 0px 3px; + font-size: 85%; + color: #d14; + white-space: nowrap; +} diff --git a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/package.manifest b/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/package.manifest deleted file mode 100644 index 5e64177046..0000000000 --- a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/package.manifest +++ /dev/null @@ -1,18 +0,0 @@ -{ - // array of files we want to inject into the application - "javascript": [ - "~/App_Plugins/ModelsBuilder/modelsbuilder.controller.js", - "~/App_Plugins/ModelsBuilder/modelsbuilder.resource.js" - ], - - // models builder dashboard - "dashboards": [ - { - "alias": "settingsModelsBuilder", - "name": "Models Builder", - "view": "/App_Plugins/ModelsBuilder/modelsbuilder.html", - "sections": [ "settings" ], - "weight": 40 - } - ] -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 7ae996a6c5..1d77505fc6 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -12,7 +12,7 @@ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} OnBuildSuccess true - 44319 + 44331 enabled disabled false @@ -31,6 +31,7 @@ (see FindAppConfigFile target in detailed build output) --> Web.config + $(AdditionalFileItemNames);Content true @@ -80,13 +81,14 @@ + - - - + + + @@ -94,17 +96,20 @@ - - all - - + + 1.0.0-beta2-19324-01 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - - 8.0.4 + + 3.3.0 + runtime; build; native; contentfiles; analyzers + all @@ -117,6 +122,10 @@ Umbraco.Examine {07FBC26B-2927-4A22-8D96-D644C667FECC} + + {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} + Umbraco.ModelsBuilder.Embedded + {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web @@ -139,9 +148,6 @@ True Settings.settings - - - @@ -167,7 +173,6 @@ - ClientDependency.config Designer @@ -340,9 +345,9 @@ False True - 8100 + 8700 / - http://localhost:8100 + http://localhost:8700 False False @@ -424,4 +429,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml b/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml index e74dbea217..6941fe0367 100644 --- a/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml @@ -19,11 +19,13 @@ -
          -
          -
          + + -
          +
          Profile updated

          } diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml index 8e3222cd93..64be8eb60c 100755 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml @@ -1,5 +1,6 @@ @using Umbraco.Core.Models.PublishedContent @using Umbraco.Web +@using Umbraco.Core @inherits Umbraco.Web.Macros.PartialViewMacroPage @* @@ -28,13 +29,13 @@ @* a single image *@ if (media.IsDocumentType("Image")) { - @Render(media); + @Render(media) } @* a folder with images under it *@ foreach (var image in media.Children()) { - @Render(image); + @Render(image) } }
          diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml index 31a3bd604d..a7effa5606 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml @@ -1,4 +1,4 @@ -@using System.Web.Mvc.Html +@using System.Web.Mvc.Html @using ClientDependency.Core.Mvc @using Umbraco.Web @using Umbraco.Web.Models @@ -7,10 +7,11 @@ @{ var loginModel = new LoginModel(); + loginModel.RedirectUrl = HttpContext.Current.Request.Url.AbsolutePath; Html.EnableClientValidation(); Html.EnableUnobtrusiveJavaScript(); - Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"); + Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"); Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.0/jquery.validate.min.js"); Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"); } @@ -20,6 +21,7 @@ @using (Html.BeginUmbracoForm("HandleLogin")) { + @Html.HiddenFor(m => loginModel.RedirectUrl)
          Login diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml index b9fbea4733..804e2307f0 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml @@ -33,7 +33,7 @@ Html.EnableClientValidation(); Html.EnableUnobtrusiveJavaScript(); - Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"); + Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"); Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.0/jquery.validate.min.js"); Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"); @@ -45,7 +45,7 @@ @if (success) { - @* This message will show if RedirectOnSucces is set to false (default) *@ + @* This message will show if registerModel.RedirectUrl is not defined (default) *@

          Registration succeeded.

          } else @@ -85,7 +85,7 @@ else easily change it. For example, if you wanted to render a custom editor for this field called "MyEditor" you would create a file at ~/Views/Shared/EditorTemplates/MyEditor.cshtml", then you will change the next line of code to render your specific editor template like: - @Html.EditorFor(m => profileModel.MemberProperties[i].Value, "MyEditor") + @Html.EditorFor(m => registerModel.MemberProperties[i].Value, "MyEditor") *@ @Html.EditorFor(m => registerModel.MemberProperties[i].Value) @Html.HiddenFor(m => registerModel.MemberProperties[i].Alias) diff --git a/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml index 3574d5d175..bd8d23a851 100644 --- a/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml @@ -66,11 +66,7 @@
          -
          -
          - -
          @@ -93,7 +89,7 @@

          - + + @@ -119,6 +117,8 @@ document.angularReady = function(app) { @Html.AngularValueExternalLoginInfoScript(ViewData.GetExternalSignInError()) @Html.AngularValueResetPasswordCodeInfoScript(ViewData["PasswordResetCode"]) + @Html.AngularValueTinyMceAssets() + //required for the noscript trick document.getElementById("mainwrapper").style.display = "inherit"; } diff --git a/src/Umbraco.Web.UI/Umbraco/Views/Preview/Index.cshtml b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Index.cshtml index 17a2f3c66a..4b7bcaee87 100644 --- a/src/Umbraco.Web.UI/Umbraco/Views/Preview/Index.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Index.cshtml @@ -1,72 +1,80 @@ -@using Umbraco.Core -@using ClientDependency.Core -@using ClientDependency.Core.Mvc -@using Umbraco.Core.IO -@using Umbraco.Web -@using Umbraco.Core.Configuration - -@inherits System.Web.Mvc.WebViewPage -@{ - var disableDevicePreview = Model.DisableDevicePreview.ToString().ToLowerInvariant(); - - Html - .RequiresCss("assets/css/canvasdesigner.css", "Umbraco"); -} - - - - - Umbraco Preview - - - - @Html.RenderCssHere( - new BasicPath("Umbraco", IOHelper.ResolveUrl(SystemDirectories.Umbraco))) - - - -
          - - @if (string.IsNullOrWhiteSpace(Model.PreviewExtendedHeaderView) == false) - { - @Html.Partial(Model.PreviewExtendedHeaderView) - } - -
          - -
          -
          -
          -
          - -
          -
            -
          • - -
          • - - @if (Model.Languages != null && Model.Languages.Count() > 1) - { - foreach (var previewLink in Model.Languages) - { -
          • - @previewLink.CultureName -
          • - } - } - -
          • - -
          • -
          -
          - -
          - - - - - - +@using Umbraco.Core +@using ClientDependency.Core +@using ClientDependency.Core.Mvc +@using Umbraco.Core.IO +@using Umbraco.Web +@using Umbraco.Core.Configuration + +@inherits System.Web.Mvc.WebViewPage +@{ + var disableDevicePreview = Model.DisableDevicePreview.ToString().ToLowerInvariant(); + + Html.RequiresCss("assets/css/canvasdesigner.css", "Umbraco"); +} + + + + + Umbraco Preview + + + + @Html.RenderCssHere( + new BasicPath("Umbraco", IOHelper.ResolveUrl(SystemDirectories.Umbraco))) + + + +
          + + @if (string.IsNullOrWhiteSpace(Model.PreviewExtendedHeaderView) == false) + { + @Html.Partial(Model.PreviewExtendedHeaderView) + } + +
          + +
          +
          + + +
          + + + + + + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml index a229898673..fe7e8ac638 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml @@ -81,7 +81,7 @@ Uložit Uložit a publikovat Uložit a odeslat ke schválení - Náhled + Náhled Náhled je deaktivován, protože není přiřazena žádná šablona Vybrat styl Zobrazit styly @@ -225,7 +225,6 @@ Místní odkaz: Při používání místních odkazů vložte znak "#" před odkaz Otevřít v novém okně? - Nastavení makra Toto makro nemá žádné vlastnosti, které by bylo možno editovat Vložit Editovat oprávnění pro diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 79ac6bc641..7cead86114 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1,1610 +1,1806 @@ - - - - The Umbraco community - https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - - - Tilføj domæne - Revisionsspor - Gennemse elementer - Skift dokumenttype - Kopier - Opret - Opret pakke - Opret gruppe - Slet - Deaktivér - Tøm papirkurv - Aktivér - Eksportér dokumenttype - Importér dokumenttype - Importér pakke - Redigér i Canvas - Log af - Flyt - Notificeringer - Offentlig adgang - Udgiv - Afpublicér - Genindlæs elementer - Genudgiv hele sitet - Omdøb - Gendan - Sæt rettigheder for siden %0% - Vælg hvor du vil kopiere - Vælg hvortil du vil flytte - til i træstrukturen nedenfor - blev flyttet til - blev kopieret til - Rettigheder - Fortryd ændringer - Send til udgivelse - Send til oversættelse - Sæt gruppe - Sortér - Send til udgivelse - Oversæt - Opdatér - Sæt rettigheder - Lås op - Opret indholdsskabelon - Standard værdi - - - Indhold - Administration - Struktur - Andet - - - Tillad adgang til at tildele sprog og domæner - Tillad adgang for at få vist en nodes historik - Tillad adgang for at få vist en node - Tillad adgang til at ændre dokumenttype for en node - Tillad adgang til at kopiere en node - Tillad adgang til at oprette noder - Tillad adgang til at slette noder - Tillad adgang til at flytte en node - Tillad adgang til at indstille og ændre offentlig adgang til en node - Tillad adgang til at udgive en node - Tillad adgang til at ændre rettigheder for en node - Tillad adgang til at returnere en node til en tidligere tilstand - Tillad adgang til at sende en node til godkendelse før den udgives - Tillad adgang til at sende en node til oversættelse - Tillad adgang til at ændre sorteringsrækkefølge for noder - Tillad adgang til at oversætte en node - Tillad adgang til at gemme en node - Tillad adgang til at oprette en indholdsskabelon - - - Indhold - Info - - - Tilladelse nægtet. - Tilføj nyt domæne - fjern - Ugyldig node. - Et eller flere domæner har et ugyldigt format. - Domæne er allerede blevet tildelt. - Sprog - Domæne - Domænet '%0%' er nu oprettet og tilknyttet siden - Domænet '%0%' er nu slettet - Domænet '%0%' er oprettet - Domænet '%0%' er nu opdateret - eller rediger nuværende domæner - - Nedarv - Sprog - eller nedarv sprog fra forældre noder. Gælder også
          - for den aktuelle node, medmindre et domæne nedenfor også indstiller et sprog.]]>
          - Domæner - - - Ryd valg - Vælg - Gør noget andet - Fed - Fortryd indryk afsnit - Indsæt formularfelt - Indsæt grafisk overskrift - Redigér Html - Indryk afsnit - Kursiv - Centrér - Venstrestil afsnit - Højrestil afsnit - Indsæt link - Indsæt lokalt link (anker) - Punktopstilling - Nummerorden - Indsæt makro - Indsæt billede - Udgiv og luk - Udgiv med undersider - Redigér relationer - Tilbage til listen - Gem - Gem og luk - Gem og udgiv - Gem og planlæg - Gem og send til udgivelse - Gem listevisning - Planlæg - Forhåndsvisning - Forhåndsvisning er deaktiveret fordi der ikke er nogen skabelon tildelt - Vælg formattering - Vis koder - Indsæt tabel - Gem og generer modeller - Fortryd - Genskab - Slet tag - Fortryd - Bekræft - - - For - Brugeren har slettet indholdet - Brugeren har afpubliceret indholdet - Brugeren har gemt og udgivet indholdet - Brugeren har gemt og udgivet indholdet for sprogene: %0% - Brugeren har gemt indholdet - Brugeren har flyttet indholdet - Brugeren har kopieret indholdet - Brugeren har tilbagerullet indholdet til en tidligere tilstand - Brugeren har sendt indholdet til udgivelse - Brugeren har sendt indholdet til oversættelse - Brugeren har sorteret de underliggende sider - Kopieret - Udgivet - Flyttet - Gemt - Slettet - Afpubliceret - Indhold tilbagerullet - Sendt til udgivelse - Sendt til oversættelse - Sorteret - Historik (alle sprog) - - - For at skifte det valgte indholds dokumenttype, skal du først vælge en ny dokumenttype, som er gyldig på denne placering. - Kontroller derefter, at alle egenskaber bliver overført rigtigt til den nye dokumenttype, og klik derefter på Gem. - Indholdet er blevet genudgivet. - Nuværende egenskab - Nuværende type - Du kan ikke skifte dokumenttype, da der ikke er andre gyldige dokumenttyper på denne placering. - Dokumenttype skiftet - Overfør egenskaber - Overfør til egenskab - Ny skabelon - Ny type - ingen - Indhold - Vælg ny dokumenttype - Dokumenttypen på detvalgte indhold blev skiftet til [new type], og følgende egenskaber blev overført: - til - Overførsel af egenskaber kunne ikke fuldføres, da en eller flere egenskaber er indstillet til at blive overført mere end én gang. - Kun andre dokumenttyper, der er gyldige på denne placering, vises. - - - Oprettelse af mappen under parent med ID %0% fejlede - Oprettelse af mappen under parent med navnet %0% fejlede - Mappens navn må ikke indeholde ugyldige tegn. - Sletning af filen/mappen fejlede: %0% - - - Udgivet - Om siden - Alias - (hvordan ville du f.eks. beskrive billedet via telefonen?) - Alternative links - Klik for at redigere dette punkt - Oprettet af - Oprindelig forfatter - Opdateret af - Oprettet den - Tidspunkt for oprettelse - Dokumenttype - Redigerer - Nedtagningsdato - Dette punkt er ændret siden udgivelsen - Dette punkt er endnu ikke udgivet - Sidst udgivet - Der er ingen elementer at vise - Der er ingen elementer at vise på listen. - Intet indhold er blevet tilføjet - Ingen medlemmer er blevet tilføjet - Medietype - Link til medie(r) - Medlemsgruppe - Rolle - Medlemstype - Der er endnu ikke lavet nogle ændringer. - Ingen dato valgt - Sidetitel - Egenskaber - Dette dokument er udgivet, men ikke synligt da den overliggende side '%0%' ikke er udgivet! - Ups: dette dokument er udgivet, men er ikke i cachen (intern fejl) - Kunne ikke hente url'en - Dette dokument er udgivet, men dets url ville kollidere med indholdet %0% - Udgiv - Udgivet - Udgivet (Ventede ændringer) - Udgivelsesstatus - Udgiv med undersider for at udgive %0% og alle sider under og dermed gøre deres indhold offentligt tilgængelige.]]> - Udgiv med undersider for at udgive de valgte sprog og de samme sprog for sider under og dermed gøre deres indhold offentligt tilgængelige.]]> - Udgivelsesdato - Afpubliceringsdato - Fjern dato - Vælg dato - Sorteringsrækkefølgen er opdateret - For at sortere, træk siderne eller klik på en af kolonnehovederne. Du kan vælge flere sider ved at holde "shift" eller "control" nede mens du vælger. - Statistik - Titel (valgfri) - Alternativ tekst (valgfri) - Type - Afpublicér - Afpubliceret - Ikke oprettet - Sidst redigeret - Tidspunkt for seneste redigering - Fjern fil - Link til dokument - Medlem af grupper(ne) - Ikke medlem af grupper(ne) - Undersider - Åben i vindue - Dette oversætter til den følgende tid på serveren: - Hvad betyder det?]]> - Er du sikker på, at du vil slette dette element? - Egenskaben %0% anvender editoren %1% som ikke er understøttet af Nested Content. + + + + The Umbraco community + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files + + + Tilføj domæne + Revisionsspor + Gennemse elementer + Skift Dokument Type + Skift Input Type + Kopier + Opret + Eksportér + Opret pakke + Opret gruppe + Slet + Deaktivér + Tøm papirkurv + Aktivér + Eksportér dokumenttype + Importér dokumenttype + Importér pakke + Redigér i Canvas + Log af + Flyt + Notificeringer + Offentlig adgang + Udgiv + Afpublicér + Genindlæs elementer + Genudgiv hele sitet + Omdøb + Gendan + Sæt rettigheder for siden %0% + Vælg hvor du vil kopiere + Vælg hvortil du vil flytte + Vælg hvor du vil flytte de valgte elementer hen + til i træstrukturen nedenfor + Vælg hvor du vil kopiere de valgte elementer til + blev flyttet til + blev kopieret til + blev slettet + Rettigheder + Fortryd ændringer + Send til udgivelse + Send til oversættelse + Sæt gruppe + Sortér + Send til udgivelse + Oversæt + Opdatér + Sæt rettigheder + Lås op + Opret indholdsskabelon + Standard værdi + + + Indhold + Administration + Struktur + Andet + + + Tillad adgang til at tildele sprog og domæner + Tillad adgang for at få vist en nodes historik + Tillad adgang for at få vist en node + Tillad adgang til at ændre dokumenttype for en node + Tillad adgang til at kopiere en node + Tillad adgang til at oprette noder + Tillad adgang til at slette noder + Tillad adgang til at flytte en node + Tillad adgang til at indstille og ændre offentlig adgang til en node + Tillad adgang til at udgive en node + Tillad adgang til at afpublicere en node + Tillad adgang til at ændre rettigheder for en node + Tillad adgang til at returnere en node til en tidligere tilstand + Tillad adgang til at sende en node til godkendelse før den udgives + Tillad adgang til at sende en node til oversættelse + Tillad adgang til at ændre sorteringsrækkefølge for noder + Tillad adgang til at oversætte en node + Tillad adgang til at gemme en node + Tillad adgang til at oprette en indholdsskabelon + + + Indhold + Info + + + Tilladelse nægtet. + Tilføj nyt domæne + fjern + Ugyldig node. + Et eller flere domæner har et ugyldigt format. + Domæne er allerede blevet tildelt. + Sprog + Domæne + Domænet '%0%' er nu oprettet og tilknyttet siden + Domænet '%0%' er nu slettet + Domænet '%0%' er oprettet + Domænet '%0%' er nu opdateret + eller rediger nuværende domæner + + Nedarv + Sprog + eller nedarv sprog fra forældre noder. Gælder også
          + for den aktuelle node, medmindre et domæne nedenfor også indstiller et sprog.]]>
          + Domæner + + + Ryd valg + Vælg + Gør noget andet + Fed + Fortryd indryk afsnit + Indsæt formularfelt + Indsæt grafisk overskrift + Redigér Html + Indryk afsnit + Kursiv + Centrér + Venstrestil afsnit + Højrestil afsnit + Indsæt link + Indsæt lokalt link (anker) + Punktopstilling + Nummerorden + Indsæt makro + Indsæt billede + Udgiv og luk + Udgiv med undersider + Redigér relationer + Tilbage til listen + Gem + Gem og luk + Gem og udgiv + Gem og planlæg + Gem og send til udgivelse + Gem listevisning + Planlæg + Forhåndsvisning + Forhåndsvisning er deaktiveret fordi der ikke er nogen skabelon tildelt + Vælg formattering + Vis koder + Indsæt tabel + Gem og generer modeller + Fortryd + Genskab + Slet tag + Fortryd + Bekræft + Flere publiseringsmuligheder + + + For + Brugeren har slettet indholdet + Brugeren har afpubliceret indholdet + Brugeren har gemt og udgivet indholdet + Brugeren har gemt og udgivet indholdet for sprogene: %0% + Brugeren har gemt indholdet + Brugeren har gemt indholdet for sprogene: %0% + Brugeren har flyttet indholdet + Brugeren har kopieret indholdet + Brugeren har tilbagerullet indholdet til en tidligere tilstand + Brugeren har sendt indholdet til udgivelse + Brugeren har sendt indholdet til oversættelse + Brugeren har sorteret de underliggende sider + Kopieret + Udgivet + Udgivet + Flyttet + Gemt + Gemt + Slettet + Afpubliceret + Indhold tilbagerullet + Sendt til udgivelse + Sendt til oversættelse + Sorteret + Historik (alle sprog) + + + For at skifte det valgte indholds dokumenttype, skal du først vælge en ny dokumenttype, som er gyldig på denne placering. + Kontroller derefter, at alle egenskaber bliver overført rigtigt til den nye dokumenttype, og klik derefter på Gem. + Indholdet er blevet genudgivet. + Nuværende egenskab + Nuværende type + Du kan ikke skifte dokumenttype, da der ikke er andre gyldige dokumenttyper på denne placering. + Dokumenttype skiftet + Overfør egenskaber + Overfør til egenskab + Ny skabelon + Ny type + ingen + Indhold + Vælg ny dokumenttype + Dokumenttypen på detvalgte indhold blev skiftet til [new type], og følgende egenskaber blev overført: + til + Overførsel af egenskaber kunne ikke fuldføres, da en eller flere egenskaber er indstillet til at blive overført mere end én gang. + Kun andre dokumenttyper, der er gyldige på denne placering, vises. + + + Oprettelse af mappen under parent med ID %0% fejlede + Oprettelse af mappen under parent med navnet %0% fejlede + Mappens navn må ikke indeholde ugyldige tegn. + Sletning af filen/mappen fejlede: %0% + + + Udgivet + Om siden + Alias + (hvordan ville du f.eks. beskrive billedet via telefonen?) + Alternative links + Klik for at redigere dette punkt + Oprettet af + Oprindelig forfatter + Opdateret af + Oprettet den + Tidspunkt for oprettelse + Dokumenttype + Redigerer + Nedtagningsdato + Dette punkt er ændret siden udgivelsen + Dette punkt er endnu ikke udgivet + Sidst udgivet + Der er ingen elementer at vise + Der er ingen elementer at vise på listen. + Intet indhold er blevet tilføjet + Ingen medlemmer er blevet tilføjet + Medietype + Link til medie(r) + Medlemsgruppe + Rolle + Medlemstype + Der er endnu ikke lavet nogle ændringer. + Ingen dato valgt + Sidetitel + Dette medie har ikke noget link + Egenskaber + Dette dokument er udgivet, men ikke synligt da den overliggende side '%0%' ikke er udgivet! + Dette sprog er udgivet, men ikke synligt, da den overliggende side '%0%' ikke er udgivet! + Ups: dette dokument er udgivet, men er ikke i cachen (intern fejl) + Kunne ikke hente url'en + Dette dokument er udgivet, men dets url ville kollidere med indholdet %0% + Dette dokument er udgivet, men dets URL kan ikke dirigeres + Udgiv + Udgivet + Udgivet (Afventende ændringer) + Udgivelsesstatus + Udgiv med undersider for at udgive %0% og alle sider under og dermed gøre deres indhold offentligt tilgængelige.]]> + Udgiv med undersider for at udgive de valgte sprog og de samme sprog for sider under og dermed gøre deres indhold offentligt tilgængelige.]]> + Udgivelsesdato + Afpubliceringsdato + Fjern dato + Vælg dato + Sorteringsrækkefølgen er opdateret + For at sortere, træk siderne eller klik på en af kolonnehovederne. Du kan vælge flere sider ved at holde "shift" eller "control" nede mens du vælger. + Statistik + Titel (valgfri) + Alternativ tekst (valgfri) + Type + Afpublicér + Afpubliceret + Ikke oprettet + Sidst redigeret + Tidspunkt for seneste redigering + Fjern fil + Link til dokument + Medlem af grupper(ne) + Ikke medlem af grupper(ne) + Undersider + Åben i vindue + Dette oversætter til den følgende tid på serveren: + Hvad betyder det?]]> + Er du sikker på, at du vil slette dette element? + Er du sikker på, at du vil slette alle elementer? + Egenskaben %0% anvender editoren %1% som ikke er understøttet af Nested Content. Der er ikke konfigureret nogen indholdstyper for denne egenskab. - Tilføj en ny tekstboks - Fjern denne tekstboks - Indholdsrod - Medtag udkast: udgiv også ikke-publicerede sider. - Denne værdi er skjult.Hvis du har brug for adgang til at se denne værdi, bedes du kontakte din web-administrator. - Denne værdi er skjult. - Hvilke sprog vil du gerne udgive? Alle sprog med indhold gemmes! - Hvilke sprog vil du gerne udgive? - Hvilke sprog vil du gerne gemme? - Hvilke sprog vil du gerne gemme? - Hvilke sprog vil du gerne sende til godkendelse? - Hvilke sprog vil du gerne planlægge? - Vælg sproget du vil afpublicere. Afpublicering af et obligatorisk sprog vil afpublicere alle sprog. - Udgivne sprog - Ikke-udgivne sprog - Uændrede sprog - Disse sprog er ikke blevet oprettet - Klar til at udgive? - Klar til at gemme? - Send til godkendelse + %0% fra %1% + Tilføj en ny tekstboks + Fjern denne tekstboks + Indholdsrod + Medtag udkast: udgiv også ikke-publicerede sider. + Denne værdi er skjult.Hvis du har brug for adgang til at se denne værdi, bedes du kontakte din web-administrator. + Denne værdi er skjult. + Hvilke sprog vil du gerne udgive? Alle sprog med indhold gemmes! + Hvilke sprog vil du gerne udgive? + Hvilke sprog vil du gerne gemme? + Hvilke sprog vil du gerne gemme? + Hvilke sprog vil du gerne sende til godkendelse? + Hvilke sprog vil du gerne planlægge? + Vælg sproget du vil afpublicere. Afpublicering af et obligatorisk sprog vil afpublicere alle sprog. + Udgivne sprog + Ikke-udgivne sprog + Uændrede sprog + Disse sprog er ikke blevet oprettet + Klar til at udgive? + Klar til at gemme? + Send til godkendelse Vælg dato og klokkeslæt for at udgive og/eller afpublicere indholdet. Opret ny - Indsæt fra udklipsmappen - - - Opret en ny indholdsskabelon fra '%0%' - Blank - Vælg en indholdsskabelon - Indholdsskabelon oprettet - En indholdsskabelon blev oprettet fra '%0%' - En anden indholdsskabelon med samme navn eksisterer allerede - En indholdskabelon er foruddefineret indhold, som en redaktør kan vælge at bruge som grundlag for at oprette nyt indhold - - - Klik for at uploade - eller klik her for at vælge filer - Du kan trække filer herind for at uploade. - Kan ikke uploade denne fil, den har ikke en godkendt filtype - Maks filstørrelse er - Medie rod - Flytning af mediet fejlede - Kopiering af mediet fejlede - Oprettelse af mappen under parent med id %0% fejlede - Omdøbning af mappen med id %0% fejlede - - - Opret et nyt medlem - Alle medlemmer - Medlemgrupper har ingen yderligere egenskaber til redigering. - - - Hvor ønsker du at oprette den nye %0% - Opret under - Vælg den dokumenttype, du vil oprette en indholdsskabelon til - Angiv et navn for mappen - Vælg en type og skriv en titel - "dokument typer".]]> - "media typer".]]> - Dokumenttype uden skabelon - Ny mappe - Ny datatype - Ny JavaScript-fil - Ny tom partial view - Ny partial view makro - Ny partial view fra snippet - Ny partial view makro fra snippet - Ny partial view makro (uden makro) - Ny stylesheet-fil - Ny Rich Text Editor stylesheet-fil - - - Til dit website - - Skjul - Hvis Umbraco ikke starter, kan det skyldes at din browser ikke tillader pop-up vinduer - er åbnet i nyt vindue - Genstart - Besøg - Velkommen - - - Bliv - Kassér ændringer - Du har ikke-gemte ændringer - Er du sikker på du vil navigere væk fra denne side? - du har ikke-gemte ændringer - Afpublicering vil fjerne denne side og alle dets undersider fra websitet. - Du har ikke-gemte ændringer. Hvis du ændrer dokumenttype, kasseres ændringerne. - - - Færdig - Slettede %0% element - Slettede %0% elementer - Slettede %0% ud af %1% element - Slettede %0% ud af %1% elementer - Udgav %0% element - Udgav %0% elementer - Udgav %0% ud af %1% element - Udgav %0% ud af %1% elementer - Fjernede %0% element fra udgivelse - Fjernede %0% elementer fra udgivelse - Fjernede %0% ud af %1% element fra udgivelse - Fjernede %0% ud af %1% elementer fra udgivelse - Flyttede %0% element - Flyttede %0% elementer - Flyttede %0% ud af %1% element - Flyttede %0% ud af %1% elementer - Kopierede %0% element - Kopierede %0% elementer - Kopierede %0% ud af %1% element - Kopierede %0% ud af %1% elementer - - - Link titel - Link - Navn på lokalt link - Rediger domæner - Luk denne dialog - Er du sikker på at du vil slette - Er du sikker på du vil deaktivere - Er du sikker på at du vil forlade Umbraco? - Er du sikker? - Klip - Rediger ordbogsnøgle - Rediger sprog - Indsæt lokalt link - Indsæt tegn - Indsæt grafisk overskrift - Indsæt billede - Indsæt link - Indsæt makro - Indsæt tabel - Dette vil slette sproget - Sidst redigeret - Link - Internt link: - Ved lokalt link, indsæt da en "#" foran linket - Åben i nyt vindue? - Makroindstillinger - Denne makro har ingen egenskaber du kan redigere - Indsæt tekst - Rediger rettigheder for - Sæt rettigheder for - Sæt rettigheder for %0% for brugergruppe %1% - Vælg de brugergrupper, du vil angive tilladelser til - Elementerne i papirkurven slettes. Luk venligst ikke dette vindue mens sletningen foregår - Papirkurven er nu tom - Når elementer slettes fra papirkurven, slettes de for altid - regexlib.com's webservice oplever i øjeblikket problemer, vi ikke har kontrol over. Beklager ulejligheden. ]]> - Søg efter et regulært udtryk for at tilføje validering til et formularfelt. Eksempel: 'e-mail', 'postnr.', 'url' - Fjern makro - Obligatorisk - Sitet er genindekseret - Sitet er nu genudgivet - Websitets cache vil blive genopfrisket. Alt udgivet indhold vil blive opdateret, mens upubliceret indhold vil forblive upubliceret. - Antal kolonner - Antal rækker - Klik på billedet for at se den fulde størrelse - Vælg - Se cache element - Relatér til original - Inkludér undersider - Link til side - Åben linket i et nyt vindue eller fane - Link til medie - Vælg indhold startnode - Vælg medie - Vælg ikon - Vælg item - Vælg link - Vælg makro - Vælg indhold - Vælg medie startnode - Vælg medlem - Vælg medlemsgruppe - Vælg node - Vælg sektioner - Vælg brugere - Ingen ikoner blev fundet - Der er ingen parametre for denne makro - Der er ikke tilføjet nogen makroer - Link dit - Fjern link fra dit - konto - Vælg editor - Vælg snippet - Dette vil slette noden og alle dets sprog. Hvis du kun vil slette et sprog, så afpublicér det i stedet. - - - Der er ingen ordbogselementer. - - - Du tilføjer flere sprog under 'sprog' i menuen til venstre - ]]> - Kulturnavn - - Ordbogsoversigt - - - Konfigurerede søgere - Viser egenskaber og værktøjer til enhver konfigureret søger (dvs. som en multi-indekssøger) - Feltværdier - Sundhedstilstand - Indeksets sundhedstilstand, og hvis det kan læses - Indeksører - Indeksinfo - Viser indeksets egenskaber - Administrer Examine indekserne - Giver dig mulighed for at se detaljerne for hvert indeks og giver nogle værktøjer til styring af indeksørerne - Genopbyg indeks - - Afhængigt af hvor meget indhold der er på dit website, kan det tage et stykke tid.
          - Det anbefales ikke at genopbygge et indeks i perioder med høj websitetrafik eller når redaktører redigerer indhold. - ]]> -
          - Søgere - Søg i indekset og se resultaterne - Værktøjer - Værktøjer til at administrere indekset - - - Indtast dit brugernavn - Indtast dit kodeord - Bekræft dit kodeord - Navngiv %0%... - Indtast navn... - Indtast en e-mail... - Indtast et brugernavn... - Label... - Indtast beskrivelse - Søg... - Filtrer... - Indtast nøgleord (tryk på Enter efter hvert nøgleord)... - Indtast din e-mail - Indtast en besked... - Dit brugernavn er typisk din e-mailadresse - Indtast alias... - Genererer alias... - - - Opret brugerdefineret listevisning - Fjern brugerdefineret listevisning - - - Omdøbt - Indtast et ny mappenavn her - %0% was renamed to %1% - - - Tilføj førværdi - Database-datatype - Data Editor GUID - Visningskontrol - Knapper - Aktiver avancerede indstillinger for - Aktiver kontekstmenu - Maks. std. størrelse på indsatte billeder - Relaterede stylesheets - Vis label - Bredde og højde - All property types & property data - using this data type will be deleted permanently, please confirm you want to delete these as well - Ja, slet - and all property types & property data using this data type - Vælg den mappe, der skal flyttes - til i træstrukturen nedenfor - blev flyttet under - - - Dine data er blevet gemt, men før du kan udgive denne side er der nogle fejl der skal rettes: - Den nuværende membership-provider understøtter ikke skift af kodeord (EnablePasswordRetrieval skal være true) - %0% der findes allerede - Der var fejl i dokumentet: - Der var fejl i formularen: - Kodeordet skal være på minimum %0% tegn og indeholde mindst %1% alfanumeriske karakterer - %0% skal være et heltal - %0% under %1% er et obligatorisk felt og skal udfyldes - %0% er et obligatorisk felt og skal udfyldes - %0% under %1% er ikke i et korrekt format - %0% er ikke i et korrekt format - - - Der skete en fejl på severen - Denne filttype er blevet deaktiveret af administratoren - OBS! Selvom CodeMirror er slået til i konfigurationen, så er den deaktiveret i Internet Explorer fordi den ikke er stabil nok. - Du skal udfylde både Alias & Navn på den nye egenskabstype! - Der mangler læse/skrive rettigheder til bestemte filer og mapper - Skriv venligst en titel - Du skal vælge en type - Du er ved at gøre billedet større end originalen. Det vil forringe kvaliteten af billedet. Ønsker du at fortsætte? - Startnode er slettet, kontakt systemadministrator - Du skal markere noget indhold, før du kan ændre stylen - Der er ingen aktive styles eller formatteringer på denne side - Du skal stå til venstre for de 2 celler du ønsker at samle! - Du kan ikke opdele en celle, som ikke allerede er delt. - - - Om - Handling - Muligheder - Tilføj - Alias - Er du sikker? - Tilbage - Tilbage til oversigt - Kant - af - Fortryd - Celle margen - Vælg - Luk - Luk vindue - Kommentar - Bekræft - Behold proportioner - Indhold - Fortsæt - Kopiér - Opret - Database - Dato - Standard - Slet - Slettet - Sletter... - Design - Dimensioner - Ned - Hent - Rediger - Redigeret - Elementer - E-mail - Fejl - Felt - Find - Første - Generelt - Grupper - Gruppe - Højde - Hjælp - Skjul - Historik - Ikon - Id - Importer - Inkludér undermapper i søgning - Indre margen - Indsæt - Installér - Ugyldig - Justering - Sprog - Sidste - Layout - Henter - Låst - Log ind - Log af - Log ud - Makro - Påkrævet - Besked - Flyt - Navn - Ny - Næste - Nej - af - OK - Åben - eller - Sortér efter - Kodeord - Sti - Et øjeblik... - Forrige - Egenskaber - Genopbyg - E-mail der skal modtage indhold af formular - Papirkurv - Din papirkurv er tom - Genindlæs - Mangler - Fjern - Omdøb - Forny - Påkrævet - Hent - Prøv igen - Rettigheder - Planlagt publicering - Søg - Beklager, vi kan ikke finde det, du leder efter. - Ingen elementer er blevet tilføjet - Server - Indstillinger - Vis - Hvilken side skal vises efter at formularen er sendt - Størrelse - Sortér - Status - Indsend - Type - Skriv for at søge... - Op - Opdatér - Opdatér - Upload - Url - Bruger - Brugernavn - Værdi - Vis - Velkommen... - Bredde - Ja - Mappe - Søgeresultater - Sorter - Afslut sortering - Eksempel - Skift kodeord - til - Listevisning - Gemmer... - nuværende - Indlejring - valgt - - - Blå - - - Tilføj fane - Tilføj egenskab - Tilføj editor - Tilføj skabelon - Tilføj child node - Tilføj child - Rediger datatype - Naviger sektioner - Genveje - Vis genveje - Brug listevisning - Tillad på rodniveau - Comment/Uncomment lines - Remove line - Copy Lines Up - Copy Lines Down - Move Lines Up - Move Lines Down - Generelt - Editor - - - Baggrundsfarve - Fed - Tekstfarve - Skrifttype - Tekst - - - Side - - - Installeringsprogrammet kan ikke forbinde til databasen. - Kunne ikke gemme web.config filen. Du bedes venligst manuelt ændre database forbindelses strengen. - Din database er blevet fundet og identificeret som - Database konfiguration - installér knappen for at installere Umbraco %0% databasen - ]]> - Næste for at fortsætte.]]> - Databasen er ikke fundet. Kontrollér venligst at informationen i database forbindelsesstrengen i "web.config" filen er korrekt.

          -

          For at fortsætte bedes du venligst rette "web.config" filen (ved at bruge Visual Studio eller dit favoritprogram), scroll til bunden, tilføj forbindelsesstrengen til din database i feltet som hedder "umbracoDbDSN" og gem filen.

          Klik på Forsøg igen knappen når du er færdig.
          Mere information om at redigere web.config her.

          ]]>
          - Kontakt venligst din ISP hvis det er nødvendigt. Hvis du installerer på en lokal maskine eller server kan du muligvis få informationerne fra din systemadministrator.]]> - Tryk på Opgradér knappen for at opgradere din database til Umbraco %0%

          Bare rolig - intet indhold vil blive slettet og alt vil stadig fungere bagefter!

          ]]>
          - Tryk på Næste for at fortsætte.]]> - Næste for at fortsætte med konfigurationsguiden.]]> - Normalbrugerens adgangskode er nødt til at blive ændret!]]> - Normalbrugeren er blevet gjort utjenstdygtig eller har ikke adgang til Umbraco!

          Du behøver ikke foretage yderligere handlinger. Tryk på Næste for at fortsætte.

          ]]>
          - Normalbrugerens adgangskode er på succesfuld vis blevet ændret siden installationen!

          Du behøver ikke foretage yderligere handlinger. Tryk på Næste for at fortsætte.

          ]]>
          - Adgangskoden er blevet ændret! - Få en fremragende start, se vores videoer - Ved at klikke på næste knappen (eller ved at ændre UmbracoConfigurationStatus i web.config filen), accepterer du licensaftalen for denne software, som specificeret i boksen nedenfor. Bemærk venligst at denne Umbraco distribution består af to forskellige licenser, MIT's Open Source Licens for frameworket og Umbraco Freeware Licensen som dækker UI'en. - Endnu ikke installeret - Berørte filer og foldere - Flere informationer om at opsætte rettigheder for Umbraco her - Du er nødt til at give ASP.NET 'modify' rettigheder på følgende filer/foldere - Dine rettighedsindstillinger er næsten perfekte!

          Du kan køre Umbraco uden problemer, men du vil ikke være i stand til at installere pakker, som er anbefalet for at få fuldt udbytte af Umbraco.]]>
          - Hvorledes besluttes - Klik her for at læse tekstversionen - video tutorials om at opsætte folderrettigheder for Umbraco eller læs tekstversionen.]]> - Dine rettighedsindstillinger kan være et problem!

          Du kan afvikle Umbraco uden problemer, men du vil ikke være i stand til at oprette foldere eller installere pakker, hvilket er anbefalet for at få fuldt udbytte af Umbraco.]]>
          - Dine rettighedsindstillinger er ikke klar til Umbraco!

          For at afvikle Umbraco er du nødt til at opdatere dine rettighedsindstillinger.]]>
          - Dine rettighedsindstillinger er perfekte!

          Du er nu parat til at afvikle Umbraco og installere pakker!]]>
          - Løser folder problem - Følg dette link for mere information om udfordringer med ASP.NET og oprettelse af foldere - Sætter folderrettigheder op - Umbraco har behov for 'write/modify' adgang til bestemte foldere, for at kunne gemme filer som billeder og PDF'er. Umbraco gemmer også midlertidige data (eksempelvis cachen) for at forbedre ydelsen på dit website. - Jeg har lyst til at begynde på bar bund - lær hvordan) Du kan stadig vælge at installere Runway senere. Gå venligst til Udvikler-sektionen og vælg Pakker.]]> - Du har lige opsat en ren Umbraco-platform. Hvad ønsker du nu at gøre? - Runway er installeret - Dette er vores liste over anbefalede moduler. Kryds dem af du ønsker at installere eller se den fulde liste af moduler ]]> - Kun anbefalet for erfarne brugere - Jeg ønsker at begynder med et simpelt website - "Runway" er et simpelt website som stiller nogle basale dokumenttyper og skabeloner til rådighed. Installeringsprogrammet kan automatisk opsætte Runway for dig, men du kan nemt redigere, udvide eller fjerne det. Det er ikke nødvendigt og du kan sagtens bruge Umbraco uden. Men Runway tilbyder et fundament, som er baseret på 'Best Practices', som får dig igang hurtigere end nogensinde før. Hvis du vælger at installere Runway, kan du efter eget valg vælge de grundlæggende byggesten kaldet 'Runway Modules' til at forbedre dine Runway-sider.

          Inkluderet med Runway:Home Page, Getting Started page, Installing Modules page.
          Valgfri Moduler: Top Navigation, Sitemap, Contact, Gallery.

          ]]>
          - Hvad er Runway - Skridt 1/5: Acceptér licens - Skridt 2/5: Database-konfiguration - Skridt 3/5: Validerer filrettigheder - Skridt 4/5: Kontrollér Umbraco sikkerhed - Skridt 5/5: Umbraco er parat til at få dig igang - Tak fordi du valgte Umbraco - Gennemse dit nye site Du installerede Runway, så hvorfor ikke se hvordan dit nye website ser ud.]]> - Yderligere hjælpe og informationer Få hjælp fra vores prisvindende fællesskab, gennemse dokumentationen eller se nogle gratis videoer om hvordan du opsætter et simpelt site, hvordan du bruger pakker og en 'quick guide' til Umbraco terminologier]]> - Umbraco %0% er installeret og klar til brug - /web.config filen og opdatére 'AppSetting' feltet UmbracoConfigurationStatus i bunden til '%0%'.]]> - komme igang med det samme ved at klikke på "Start Umbraco" knappen nedenfor.
          Hvis du er ny med Umbraco, kan du finde masser af ressourcer på vores 'getting started' sider. -]]>
          - Start UmbracoFor at administrere dit website skal du blot åbne Umbraco administrationen og begynde at tilføje indhold, opdatere skabelonerne og stylesheets'ene eller tilføje ny funktionalitet.]]> - Forbindelse til databasen fejlede. - Umbraco Version 3 - Umbraco Version 4 - Se - Umbraco %0% for en frisk installation eller for en opgradering fra version 3.0.

          Tryk på Næste for at begynde på guiden.]]>
          - - - Culture Code - Culture Name - - - Du har været inaktiv, og du vil blive logget ud om - Forny for at gemme dine ændringer - - - Så er det søndag! - Smil, det er mandag! - Hurra, det er tirsdag! - Hvilken herlig onsdag! - Glædelig torsdag! - Endelig fredag! - Glædelig lørdag - Log ind nedenfor - Log ind med - Din session er udløbet - © 2001 - %0%
          umbraco.com

          ]]>
          - Glemt adgangskode? - En e-mail vil blive sendt til den angivne adresse med et link til at nulstille din adgangskode - En e-mail med instruktioner for nulstilling af adgangskoden vil blive sendt til den angivne adresse, hvis det matcher vores optegnelser - Vis adgangskode - Skjul adgangskode - Tilbage til login formular - Angiv en ny adgangskode - Din adgangskode er blevet opdateret - Det link, du har klikket på, er ugyldigt eller udløbet - Umbraco: Nulstil adgangskode - Dit brugernavn til at logge på Umbraco backoffice er: %0%

          Klik her for at nulstille din adgangskode eller kopier/indsæt denne URL i din browser:

          %1%

          ]]>
          - - - Skrivebord - Sektioner - Indhold - - - Vælg siden ovenover... - %0% er nu kopieret til %1% - Kopier til - %0% er nu flyttet til %1% - Flyt til - er blevet valgt som roden for dit nye indhold, klik 'ok' nedenunder. - Intet element valgt, vælg et element i listen ovenfor før der klikkes 'fortsæt' - Det nuværende element kan ikke lægges under denne pga. sin type - Det nuværende element kan ikke ligge under en af dens undersider - Dette element må ikke findes på rodniveau - Denne handling er ikke tilladt fordi du ikke har de fornødne rettigheder på et eller flere af under-dokumenterne - Relater det kopierede element til originalen - - - %0%]]> - Notificeringer er gemt for - - Hej %0%

          -

          Dette er en automatisk mail for at informere dig om at opgaven '%1%' - er blevet udførtpå siden '%2%' af brugeren '%3%'

          -

          Opdateringssammendrag:

          %6%

          Hav en fortsat god dag!

          De bedste hilsner fra umbraco robotten

          ]]>
          - [%0%] Notificering om %1% udført på %2% - Notificeringer - - - Handlinger - Oprettet - Opret pakke - Vælg pakken fra din computer. Umbraco pakker er oftest en ".zip" fil - Dette vil slette pakken - Slip her for at uploade - Inkludér alle underliggende sider - eller klik her for at vælge pakkefil - Upload pakke - Installer en lokal pakke ved at vælge den fra din computer. Installer kun pakker fra kilder, du kender og stoler på - Upload en anden pakke - Annuller og upload en anden pakke - Jeg accepterer - betingelser for anvendelse - Sti til fil - Absolut sti til fil (f.eks.: /bin/umbraco.bin) - Installeret - Installeret pakker - Installér lokal - Afslut - Denne pakke har ingen konfigurationsvisning - Der er ikke blevet oprettet nogle pakker endnu - Du har ingen pakker installeret - 'Pakker' øverst til højre på din skærm]]> - Pakkehandlinger - Forfatter URL - Pakkeindhold - Pakkefiler - Ikon URL - Installér pakke - Licens - Licens URL - Pakkeegenskaber - Søg efter pakker - Resultater for - Vi kunne ikke finde resultater for - Prøv venligst at søge efter en anden pakke eller gennemse kategorierne - Populære - Nye udgivelser - har - karma points - Information - Ejer - Bidragsydere - Oprettet - Nuværende version - .NET version - Downloads - Likes - Kompatibilitet - Denne pakke er kompatibel med de følgende versioner af Umbraco, som rapporteret af community-medlemmer. Fuld kompatibilitet kan ikke garanteres for versioner rapporteret nedenfor 100% - Eksterne kilder - Forfatter - Dokumentation - Pakke meta data - Pakkenavn - Pakken indeholder ingen elementer -
          Du kan roligt fjerne denne fra systemet ved at klikke på "Fjern pakke" nedenfor.]]>
          - Pakkevalg - Pakke læs mig - Pakke opbevaringsbase - Bekræft af-installering - Pakken blev fjernet - Pakken er på succesfuld vis blevet fjernet - Afinstallér pakke - -Bemærk: at dokumenter og medier som afhænger af denne pakke vil muligvis holde op med at virke, så vær forsigtig. Hvis i tvivl, kontakt personen som har udviklet pakken.]]> - Pakke version - Pakke allerede installeret - Denne pakke kan ikke installeres, den kræver en minimum Umbraco version af - Afinstallerer... - Downloader... - Importeret... - Installerer... - Genstarter, vent venligst... - Færdig, din browser opdateres nu, vent venligst... - Klik venligst på 'Afslut' for at gennemføre installation og opdatere siden. - Uploader pakke... - - - Indsæt med fuld formattering (Anbefales ikke) - Den tekst du er ved at indsætte indeholder specialtegn eller formattering. Dette kan skyldes at du kopierer fra f.eks. Microsoft Word. Umbraco kan fjerne denne specialformattering automatisk så indholdet er mere velegnet til visning på en webside. - Indsæt som ren tekst, dvs. fjern al formattering - Indsæt, men fjern formattering som ikke bør være på en webside (Anbefales) - - - Gruppebaseret beskyttelse - Hvis du ønsker at give adgang til alle medlemmer af specifikke medlemsgrupper - Du skal oprette en medlemsgruppe før du kan bruge gruppebaseret beskyttelse - Fejlside - Brugt når folk er logget ind, men ingen adgang - %0% skal beskyttes]]> - %0% er nu beskyttet]]> - %0%]]> - Log ind-side - Vælg siden der indeholder log ind-formularen - Fjern beskyttelse... - %0%?]]> - Vælg siderne der indeholder log ind-formularer og fejlmeddelelser - %0%]]> - %0%]]> - Adgang til enkelte medlemmer - Hvis du ønsker at give adgang til enkelte medlemmer - - - Udgivelsen kunne ikke udgives da publiceringsdato er sat - - - %0% kunne ikke udgives, fordi et 3. parts modul annullerede handlingen - - Medtag ikke-udgivede undersider - Publicerer - vent venligst... - %0% ud af %1% sider er blevet udgivet... - %0% er nu publiceret - %0% og alle undersider er nu publiceret - Publicér alle undersider - ok for at udgive %0% og derved gøre indholdet offentligt tilgængeligt..

          Du kan udgive denne side og dens undersider ved at klikke Udgiv alle undersider forneden]]>
          - - - Du har ikke konfigureret nogen godkendte farver - - - Du kan kun vælge følgende type(r) dokumenter: %0% - Du har valgt et dokument som er slettet eller lagt i papirkurven - Du har valgt dokumenter som er slettede eller lagt i papirkurven - - - Du har valgt et medie som er slettet eller lagt i papirkurven - Du har valgt medier som er slettede eller lagt i papirkurven - - - indtast eksternt link - vælg en intern side - Tekst - Link - Nyt vindue - Indtast en tekst - Indtast et link - - - Nulstil - Acceptér - Fortryd - - - Vælg en version at sammenligne med den nuværende version - Nuværende version - Rød tekst vil ikke blive vist i den valgte version. Grøn betyder tilføjet]]> - Dokument tilbagerullet - Her vises den valgte version som html. Hvis du ønsker at se forskellen mellem de 2 versioner på samme tid, brug 'diff'-oversigten - Tilbagerulning til - Vælg version - Vis - - - Rediger script - - - Concierge - Indhold - Courier - Udvikler - Umbraco konfigurationsguide - Mediearkiv - Medlemmer - Pakker - Nyhedsbreve - Indstillinger - Statistik - Oversættelse - Brugere - Hjælp - Formularer - - - De bedste Umbraco video tutorials - - - Standardskabelon - For at importere en dokumenttype, find ".udt"-filen på din computer ved at klikke på "Gennemse"-knappen og klik "Import" (Du vil blive bedt om bekræftelse på næste skærmbillede) - Ny titel på faneblad - Nodetype - Type - Stylesheet - Script - Faneblad - Titel på faneblad - Faneblade - - - Sorteringsrækkefølge - Oprettelsesdato - Sortering udført - Træk de forskellige sider op eller ned for at indstille hvordan de skal arrangeres, eller klik på kolonnehovederne for at sortere hele rækken af sider - - - - Validering - Valideringsfejl skal rettes før elementet kan gemmes - Fejlet - Gemt - Utilstrækkelige brugerrettigheder, kunne ikke fuldføre handlingen - Annulleret - Handlingen blev annulleret af et 3. part tilføjelsesprogram - Property type eksisterer allerede - Egenskabstype oprettet - DataType: %1%]]> - Egenskabs type slettet - Indholdstype gemt - Du har oprettet et faneblad - Faneblad slettet - Faneblad med id: %0% slettet - Stylesheetet blev ikke gemt - Stylesheet gemt - Stylesheet gemt uden fejl - Datatype gemt - Ordbogsnøgle gemt - Indhold publiceret - og nu synligt for besøgende - Indhold gemt - Husk at publicere for at gøre det synligt for besøgende - Send til Godkendelse - Rettelser er blevet sendt til godkendelse - Medie gemt - Medie gemt uden problemer - Medlem gemt - Medlemsgruppe gemt - Stylesheetegenskab gemt - Stylesheet gemt - Skabelon gemt - Der er opstået en fejl under redigering - Bruger gemt - Brugertype gemt - Brugergruppe gemt - Fil ikke gemt - Filen kunne ikke gemmes. Tjek filrettighederne - Fil gemt - Fil gemt uden fejl - Sprog gemt - Medietype gemt - Medlemstype gemt - Medlemsgruppe gemt - Skabelon ikke gemt - Undgå venligst at du har 2 templates med samme alias - Skabelon gemt - Skabelon gemt uden fejl! - Indhold fjernet fra udgivelse - Partial view gemt - Partial view gemt uden fejl! - Partial view ikke gemt - Der opstod en fejl ved at gemme filen. - Rettigheder gemt for - Slettede %0% brugergrupper - %0% blev slettet - Aktiverede %0% brugere - Deaktiverede %0% brugere - %0% er nu aktiveret - %0% er nu deaktiveret - Brugergrupper er blevet indstillet - Låste %0% brugere op - %0% er nu låst op - - - Tilføj style - Redigér style - Teksteditor-styles - Definér de styles, der skal være tilgængelige i teksteditoren for dette stylesheet - Selector - Bruger CSS-syntaks, f.eks. "h1" eller ".redheader" - Rediger stylesheet - Rediger CSS-egenskab - Det navn der vises i teksteditoren - Forhåndsvisning - Hvordan teksten vil se ud i teksteditoren. - Styles - Den CSS der skal anvendes i teksteditoren, f.eks. "color:red;" - Kode - Editor - - - Sletning af skabelonen med ID %0% fejlede - Rediger skabelon - Sektioner - Indsæt indholdsområde - Indsæt pladsholder for indholdsområde - Indsæt - Hvad vil du indsætte? - Oversættelse - Indsætter en oversætbar tekst, som skifter efter det sprog, som websitet vises i. - Makro - - En makro er et element, som kan have forskellige indstillinger, når det indsættes. - Brug det som en genbrugelig del af dit design såsom gallerier, formularer og lister. - - Sideværdi - - Viser værdien af et felt fra den nuværende side. Kan indstilles til at bruge rekursive værdier eller - vise en standardværdi i tilfælde af, at feltet er tomt. - - Partial view - - Et Partial View er et skabelonelement, som kan indsættes i andre skabeloner og derved - genbruges og deles på tværs af sideskabelonerne. - - Master skabelon - Lynguide til Umbracos skabelontags - Ingen master - Indsæt en underliggende skabelon - @RenderBody() element. - ]]> - Definer en sektion - @section { ... }. Herefter kan denne sektion flettes ind i - overliggende skabelon ved at indsætte et @RenderSection element. - ]]> - Indsæt en sektion - @RenderSection(name) element. Den underliggende skabelon skal have - defineret en sektion via et @section [name]{ ... } element. - ]]> - Sektionsnavn - Sektionen er obligatorisk - - Hvis obligatorisk, skal underskabelonen indeholde en @section -definition. - - Query builder - sider returneret, på - Returner - alt indhold - indhold af typen "%0%" - fra - mit website - hvor - og - er - ikke er - er før - er før (inkl. valgte dato) - er efter - er efter (inkl. valgte dato) - er - ikke er - indeholder - ikke indeholder - er større end - er større end eller det samme som - er mindre end - er mindre end eller det samme som - Id - Navn - Oprettelsesdato - Sidste opdatering - Sortér efter - stigende rækkefølge - faldende rækkefølge - Skabelon - - - Billede - Macro - Vælg indholdstype - Vælg layout - Tilføj række - Tilføj indhold - Slip indhold - Indstillinger tilføjet - Indholdet er ikke tilladt her - Indholdet er tilladt her - Klik for at indlejre - Klik for at indsætte et billede - Billedtekst... - Skriv her... - Grid layout - Et layout er det overordnede arbejdsområde til dit grid - du vil typisk kun behøve ét eller to - Tilføj grid layout - Juster dit layout ved at justere kolonnebredder og tilføj yderligere sektioner - Rækkekonfigurationer - Rækker er foruddefinerede celler, der arrangeres vandret - Tilføj rækkekonfiguration - Juster rækken ved at indstille cellebredder og tilføje yderligere celler - Kolonner - Det totale antal kolonner i dit grid - Indstillinger - Konfigurer, hvilket indstillinger, brugeren kan ændre - Typografi - Vælg hvilke typografiværdier en redaktør kan ændre - Tillad alle editorer - Tillad alle rækkekonfigurationer - Sæt som standard - Vælg ekstra - Vælg standard - er tilføjet - Maksimalt emner - Efterlad blank eller sæt til 0 for ubegrænset - - - Kompositioner - Gruppe - Du har ikke tilføjet nogle grupper - Tilføj gruppe - Nedarvet fra - Tilføj egenskab - Påkrævet label - Aktiver listevisning - Konfigurer indholdet til at blive vist i en sorterbar og søgbar liste, dens børn vil ikke blive vist i træet - Tilladte skabeloner - Vælg hvilke skabeloner der er tilladt at bruge på dette indhold - Tillad på rodniveau - Kun dokumenttyper med denne indstilling aktiveret oprettes i rodniveau under inhold og mediearkiv - Tilladte typer - Tillad at oprette indhold af en specifik type under denne - Vælg child node - Nedarv faner og egenskaber fra en anden dokumenttype. Nye faner vil blive tilføjet den nuværende dokumenttype eller sammenflettet hvis fanenavnene er ens. - Indholdstypen bliver brugt i en komposition og kan derfor ikke blive anvendt som komposition - Der er ingen indholdstyper tilgængelige at bruge som komposition - Tilgængelige editors - Genbrug - Editor indstillinger - Konfiguration - Ja, slet - blev flyttet til - Vælg hvor - skal flyttes til - Alle dokumenttyper - Alle dokumenter - Alle medier - som benytter denne dokumenttype vil blive slettet permanent. Bekræft at du også vil slette dem. - som benytter denne medietype vil blive slettet permanent. Bekræft at du også vil slette dem. - som benytter denne medlemstype vil blive slettet permanent. Bekræft at du også vil slette dem. - og alle dokumenter, som benytter denne type - og alle medier, som benytter denne type - og alle medlemmer, som benytter denne type - Medlem kan redigere - Vis på medlemsprofil - fane har ingen sorteringsrækkefølge - Element-type - Er en Element-type - En Element-type er tiltænkt brug i f.eks. Nested Content, ikke i indholdstræet - Dette benyttes ikke for en Element-type - - - Tilføj sprog - Påkrævet sprog - Egenskaber på dette sprog skal være udfyldt før noden kan blive udgivet. - Standard sprog - Et Umbraco site kan kun have et standard sprog. - Ved at skifte standardsprog kan resultere i standard indhold mangler. - Fallsback til - Ingen fallback sprog - For at tillade flersproget indhold til at falde tilbage på et andet sprog, hvis det ikke er tilgængelig i det anmodet sprog, vælg det her. - Fallback sprog - - - Tilføj parameter - Rediger parameter - Indtast makronavn - Parametre - Definer de parametre, der skal være tilgængelige, når du bruger denne makro. - Vælg partial view makrofil - - - Alternativt felt - Alternativ tekst - Casing - Felt som skal indsættes - Konvertér linieskift - Erstatter et linieskift med html-tag'et &lt;br&gt; - Ja, kun dato - Formatér som dato - HTML indkod - Vil erstatte specielle karakterer med deres HTML jævnbyrdige. - Denne tekst vil blive sat ind lige efter værdien af feltet - Denne tekst vil blive sat ind lige før værdien af feltet - Lowercase - Ingen - Indsæt efter felt - Indsæt før felt - Rekursivt - Fjern paragraf-tags - Fjerner eventuelle &lt;P&gt; omkring teksten - Standard felter - Store bogstaver - URL encode - Hvis indholdet af felterne skal sendes til en url, skal denne slåes til så specialtegn formateres - Denne tekst bruges hvis ovenstående felter er tomme - Dette felt vil blive brugt hvis ovenstående felt er tomt - Ja, med klokkeslæt. Dato/tid separator: - - - Oversættelsesdetaljer - Download XML DTD - Felter - Inkluder undersider - Hej %0%. Dette er en automatisk mail sendt for at informere dig om at dokumentet '%1' har en forespørgsel omkring oversættelse til '%5%' af %2%. Gå til http://%3%/translation/details.aspx?id=%4% for at redigere. Eller log ind i Umbraco for at få en oversigt over dine oversættelsesopgaver: http://%3%/Umbraco Hav en god dag! Mange hilsner Umbraco-robotten - Ingen oversættelsesbrugere er fundet. Opret venligst en oversættelsesbruger før du begynder at sende indhold til oversættelse - Siden '%0%' er blevet sent til oversættelse - Send siden '%0%' til oversættelse - Totalt antal ord - Oversæt til - Oversættelse gennemført. - Du kan gennemse de sider, som du lige har oversat, ved at klikke nedenfor. Hvis den originale side bliver fundet, vil du blive præsenteret for en sammenligning af de to sider. - Oversættelse fejlede, XML-filen kan være korrupt (indeholde fejl) - Oversættelsesmuligheder - Oversætter - Upload oversættelse (xml) - - - Indhold - Indholdsskabeloner - Mediearkiv - Cacheviser - Papirkurv - Oprettede pakker - Datatyper - Ordbog - Installerede pakker - Installér et skin - Installér et starterkit - Sprog - Installér lokal pakke - Makroer - Medietyper - Medlemmer - Medlemsgrupper - Roller - Medlemstyper - Dokumenttyper - Relationstyper - Pakker - Pakker - Partial Views - Partial View makrofiler - Installer fra "repository" - Installer Runway - Runway-moduler - Scripting filer - Scripts - Stylesheets - Skabeloner - Brugertilladelser - Brugertyper - Brugere - Indstillinger - Design og layout - Tredjepart - - - Ny opdatering er klar - %0% er klar, klik her for at downloade - Ingen forbindelse til server - Der kunne ikke tjekkes for ny opdatering. Se trace for mere info. - - - Adgang - Baseret på de tildelte grupper og startnoder har brugeren adgang til følgende noder - Tildel adgang - Administrator - Kategorifelt - Bruger oprettet - Skift dit kodeord - Skift billede - Nyt kodeord - er ikke blevet låst ude - Kodeordet er ikke blevet ændret - Gentag dit nye kodeord - Du kan ændre dit kodeord, som giver dig adgang til Umbraco Back Office ved at udfylde formularen og klikke på knappen 'Skift dit kodeord' - Indholdskanal - Opret nye brugere for at give dem adgang til Umbraco. Når en ny bruger oprettes, genereres der en adgangskode, som du kan dele med brugeren. - Beskrivelsesfelt - Deaktivér bruger - Dokumenttype - Redaktør - Uddragsfelt - Fejlede loginforsøg - Gå til brugerprofil - Tilføj grupper for at tildele adgang og tilladelser - Invitér anden bruger - Invitér nye brugere til at give dem adgang til Umbraco. En invitation vil blive sendt via e-mail til brugeren med oplysninger om, hvordan man logger ind i Umbraco. - Sprog - Indstil det sprog, du vil se i menuer og dialoger - Seneste låst ude dato - Seneste login - Kodeord sidst ændret - Brugernavn - Startnode i mediearkivet - Begræns mediebiblioteket til en bestemt startnode - Medie startnoder - Begræns mediebiblioteket til bestemte startnoder - Moduler - Deaktivér adgang til Umbraco - har endnu ikke logget ind - Gammelt kodeord - Adgangskode - Nulstil kodeord - Dit kodeord er blevet ændret! - Bekræft venligst dit nye kodeord - Indtast dit nye kodeord - Nuværende kodeord - ugyldig nuværende kodeord - Dit nye kodeord må ikke være tomt! - Dit nye kodeord og dit bekræftede kodeord var ikke ens, forsøg venligst igen! - Det bekræftede kodeord matcher ikke det nye kodeord - Erstat underelement-rettigheder - Du ændrer i øjeblikket rettigheder for siderne: - Vælg sider for at ændre deres rettigheder - Fjern billede - Standard rettigheder - Granulære rettigheder - Sæt rettigheder for specifikke noder - Profil - Søg alle 'børn' - Startnode - Tilføj sektioner for at give brugerne adgang - Vælg brugergrupper - Ingen startnode valgt - Ingen startnoder valgt - Begræns indholdstræet til en bestemt startnode - Indhold startnoder - Begræns indholdstræet til bestemte startnoder - Bruger sidst opdateret - er blevet oprettet - Den nye bruger er blevet oprettet. For at logge ind i Umbraco skal du bruge adgangskoden nedenfor. - Brugeradministration - Navn - Brugertilladelser - Brugergruppe - er blevet inviteret - En invitation er blevet sendt til den nye bruger med oplysninger om, hvordan man logger ind i Umbraco. - Hej og velkommen til Umbraco! På bare 1 minut vil du være klar til at komme i gang, vi skal bare have dig til at oprette en adgangskode og tilføje et billede til din avatar. - Hvis du uploader et billede af dig selv, gør du det nemt for andre brugere at genkende dig. Klik på cirklen ovenfor for at uploade et billede. - Forfatter - Skift - Din profil - Din historik - Session udløber - Invitér bruger - Opret bruger - Send invitation - Tilbage til brugere - Umbraco: Invitation -

          Hej %0%, du er blevet inviteret af %1% til Umbraco backoffice.

          Besked fra %1%: %2%

          Klik på dette link for acceptere invitationen

          Hvis du ikke kan klikke på linket, så kopier og indsæt denne URL i dit browservindue

          %3%

          ]]>
          - Alle - Aktiv - Deaktiveret - Låst ude - Inviteret - Inaktiv - Navn (A-Å) - Navn (Å-A) - Nyeste - Ældste - Sidst logget ind - - - Validering - Valider som e-mail - Valider som tal - Valider som Url - ...eller indtast din egen validering - Feltet er påkrævet - Indtast et regulært udtryk - Du skal tilføje mindst - Du kan kun have - elementer - elementer valgt - Ugyldig dato - Ikke et tal - Ugyldig e-mail - - - Slå URL tracker fra - Slå URL tracker til - Original URL - Viderestillet til - Der er ikke lavet nogen viderestillinger - Når en udgivet side bliver omdøbt eller flyttet, vil en viderestilling automatisk blive lavet til den nye side. - Er du sikker på at du vil fjerne viderestillingen fra '%0%' til '%1%'? - Viderestillings URL fjernet. - Fejl under fjernelse af viderestillings URL. - Dette vil fjerne viderestillingen - Er du sikker på at du vil slå URL trackeren fra? - URL tracker er nu slået fra. - Der opstod en fejl under forsøget på at slå URL trackeren fra, der findes mere information i logfilen. - URL tracker er nu slået fra. - Der opstod en fejl under forsøget på at slå URL trackeren til, der findes mere information i logfilen. - - - Ingen ordbog elementer at vælge imellem - + Indsæt fra udklipsmappen + + + Opret en ny indholdsskabelon fra '%0%' + Blank + Vælg en indholdsskabelon + Indholdsskabelon oprettet + En indholdsskabelon blev oprettet fra '%0%' + En anden indholdsskabelon med samme navn eksisterer allerede + En indholdskabelon er foruddefineret indhold, som en redaktør kan vælge at bruge som grundlag for at oprette nyt indhold + + + Klik for at uploade + eller klik her for at vælge filer + Du kan trække filer herind for at uploade. + Kan ikke uploade denne fil, den har ikke en godkendt filtype + Maks filstørrelse er + Medie rod + Flytning af mediet fejlede + Kopiering af mediet fejlede + Oprettelse af mappen under parent med id %0% fejlede + Omdøbning af mappen med id %0% fejlede + Træk dine filer ind i dropzonen for, at uploade dem til mediebiblioteketet. + + + Opret et nyt medlem + Alle medlemmer + Medlemgrupper har ingen yderligere egenskaber til redigering. + + + Hvor ønsker du at oprette den nye %0% + Opret under + Vælg den dokumenttype, du vil oprette en indholdsskabelon til + Angiv et navn for mappen + Vælg en type og skriv en titel + "dokumenttyper".]]> + Document Types within the Settings section.]]> + Den valgte side i træet tillader ikke at sider oprettes under den. + Rediger tilladelser for denne dokumenttype. + Opret en ny dokumenttype + "media typer".]]> + Det valgte medie i træet tillader ikke at medier oprettes under det. + Rediger tilladelser for denne medietype. + Dokumenttype uden skabelon + Ny mappe + Ny datatype + Ny JavaScript-fil + Ny tom partial view + Ny partial view makro + Ny partial view fra snippet + Ny partial view makro fra snippet + Ny partial view makro (uden makro) + Ny stylesheet-fil + Ny Rich Text Editor stylesheet-fil + + + Til dit website + - Skjul + Hvis Umbraco ikke starter, kan det skyldes at din browser ikke tillader pop-up vinduer + er åbnet i nyt vindue + Genstart + Besøg + Velkommen + + + Bliv + Kassér ændringer + Du har ikke-gemte ændringer + Er du sikker på du vil navigere væk fra denne side? - du har ikke-gemte ændringer + Udgivelse vil gøre de valgte sider synlige på sitet. + Afpublicering vil fjerne de valgte sider og deres undersider fra sitet. + Afpublicering vil fjerne denne side og alle dets undersider fra websitet. + Du har ikke-gemte ændringer. Hvis du ændrer dokumenttype, kasseres ændringerne. + + + Færdig + Slettede %0% element + Slettede %0% elementer + Slettede %0% ud af %1% element + Slettede %0% ud af %1% elementer + Udgav %0% element + Udgav %0% elementer + Udgav %0% ud af %1% element + Udgav %0% ud af %1% elementer + Fjernede %0% element fra udgivelse + Fjernede %0% elementer fra udgivelse + Fjernede %0% ud af %1% element fra udgivelse + Fjernede %0% ud af %1% elementer fra udgivelse + Flyttede %0% element + Flyttede %0% elementer + Flyttede %0% ud af %1% element + Flyttede %0% ud af %1% elementer + Kopierede %0% element + Kopierede %0% elementer + Kopierede %0% ud af %1% element + Kopierede %0% ud af %1% elementer + + + Link titel + Link + Lokalt link / querystreng + Navn på lokalt link + Rediger domæner + Luk denne dialog + Er du sikker på at du vil slette + Er du sikker på du vil deaktivere + Er du sikker på at du vil forlade Umbraco? + Er du sikker? + Klip + Rediger ordbogsnøgle + Rediger sprog + Rediger det valgte medie + Indsæt lokalt link + Indsæt tegn + Indsæt grafisk overskrift + Indsæt billede + Indsæt link + Indsæt makro + Indsæt tabel + Dette vil slette sproget + Sidst redigeret + Link + Internt link: + Ved lokalt link, indsæt da en "#" foran linket + Åben i nyt vindue? + Denne makro har ingen egenskaber du kan redigere + Indsæt tekst + Rediger rettigheder for + Sæt rettigheder for + Sæt rettigheder for %0% for brugergruppe %1% + Vælg de brugergrupper, du vil angive tilladelser til + Elementerne i papirkurven slettes. Luk venligst ikke dette vindue mens sletningen foregår + Papirkurven er nu tom + Når elementer slettes fra papirkurven, slettes de for altid + regexlib.com's webservice oplever i øjeblikket problemer, vi ikke har kontrol over. Beklager ulejligheden. ]]> + Søg efter et regulært udtryk for at tilføje validering til et formularfelt. Eksempel: 'e-mail', 'postnr.', 'url' + Fjern makro + Obligatorisk + Sitet er genindekseret + Sitet er nu genudgivet + Websitets cache vil blive genopfrisket. Alt udgivet indhold vil blive opdateret, mens upubliceret indhold vil forblive upubliceret. + Antal kolonner + Antal rækker + Klik på billedet for at se den fulde størrelse + Vælg + Se cache element + Relatér til original + Inkludér undersider + Det venligste community + Link til side + Åben linket i et nyt vindue eller fane + Link til medie + Vælg startnode for indhold + Vælg medie + Vælg medietype + Vælg ikon + Vælg item + Vælg link + Vælg makro + Vælg indhold + Vælg indholdstype + Vælg startnode for mediearkivet + Vælg medlem + Vælg medlemsgruppe + Vælg medlemstype + Vælg node + Vælg sektioner + Vælg brugere + Ingen ikoner blev fundet + Der er ingen parametre for denne makro + Der er ikke tilføjet nogen makroer + Eksterne login-udbydere + Undtagelsesdetaljer + Stacktrace + Indre undtagelse + Link dit + Fjern link fra dit + konto + Vælg editor + Vælg konfiguration + Vælg snippet + Dette vil slette noden og alle dets sprog. Hvis du kun vil slette et sprog, så afpublicér det i stedet. + + + Der er ingen ordbogselementer. + + + Du tilføjer flere sprog under 'sprog' i menuen til venstre + ]]> + Kulturnavn + + Ordbogsoversigt + + + Konfigurerede søgere + Viser egenskaber og værktøjer til enhver konfigureret søger (dvs. som en multi-indekssøger) + Feltværdier + Sundhedstilstand + Indeksets sundhedstilstand, og hvis det kan læses + Indeksører + Indeksinfo + Viser indeksets egenskaber + Administrer Examine indekserne + Giver dig mulighed for at se detaljerne for hvert indeks og giver nogle værktøjer til styring af indeksørerne + Genopbyg indeks + + Afhængigt af hvor meget indhold der er på dit website, kan det tage et stykke tid.
          + Det anbefales ikke at genopbygge et indeks i perioder med høj websitetrafik eller når redaktører redigerer indhold. + ]]> +
          + Søgere + Søg i indekset og se resultaterne + Værktøjer + Værktøjer til at administrere indekset + felter + Indexet skal bygges igen, for at kunne læses + Processen tager længere tid end forventet. Kontrollér Umbraco loggen for at se om der er sket fejl under operationen + Dette index kan ikke genbygess for det ikke har nogen + IIndexPopulator + + + Indtast dit brugernavn + Indtast dit kodeord + Bekræft dit kodeord + Navngiv %0%... + Indtast navn... + Indtast en e-mail... + Indtast et brugernavn... + Label... + Indtast beskrivelse + Søg... + Filtrér... + Indtast nøgleord (tryk på Enter efter hvert nøgleord)... + Indtast din e-mail + Indtast en besked... + Dit brugernavn er typisk din e-mailadresse + #value eller ?key=value + Indtast alias... + Genererer alias... + + + Opret brugerdefineret listevisning + Fjern brugerdefineret listevisning + En dokumenttype, medietype eller medlemstype med dette alias findes allerede + + + Omdøbt + Indtast et ny mappenavn her + %0% was renamed to %1% + + + Tilføj førværdi + Database-datatype + Data Editor GUID + Visningskontrol + Knapper + Aktiver avancerede indstillinger for + Aktiver kontekstmenu + Maks. std. størrelse på indsatte billeder + Relaterede stylesheets + Vis label + Bredde og højde + All property types & property data + using this data type will be deleted permanently, please confirm you want to delete these as well + Ja, slet + and all property types & property data using this data type + Vælg den mappe, der skal flyttes + til i træstrukturen nedenfor + blev flyttet under + %0% fjernes egnskaber og egnskabernes data fra følgende elementer]]> + I understand this action will delete the properties and data based on this Data Type + + + Dine data er blevet gemt, men før du kan udgive denne side er der nogle fejl der skal rettes: + Den nuværende membership-provider understøtter ikke skift af kodeord (EnablePasswordRetrieval skal være true) + %0% der findes allerede + Der var fejl i dokumentet: + Der var fejl i formularen: + Kodeordet skal være på minimum %0% tegn og indeholde mindst %1% alfanumeriske karakterer + %0% skal være et heltal + %0% under %1% er et obligatorisk felt og skal udfyldes + %0% er et obligatorisk felt og skal udfyldes + %0% under %1% er ikke i et korrekt format + %0% er ikke i et korrekt format + + + Der skete en fejl på severen + Denne filttype er blevet deaktiveret af administratoren + OBS! Selvom CodeMirror er slået til i konfigurationen, så er den deaktiveret i Internet Explorer fordi den ikke er stabil nok. + Du skal udfylde både Alias & Navn på den nye egenskabstype! + Der mangler læse/skrive rettigheder til bestemte filer og mapper + Fejl ved indlæsning af Partial View script (fil: %0%) + Skriv venligst en titel + Du skal vælge en type + Du er ved at gøre billedet større end originalen. Det vil forringe kvaliteten af billedet. Ønsker du at fortsætte? + Startnode er slettet, kontakt systemadministrator + Du skal markere noget indhold, før du kan ændre stylen + Der er ingen aktive styles eller formatteringer på denne side + Du skal stå til venstre for de 2 celler du ønsker at samle! + Du kan ikke opdele en celle, som ikke allerede er delt. + Denne egenskab er ugyldig + + + Om + Handling + Muligheder + Tilføj + Alias + Alle + Er du sikker? + Tilbage + Tilbage til oversigt + Kant + af + Fortryd + Celle margen + Vælg + Ryd + Luk + Luk vindue + Kommentar + Bekræft + Proportioner + Behold proportioner + Indhold + Fortsæt + Kopiér + Opret + Database + Dato + Standard + Slet + Slettet + Sletter... + Design + Ordbog + Dimensioner + Ned + Hent + Rediger + Redigeret + Elementer + E-mail + Fejl + Felt + Find + Første + Fokuspunkt + Generelt + Grupper + Gruppe + Højde + Hjælp + Skjul + Historik + Ikon + Id + Importer + Søg kun i denne mappe + Info + Indre margen + Indsæt + Installér + Ugyldig + Justering + Mærke + Sprog + Sidste + Layout + Links + Henter + Låst + Log ind + Log af + Log ud + Makro + Påkrævet + Besked + Flyt + Navn + Ny + Næste + Nej + af + Fra + OK + Åben + Valgmuligheder + Til + eller + Sortér efter + Kodeord + Sti + Et øjeblik... + Forrige + Egenskaber + Genopbyg + E-mail der skal modtage indhold af formular + Papirkurv + Din papirkurv er tom + Genindlæs + Mangler + Fjern + Omdøb + Forny + Påkrævet + Hent + Prøv igen + Rettigheder + Planlagt publicering + Søg + Beklager, vi kan ikke finde det, du leder efter. + Ingen elementer er blevet tilføjet + Server + Indstillinger + Vis + Hvilken side skal vises efter at formularen er sendt + Størrelse + Sortér + Status + Indsend + Type + Skriv for at søge... + under + Op + Opdatér + Opdatér + Upload + Url + Bruger + Brugernavn + Værdi + Vis + Velkommen... + Bredde + Ja + Mappe + Søgeresultater + Sortér + Afslut sortering + Eksempel + Skift kodeord + til + Listevisning + Gemmer... + nuværende + Indlejring + valgt + Andet + Artikler + Videoer + + + Blå + + + Tilføj fane + Tilføj egenskab + Tilføj editor + Tilføj skabelon + Tilføj child node + Tilføj child + Rediger datatype + Naviger sektioner + Genveje + Vis genveje + Brug listevisning + Tillad på rodniveau + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + Generelt + Editor + Skift tillad sprogvarianter + Skift tillad segmentering + + + Baggrundsfarve + Fed + Tekstfarve + Skrifttype + Tekst + + + Side + + + Installeringsprogrammet kan ikke forbinde til databasen. + Kunne ikke gemme web.config filen. Du bedes venligst manuelt ændre database forbindelses strengen. + Din database er blevet fundet og identificeret som + Database konfiguration + installér knappen for at installere Umbraco %0% databasen + ]]> + Næste for at fortsætte.]]> + Databasen er ikke fundet. Kontrollér venligst at informationen i database forbindelsesstrengen i "web.config" filen er korrekt.

          +

          For at fortsætte bedes du venligst rette "web.config" filen (ved at bruge Visual Studio eller dit favoritprogram), scroll til bunden, tilføj forbindelsesstrengen til din database i feltet som hedder "umbracoDbDSN" og gem filen.

          Klik på Forsøg igen knappen når du er færdig.
          Mere information om at redigere web.config her.

          ]]>
          + Kontakt venligst din ISP hvis det er nødvendigt. Hvis du installerer på en lokal maskine eller server kan du muligvis få informationerne fra din systemadministrator.]]> + Tryk på Opgradér knappen for at opgradere din database til Umbraco %0%

          Bare rolig - intet indhold vil blive slettet og alt vil stadig fungere bagefter!

          ]]>
          + Tryk på Næste for at fortsætte.]]> + Næste for at fortsætte med konfigurationsguiden.]]> + Normalbrugerens adgangskode er nødt til at blive ændret!]]> + Normalbrugeren er blevet gjort utjenstdygtig eller har ikke adgang til Umbraco!

          Du behøver ikke foretage yderligere handlinger. Tryk på Næste for at fortsætte.

          ]]>
          + Normalbrugerens adgangskode er på succesfuld vis blevet ændret siden installationen!

          Du behøver ikke foretage yderligere handlinger. Tryk på Næste for at fortsætte.

          ]]>
          + Adgangskoden er blevet ændret! + Få en fremragende start, se vores videoer + Ved at klikke på næste knappen (eller ved at ændre UmbracoConfigurationStatus i web.config filen), accepterer du licensaftalen for denne software, som specificeret i boksen nedenfor. Bemærk venligst at denne Umbraco distribution består af to forskellige licenser, MIT's Open Source Licens for frameworket og Umbraco Freeware Licensen som dækker UI'en. + Endnu ikke installeret + Berørte filer og foldere + Flere informationer om at opsætte rettigheder for Umbraco her + Du er nødt til at give ASP.NET 'modify' rettigheder på følgende filer/foldere + Dine rettighedsindstillinger er næsten perfekte!

          Du kan køre Umbraco uden problemer, men du vil ikke være i stand til at installere pakker, som er anbefalet for at få fuldt udbytte af Umbraco.]]>
          + Hvorledes besluttes + Klik her for at læse tekstversionen + video tutorials om at opsætte folderrettigheder for Umbraco eller læs tekstversionen.]]> + Dine rettighedsindstillinger kan være et problem!

          Du kan afvikle Umbraco uden problemer, men du vil ikke være i stand til at oprette foldere eller installere pakker, hvilket er anbefalet for at få fuldt udbytte af Umbraco.]]>
          + Dine rettighedsindstillinger er ikke klar til Umbraco!

          For at afvikle Umbraco er du nødt til at opdatere dine rettighedsindstillinger.]]>
          + Dine rettighedsindstillinger er perfekte!

          Du er nu parat til at afvikle Umbraco og installere pakker!]]>
          + Løser folder problem + Følg dette link for mere information om udfordringer med ASP.NET og oprettelse af foldere + Sætter folderrettigheder op + Umbraco har behov for 'write/modify' adgang til bestemte foldere, for at kunne gemme filer som billeder og PDF'er. Umbraco gemmer også midlertidige data (eksempelvis cachen) for at forbedre ydelsen på dit website. + Jeg har lyst til at begynde på bar bund + lær hvordan) Du kan stadig vælge at installere Runway senere. Gå venligst til Udvikler-sektionen og vælg Pakker.]]> + Du har lige opsat en ren Umbraco-platform. Hvad ønsker du nu at gøre? + Runway er installeret + Dette er vores liste over anbefalede moduler. Kryds dem af du ønsker at installere eller se den fulde liste af moduler ]]> + Kun anbefalet for erfarne brugere + Jeg ønsker at begynder med et simpelt website + "Runway" er et simpelt website som stiller nogle basale dokumenttyper og skabeloner til rådighed. Installeringsprogrammet kan automatisk opsætte Runway for dig, men du kan nemt redigere, udvide eller fjerne det. Det er ikke nødvendigt og du kan sagtens bruge Umbraco uden. Men Runway tilbyder et fundament, som er baseret på 'Best Practices', som får dig igang hurtigere end nogensinde før. Hvis du vælger at installere Runway, kan du efter eget valg vælge de grundlæggende byggesten kaldet 'Runway Modules' til at forbedre dine Runway-sider.

          Inkluderet med Runway:Home Page, Getting Started page, Installing Modules page.
          Valgfri Moduler: Top Navigation, Sitemap, Contact, Gallery.

          ]]>
          + Hvad er Runway + Skridt 1/5: Acceptér licens + Skridt 2/5: Database-konfiguration + Skridt 3/5: Validerer filrettigheder + Skridt 4/5: Kontrollér Umbraco sikkerhed + Skridt 5/5: Umbraco er parat til at få dig igang + Tak fordi du valgte Umbraco + Gennemse dit nye site Du installerede Runway, så hvorfor ikke se hvordan dit nye website ser ud.]]> + Yderligere hjælpe og informationer Få hjælp fra vores prisvindende fællesskab, gennemse dokumentationen eller se nogle gratis videoer om hvordan du opsætter et simpelt site, hvordan du bruger pakker og en 'quick guide' til Umbraco terminologier]]> + Umbraco %0% er installeret og klar til brug + /web.config filen og opdatére 'AppSetting' feltet UmbracoConfigurationStatus i bunden til '%0%'.]]> + komme igang med det samme ved at klikke på "Start Umbraco" knappen nedenfor.
          Hvis du er ny med Umbraco, kan du finde masser af ressourcer på vores 'getting started' sider. +]]>
          + Start UmbracoFor at administrere dit website skal du blot åbne Umbraco administrationen og begynde at tilføje indhold, opdatere skabelonerne og stylesheets'ene eller tilføje ny funktionalitet.]]> + Forbindelse til databasen fejlede. + Umbraco Version 3 + Umbraco Version 4 + Se + Umbraco %0% for en frisk installation eller for en opgradering fra version 3.0.

          Tryk på Næste for at begynde på guiden.]]>
          + + + Culture Code + Culture Name + + + Du har været inaktiv, og du vil blive logget ud om + Forny for at gemme dine ændringer + + + Så er det søndag! + Smil, det er mandag! + Hurra, det er tirsdag! + Hvilken herlig onsdag! + Glædelig torsdag! + Endelig fredag! + Glædelig lørdag + Log ind nedenfor + Log ind med + Din session er udløbet + © 2001 - %0%
          umbraco.com

          ]]>
          + Glemt adgangskode? + En e-mail vil blive sendt til den angivne adresse med et link til at nulstille din adgangskode + En e-mail med instruktioner for nulstilling af adgangskoden vil blive sendt til den angivne adresse, hvis det matcher vores optegnelser + Vis adgangskode + Skjul adgangskode + Tilbage til login formular + Angiv en ny adgangskode + Din adgangskode er blevet opdateret + Det link, du har klikket på, er ugyldigt eller udløbet + Umbraco: Nulstil adgangskode + Dit brugernavn til at logge på Umbraco backoffice er: %0%

          Klik her for at nulstille din adgangskode eller kopier/indsæt denne URL i din browser:

          %1%

          ]]>
          + + + Skrivebord + Sektioner + Indhold + + + Vælg siden ovenover... + %0% er nu kopieret til %1% + Kopier til + %0% er nu flyttet til %1% + Flyt til + er blevet valgt som roden for dit nye indhold, klik 'ok' nedenunder. + Intet element valgt, vælg et element i listen ovenfor før der klikkes 'fortsæt' + Det nuværende element kan ikke lægges under denne pga. sin type + Det nuværende element kan ikke ligge under en af dens undersider + Dette element må ikke findes på rodniveau + Denne handling er ikke tilladt fordi du ikke har de fornødne rettigheder på et eller flere af under-dokumenterne + Relater det kopierede element til originalen + + + %0%]]> + Notificeringer er gemt for + + Hej %0%

          +

          Dette er en automatisk mail for at informere dig om at opgaven '%1%' + er blevet udførtpå siden '%2%' af brugeren '%3%'

          +

          Opdateringssammendrag:

          %6%

          Hav en fortsat god dag!

          De bedste hilsner fra umbraco robotten

          ]]>
          + [%0%] Notificering om %1% udført på %2% + Notificeringer + + + Handlinger + Oprettet + Opret pakke + Vælg pakken fra din computer. Umbraco pakker er oftest en ".zip" fil + Dette vil slette pakken + Slip her for at uploade + Inkludér alle underliggende sider + eller klik her for at vælge pakkefil + Upload pakke + Installér en lokal pakke ved at vælge den fra din computer. Installér kun pakker fra kilder, du kender og stoler på + Upload en anden pakke + Annuller og upload en anden pakke + Jeg accepterer + betingelser for anvendelse + Sti til fil + Absolut sti til fil (f.eks.: /bin/umbraco.bin) + Installeret + Installerede pakker + Installér lokal + Afslut + Denne pakke har ingen konfigurationsvisning + Der er ikke blevet oprettet nogle pakker endnu + Du har ingen pakker installeret + 'Pakker' øverst til højre på din skærm]]> + Pakkehandlinger + Forfatter URL + Pakkeindhold + Pakkefiler + Ikon URL + Installér pakke + Licens + Licens URL + Pakkeegenskaber + Søg efter pakker + Resultater for + Vi kunne ikke finde resultater for + Prøv venligst at søge efter en anden pakke eller gennemse kategorierne + Populære + Nye udgivelser + har + karma points + Information + Ejer + Bidragsydere + Oprettet + Nuværende version + .NET version + Downloads + Likes + Kompatibilitet + Denne pakke er kompatibel med de følgende versioner af Umbraco, som rapporteret af community-medlemmer. Fuld kompatibilitet kan ikke garanteres for versioner rapporteret nedenfor 100% + Eksterne kilder + Forfatter + Dokumentation + Pakke meta data + Pakkenavn + Pakken indeholder ingen elementer +
          Du kan roligt fjerne denne fra systemet ved at klikke på "Fjern pakke" nedenfor.]]>
          + Pakkevalg + Pakke læs mig + Pakke opbevaringsbase + Bekræft af-installering + Pakken blev fjernet + Pakken er på succesfuld vis blevet fjernet + Afinstallér pakke + +Bemærk: at dokumenter og medier som afhænger af denne pakke vil muligvis holde op med at virke, så vær forsigtig. Hvis i tvivl, kontakt personen som har udviklet pakken.]]> + Pakke version + Pakke allerede installeret + Denne pakke kan ikke installeres, den kræver en minimum Umbraco version af + Afinstallerer... + Downloader... + Importeret... + Installerer... + Genstarter, vent venligst... + Færdig, din browser opdateres nu, vent venligst... + Klik venligst på 'Afslut' for at gennemføre installation og opdatere siden. + Uploader pakke... + + + Indsæt med fuld formattering (Anbefales ikke) + Den tekst du er ved at indsætte indeholder specialtegn eller formattering. Dette kan skyldes at du kopierer fra f.eks. Microsoft Word. Umbraco kan fjerne denne specialformattering automatisk så indholdet er mere velegnet til visning på en webside. + Indsæt som ren tekst, dvs. fjern al formattering + Indsæt, men fjern formattering som ikke bør være på en webside (Anbefales) + + + Gruppebaseret beskyttelse + Hvis du ønsker at give adgang til alle medlemmer af specifikke medlemsgrupper + Du skal oprette en medlemsgruppe før du kan bruge gruppebaseret beskyttelse + Fejlside + Brugt når folk er logget ind, men ingen adgang + %0% skal beskyttes]]> + %0% er nu beskyttet]]> + %0%]]> + Log ind-side + Vælg siden der indeholder log ind-formularen + Fjern beskyttelse... + %0%?]]> + Vælg siderne der indeholder log ind-formularer og fejlmeddelelser + %0%]]> + %0%]]> + Adgang til enkelte medlemmer + Hvis du ønsker at give adgang til enkelte medlemmer + + + Udgivelsen kunne ikke udgives da publiceringsdato er sat + + + + + %0% kunne ikke udgives, fordi et 3. parts modul annullerede handlingen + + Medtag ikke-udgivede undersider + Publicerer - vent venligst... + %0% ud af %1% sider er blevet udgivet... + %0% er nu publiceret + %0% og alle undersider er nu publiceret + Publicér alle undersider + ok for at udgive %0% og derved gøre indholdet offentligt tilgængeligt..

          Du kan udgive denne side og dens undersider ved at klikke Udgiv alle undersider forneden]]>
          + + + Du har ikke konfigureret nogen godkendte farver + + + Du kan kun vælge følgende type(r) dokumenter: %0% + Du har valgt et dokument som er slettet eller lagt i papirkurven + Du har valgt dokumenter som er slettede eller lagt i papirkurven + + + Slettet medie + Du har valgt et medie som er slettet eller lagt i papirkurven + Du har valgt medier som er slettede eller lagt i papirkurven + Slettet + + + indtast eksternt link + vælg en intern side + Tekst + Link + Nyt vindue + Indtast en tekst + Indtast et link + + + Nulstil + Gem beskæring + Tilføj ny beskæring + Acceptér + Fortryd + Brugerdefineret + + + Vælg en version at sammenligne med den nuværende version + Nuværende version + Rød tekst vil ikke blive vist i den valgte version. Grøn betyder tilføjet]]> + Dokument tilbagerullet + Her vises den valgte version som html. Hvis du ønsker at se forskellen mellem de 2 versioner på samme tid, brug 'diff'-oversigten + Tilbagerulning til + Vælg version + Vis + + + Rediger script + + + Concierge + Indhold + Courier + Udvikler + Umbraco konfigurationsguide + Mediearkiv + Medlemmer + Pakker + Nyhedsbreve + Indstillinger + Statistik + Oversættelse + Brugere + Hjælp + Formularer + + + Tours + De bedste Umbraco video tutorials + Besøg our.umbraco.com + Besøg umbraco.tv + + + Standardskabelon + For at importere en dokumenttype, find ".udt"-filen på din computer ved at klikke på "Gennemse"-knappen og klik "Import" (Du vil blive bedt om bekræftelse på næste skærmbillede) + Ny titel på faneblad + Nodetype + Type + Stylesheet + Script + Faneblad + Titel på faneblad + Faneblade + Opret tilsvarende skabelon + Tilføj ikon + + + Sorteringsrækkefølge + Oprettelsesdato + Sortering udført + Træk de forskellige sider op eller ned for at indstille hvordan de skal arrangeres, eller klik på kolonnehovederne for at sortere hele rækken af sider + + + + Validering + Valideringsfejl skal rettes før elementet kan gemmes + Fejlet + Gemt + Utilstrækkelige brugerrettigheder, kunne ikke fuldføre handlingen + Annulleret + Handlingen blev annulleret af et 3. part tilføjelsesprogram + Udgivelsen blev annulleret af et 3. part tilføjelsesprogram + Property type eksisterer allerede + Egenskabstype oprettet + DataType: %1%]]> + Egenskabs type slettet + Indholdstype gemt + Du har oprettet et faneblad + Faneblad slettet + Faneblad med id: %0% slettet + Stylesheetet blev ikke gemt + Stylesheet gemt + Stylesheet gemt uden fejl + Datatype gemt + Ordbogsnøgle gemt + Udgivelse fejlede da overliggende side ikke er udgivet + Indhold publiceret + og nu synligt for besøgende + Indhold gemt + Husk at publicere for at gøre det synligt for besøgende + Send til Godkendelse + Rettelser er blevet sendt til godkendelse + Medie gemt + Medie gemt uden problemer + Medlem gemt + Medlemsgruppe gemt + Stylesheetegenskab gemt + Stylesheet gemt + Skabelon gemt + Der er opstået en fejl under redigering + Bruger gemt + Brugertype gemt + Brugergruppe gemt + Sprog og domæner gemt + Der opstod en fejl ved at gemme sprog og domæner + Fil ikke gemt + Filen kunne ikke gemmes. Tjek filrettighederne + Fil gemt + Fil gemt uden fejl + Sprog gemt + Medietype gemt + Medlemstype gemt + Medlemsgruppe gemt + Skabelon ikke gemt + Undgå venligst at du har 2 templates med samme alias + Skabelon gemt + Skabelon gemt uden fejl! + Indhold fjernet fra udgivelse + Partial view gemt + Partial view gemt uden fejl! + Partial view ikke gemt + Der opstod en fejl ved at gemme filen. + Rettigheder gemt for + Slettede %0% brugergrupper + %0% blev slettet + Aktiverede %0% brugere + Deaktiverede %0% brugere + %0% er nu aktiveret + %0% er nu deaktiveret + Brugergrupper er blevet indstillet + Låste %0% brugere op + %0% er nu låst op + Brugeren %0% blev slettet + Invitér bruger + Invitationen blev gensendt til %0% + Dokumenttypen blev eksporteret til en fil + Der skete en fejl under eksport af en dokumenttype + + + Tilføj style + Redigér style + Teksteditor-styles + Definér de styles, der skal være tilgængelige i teksteditoren for dette stylesheet + Selector + Bruger CSS-syntaks, f.eks. "h1" eller ".redheader" + Rediger stylesheet + Rediger CSS-egenskab + Det navn der vises i teksteditoren + Forhåndsvisning + Hvordan teksten vil se ud i teksteditoren. + Styles + Den CSS der skal anvendes i teksteditoren, f.eks. "color:red;" + Kode + Editor + + + Sletning af skabelonen med ID %0% fejlede + Rediger skabelon + Sektioner + Indsæt indholdsområde + Indsæt pladsholder for indholdsområde + Indsæt + Hvad vil du indsætte? + Oversættelse + Indsætter en oversætbar tekst, som skifter efter det sprog, som websitet vises i. + Makro + + En makro er et element, som kan have forskellige indstillinger, når det indsættes. + Brug det som en genbrugelig del af dit design såsom gallerier, formularer og lister. + + Sideværdi + + Viser værdien af et felt fra den nuværende side. Kan indstilles til at bruge rekursive værdier eller + vise en standardværdi i tilfælde af, at feltet er tomt. + + Partial view + + Et Partial View er et skabelonelement, som kan indsættes i andre skabeloner og derved + genbruges og deles på tværs af sideskabelonerne. + + Master skabelon + Lynguide til Umbracos skabelontags + Ingen master + Indsæt en underliggende skabelon + @RenderBody() element. + ]]> + Definer en sektion + @section { ... }. Herefter kan denne sektion flettes ind i + overliggende skabelon ved at indsætte et @RenderSection element. + ]]> + Indsæt en sektion + @RenderSection(name) element. Den underliggende skabelon skal have + defineret en sektion via et @section [name]{ ... } element. + ]]> + Sektionsnavn + Sektionen er obligatorisk + + Hvis obligatorisk, skal underskabelonen indeholde en @section -definition. + + Query builder + sider returneret, på + Returner + alt indhold + indhold af typen "%0%" + fra + mit website + hvor + og + er + ikke er + er før + er før (inkl. valgte dato) + er efter + er efter (inkl. valgte dato) + er + ikke er + indeholder + ikke indeholder + er større end + er større end eller det samme som + er mindre end + er mindre end eller det samme som + Id + Navn + Oprettelsesdato + Sidste opdatering + Sortér efter + stigende rækkefølge + faldende rækkefølge + Skabelon + + + Billede + Macro + Vælg indholdstype + Vælg layout + Tilføj række + Tilføj indhold + Slip indhold + Indstillinger tilføjet + Indholdet er ikke tilladt her + Indholdet er tilladt her + Klik for at indlejre + Klik for at indsætte et billede + Billedtekst... + Skriv her... + Grid layout + Et layout er det overordnede arbejdsområde til dit grid - du vil typisk kun behøve ét eller to + Tilføj grid layout + Juster dit layout ved at justere kolonnebredder og tilføj yderligere sektioner + Rækkekonfigurationer + Rækker er foruddefinerede celler, der arrangeres vandret + Tilføj rækkekonfiguration + Juster rækken ved at indstille cellebredder og tilføje yderligere celler + Kolonner + Det totale antal kolonner i dit grid + Indstillinger + Konfigurer, hvilket indstillinger, brugeren kan ændre + Typografi + Vælg hvilke typografiværdier en redaktør kan ændre + Tillad alle editorer + Tillad alle rækkekonfigurationer + Sæt som standard + Vælg ekstra + Vælg standard + er tilføjet + Maksimalt emner + Efterlad blank eller sæt til 0 for ubegrænset + + + Kompositioner + Gruppe + Du har ikke tilføjet nogen grupper + Tilføj gruppe + Nedarvet fra + Tilføj egenskab + Påkrævet label + Aktivér listevisning + Konfigurér indholdet til at blive vist i en sortérbar og søgbar liste; undersider vil ikke blive vist i træet + Tilladte skabeloner + Vælg hvilke skabeloner, der er tilladt at bruge på dette indhold. + Tillad på rodniveau + Kun dokumenttyper med denne indstilling aktiveret kan oprettes i rodniveau under indhold og mediearkiv. + Tilladte typer + Tillad at oprette indhold af en specifik type under denne. + Vælg child node + Nedarv faner og egenskaber fra en anden dokumenttype. Nye faner vil blive tilføjet den nuværende dokumenttype eller sammenflettet hvis fanenavnene er ens. + Indholdstypen bliver brugt i en komposition og kan derfor ikke blive anvendt som komposition + Der er ingen indholdstyper tilgængelige at bruge som komposition + Når du fjerner en komposition vil alle associerede indholdsdata blive slettet. Når først dokumenttypen er gemt, er der ingen vej tilbage. + Opret ny indstilling + Genbrug + Input indstillinger + Tilgængelige indstillinger + Opret ny indstilling + Konfiguration + Ja, slet + blev flyttet til + blev kopieret til + Vælg hvor + Vælg hvor + skal flyttes til + Alle dokumenttyper + Alle dokumenter + Alle medier + som benytter denne dokumenttype vil blive slettet permanent. Bekræft at du også vil slette dem. + som benytter denne medietype vil blive slettet permanent. Bekræft at du også vil slette dem. + som benytter denne medlemstype vil blive slettet permanent. Bekræft at du også vil slette dem. + og alle dokumenter, som benytter denne type + og alle medier, som benytter denne type + og alle medlemmer, som benytter denne type + Medlem kan redigere + Tillad at denne egenskab kan redigeres af medlemmet på dets profil. + Er følsom data + Skjul værdien af denne egenskab for indholdsredaktører der ikke har adgang til at se følsomme data + Vis på medlemsprofil + Tillad at denne egenskab kan vises på medlemmets profil. + fane har ingen sorteringsrækkefølge + Hvor er denne komposition brugt? + Denne komposition brugt i kompositionen af de følgende indholdstyper: + Tillad variationer + Tillad sprogvariation + Tillad segmentering + Tillader sprogvariationer + Tillader segmentering + Tillad at redaktører kan oprette indhold af denne type på flere sprog. + Tillad at redaktører kan oprette dette indhold på flere sprog. + Tillad at redaktører kan oprette flere udgaver af denne type indhold. + Tillad sprogvariation + Tillad segmentering + Element-type + Er en Element-type + En Element-type er tiltænkt brug i f.eks. Nested Content, ikke i indholdstræet. + Dette benyttes ikke for en Element-type + Du har lavet ændringer til denne egenskab. Er du sikker på at du vil kassere dem? + + + Tilføj sprog + Påkrævet sprog + Egenskaber på dette sprog skal være udfyldt før noden kan blive udgivet. + Standardsprog + Et Umbraco-site kan kun have ét standardsprog. + At skifte standardsprog kan resultere i at standardindhold mangler. + Fallback til + Intet fallback-sprog + For at tillade flersproget indhold, som ikke er tilgængeligt i det anmodede sprog, skal du her vælge et sprog at falde tilbage på. + Fallback-sprog + ingen + + + Tilføj parameter + Redigér parameter + Indtast makronavn + Parametre + Definér de parametre der skal være tilgængelige, når du bruger denne makro. + Vælg partial view makrofil + + + Bygger modeller + dette kan tage lidt tid + Modeller genereret + Modeller kunne ikke genereres + Modelgeneration fejlet, se fejlmeddelelse i log + + + Tilføj fallback felt + Fallback felt + Tilføj standard værdi + Standard værdi + Alternativt felt + Alternativ tekst + Casing + Kodning + Felt som skal indsættes + Konvertér linieskift + Erstatter et linieskift med html-tag'et &lt;br&gt; + Custom felter + Ja, kun dato + Format og kodning + Formatér som dato + HTML indkod + Vil erstatte specielle karakterer med deres HTML jævnbyrdige. + Denne tekst vil blive sat ind lige efter værdien af feltet + Denne tekst vil blive sat ind lige før værdien af feltet + Lowercase + Ingen + Indsæt efter felt + Indsæt før felt + Rekursivt + Fjern paragraf-tags + Fjerner eventuelle &lt;P&gt; omkring teksten + Standard felter + Store bogstaver + URL encode + Hvis indholdet af felterne skal sendes til en url, skal denne slåes til så specialtegn formateres + Denne tekst bruges hvis ovenstående felter er tomme + Dette felt vil blive brugt hvis ovenstående felt er tomt + Ja, med klokkeslæt. Dato/tid separator: + + + Oversættelsesdetaljer + Download XML DTD + Felter + Inkluder undersider + Hej %0%. Dette er en automatisk mail sendt for at informere dig om at dokumentet '%1' har en forespørgsel omkring oversættelse til '%5%' af %2%. Gå til http://%3%/translation/details.aspx?id=%4% for at redigere. Eller log ind i Umbraco for at få en oversigt over dine oversættelsesopgaver: http://%3%/Umbraco Hav en god dag! Mange hilsner Umbraco-robotten + Ingen oversættelsesbrugere er fundet. Opret venligst en oversættelsesbruger før du begynder at sende indhold til oversættelse + Siden '%0%' er blevet sent til oversættelse + Send siden '%0%' til oversættelse + Totalt antal ord + Oversæt til + Oversættelse gennemført. + Du kan gennemse de sider, som du lige har oversat, ved at klikke nedenfor. Hvis den originale side bliver fundet, vil du blive præsenteret for en sammenligning af de to sider. + Oversættelse fejlede, XML-filen kan være korrupt (indeholde fejl) + Oversættelsesmuligheder + Oversætter + Upload oversættelse (xml) + + + Indhold + Indholdsskabeloner + Mediearkiv + Cacheviser + Papirkurv + Oprettede pakker + Datatyper + Ordbog + Installerede pakker + Installér et skin + Installér et starterkit + Sprog + Installér lokal pakke + Makroer + Medietyper + Medlemmer + Medlemsgrupper + Roller + Medlemstyper + Dokumenttyper + Relationstyper + Pakker + Pakker + Partial Views + Partial View makrofiler + Installer fra "repository" + Installer Runway + Runway-moduler + Scripting filer + Scripts + Stylesheets + Skabeloner + Logfremviser + Brugertilladelser + Brugertyper + Brugere + Indstillinger + Design og layout + Tredjepart + + + Ny opdatering er klar + %0% er klar, klik her for at downloade + Ingen forbindelse til server + Der kunne ikke tjekkes for ny opdatering. Se trace for mere info. + + + Adgang + Baseret på de tildelte grupper og startnoder har brugeren adgang til følgende noder + Tildel adgang + Administrator + Kategorifelt + Bruger oprettet + Skift dit kodeord + Skift billede + Nyt kodeord + er ikke blevet låst ude + Kodeordet er ikke blevet ændret + Gentag dit nye kodeord + Du kan ændre dit kodeord, som giver dig adgang til Umbraco Back Office ved at udfylde formularen og klikke på knappen 'Skift dit kodeord' + Indholdskanal + Opret endnu en bruger + Opret nye brugere for at give dem adgang til Umbraco. Når en ny bruger oprettes, genereres der en adgangskode, som du kan dele med brugeren. + Beskrivelsesfelt + Deaktivér bruger + Dokumenttype + Redaktør + Uddragsfelt + Fejlede loginforsøg + Gå til brugerprofil + Tilføj grupper for at tildele adgang og tilladelser + Invitér anden bruger + Invitér nye brugere til at give dem adgang til Umbraco. En invitation vil blive sendt via e-mail til brugeren med oplysninger om, hvordan man logger ind i Umbraco. + Sprog + Indstil det sprog, du vil se i menuer og dialoger + Senest låst ude + Seneste login + Kodeord sidst ændret + Brugernavn + Startnode i mediearkivet + Begræns mediebiblioteket til en bestemt startnode + Medie startnoder + Begræns mediebiblioteket til bestemte startnoder + Moduler + Deaktivér adgang til Umbraco + har endnu ikke logget ind + Gammelt kodeord + Adgangskode + Nulstil kodeord + Dit kodeord er blevet ændret! + Bekræft venligst dit nye kodeord + Indtast dit nye kodeord + Nuværende kodeord + ugyldig nuværende kodeord + Dit nye kodeord må ikke være tomt! + Dit nye kodeord og dit bekræftede kodeord var ikke ens, forsøg venligst igen! + Det bekræftede kodeord matcher ikke det nye kodeord + Erstat underelement-rettigheder + Du ændrer i øjeblikket rettigheder for siderne: + Vælg sider for at ændre deres rettigheder + Fjern billede + Standardrettigheder + Granulære rettigheder + Sæt rettigheder for specifikke noder + Profil + Søg alle 'børn' + Startnode + Tilføj sektioner for at give brugerne adgang + Vælg brugergrupper + Ingen startnode valgt + Ingen startnoder valgt + Begræns indholdstræet til en bestemt startnode + Indhold startnoder + Begræns indholdstræet til bestemte startnoder + Bruger sidst opdateret + er blevet oprettet + Den nye bruger er blevet oprettet. For at logge ind i Umbraco skal du bruge adgangskoden nedenfor. + Brugeradministration + Navn + Brugertilladelser + Brugergruppe + er blevet inviteret + En invitation er blevet sendt til den nye bruger med oplysninger om, hvordan man logger ind i Umbraco. + Hej og velkommen til Umbraco! På bare 1 minut vil du være klar til at komme i gang, vi skal bare have dig til at oprette en adgangskode og tilføje et billede til din avatar. + Velkommen til Umbraco! Desværre er din invitation udløbet. Kontakt din administrator og bed om at gensende invitationen. + Hvis du uploader et billede af dig selv, gør du det nemt for andre brugere at genkende dig. Klik på cirklen ovenfor for at uploade et billede. + Forfatter + Skift + Din profil + Din historik + Session udløber + Invitér bruger + Opret bruger + Send invitation + Tilbage til brugere + Umbraco: Invitation +

          Hej %0%, du er blevet inviteret af %1% til Umbraco backoffice.

          Besked fra %1%: %2%

          Klik på dette link for acceptere invitationen

          Hvis du ikke kan klikke på linket, så kopier og indsæt denne URL i dit browservindue

          %3%

          ]]>
          + Alle + Aktiv + Deaktiveret + Låst ude + Inviteret + Inaktiv + Navn (A-Å) + Navn (Å-A) + Nyeste + Ældste + Sidst logget ind + + + Validering + Valider som e-mail + Valider som tal + Valider som Url + ...eller indtast din egen validering + Feltet er påkrævet + Indtast et regulært udtryk + Du skal tilføje mindst + Du kan kun have + elementer + elementer valgt + Ugyldig dato + Ikke et tal + Ugyldig e-mail + %1% more.]]> + %1% too many.]]> + + + Slå URL tracker fra + Slå URL tracker til + Original URL + Viderestillet til + Der er ikke lavet nogen viderestillinger + Når en udgivet side bliver omdøbt eller flyttet, vil en viderestilling automatisk blive lavet til den nye side. + Er du sikker på at du vil fjerne viderestillingen fra '%0%' til '%1%'? + Viderestillings URL fjernet. + Fejl under fjernelse af viderestillings URL. + Dette vil fjerne viderestillingen + Er du sikker på at du vil slå URL trackeren fra? + URL tracker er nu slået fra. + Der opstod en fejl under forsøget på at slå URL trackeren fra, der findes mere information i logfilen. + URL tracker er nu slået fra. + Der opstod en fejl under forsøget på at slå URL trackeren til, der findes mere information i logfilen. + + + Ingen ordbog elementer at vælge imellem + %0% tegn tilbage.]]> - %1% for mange.]]> - - - Slettet indhold med Id: {0} Relateret til original "parent" med id: {1} - Slettet medie med Id: {0} relateret til original "parent" / mappe med id: {1} - Kan ikke automatisk genoprette dette element - Der er ikke nogen placering hvor dette element automatisk kan genoprettes. Du kan flytte elementet manuelt i træet nedenfor. - blev genoprettet under - - - Retning - Forælder til barn - Tovejs - Forælder - Barn - Antal - Relationer - Oprettet - Kommentar - Navn - Der er ingen relationer for denne relationstype. - Relationstype - Relationer - - - Kom godt i gang - Redirects håndtering - Indhold - Velkommen - Examine Management - Published Cache - Models Builder - Health Check - Kom godt i gang - Installer Umbraco Forms - -
          + %1% for mange.]]> + + + Slettet indhold med Id: {0} Relateret til original "parent" med id: {1} + Slettet medie med Id: {0} relateret til original "parent" / mappe med id: {1} + Kan ikke automatisk genoprette dette element + Der er ikke nogen placering hvor dette element automatisk kan genoprettes. Du kan flytte elementet manuelt i træet nedenfor. + blev genoprettet under + + + Retning + Forælder til barn + Tovejs + Forælder + Barn + Antal + Relationer + Oprettet + Kommentar + Navn + Der er ingen relationer for denne relationstype. + Relationstype + Relationer + + + Kom godt i gang + Redirects håndtering + Indhold + Velkommen + Examine Management + Published Cache + Models Builder + Health Check + Profiling + Kom godt i gang + Installer Umbraco Forms + + + Gå tilbage + Aktivt layout: + Gå til + gruppe + bestået + advarsel + fejlet + forslag + Test bestået + Test fejlet + Åben backoffice søgning + Åben/Luk backoffice hjælp + Åben/Luk dine profil indstillinger + Aktivt sprog + Skift sprog til + Opret ny mappe + Opret element + Rediger + Navn + + + Referencer + Denne Data Type har ingen referencer. + Brugt i Dokument Typer + Ingen referencer til Dokument Typer. + Brugt i Medie Typer + Ingen referencer til Medie Typer. + Brugt i Medlems Typer + Ingen referencer til Medlems Typer. + Brugt af + + + Log type + Gemte søgninger + Samlet resultat + Dato + Type + Maskine + Besked + Exception + Egenskaber + Søg med Google + Søg efter denne besked på Google + Søg med Bing + Søg efter denne besked på Bing + Søg på Our Umbraco + Søg efter denne besked på Our Umbraco forum og dokumentation + Søg på Our Umbraco med Google + Søg på Our Umbraco forum med Google + Søg i Umbraco kildekoden + Søg i Umbraco kildekoden på Github + Søg i Umbraco issues + Søg i Umbraco issues på Github + Slet denne søgning + Find logs med request Id + Find logs med Namespace + Find logs med maskin navn + Åben + + + Kopier %0% + %0% fra %1% + Fjern alle elementer + + + Åben egenskabshandlinger + +
          diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml index 3c8d24ec9c..529074f24d 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml @@ -4,17 +4,24 @@ The Umbraco community https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files + + + + - Hostnamen verwalten + Kulturen und Hostnamen Protokoll Durchsuchen Dokumenttyp ändern Kopieren - Erstellen - Paket erstellen - Löschen + Neu + Exportieren + Neues Paket + Neue Gruppe + Entfernen Deaktivieren Papierkorb leeren + Aktivieren Dokumenttyp exportieren Dokumenttyp importieren Paket importieren @@ -27,16 +34,57 @@ Veröffentlichung zurücknehmen Aktualisieren Erneut veröffentlichen + Umbenennen Wiederherstellen + Berechtigungen setzen für Seite %0% + Wähle worunter kopiert werden soll + Wähle worunter verschoben werden soll + in der Baumstrukture + wurde verschoben nach + wurde kopiert nach Berechtigungen Zurücksetzen Zur Veröffentlichung einreichen Zur Übersetzung senden + Gruppe festlegen Sortieren - Zur Veröffentlichung einreichen Übersetzen Aktualisieren - Standardwert + Berechtigung festlegen + Freigeben + Inhaltsvorlage anlegen + Einladung erneut versenden + + + Inhalt + Administration + Struktur + Anderes + + + Erlaube Zugriff auf "Kultur und Hostname"-Einstellungen + Erlaube Zugriff auf Bearbeiten-Verlauf + Erlaube das Anzeigen eines Knotens + Erlaube Ändern des Dokumenten-Typs + Erlaube Kopieren + Erlaube Erzeugen + Erlaube Entfernen + Erlaube Verschieben + Erlaube Zugriff auf "Öffentlich zugänglich"-Einstellungen + Erlaube Veröffentlichung + Erlaube Rücknahme der Veröffentlichung + Erlaube Zugriff auf die Berechtigungen + Erlaube Zurücksetzen auf eine vorherige Version + Erlaube Anforderungen von Veröffentlichungen + Erlaube Anfordern von Übersetzungen + Erlaube Sortieren + Erlaube Übersetzung + Erlaube Sichern von Änderungen + Erlaube Anlegen von Inhaltsvorlagen + + + Inhalt + Info Erlaubnis verweigert. @@ -52,15 +100,17 @@ Die Domain '%0%' ist bereits zugeordnet Domain '%0%' aktualisiert Domains bearbeiten + + + Vererben Kultur Definiert die Kultureinstellung für untergeordnete Elemente dieses Elements oder vererbt vom übergeordneten Element. Wird auch auf das aktuelle Element angewendet, sofern auf tieferer Ebene keine Domain zugeordnet ist. - Domains - - - Ansicht für + Domainen + Auswahl aufheben Auswählen Etwas anderes machen Fett @@ -79,17 +129,61 @@ Nummerierung Makro einfügen Abbildung einfügen + Veröffentlichen und schliessen + Veröffentlichen mit Unterknoten Datenbeziehungen bearbeiten Zurück zur Liste Speichern + Sichern und schliessen Speichern und veröffentlichen Speichern und planen Speichern und zur Abnahme übergeben - Vorschau + Listenansicht sichern + Veröffentlichung planen + Vorschau Die Vorschaufunktion ist deaktiviert, da keine Vorlage zugewiesen ist Stil auswählen Stil anzeigen Tabelle einfügen + Erzeuge Daten-Model und schliesse + Sichern und Daten-Model erzeugen + Zurücknehmen + Erneut anwenden + Vorgängerversion herstellen + TAG entfernen + Abbrechen + Bestätigen + + + Anzeigen als + + Inhalt gelöscht + Inhalt unveröffentlicht + Inhalt unveröffentlicht für Sprache: %0% + Inhalt veröffentlicht + Inhalt veröffentlicht für Sprache: %0% + Inhalt gesichert + Inhalt gesichert für Sprache: %0% + Inhalt verschoben + Inhalt kopiert + Inhalt auf vorherige Version geändert + Veröffentlichung für Inhalt angefordert + Veröffentlichung für Inhalt angefordert in Sprache: %0% + Unterknoten wurden sortiert von Benutzer + Kopieren + Veröffentlichen + Veröffentlichen + Verschieben + Sichern + Sichern + Entfernen + Veröffentlichung zurücknehmen + Veröffentlichung zurücknehmen + Vorgängerversion wieder herstellen + Veröffentlichung anfordern + Veröffentlichung anfordern + Sortieren + Verlauf (alle Variationen) Um den Typ des ausgewählten Dokuments zu ändern, wählen Sie bitte zunächst aus der Liste der an dieser Stelle erlaubten Dokumenttypen. @@ -111,6 +205,12 @@ Die Zuordnung der Eigenschaften kann nicht abgeschlossen werden, da mindestens eine Eigenschaft mehrfach zugeordnet werden soll. Nur an dieser Stelle erlaubte Dokumenttypen werden angezeigt. + + Es konnte kein Verzeichnis unter dem Knoten mit der ID %0% angelegt werden. + Es konnte kein Verzeichnis unter dem Knoten mit dem Namen %0% angelegt werden. + Der Verzeichnisname darf keine ungültigen Zeichen enthalten. + Folgendes Element konnte nicht entfernt werden: %0% + Ist veröffentlicht Über dieses Dokument @@ -129,22 +229,45 @@ Dieses Dokument wurde nach dem Veröffentlichen bearbeitet. Dieses Dokument ist nicht veröffentlicht. Zuletzt veröffentlicht + Keine Elemente anzuzeigen Diese Liste enthält keine Einträge. + Es wurden keine untergeordneten Elemente hinzugefügt + Es wurden keine Mitglieder hinzugefügt Medientyp Verweis auf Medienobjekt(e) Mitgliedergruppe Mitgliederrolle Mitglieder-Typ + Es wurden keine Änderungen vorgenommen Kein Datum gewählt Name des Dokument + Dieses Media-Element hat keinen Link + Diesem Element kann kein Inhalt zugewiesen werden Eigenschaften Dieses Dokument ist veröffentlicht aber nicht sichtbar, da das übergeordnete Dokument '%0%' nicht publiziert ist + Diese Kultur wurde veröffentlicht, aber wird nicht angezeigt, weil sie auf dem Oberknoten '%0%' unveröffentlicht ist Ups! Dieses Dokument ist veröffentlicht aber nicht im internen Cache aufzufinden: Systemfehler. + Der URL wurde nicht gefunden + Dieses Dokument wurde veröffentlicht, aber sein URL würde mit Inhalt %0% kollidieren + Dieses Dokument wurde veröffentlicht, aber sein URL kann nicht aufgelöst (routed) werden Veröffentlichen + Veröffentlicht + Veröffentlicht (Änderungen bereit) Publikationsstatus + + Veröffentlichen mit Unterknoten zum Veröffentlichen der gewählten Sprache + samt aller Unterknoten der selben Sprache, um ihren Inhalt öffentlich verfügbar zu machen.]]> + + + Veröffentlichen mit Unterknoten zum Veröffentlichen der gewählten Sprache + samt aller Unterknoten der selben Sprache, um ihren Inhalt öffentlich verfügbar zu machen.]]> + Veröffentlichen am Veröffentlichung widerrufen am Datum entfernen + Datum wählen Sortierung abgeschlossen Um die Dokumente zu sortieren, ziehen Sie sie einfach an die gewünschte Position. Sie können mehrere Zeilen markieren indem Sie die Umschalttaste ("Shift") oder die Steuerungstaste ("Strg") gedrückt halten Statistiken @@ -152,6 +275,8 @@ Alternativtext (optional) Typ Veröffentlichung widerrufen + Entwurf + Nicht angelegt Zuletzt bearbeitet am Letzter Änderungszeitpunkt des Dokuments Datei entfernen @@ -160,20 +285,89 @@ Kein Mitglied der Gruppe(n) Untergeordnete Elemente Ziel + Dies führt zur folgenden Zeit auf dem Server: + + Was bedeutet dies? + ]]> + + Wollen Sie dieses Element wirklich entfernen? + Eigenschaft %0% verwendet Editor %1%, welcher nicht von Nested Content unterstützt wird. + Es wurden keine Dokument-(Inhalts-)Typen für diese Eigenschaft definiert. + Füge ein weiteres Textfeld hinzu + Entferne dieses Textfeld + Inhalt-Basis + Inklusive Entwürfen: veröffentliche auch unveröffentlichte Elemente. + Dieser Wert ist verborgen. Wenn Sie diesen Wert einsehen müssen, wenden Sie sich bitte an einen Administrator. + Dieser Wert ist verborgen. + Welche Sprache möchten Sie veröffentlichen? Alle Sprachinhalte wurden gesichert! + Welche Sprache möchten Sie veröffentlichen? + Welche Sprache möchten Sie sichern? + Alle Sprachinhalte wurden beim Anlegen gesichert! + Welche Sprachen möchten Sie zur Freigabe schicken? + Welche Sprachen möchten Sie zu einer bestimmten Zeit veröffentlichen? + Wählen Sie die Sprachen, deren Veröffentlichung zurück genommen werden soll. Das Zurücknehmen der Veröffentlichung einer Pflichtsprache betrifft alle Sprachen. + Veröffentlichte Sprachen + Unveröffentlichte Sprachen + Unveränderte Sprachen + Diese Sprachen wurden nicht angelegt + Bereit zu Veröffentlichen? + Bereit zu Sichern? + Freigabe anfordern + Wählen Sie Datum und Uhrzeit für die Veröffentlichung bzw. deren Rücknahme. + Neues Element anlegen + Aus der Zwischenablage einfügen + + + Erzeuge eine neue Inhaltsvorlage von '%0%' + Leer + Wählen Sie eine Inhaltsvorlage + Inhaltsvorlage erzeugt + Inhaltsvorlage von '%0%' wurde erzeugt + Eine gleichnamige Inhaltsvorlage ist bereits vorhanden + Eine Inhaltsvorlage ist vordefinierter Inhalt, den ein Redakteur als Basis für neuen Inhalt verwenden kann Für Upload klicken + oder klicken Sie hier um eine Datei zu wählen + Sie können Dateien hierher ziehen, um sie hochzuladen. + Dieser Dateityp darf nicht hochgeladen werden + Max. Dateigröße ist + Media-Basis + Media-Element konnte nicht verschoben werden + Eltern- und Ziel-Verzeichnis dürfen nicht übereinstimmen + Media-Element konnte nicht kopiert werden + Unter Element Id %0% konnte kein Verzeichnis angelegt werden + Das Verzeichnis mit Id %0% konnte nicht umbenannt werden Neues Mitglied anlegen Alle Mitglieder + Mitgliedsgruppen haben keine weiteren editierbaren Eigenschaften. An welcher Stellen wollen Sie das Element erstellen - Erstellen unter + Neues Element unterhalb von + Wählen Sie einen Dokumenttyp für eine Inhaltsvorlage + Geben Sie einen Verzeichnisnamen ein Wählen Sie einen Namen und einen Typ - Es stehen keine erlaubten Dokumenttypen zur Verfügung. Sie müssen diese in den Einstellungen (unter "Dokumenttypen") aktivieren. - Es stehen keine erlaubten Medientypen zur Verfügung. Sie müssen diese in den Einstellungen (unter "Medientypen") aktivieren. + + Die im Inhaltsbaum ausgewählte Seite erlaubt keine Unterseiten. + Bearbeitungsrechte für diesen Dokumenttyp + + Das im Strukturbaum ausgewählte Medienelement erlaubt keine untergeordneten Elemente. + Bearbeitungsrechte für diesen Medientyp + Dokumenttyp ohne Vorlage + Neues Verzeichnis + Neuer Datentyp + Neue JavaScript-Datei + Neue leere Partial-View + Neues Partial-View-Makro + Neue Partial-View nach Vorlage + Neues Partial-View-Makro nach Vorlage + Neues Partial-View-Makro (ohne Makro) + Neue Style-Sheet-Datei + Neue Rich-Text-Editor Style-Sheet-Datei Website anzeigen @@ -185,43 +379,47 @@ Willkommen - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes + Bleiben + Änderungen verwerfen + Es gibt ungesicherte Änderungen + Wollen Sie diese Seite wirklich verlassen? - es gibt ungesicherte Änderungen + Veröffentlichen macht die ausgewählten Elemente auf der Website sichtbar. + Aufheben der Veröffentlichung entfernt die ausgewählten Elemente und ihre Unterknoten von der Website. + Aufheben der Veröffentlichung entfernt diese Seite und ihre Unterseiten von der Website. + Es gibt ungesicherte Änderungen. Ändern des Dokumenttyps macht diese rückgängig. - Done - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items + Fertig + %0% Element entfernt + %0% Elemente entfernt + %0% von %1% Element entfernt + %0% von %1% Elementen entfernt + %0% Element veröffentlicht + %0% Elemente veröffentlicht + %0% von %1% Element veröffentlicht + %0% von %1% Elementen veröffentlicht + %0% Veröffentlichung aufgehoben + %0% Veröffentlichungen aufgehoben + %0% von %1% Veröffentlichung aufgehoben + %0% von %1% Veröffentlichungen aufgehoben + %0% Element verschoben + %0% Elemente verschoben + %0% von %1% Element verschoben + %0% von %1% Elementen verschoben + %0% Element kopiert + %0% Elemente kopiert + %0% von %1% Element kopiert + %0% von %1% Elementen kopiert Name des Link Link + Anker / querystring Name - Hostnamen verwalten Fenster schließen - Sind Sie sicher? - Deaktivieren bestätigen - Sind Sie sicher? + Wollen Sie dies wirklich entfernen + Wollen Sie folgendes wirklich deaktivieren + Sind Sie sich wirklich abmelden? Sind Sie sicher? Ausschneiden Wörterbucheintrag bearbeiten @@ -233,8 +431,11 @@ Link einfügen klicken um Macro hinzuzufügen Tabelle einfügen + Dies entfernt die Sprache + + Die Kultur-Variante einer Sprache zu ändern ist möglicherweise eine aufwendige Operation und führt zum Erneuern von Inhalts-Zwischenspeicher und Such-Index. Zuletzt bearbeitet - Link + Verknüpfung Anker: Wenn lokale Links verwendet werden, füge ein "#" vor den Link ein In einem neuen Fenster öffnen? @@ -242,6 +443,9 @@ Dieses Makro enthält keine einstellbaren Eigenschaften. Einfügen Berechtigungen bearbeiten für + Berechtigungen vergeben für + Berechtigungen vergeben für %0% für Benutzer-Gruppe %1% + Wählen Sie die Benutzer-Gruppe, deren Berechtigungen Sie setzen möchten Der Papierkorb wird geleert. Bitte warten Sie und schließen Sie das Fenster erst, wenn der Vorgang abgeschlossen ist. Der Papierkorb ist leer Wenn Sie den Papierkorb leeren, werden die enthaltenen Elemente endgültig gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden. @@ -257,27 +461,123 @@ Für Originalgröße auf die Abbildung klicken Element auswählen Zwischenspeicher-Element anzeigen + Verknüpfe mit Original + Einschliesslich Unterknoten + Die freundlichste Community + Seiten-Link + In neuem Fenster / Tab öffnen + Medien-Link + Inhalts-Startknoten wählen + Medienelement wählen + Bildzeichen wählen + Element wählen + Link wählen + Makro wählen + Inhalt wählen + Inhaltstyp wählen + Medien-Startknoten wählen + Mitglied wählen + Mitgliedergruppe wählen + Knoten wählen + Bereich wählen + Benutzer wählen + Keine Bildzeichen gefunden + Für dieses Makro gibt es keine Parameter + Es gibt keine Makros zum Einfügen + Externe Login-Anbieter + Ausnahmedetails + Stacktrace + Inner Exception + Verknüpfen Sie Ihr + Trennen Sie Ihr + Konto + Editor wählen + Kode-Vorlage wählen + + Dies wird den Knoten und all seine Sprachen entfernen. + Wenn Sie nur eine Sprache entfernen wollen, wählen Sie diese und setzen sie auf unveröffentlicht. + + + + Es gibt keine Einträge im Wörterbuch. - Bearbeiten Sie nachfolgend die verschiedenen Sprachversionen für den Wörterbucheintrag '<em>%0%</em>'. <br/>Unter dem links angezeigten Menüpunkt 'Sprachen' können Sie weitere hinzufügen. + + %0%'. +
          Unter dem links angezeigten Menüpunkt 'Sprachen' können Sie weitere hinzufügen. + ]]> +
          Name der Kultur + + + + Wörterbuch Übersicht + + + Sucher einrichten ]]> + + Sucher (z.B.: multi-index searcher) + ]]> + + Feldwerte + Gesundheitsstatus + Der Gesundheitsstatus und Lesbarkeit des Indizes. + Indizierer + Indexinformationen + Zeigt die Eigenschaften des Indizes + Examine Index-Verwaltung + + Index Detailanzeige und Verwaltungswerkzeuge + + Index erneuern + + + Abhängig von der Inhaltsmenge Ihrer Website kann das eingie Zeit dauern.
          + Es wird davon abgeraten, einen Index einer Website während hoher Auslastung- oder Inhaltbearbeitungszeiten zu erneuern. + ]]> +
          + Sucher + Durchsuche den Index und betrachte die Ergebnisse + Werkzeuge + Werkzeuge zur Indexverwaltung Benutzername eingeben Kennwort eingeben + Bestätige das Kennwort %0% benennen ... - Name angeben ... + Bitte Name angeben ... + Bitte E-Mail eingeben... + Bitte Benutzernamen eingeben... + Label... + Bitte eine Beschreibung eingeben... Durchsuchen ... Filtern ... Tippen, um Tags hinzuzufügen (nach jedem Tag die Eingabetaste drücken) ... + Bitte E-Mail eingeben + Bitte Nachricht eingeben... Der Benutzername ist normalerweise Ihre E-Mail-Adresse + #value oder ?key=value + Bitte einen Alias eingeben... + Alias erzeugen... Angepasste Listenansicht erstellen Angepasste Listenansicht entfernen + Ein Inhalts-, Medien oder Mitgliedstyp mit gleichem Alias ist bereits vorhanden. + + + Umbenannt + Tragen Sie hier einen neuen Verzeichnisnamen ein + %0% wurde umbenannt in %1% - Vorgabewert hinzufügen + Neuer Vorgabewert Feldtyp in der Datenbank Datentyp-GUID Steuerelement zur Darstellung @@ -288,6 +588,16 @@ Verknüpfte Stylesheets Beschriftung anzeigen Breite und Höhe + Alle Eigenschaftstypen und -daten + + die auf diesem Datentyp basierenden werden endgültig entfernt, + bitte bestätigen Sie, dass Sie diese ebenfalls entfernen wollen + + Ja, entferne + und alle Eigenschaftstypen und -daten die auf diesem Datentyp basierenden! + Wählen Sie das Verzeichnis aus der untenstehenden Baumstruktur, in das + verschoben werden soll. + wurde verschoben in Ihre Daten wurden gespeichert. Bevor Sie diese Seite jedoch veröffentlichen können, müssen Sie die folgenden Korrekturen vornehmen: @@ -303,10 +613,12 @@ '%0%' hat ein falsches Format + Der Server hat einen Fehler gemeldet Dieser Dateityp wird durch die Systemeinstellungen blockiert ACHTUNG! Obwohl CodeMirror in den Einstellungen aktiviert ist, bleibt das Modul wegen mangelnder Stabilität in Internet Explorer deaktiviert. Bitte geben Sie die Bezeichnung und den Alias des neuen Dokumenttyps ein. Es besteht ein Problem mit den Lese-/Schreibrechten auf eine Datei oder einen Ordner + Fehler beim Laden einer "Partial View Kodedatei" (Datei: %0%) Bitte geben Sie einen Titel ein Bitte wählen Sie einen Typ Soll die Abbildung wirklich über die Originalgröße hinaus vergrößert werden? @@ -317,12 +629,16 @@ Sie können keine Zelle trennen, die nicht zuvor aus mehreren zusammengeführt wurde. + Optionen Info Aktion Aktionen Hinzufügen Alias + Alles Sind Sie sicher? + Zurück + Zurück zur Übersicht Rahmen von Abbrechen @@ -332,10 +648,13 @@ Fenster schließen Kommentar bestätigen + Beschneiden + Seitenverhältnis beibehalten + Inhalt Weiter Kopieren - Erstellen + Neu Datenbank Datum Standard @@ -343,6 +662,7 @@ Gelöscht Löschen ... Design + Wörterbuch Abmessungen nach unten Herunterladen @@ -351,55 +671,86 @@ Elemente E-Mail Fehler + Feld Suche + Erste(s) + Allgemein + Gruppen + Gruppe Höhe Hilfe - Symbol + Verbergen + Verlauf + Bildzeichen + Id Import + Unterverzeichnisse ebenfalls durchsuchen + Info Innerer Abstand Einfügen - installieren + Installieren + Ungültig Zentrieren + Bezeichnung Sprache + Letzte(s) Layout + Links Laden Gesperrt Anmelden Abmelden Abmelden Makro + Pflichtfeld + Nachricht Verschieben Name Neu Weiter Nein von + Aus Ok Öffnen + Optionen + An + oder + Sortieren nach Kennwort Pfad Einen Moment bitte... Zurück Eigenschaften + Erneuern E-Mail-Empfänger für die Formulardaten Papierkorb + Ihr Mülleimer ist leer + Neu laden Verbleibend + Entfernen Umbenennen Erneuern Pflichtangabe + Wiederherstellen Wiederholen Berechtigungen + Geplantes Veröffentlichen Suchen + Leider können wir nicht finden, wonach Sie suchen. + Es wurden keine Elemente hinzugefügt Server + Einstellungen Anzeigen Seite beim Senden anzeigen Größe Sortieren - Submit - + Status + Senden Typ Durchsuchen ... + unter nach oben Aktualisieren Update @@ -414,8 +765,43 @@ Ja Ordner Suchergebnisse - Reorder - I am done reordering + Sortieren + Sortierung abschließen + Vorschau + Kennwort ändern + nach + + Listenansicht + Sichern läuft... + Aktuelle(s) + Eingebettet + ausgewählt + + + Blau + + + Neue Gruppe + Neue Eigenschaft + Editor hinzufügen + Vorlage hinzufügen + Knoten unterhalb hinzufügen + Element unterhalb hinzufügen + Datentyp bearbeiten + Bereiche wechseln + Abkürzungen + Abkürzungen anzeigen + Listenansicht wechseln + Wurzelknotenberechtigung wechseln + Zeile ein-/auskommentieren + Zeile entfernen + Zeilen oberhalb kopieren + Zeilen unterhalb kopieren + Zeilen nach oben schieben + Zeilen nach unten schieben + Standard + Editor + Kulturvariantenberechtigung wechseln Hintergrundfarbe @@ -432,20 +818,28 @@ Die "web.config"-Datei konnte nicht angepasst werden (Zugriffsrechte?). Bitte passen Sie die Verbindungszeichenfolge manuell an. Die Datenbank ist erreichbar und wurde identifiziert als Datenbank - Klicken Sie auf <strong>Installieren</strong>, um die Datenbank für Umbraco %0% einzurichten. + + Installieren, um die Datenbank für Umbraco %0% einzurichten. + ]]> + Die Datenbank wurde für Umbraco %0% konfiguriert. Klicken Sie auf <strong>weiter</strong>, um fortzufahren. - <p>Die angegebene Datenbank ist leider nicht erreichbar. Bitte prüfen Sie die Verbindungszeichenfolge ("Connection String") in der "web.config"-Datei.</p> -<p>Um fortzufahren, passen Sie bitte die "web.config"-Datei mit einem beliebigen Text-Editor an. Scrollen Sie dazu nach unten, fügen Sie die Verbindungszeichenfolge für die zuverbindende Datenbank als Eintrag "UmbracoDbDSN" hinzu und speichern Sie die Datei.</p> -<p>Klicken Sie nach erfolgter Anpassung auf <strong>Wiederholen</strong>.<br />Wenn Sie weitere technische Informationen benötigen, besuchen Sie <a href="https://our.umbraco.com/wiki" target="_blank">The Umbraco documentation wiki</a>.</p> - + + <p>Die angegebene Datenbank ist leider nicht erreichbar. Bitte prüfen Sie die Verbindungszeichenfolge ("Connection String") in der "web.config"-Datei.</p> + <p>Um fortzufahren, passen Sie bitte die "web.config"-Datei mit einem beliebigen Text-Editor an. Scrollen Sie dazu nach unten, fügen Sie die Verbindungszeichenfolge für die zuverbindende Datenbank als Eintrag "UmbracoDbDSN" hinzu und speichern Sie die Datei.</p> + <p>Klicken Sie nach erfolgter Anpassung auf <strong>Wiederholen</strong>.<br />Wenn Sie weitere technische Informationen benötigen, besuchen Sie <a href="https://our.umbraco.com/wiki" target="_blank">The Umbraco documentation wiki</a>.</p> + Um diesen Schritt abzuschließen, müssen Sie die notwendigen Informationen zur Datenbankverbindung angeben.<br />Bitte kontaktieren Sie Ihren Provider bzw. Server-Administrator für weitere Informationen. - <p> - Bitte bestätigen Sie mit einem Klick auf <strong>Update</strong>, dass die Datenbank auf Umbraco %0% aktualisiert werden soll.</p> - <p> - Keine Sorge - Dabei werden keine Inhalte gelöscht und alles wird weiterhin funktionieren! - </p> - + + Bitte bestätigen Sie mit einem Klick auf Update, dass die Datenbank auf Umbraco %0% aktualisiert werden soll. +

          +

          + Keine Sorge - Dabei werden keine Inhalte gelöscht und alles wird weiterhin funktionieren! +

          + ]]> + Die Datenbank wurde auf die Version %0% aktualisiert. Klicken Sie auf <strong>weiter</strong>, um fortzufahren. Die Datenbank ist fertig eingerichtet. Klicken Sie auf <strong>"weiter"</strong>, um mit der Einrichtung fortzufahren. <strong>Das Kennwort des Standard-Benutzers muss geändert werden!</strong> @@ -463,7 +857,10 @@ Klicken Sie hier, um den technischen Artikel zu lesen Schauen Sie sich die <strong>Video-Lehrgänge</strong> zum Thema Verzeichnisberechtigungen für Umbraco an oder lesen Sie den technischen Artikel. <strong>Die Dateiberechtigungen sind möglicherweise fehlerhaft!</strong>Sie können Umbraco vermutlich ohne Probleme verwenden, werden aber viele Erweiterungspakete können nicht installiert werden. - <strong>Die Dateiberechtigungen sind nicht geeignet!</strong><br /><br />Die Dateiberechtigungen müssen angepasst werden. + + <strong>Die Dateiberechtigungen sind nicht geeignet!</strong><br /><br /> + Die Dateiberechtigungen müssen angepasst werden. + <strong>Die Dateiberechtigungen sind perfekt eingestellt!</strong><br /><br /> Damit ist Umbraco komplett eingerichtet und es können problemlos Erweiterungspakete installiert werden. Verzeichnisprobleme lösen Folgen Sie diesem Link für weitere Informationen zum Thema ASP.NET und der Erstellung von Verzeichnissen. @@ -471,30 +868,30 @@ Umbraco benötigt Schreibrechte auf verschiedene Verzeichnisse, um Dateien wie Bilder oder PDF-Dokumente speichern zu können. Außerdem werden temporäre Daten zur Leistungssteigerung der Website angelegt. Ich möchte mit einem leeren System ohne Inhalte und Vorgaben starten - Die Website ist zur Zeit komplett leer und ohne Inhalte und Vorgaben zu Erstellung eigener Dokumenttypen und Vorlagen bereit. - (<a href="http://Umbraco.tv/documentation/videos/for-site-builders/foundation/document-types">So geht's</a>) - Sie können "Runway" auch jederzeit später installieren. Verwenden Sie hierzu den Punkt "Pakete" im Entwickler-Bereich. - + Die Website ist zur Zeit komplett leer und ohne Inhalte und Vorgaben zu Erstellung eigener Dokumenttypen und Vorlagen bereit. + (<a href="http://Umbraco.tv/documentation/videos/for-site-builders/foundation/document-types">So geht's</a>) + Sie können "Runway" auch jederzeit später installieren. Verwenden Sie hierzu den Punkt "Pakete" im Entwickler-Bereich. + Die Einrichtung von Umbraco ist abgeschlossen und das Content-Management-System steht bereit. Wie soll es weitergehen? 'Runway' wurde installiert -Die Basis ist eingerichtet. Wählen Sie die Module aus, die Sie nun installieren möchten.<br /> -Dies sind unsere empfohlenen Module. Schauen Sie sich die an, die Sie installieren möchten oder Sie sich die <a href="#" onclick="toggleModules(); return false;" id="toggleModuleList">komplette Liste der Module an.</a> - + Die Basis ist eingerichtet. Wählen Sie die Module aus, die Sie nun installieren möchten.<br /> + Dies sind unsere empfohlenen Module. Schauen Sie sich die an, die Sie installieren möchten oder Sie sich die <a href="#" onclick="toggleModules(); return false;" id="toggleModuleList">komplette Liste der Module an.</a> + Nur für erfahrene Benutzer empfohlen Ich möchte mit einer einfache Website starten -<p> -"Runway" ist eine einfache Website mit einfachen Dokumententypen und Vorlagen. Der Installer kann Runway automatisch einrichten, -aber es kann einfach verändert, erweitert oder entfernt werden. Es ist nicht zwingend notwendig und Umbraco kann auch ohne Runway verwendet werden. -Runway bietet eine einfache Basis zum schnellen Start mit Umbraco. -Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die "Runway Modules" und Ihre Runway-Seite erweitern. -</p> -<small> -<em>Runway umfasst:</em> Home page, Getting Started page, Installing Modules page.<br /> -<em>Optionale Module:</em> Top Navigation, Sitemap, Contact, Gallery. -</small> - + <p> + "Runway" ist eine einfache Website mit einfachen Dokumententypen und Vorlagen. Der Installer kann Runway automatisch einrichten, + aber es kann einfach verändert, erweitert oder entfernt werden. Es ist nicht zwingend notwendig und Umbraco kann auch ohne Runway verwendet werden. + Runway bietet eine einfache Basis zum schnellen Start mit Umbraco. + Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die "Runway Modules" und Ihre Runway-Seite erweitern. + </p> + <small> + <em>Runway umfasst:</em> Home page, Getting Started page, Installing Modules page.<br /> + <em>Optionale Module:</em> Top Navigation, Sitemap, Contact, Gallery. + </small> + Was ist 'Runway'? Schritt 1/5 Lizenz Schritt 2/5: Datenbank @@ -515,7 +912,7 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Dieser Assistent führt Sie durch die Einrichtung einer neuen Installation von <strong>Umbraco %0%</strong> oder einem Upgrade von Version 3.0.<br /><br />Klicken Sie auf <strong>weiter</strong>, um zu beginnen. - Code der Kultur + Kode der Kultur Name der Kultur @@ -534,6 +931,99 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Anmelden mit Sitzung abgelaufen <p style="text-align:right;">&copy; 2001 - %0% <br /><a href="http://umbraco.com" style="text-decoration: none" target="_blank">umbraco.org</a></p> + Kennwort vergessen? + Es wird eine E-Mail mit einem Kennwort-Zurücksetzen-Link an die angegebene Adresse geschickt. + Es wird eine E-Mail mit Anweisungen zum Zurücksetzen des Kennwortes an die angegebene Adresse geschickt sofern diese im Datenbestand gefunden wurde. + Kennwort zeigen + Kennwort verbergen + Zurück zur Anmeldung + Bitte wählen Sie ein neues Kennwort + Ihr Kennwort wurde aktualisiert + Der aufgerufene Link ist ungültig oder abgelaufen + Umbraco: Kennwort zurücksetzen + + + + + + + + + + + + +
          + + + + + +
          + +
          + +
          +
          + + + + + + +
          +
          +
          + + + + +
          + + + + +
          +

          + Das Zurücksetzen Ihres Kennwortes wurde angefordert +

          +

          + Ihr Benutzername für das Umbraco-Administration lautet: %0% +

          +

          + + + + + + +
          + + Klicken Sie hier, um Ihr Kennwort zurück zu setzen + +
          +

          +

          Wenn Sie den Link nicht klicken können, kopieren Sie den fogenden URL und fügen Sie ihn direkt im Browser-Fenster ein:

          + + + + +
          + + %1% + +
          +

          +
          +
          +


          +
          +
          + + + ]]> +
          Dashboard @@ -556,6 +1046,7 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Bearbeiten Sie Ihre Benachrichtigungseinstellungen für '%0%' + Benachrichtigungseinstellungen wurden gesichert für Hallo %0%, @@ -566,44 +1057,173 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Einen schönen Tag wünscht Ihr freundlicher Umbraco-Robot - Hallo %0%

          -

          Dies ist eine automatisch E-Mail, welche Sie informiert, dass die Aufgabe '%1%' - an der Seite '%2%' - vom Benutzer '%3%' ausgeführt wurde. -

          - -

          -

          Zusammenfassung der Aktualisierung:

          - - %6% -
          -

          - - - -

          Einen schönen Tag wünscht

          - Ihr freundlicher Umbraco-Robot -

          -]]>
          + Die folgenden Sprachen wurden geändert %0% + + + + + + + + + + + + +
          + + + + + +
          + +
          + +
          +
          + + + + + + +
          +
          +
          + + + + +
          + + + + +
          +

          + Hallo %0%, +

          +

          + Diese automatische E-Mail soll Sie informiern, dass die Aufgabe '%1%' auf Seite '%2%' von Benutzer '%3%' ausgeführt wurde. +

          + + + + + + +
          + +
          + Bearbeiten
          +
          +

          +

          Zusammenfassung der Änderungen:

          + %6% +

          +

          + Einen schönen Tag wünscht
          +
          + Ihr freundlicher Umbraco-Robot +

          +
          +
          +


          +
          +
          + + +]]> +
          + + Folgende Sprachen wurden geändert:

          + %0% + ]]> +
          [%0%] Benachrichtigung: %1% ausgeführt an Seite '%2%' Benachrichtigungen - Wählen Sie ein Paket auf Ihrem lokalen Computer über "Datei auswählen" aus. <br />Umbraco-Pakete besitzen üblicherweise die Dateiendungen ".umb" oder ".zip". + Aktionen + Angelegt + Neues Paket + + + Umbraco-Pakete besitzen üblicherweise die Dateiendungen ".umb" oder ".zip". + ]]> + + Diese Aktion entfernt das Paket + Hierher ziehen zum Hochladen + Alle Unterknoten einschließen + oder hier klicken, um die Paket-Datei auszuwählen + Paket hochladen + + Wählen Sie ein Paket auf Ihrem lokalen Computer über "Datei auswählen" aus.
          + Installieren Sie nur Pakete von Quellen, denen Sie vertrauen. +
          + Ein weiteres Paket hochladen + Abbrechen und anderes Paket hochladen + Ich akzeptiere + Benutzungsbedingungen + Dateipfad + Absoluter Dateipfad (z. B.: /bin/umbraco.bin) + Installiert + Installierte Pakete + Lokale Installation + Abschließen + Diese Paket hat keine Einstellungen + Es wurden noche keine Pakete angelegt + Sie haben keine Pakete installiert + + 'Pakete' rechts, oben), um es zu installieren + ]]> + + Packet Aktionen + Autor URL + Paketinhalt + Paketdateien + Bildsymbol URL + Paket installieren + Lizenz + Lizenz URL + Paketeigenschaften + Paket suchen + Ergebnis(se) für + Keine Ergebnisse für + Bitte versuchen Sie einen anderen Begriff oder stöbern Sie in den Kategorien + Beliebt + Neue Veröffentlichungen + hat + Karma Punkte + Information + Besitzer + Beitragende + Angelegt + Aktuelle Version + .NET Version + Heruntergeladenes + Likes + Kompatibilität + + Dieses Paket ist nach Berichten von Community-Mitgliedern mit folgenden Umbraco-Version kompatibel. + Es kann keine vollständige Kompatibilität garantiert werden für Versionen mit weniger als 100% Bewertungen. + + Externe Quellen Autor Dokumentation Paket-Meta-Daten Name des Pakets Paket enthält keine Elemente - Die Paket-Datei enthält keine Elemente die deinstalliert werden können.<br/><br/>Sie können das Paket ohne Gefahr deinstallieren indem Sie "Paket deinstallieren" anklicken. + +
          + Sie können das Paket ohne Gefahr deinstallieren indem Sie "Paket deinstallieren" anklicken.]]> +
          Paket-Optionen Informationen zum Paket Paket-Repository @@ -611,8 +1231,25 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Paket wurde deinstalliert Das Paket wurde erfolgreich deinstalliert Paket deinstallieren - Sie können einzelne Elemente, die Sie nicht deinstallieren möchten, unten abwählen. Wenn Sie "Deinstallation bestätigen" klicken, werden alle markierten Elemente entfernt.<br /><span style="color: Red; font-weight: bold;">Achtung:</span> alle Dokumente, Medien, etc, die von den zu entfernenden Elementen abhängen, werden nicht mehr funktionieren und im Zweifelsfall kann dass gesamte CMS instabil werden. Bitte deinstallieren Sie also mit Vorsicht. Falls Sie unsicher sind, kontaktieren Sie den Autor des Pakets. - Version des Pakets + + + Achtung: alle Dokumente, Medien, etc, die von den zu entfernenden Elementen abhängen, + werden nicht mehr funktionieren und im Zweifelsfall kann dass gesamte CMS instabil werden. + Bitte deinstallieren Sie also mit Vorsicht. Falls Sie unsicher sind, kontaktieren Sie den Autor des Pakets.]]> + + Paketversion + "Upgrading" von Version + Packet bereits installiert + Diese Paket kann nicht installiert werden. Es benötigt mindestens die Umbracoversion + Es wird deinstalliert... + Es wird heruntergeladen... + Es wird importiert... + Es wird installiert... + Es wird neu gestartet, bitte warten... + Fertig, Ihr Browser aktualisiert jetzt, bitte warten... + Bitte klicken Sie 'Beenden', um die Installation abzuschließen und die Seite neu zu laden. + Paket wird hochgeladen... Einfügen mit Formatierung (Nicht empfohlen) @@ -621,9 +1258,9 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Einfügen, aber Formatierung bereinigen (Empfohlen) - Rollenbasierter Zugriffschutz - Wenn Sie rollenbasierte Authentifikation mit Umbraco-Mitgliedsgruppen verwenden wollen. - Sie müssen zuerst eine Mitgliedsgruppe erstellen, bevor derrollenbasierte Zugriffschutz aktiviert werden kann. + Rollenbasierter Zugriffschutz + Wenn Sie rollenbasierte Authentifikation mit Umbraco-Mitgliedsgruppen verwenden wollen. + Sie müssen zuerst eine Mitgliedsgruppe erstellen, bevor derrollenbasierte Zugriffschutz aktiviert werden kann. Fehlerseite Seite mit Fehlermeldung (Benutzer-Login erfolgt, aber keinen Zugriff auf die aufgerufene Seite erlaubt) Bitte wählen Sie, auf welche Art der Zugriff auf diese Seite geschützt werden soll @@ -632,28 +1269,73 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Login-Seite Seite mit Login-Formular Zugriffsschutz entfernen + + %0% wirklich entfernen? + ]]> + Auswahl der Seiten, die das Login-Formular und die Fehlermeldung enthalten - Auswahl der Benutzerrollen, die Zugriff haben sollen - Kennwort und Login für diese Seite setzen - Zugriffsschutz durch einzelnen Benutzerzugang - Wenn Sie einen einfachen Zugriffsschutz unter Verwendung eines einzelnen Logins mit Kennwort aktivieren wollen + + Auswahl der Benutzergruppen, die Zugriff auf Seite %0% haben sollen + + %0% haben sollen.]]> + Mitglieder basierte Zugriffsberechtigung + Falls Sie Mitglieder basierte Zugriffsberechtigung gewähren wollen + - %0% kann nicht veröffentlicht werden, da die Veröffentlichung zeitlich geplant ist. - %0% konnte nicht veröffentlicht werden, da einige Daten die Gültigkeitsprüfung nicht bestanden haben. - %0% konnte nicht veröffentlicht werden, da ein Plug-In die Aktion abgebrochen hat. - %0% kann nicht veröffentlicht werden, da das übergeordnete Dokument nicht veröffentlicht ist. + Die Zugriffsrechte des Benutzers sind ungenügend, um alle Unterknoten zu veröffentlichen + + + + + %0% kann nicht veröffentlicht werden, da die Veröffentlichung zeitlich geplant ist. + + + + + + + + + %0% konnte nicht veröffentlicht werden, da ein Plug-In die Aktion abgebrochen hat. + + + %0% kann nicht veröffentlicht werden, da das übergeordnete Dokument nicht veröffentlicht ist. + Unveröffentlichte Unterelemente einschließen Bitte warten, Veröffentlichung läuft... %0% Elemente veröffentlicht, %1% Elemente ausstehend ... %0% wurde veröffentlicht %0% und die untergeordneten Elemente wurden veröffentlicht %0% und alle untergeordneten Elemente veröffentlichen - Mit <em>Ok</em> wird <strong>%0%</strong> veröffentlicht und auf der Website sichtbar.<br/><br />Sie können dieses Element mitsamt seinen untergeordneten Elementen veröffentlichen, indem Sie <em>Unveröffentlichte Unterelemente einschließen</em> aktivieren. + + Sichern und Veröffentlichen, um %0% zu veröffentlicht und auf der Website sichtbar zu machen.

          + Sie können dieses Element mitsamt seinen untergeordneten Elementen veröffentlichen, indem Sie Unveröffentlichte Unterelemente einschließen markieren. + ]]> +
          Sie haben keine freigegeben Farben konfiguriert + + Sie können nur Elemente folgender Typen wählen: %0% + Sie haben ein entferntes oder im Papierkorb befindliches Inhaltselement ausgewählt + Sie haben entfernte oder im Papierkorb befindliche Inhaltselemente ausgewählt + + + Element entfernen + Sie haben ein entferntes oder im Papierkorb befindliches Medienelement ausgewählt + Sie haben entfernte oder im Papierkorb befindliche medienelemente ausgewählt + Verworfen + Externen Link eingeben Internen Link auswählen @@ -665,8 +1347,13 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Zurücksetzen + Ausschnitt sichern + Neuer Ausschnitt + Fertig + Rückgängig machen + Wählen Sie eine Version, um diese mit der aktuellen zu vergleichen Aktuelle Version Zeigt die Unterschiede zwischen der aktuellen und der ausgewählten Version an.<br />Text in <del>rot</del> fehlen in der ausgewählten Version, <ins>grün</ins> markierter Text wurde hinzugefügt. Dokument wurde zurückgesetzt @@ -679,20 +1366,14 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Skript bearbeiten - Umbraco Concierge Inhalte - Umbraco Courier - Entwickler - Konfigurationsassistent + Formulare Medien Mitglieder - Newsletter + Pakete Einstellungen - Statistiken Übersetzung Benutzer - Hilfe - Formulare Die besten Umbraco-Video-Tutorials @@ -712,6 +1393,8 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Dieser Dokumenttyp verwendet als Masterdokumenttyp. Register vom Masterdokumenttyp werden nicht angezeigt und können nur im Masterdokumenttyp selbst bearbeitet werden Für dieses Register sind keine Eigenschaften definiert. Klicken Sie oben auf "neue Eigenschaft hinzufügen", um eine neue Eigenschaft hinzuzufügen. + Zugehörige Vorlage anlegen + Bildsymbol hinzufügen Sortierreihenfolge @@ -719,13 +1402,16 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Sortierung abgeschlossen. Ziehen Sie die Elemente an ihre gewünschte neue Position. Bitte warten, die Seiten werden sortiert. Das kann einen Moment dauern. + Dieser Knoten hat keine Unterknoten zum Sortieren + Validierung + Validierungsfehler müssen behoben werden, bevor das Element gesichert werden kann Fehlgeschlagen + Gesichert Unzureichende Benutzerberechtigungen. Vorgang kann nicht abgeschlossen werden. Abgebrochen Vorgang wurde durch eine benutzerdefinierte Erweiterung abgebrochen - Das Veröffentlichen wurde von einem individuellen Ereignishandler abgebrochen Eigenschaft existiert bereits Eigenschaft erstellt Name: %0% Datentyp: %1% @@ -739,63 +1425,193 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Stylesheet erfolgreich gespeichert Datentyp gespeichert Wörterbucheintrag gespeichert - Veröffentlichung nicht möglich, da das übergeordnete Dokument nicht veröffentlicht ist. - Inhalte veröffentlicht - Sichtbar auf der Webseite + Inhalt veröffentlicht + und ist auf der Website sichtbar + %0% Documente veröffentlicht und auf der Website sichtbar + %0% veröffentlicht und auf der Website sichtbar + %0% Documente veröffentlicht in Sprache %1% und auf der Website sichtbar Inhalte gespeichert Denken Sie daran, die Inhalte zu veröffentlichen, um die Änderungen sichtbar zu machen + Der Termin für die Veröffentlichung wurde geändert + %0% gesichert Zur Abnahme eingereicht Die Änderungen wurden zur Abnahme eingereicht + %0% Änderungen wurden zur Abnahme eingereicht Medium gespeichert Medium fehlerfrei gespeichert Mitglied gespeichert + Mitgliedsgruppe gespeichert Stylesheet-Regel gespeichert Stylesheet gespeichert Vorlage gespeichert Fehler beim Speichern des Benutzers. Benutzer gespeichert Benutzertyp gepsichert + Benutzergruppe gepsichert Datei wurde nicht gespeichert Datei konnte nicht gespeichert werden. Bitte überprüfen Sie die Schreibrechte auf Dateiebene. Datei gespeichert Datei erfolgreich gespeichert Sprache gespeichert + Medientyp gespeichert + Mitgliedertyp gespeichert + Mitgliedergruppe gespeichert Vorlage wurde nicht gespeichert Bitte prüfen Sie, ob möglicherweise zwei Vorlagen den gleichen Alias verwenden. Vorlage gespeichert Vorlage erfolgreich gespeichert! Veröffentlichung des Inhalts aufgehoben + Inhaltsvariante %0% unveröffentlicht + Die Veröffentlichung der Pflichtsprache '%0%' wurde zurück genommen. Das gleiche gilt für alle Sprachen dieses Inhalts. Partielle Ansicht gespeichert Partielle Ansicht ohne Fehler gespeichert. Partielle Ansicht nicht gespeichert Fehler beim Speichern der Datei. + Berechtigungen gesichert für + %0% Benutzergruppen entfernt + %0% wurde entfernt + %0% Benutzer aktiviert + %0% Benutzer deaktiviert + %0% ist jetzt aktiviert + %0% ist jetzt deaktiviert + Benutzergruppen wurden gesetzt + %0% Benutzer freigegeben + %0% ist jetzt freigegeben + Mitglied wurde in Datei exportiert + Beim Exportieren des Mitglieds trat ein Fehler auf + Benutzer %0% wurde entfernt + Benutzer einladen + Einladung wurde erneut an %0% geschickt + Das Dokument kann nicht veröffentlicht werden, solange '%0%' nicht veröffentlicht wurde + Validierung fehlgeschlagen für Sprache '%0%' + Dokumenttyp wurde in eine Datei exportiert + Beim Exportieren des Dokumenttyps trat ein Fehler auf + Das Veröffentlichungsdatum kann nicht in der Vergangenheit liegen + Die Veröffentlichung kann nicht eingeplant werden, solange '%0%' (benötigt) nicht veröffentlicht wurde + Die Veröffentlichung kann nicht eingeplant werden, solange '%0%' (benötigt) ein späteres Veröffentlichungsdatum hat als eine optionale Sprache + Das Ablaufdatum darf nicht in der Vergangenheit liegen + Das Ablaufdatum darf nicht vor dem Veröffentlichungsdatum liegen - Gewünschter CSS-Selektor, zum Beispiel 'h1', '.bigHeader' oder 'p.infoText' + Neuer Stil + Stil bearbeiten + Rich text editor Stile + + Definiere die Stile, die im 'Rich-Text-Editor' verfügbar sein sollen. + + Definiere die Styles, die im Rich-Text-Editor dieses Stylesheets verfügbar sein sollen. Stylesheet bearbeiten Stylesheet-Regel bearbeiten Bezeichnung im Auswahlmenü des Rich-Text-Editors Vorschau + So wird der Text im Rich-Text-Editor aussehen. + Selector + Benutze CSS Syntax, z. B.: "h1" oder ".redHeader" Stile + Die CSS-Auszeichnungen, die im Rich-Text-Editor verwendet werden soll, z. B.: "color:red;" + Kode + Rich Text Editor + Beim Entfernen der Vorlage mit Id %0% trat ein Fehler auf Vorlage bearbeiten + Bereich Platzhalter-Bereich verwenden Platzhalter einfügen + Einfügen + Wählen Sie, was in die Vorlage eingefügt werden soll Wörterbucheintrag einfügen - Makro einfügen - Umbraco-Feld einfügen - Mastervorlage - Schnellübersicht zu den verfügbaren Umbraco-Feldern + Ein Wörterbuchelement ist ein Platzhalter für lokalisierbaren Text. Das macht es einfach mehrsprachige Websites zu gestalten. + Makro + + Ein Makro ist eine konfigurierbare Komponente, die großartig + für wiederverwendbare Teile Ihres Entwurfes sind, + für welche Sie optionale Parameter benötigen, wie z. B. Galerien, Formulare oder Listen. + + Umbraco-Feld + + Zeigt den Wert eines benannten Feldes der aktuellen Seite an, mit der Möglichkeit den Wert zu verändern + oder einen alternativen Ersatzwert zu wählen. + + Teilansicht (Partial View) + + Eine Teilansicht ist eine eigenständige Vorlagen-Datei, die innerhalb einer anderen Vorlage verwendet werden kann. + Sie ist gut geeignet, um "Markup"-Kode wiederzuverwenden oder komplexe Vorlagen in mehrere Dateien aufzuteilen. + + Basisvorlage + Keine Basis + Untergeordnete Vorlage einfügen + + @RenderBody() Platzhalters. + ]]> + + Definiert einen benannten Bereich + + @section { ... }. + Dieser benannte Bereich kann in der übergeordneten Vorlage + durch Verwendung von @RenderSection eingefügt werden. + ]]> + + Füge einen benannten Bereich ein + + @RenderSection(name) ein. + Dies verarbeitet einen benannten Bereich einer untergeordneten Vorlage, der mit @section [name]{ ... } umschlossen, definiert wurde. + ]]> + + Bereichsname + Bereich ist notwendig + + Wenn notwendig, dann muss die untergeordnete Vorlage eine @section Definition gleichen Namens enthalten, + anderfalls tritt ein Fehler auf. + + Abfrage-Generator + zurückgegebene Elemente, in + Ich möchte + den ganzen Inhalt + Inhalt vom Typ "%0%" + von + meiner website + wobei + und + ist + ist nicht + vor + vor (inkl. gewähltes Datum) + nach + nach (inkl. gewähltes Datum) + gleich + ungleich + enthält + ohne + größer als + größer als oder gleich + weniger als + weniger als oder gleich + Id + Name + Datum der Erzeugung + Datum der letzten Aktualisierung + sortiert nach + aufsteigend + absteigend + Vorlage Image Macro - Element hinzufügen - Zeilenlayout auswählen - Einfach auf <i class="icon icon-add blue"></i> klicken, um das erste Element anzulegen - Drop content + Neues Element + Layout auswählen + Neue Zeile + Neuer Inhalt + Inhalt entfernen + Einstellungen anwenden + nicht zugelassen]]> + Dieser Inhalt ist hier zugelassen Klicken, um Inhalt einzubetten Klicken, um Abbildung einzufügen Beschriftung ... @@ -807,7 +1623,7 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Einstellungen für das Zeilenlayout Zeilen sind vordefinierte horizontale Zellenanordnungen Zeilenlayout hinzufügen - Pasen Sie das Zeilenlayout an, indem Sie die Zellenbreite einstellen und Zellen hinzufügen. + Passen Sie das Zeilenlayout an, indem Sie die Zellenbreite einstellen und Zellen hinzufügen. Spalten Insgesamte Spaltenanzahl im Layout Einstellungen @@ -816,36 +1632,155 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Legen Sie fest, welche Stile die Autoren anpassen können. Alle Elemente erlauben Alle Zeilenlayouts erlauben + Maximal erlaubte Elemente + Leer lassen oder auf 0 setzen für unbegrenzt + Als Standard setzen + Extra wählen + Standard wählen + wurde hinzugefügt + + + Mischungen + Gruppe + Sie haben keine Gruppen hinzugefügt + Gruppe hinzufügen + Übernimm von + Eigenschaft hinzufügen + Notwendige Bezeichnung + Listenansicht aktivieren + + Konfiguriert die Verwendung einer sortier- und filterbaren Listenansicht der Unterknoten für diesen Dokumenttyp. + Die Unterknoten werden nicht in Baumstruktur angezeigt. + + Erlaubte Vorlagen + + Wählen Sie die Vorlagen, die Editoren für diesen Dokumenttyp wählen dürfen + + Als Wurzelknoten zulassen + + Ermöglicht es Editoren diesen Dokumenttyp in der obersten Ebene der Inhalt-Baum-Strukur zu wählen + + Erlaubte Dokumenttypen für Unterknoten + + Erlaubt es Inhalt der angegebenen Typen unterhalb Inhalten dieses Typs anzulegen + + Wählen Sie einen Unterknoten + + Übernimm Tabs und Eigenschaften vone einem vorhandenen Dokumenttyp. Neue Tabs werden zum vorliegenden Dokumenttyp hinzugefügt oder mit einem gleichnamigen Tab zusammengeführt. + Dieser Dokumenttyp wird in einer Mischung verwendet und kann deshalb nicht selbst zusammengemischt werden. + Es sind keine Dokumenttypen für eine Mischung vorhanden. + Neu anlegen + Vorhandenen nutzen + Editor-Einstellungen + Konfiguration + Ja, entferne + wurde verschoben unter + wurde kopiert unter + Wähle den Ordner in den verschoben wird + Wähle den Ordner in den kopiert wird + in der untenstehenden Baumstruktur + Alle Dokumenttypen + Alle Inhalte + Alle Medien + welche auf diesem Dokumenttyp beruhen, werden unwiderruflich entfernt, bitte bestätigen Sie, dass diese ebenfalls entfernt werden sollen. + welche auf diesem Medientyp beruhen, werden unwiderruflich entfernt, bitte bestätigen Sie, dass diese ebenfalls entfernt werden sollen. + welche auf diesem Mitgliedstyp beruhen, werden unwiderruflich entfernt, bitte bestätigen Sie, dass diese ebenfalls entfernt werden sollen. + und alle Inhalte, die auf diesem Typ basieren + und alle Medienelemente, die auf diesem Typ basieren + und alle Mitglieder, die auf diesem Typ basieren + Mitglied kann bearbeiten + + Diese Eigenschaft zur Bearbeitung des Mitglieds auf seiner Profileseite freigeben + + sensibelle Daten + + Diese Eigenschaft für Editoren, die keine Berechtigung für sensibelle Daten haben, verbergen + + Auf Mitgliedsprofil anzeigen + Diesen Eigenschaftswert für die Anzeige auf der Profilseite des Mitglieds zulassen + Tab hat keine Sortierung + Wo wird diese Mischung verwendet? + + Diese Mischung wird aktuell in den Mischungen folgender Dokumenttypen verwendet: + + Kultur basierte Variationen zulassen + Editoren erlauben, Inhalt dieses Typs in verschiedenen Sprachen anzulegen + Kultur basierte Variationen zulassen + Ist ein Elementtyp + + Nested Content vorgesehen, nicht jedoch als Inhalt-Knoten in der Baumstruktur + ]]> + + Dies kann nicht für Elementtypen verwendet werden + + + Sparche hinzufügen + Notwendige Sprache + Eigenschaften müssen für diese Sprache ausgefüllt sein bevor ein Knoten veröffentlicht werden kann. + Standardsprache + Eine Umbraco site kann nur eine Standardsprache haben. + Ändern der Standardsprache kann zum Fehlen von Standard-Inhalt führen. + Wird ersetzt durch + Kein Ersatzsprache + + Um mehrsprachigem Inhalt zu ermöglichen durch eine andere Sprache ersetzt zu werden, + falls die angefragte Sprache nicht verfügbar ist, wählen Sie diese Option hier aus. + + Ersatzsprache + + + Parameter hinzufügen + Parameter bearbeiten + Makroname vergeben + Parameter + Definiere die Parameter, die verfügbar sein sollen, wenn dieses Makro verwendet wird. + + + Datenmodel erzeugen + Keine Sorge, das kann eine Weile dauern + Datenmodel erzeugt + Datenmodel konnte nicht erzeugt werden + Erzeugung des Datenmodels fehlgeschlagen, siehe Ausnahmen in den Log-Daten + Ersatzfeld hinzufügen + Ersatzfeld + Standardwert hinzufügen + Standardwert Alternatives Feld Alternativer Text Groß- und Kleinschreibung Kodierung Feld auswählen Zeilenumbrüche ersetzen + Ja, Zeilenumbrüche konvertieren Ersetzt Zeilenumbrüche durch das HTML-Tag <br /> Benutzerdefinierte Felder nur Datum + Format und Kodierung Als Datum formatieren + Formatire den Wert als Datum mit oder ohne Zeit entsprechend der aktiven Kultur HTML kodieren Wandelt Sonderzeichen in HTML-Zeichencodes um Wird nach dem Feldinhalt eingefügt Wird vor dem Feldinhalt eingefügt Kleinbuchstaben + Modifiziere Ausgabe Keine + Beispiel-Ausgabe An den Feldinhalt anhängen Dem Feldinhalt voranstellen Rekursiv - Textabsatz entfernen - Alle <p> am Anfang und am Ende des Feldinhalts werden entfernt + Ja, verwende es rekursiv + Trenner Standardfelder Großbuchstaben URL kodieren Wandelt Sonderzeichen zur Verwendung in URLs um Wird nur verwendet, wenn beide vorgenannten Felder leer sind Dieses Feld wird nur verwendet, wenn das primäre Feld leer ist - Datum und Zeit mit Trennzeichen: + Datum und Zeit Details zur Übersetzung @@ -853,17 +1788,19 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Felder Einschließlich der Unterseiten -Hallo %0%, + + Einen schönen Tag wünscht + Ihr freundlicher Umbraco-Robot + ]]> + Bitte erstellen Sie zuerst mindestens einen Übersetzer. Die Seite '%0%' wurde zur Übersetzung gesendet Sendet die Seite '%0%' zur Übersetzung @@ -877,6 +1814,9 @@ Ihr freundlicher Umbraco-Robot Hochladen der XML-Übersetzungsdatei + Inhalt + Inhalt-Vorlage + Medien Zwischenspeicher Papierkorb Erstellte Pakete @@ -894,8 +1834,11 @@ Ihr freundlicher Umbraco-Robot Mitgliederrollen Mitglieder-Typen Dokumententypen + Relationstypen Pakete Pakete + Teilansicht (Partial View) + Makro-Teilansicht(Partial View Macro Files) Paket-Repositories 'Runway' installieren Runway-Module @@ -903,9 +1846,11 @@ Ihr freundlicher Umbraco-Robot Client-Skripte Stylesheets Vorlagen - Berechtigungen - Benutzertypen + Log-Einträge anzeigen Benutzer + Einstellungen + Vorlagen + Drittanbieter Neues Update verfügbar @@ -914,23 +1859,53 @@ Ihr freundlicher Umbraco-Robot Fehler beim Überprüfen der Updates. Weitere Informationen finden Sie im Stacktrace. + Zugang + Basierend auf den zugewiesenen Gruppen und Startknoten, hat der Benutzer Zugang zu folgenden Knoten + Zugang zuweisen Administrator Feld für Kategorie + Benutzer angelegt Kennwort ändern + Foto ändern Neues Kennwort + wurde nicht ausgeschlossen + Das Kennwort wurde nicht geändert Neues Kennwort (Bestätigung) Sie können Ihr Kennwort für den Zugriff auf den Umbraco-Verwaltungsbereich ändern, indem Sie das nachfolgende Formular ausfüllen und auf 'Kennwort ändern' klicken Schnittstelle für externe Editoren + Weiteren Benutzer anlegen + + Lege neue Benutzer an, um ihnen Zugang zum Umbraco-Back-Office zu geben. + Während des Anlegens eines neuen Benutzer wird ein Kennwort erzeugt, das Sie dem Benutzer mitteilen können. + Feld für Beschreibung Benutzer endgültig deaktivieren Dokumenttyp Editor Feld für Textausschnitt + Fehlgeschlagene Anmeldeversuche + Benutzerprofil aufrufen + Gruppen hinzufügen, um Zugang und Berechtigungen zuzuweisen + Weitere Benutzer einladen + + Laden Sie neue Benutzer ein, um ihnen Zugang zum Umbraco-Back-Office zu geben. + Eine Einladungs-E-Mail wird an dem Benutzer geschickt. Diese enthält Informationen, wie sich der Benutzer im Umbraco-Back-Office anmelden kann. + Einladungen sind 72 Stunden lang gültig. + Sprache - Login + Bestimmen Sie die Sprache für Menüs und Dialoge + Letztes Abmeldedatum + Letzte Anmeldung + letzte Änderung des Kennworts + Benutzername Startelement in der Medienbibliothek - Freigegebene Bereiche - Zugang sperren + Beschränke die Medien-Bibliothek auf einen bestimmen Startknoten + Medien-Startknoten + Beschränke die Medien-Bibliothek auf bestimme Startknoten + Bereiche + Umbraco-Back-Office sperren + hat sich noch nie angemeldet + Altes Kennwort Kennwort Kennwort zurücksetzen Ihr Kennwort wurde geändert! @@ -944,15 +1919,343 @@ Ihr freundlicher Umbraco-Robot Die Berechtigungen der untergeordneten Elemente ersetzen Die Berechtigungen für folgende Seiten werden angepasst: Dokumente auswählen, um deren Berechtigungen zu ändern - Auch untergeordnete Elemente - Startelement in den Inhalten + Foto entfernen + Normale Berechtigungen + Detailierte Berechtigungen + Knoten basierte Berechtigungen vergeben + Profil + Untergeordnete Elemente durchsuchen + Bereiche hinzufügen, um Benutzern Zugang zu gewähren + Wählen Sie Benutzergruppen + Kein Startknoten ausgewählt + Keine Startknoten ausgewählt + Startknoten in den Inhalten + Inhalt auf bestimmt Startknoten beschränken + Startknoten in den Inhalten + Inhalt auf bestimmte Startknoten beschränken + Benutzer zuletzt aktualiert + wurde angelegt + Der Benutzer wurde erfolgreich angelegt. Zu Anmelden im Umbraco-Back-Office verwenden Sie bitte folgendes Kennwort: + Benutzer Verwaltung Benutzername Berechtigungen - Rolle - Rollen + Benutzergruppe + wurde eingeladen + Eine Einladung mit Anweisungen zur Anmeldung im Umbraco-Back-Office wurde dem neuen Benutzer zugeschickt. + Hallo und Willkommen bei Umbraco! In nur einer Minute sind Sie bereit loszulegen, Sie müssen nur ein Kennwort festlegen und optinal Ihrem Avatar ein Bild hinzufügen. + Willkommen bei Umbraco! Bedauerlicherweise ist Ihre Einladung verfallen. Bitte kontaktieren Sie Ihren Administrator und bitten Sie ihn, diese erneut zu schicken. + Laden Sie ein Foto von sich hoch, um es anderen Benutzern zu erleichtern, sie zu erkennen. Klicken Sie auf den Kreis oben, um Ihr Foto hochzuladen. Autor + Änderung + Ihr Profil Ihr Verlauf Sitzung läuft ab in + Benutzer einladen + Benutzer anlegen + Einladung schicken + Zurück zu den Benutzern + Umbraco: Einladung + + + + + + + + + + + + +
          + + + + + +
          + +
          + +
          +
          + + + + + + +
          +
          +
          + + + + +
          + + + + +
          +

          + Hallo %0%, +

          +

          + Sie wurden von %1% ins Umbraco-Back-Office eingeladen. +

          +

          + Nachricht von %1%: +
          + %2% +

          + + + + + + +
          + + + + + + +
          + + Um diese Einladung anzunehmen,
          klicken Sie diese Schaltfläche +
          +
          +
          +

          + Falls die Schaltfläche nicht funktioniert, kopieren Sie folgenden URL und fügen ihn im Browser ein:

          + + + + +
          + + %3% + +
          +

          +
          +
          +


          +
          +
          + + +]]> +
          + Einladen + Einladung erneut verschicken... + Benutzer entfernen + Wollen Sie dieses Benutzerkonto wirklich entfernen? + Alle + Aktiv + Gesperrt + Ausgeschlossen + Eingeladen + Nicht aktiv + Name (A-Z) + Name (Z-A) + Newest + Oldest + Last login + + + Validierung + Prüfe auf gültiges E-Mail-Format + Prüfe auf gültiges Zahlen-Format + Prüfe auf gültiges URL-Format + ...oder verwende eigene Validierung + Pflichtfeld + Regulären Ausdruck eingeben + Fügen Sie mindestens + Fügen Sie maximal + Element(e) hinzu + Element(e) ausgewählt + Ungültiges Datum + Keine Zahl + Ungültiges E-Mail-Format + Der Wert darf nicht ungesetzt bleiben + Der Wert darf nicht leer bleiben + Der Wert ist ungültig + Eigene Validierung + + + + Wert wurde auf den empfohlenen Wert gesetzt: '%0%'. + Wert wurde für XPath '%2%' auf '%1%' in der Konfigurationsdatei '%3%' gesetzt. + Erwartete Wert '%1%' für '%2%' in der Konfigurationsdatei '%3%', '%0%' wurde jedoch gefunden. + Unerwarteten Wert '%0%' für '%2%' in der Konfigurationsdatei '%3%' gefunden. + "customErrors" auf '%0%' gesetzt. + "customErrors" aktuell auf '%0%' gesetzt. Es wird empfohlen, diese vor dem Live-Gang auf '%1%' zu setzen. + "customErrors" erfolgreich auf '%0%' gesetzt. + "MacroErrors" auf '%0%' gesetzt. + + "MacroErrors" sind auf '%0%' gesetzt, + was verhindert, dass einige oder alle Seiten Ihrer Website vollständig geladen werden, falls Fehler in Makros auftreten. Schaltfläche "Beheben" setzt den Wert auf '%1%'. + + "MacroErrors" sind jetzt auf '%0%' gesetzt. + "trySkipIisCustomErrors" ist auf '%0%' gestellt und Sie verwenden IIS-Version '%1%'. + "trySkipIisCustomErrors" ist aktuell auf '%0%' gestellt. Für Ihre IIS-Version (%2%) wird empfohlen, diese auf '%1%' zu stellen. + "trySkipIisCustomErrors" wurde erfolgreich auf '%0%' gestellt. + Folgende Datei ist nicht vorhanden: '%0%'. + '%0%' wurde nicht in der Konfigurationsdatei '%1%' gefunden.]]> + Es trat ein Fehler auf, für eine vollständige Fehlermeldung suchen Sie in den Logs nach: %0% + Datenbank - Das Datenbank-Schema ist korrekt für diese Umbraco-Version + %0% Probleme wurden in Ihrem Datenbank-Schema entdeckt (Weitere Details finden Sie in den Logs) + Es wurden einige Fehler während der Abgleichs des Datenbank-Schemas mit der vorliegenden Umbraco-Version. + Ihr Website-Zertifikat (SSL) ist gültig. + (SSL-)Zertifikat-Validierungsfehler: '%0%' + Ihr Website-Zertifikat (SSL) ist abgelaufen. + Ihr Website-Zertifikat (SSL) wird in %0% Tagen ablaufen. + Fehler beim PINGen der URL %0% - '%1%' + Sie betrachten diese Website %0% unter Verwendung des HTTPS-Schemas. + Der Schlüssel 'Umbraco.Core.UseHttps' im Abschnitt 'appSettings' der 'web.config'-Datei ist auf 'false' gesetzt. Sobald Sie diese Site über HTTPS nutzen, sollte dieser auf 'true' gestellt werden. + Der Schlüssel 'Umbraco.Core.UseHttps' im Abschnitt 'appSettings' der 'web.config'-Datei ist auf '%0%' gesetzt, Cookies sind %1% als sicher markiert. + Der Schlüssel 'Umbraco.Core.UseHttps' im Abschnitt 'appSettings' der 'web.config'-Datei konnte nicht geändert werden. Fehler: %0% + HTTPS aktivieren + Setzt Schlüssel 'umbracoSSL' im Abschnitt 'appSettings' der 'web.config'-Datei auf 'true'. + Der Schlüssel 'Umbraco.Core.UseHttps' im Abschnitt 'appSettings' der 'web.config'-Datei steht jetzt auf 'true', Cookies werden als sicher markiert. + Beheben + Überprüfungen mit Wertvergleichen vom Typ 'ShouldNotEqual' können nicht behoben werden. + Überprüfungen mit Wertvergleichen vom Typ 'ShouldEqual' mit einem gelieferten Wert können nicht behoben werden. + Es wurde kein Wert geliefert, um den Fehler bei der Überprüfungen zu beheben. + 'Debug' Kompilierungsmodus ist abgeschaltet. + 'Debug' Kompilierungsmodus ist gegenwertig eingeschaltet. Es ist empfehlenswert diesen vor Live-Gang abzuschalten. + 'Debug' Kompilierungsmodus wurde erfolgreich abgeschaltet. + Trace-Modus ist abgeschaltet. + Trace-Modus ist gegenwertig eingeschaltet. Es ist empfehlenswert diesen vor Live-Gang abzuschalten. + Trace-Modus wurde erfolgreich abgeschaltet. + Alle Ordner haben die korrekten Zugriffsrechte. + %0%.]]> + %0%. Falls nicht in diese geschrieben wird, brauchen Sie nichts zu unternehmen.]]> + Alle Dateien haben die korrekten Zugriffsrechte. + %0%.]]> + %0%. Falls nicht in diese geschrieben wird, brauchen Sie nichts zu unternehmen.]]> + X-Frame-Options ist vorhanden. Diese dienen zur Kontrolle, ob eine Site in IFRAMES anderer Sites angezeigt werden kann.]]> + X-Frame-Options ist nicht vorhanden. Es dient zur Kontrolle, ob eine Site in IFRAMES anderer Sites angezeigt werden kann.]]> + Header konfigurieren + Fügt einen Wert im Abschnitt 'httpProtocol/customHeaders' der 'web.config'-Datei hinzu um zu verhindern, dass diese Website in IFRAMES anderer Websites verwendet werden kann. + Der 'web.config'-Datei wurde eine Einstellung zum Erzeugen eines Headers, der andere Websites daran hindert, diese Site im IFRAME zu verwenden, hinzugefügt. + Die 'web.config'-Datei konnte nicht geändert werden. Fehler: %0% + X-Content-Type-Options ist vorhanden. Diese dienen zum Schutz gegen MIME-'Schnüffeln'-Schwachstellen. ]]> + X-Content-Type-Options ist nicht vorhanden. Diese dienen zum Schutz gegen MIME-'Schnüffeln'-Schwachstellen. ]]> + Fügt einen Wert im Abschnitt 'httpProtocol/customHeaders' der 'web.config'-Datei zum Schutz gegen MIME-'Schnüffeln'-Schwachstellen hinzu. + Der 'web.config'-Datei wurde eine Einstellung zum Schutz gegen MIME-'Schnüffeln'-Schwachstellen hinzugefügt. + Strict-Transport-Security, auch bekannt als HSTS-Header, ist vorhanden.]]> + Strict-Transport-Security, auch bekannt als HSTS-Header, ist nicht vorhanden.]]> + + Fügt den Header 'Strict-Transport-Security' mit dem Wert 'max-age=10886400; preload' im Abschnitt 'httpProtocol/customHeaders' der 'web.config'-Datei hinzu. Benutzen Sie dies nur, wenn Sie Ihre Domainen für mindestens 18 Wochen unter HTTPS laufen lassen. + + Die HSTS-Header-Einstellung wurde der 'web.config'-Datei hinzugefügt. + X-XSS-Protection ist vorhanden.]]> + X-XSS-Protection ist nicht vorhanden]]> + Fügt den Header 'X-XSS-Protection' mit dem Wert '1; mode=block' im Abschnitt 'httpProtocol/customHeaders' der 'web.config'-Datei hinzu. + Die X-XSS-Protection-Header-Einstellung wurde der 'web.config'-Datei hinzugefügt. + %0%.]]> + Es sind keine Header, die Informationen über die Website-Technologie preisgeben, vorhanden. + In der 'web.config'-Datei ist der Abschnitt 'system.net/mailsettings' nicht vorhanden. + In der 'web.config'-Datei ist der Host im Abschnitt 'system.net/mailsettings' nicht konfiguriert. + Die SMTP-Einstellungen sind korrekt konfiguriert und der Dienst arbeitet wie erwartet. + Der konfigurierte SMTP-Server mit Host '%0%' und Port '%1%' konnte nicht erreicht werden. Bitte überprüfen Sie die SMTP-Einstellungen in der 'web.config'-Datei im Abschnitt 'system.net/mailsettings'. + %0% eingestellt.]]> + %0% gestellt.]]> + +

          + Die Ergebnisse der geplanten Systemzustandsprüfung läuft am %0% um %1% lauten wie folgt: +

          %2% + ]]> +
          + Status der Umbraco Systemzustand: %0% + + + URL-Änderungsaufzeichnung abschalten + URL-Änderungsaufzeichnung einschalten + Kultur + Original URL + Weiterleiten zu + Url-Weiterleitungen verwalten + Die folgenden URLs leiten auf diesen Inhalt: + Es wurden keine Weiterleitungen angelegt + + Wenn eine veröffentlichte Seite umbenannt oder verschoben wird, + erzeugt dieses CMS automatisch eine entsprechende Weiterleitung. + + Wollen Sie wirklich die Weiterleitung von '%0%' nach '%1%' entfernen? + URL-Weiterleitung wurde entfernt. + Beim Entfernen der URL-Weiterleitung ist ein Fehler aufgetreten. + Dies entfernt die Weiterleitung + Wollen Sie die URL-Änderungsaufzeichnung wirklich abschalten? + Die URL-Änderungsaufzeichnung wurde abgeschaltet. + Fehler während der Abschaltung der URL-Änderungsaufzeichnung, weitere Information finden Sie in den Log-Dateien. + Die URL-Änderungsaufzeichnung wurde eingeschaltet. + Fehler während der Aktivierung der URL-Änderungsaufzeichnung, weitere Information finden Sie in den Log-Dateien. + + + Das Wörterbuch ist leer + + + Buchstaben verbleiben + + + Inhalt mit Id = {0} des Oberknotens mit Id = {1} wurde verworfen + Medienelement mit Id = {0} des Oberknotens mit Id = {1} wurde verworfen + Dieses Element kann nicht automatisch wiederhergestellt werden + + Es gibt keine Position für das automatische Wiederherstellen dieses Elementes. + Sie können es manuell mit Hilfe der untenstehenden Baumstruktur verschieben. + + wurde wiederhergestellt unterhalb von + + + Richtung + Ober- zu Unterknoten + Bidirektional + Oberknoten + Unterknoten + Anzahl + + Relationen + Angelegt + Kommentar + Name + Es gibt keine Relationen für diesen Typ. + Relationentyp + Relationen + + + Lassen Sie uns beginnen + URL-Weiterleitungen verwalten + Inhalt + Begrüßung + Examine Management + Status der Veröffentlichungen + Models Builder + Systemzustand prüfen + Lassen Sie uns beginnen + Umbraco Forms installieren + + + zurück gehen + Aktives Layout: + Springe zu + Gruppe + bestanden + alarmierend + fehlgeschlagen + Vorschlag + Prüfung bestanden + Prüfung fehlgeschlagen + Back-Office Suche öffnen + Back-Office Hilfe öffnen / schliessen + Ihre Profil-Einstellungen öffnen / schliessen diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d00f428b09..d7ccb13193 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -36,8 +36,11 @@ Choose where to copy Choose where to move to in the tree structure below + Choose where to copy the selected item(s) + Choose where to move the selected item(s) was moved to was copied to + was deleted Permissions Rollback Send To Publish @@ -138,7 +141,7 @@ Save and send for approval Save list view Schedule - Preview + Save and preview Preview is disabled because there's no template assigned Choose style Show styles @@ -149,6 +152,7 @@ Delete tag Cancel Confirm + More publishing options Viewing for @@ -246,8 +250,8 @@ Published Published (pending changes) Publication Status - Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> - Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> + %0% and all content items underneath and thereby making their content publicly available.]]> + Publish at Unpublish at Clear Date @@ -263,6 +267,8 @@ Last edited Date/time this document was edited Remove file(s) + Click here to remove the image from the media item + Click here to remove the file from the media item Link to document Member of group(s) Not a member of group(s) @@ -272,11 +278,17 @@ What does this mean?]]> Are you sure you want to delete this item? Property %0% uses editor %1% which is not supported by Nested Content. + Are you sure you want to delete all items? No content types are configured for this property. + Add element type + Select element type + Select the group whose properties should be displayed. If left blank, the first group on the element type will be used. + Enter an angular expression to evaluate against each item for its name. Use + to display the item index Add another text box Remove this text box Content root - Include drafts: also publish unpublished content items. + Include drafts and unpublished content items. This value is hidden. If you need access to view this value please contact your website administrator. This value is hidden. What languages would you like to publish? All languages with content are saved! @@ -290,12 +302,23 @@ Unpublished Languages Unmodified Languages These languages haven't been created - Ready to Publish? + + All new variants will be saved. + Which variants you would like to publish? + Choose which variants to be saved. + Pick variants to send for approval. + Set scheduled publishing... + Select the variants to unpublish. Unpublishing a mandatory language will unpublish all variants. + The following variants is required for publishing to take place: + + We are not ready to Publish + Ready to publish? Ready to Save? Send for approval - Select the date and time to publish and/or unpublish the content item. - Create new + Select the date and time to publish and/or unpublish the content item. + Create new Paste from clipboard + This item is in the Recycle Bin Create a new Content Template from '%0%' @@ -304,7 +327,7 @@ Content Template created A Content Template was created from '%0%' Another Content Template with the same name already exists - A Content Template is pre-defined content that an editor can select to use as the basis for creating new content + A Content Template is predefined content that an editor can select to use as the basis for creating new content Click to upload @@ -317,6 +340,7 @@ Failed to copy media Failed to create a folder under parent id %0% Failed to rename the folder with id %0% + Drag and drop your file(s) into the area Create a new member @@ -329,8 +353,15 @@ Select the document type you want to make a content template for Enter a folder name Choose a type and a title - "document types".]]> - "media types".]]> + Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Document Types within the Settings section.]]> + The selected page in the content tree doesn't allow for any pages to be created below it. + Edit permissions for this document type + Create a new document type + Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> + Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + The selected media in the tree doesn't allow for any other media to be created below it. + Edit permissions for this media type Document Type without a template New folder New data type @@ -399,6 +430,7 @@ Cut Edit Dictionary Item Edit Language + Edit selected media Insert local link Insert character Insert graphic headline @@ -407,12 +439,13 @@ Click to add a Macro Insert table This will delete the language + Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt Last Edited Link Internal link: When using local links, insert "#" in front of link Open in new window? - Macro Settings + Macro Settings This macro does not contain any properties you can edit Paste Edit permissions for @@ -442,14 +475,17 @@ Link to media Select content start node Select media + Select media type Select icon Select item Select link Select macro Select content + Select content type Select media start node Select member Select member group + Select member type Select node Select sections Select users @@ -465,14 +501,14 @@ account Select editor Select snippet - This will delete the node and all its languages. If you only want to delete one language go and unpublish it instead. + This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. There are no dictionary items. %0%' below
          You can add additional languages under the 'languages' in the menu on the left + Edit the different language versions for the dictionary item '%0%' below ]]>
          Culture Name Search the index and view the results Tools Tools to manage the index + fields + The index cannot be read and will need to be rebuilt + The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation + This index cannot be rebuilt because it has no assigned + IIndexPopulator Enter your username @@ -520,8 +561,12 @@ Enter a message... Your username is usually your email #value or ?key=value - Enter alias... - Generating alias... + Enter alias... + Generating alias... + Create item + Create + Edit + Name Create custom list view @@ -581,6 +626,7 @@ No active styles available Please place cursor at the left of the two cells you wish to merge You cannot split a cell that hasn't been merged. + This property is invalid About @@ -597,6 +643,7 @@ Cancel Cell margin Choose + Clear Close Close Window Comment @@ -626,6 +673,7 @@ Field Find First + Focal point General Groups Group @@ -636,7 +684,7 @@ Icon Id Import - Include subfolders in search + Search only this folder Info Inner margin Insert @@ -699,6 +747,7 @@ Sort Status Submit + Success Type Type to search... under @@ -726,6 +775,11 @@ current Embed selected + Other + Articles + Videos + Clear + Installing Blue @@ -752,6 +806,7 @@ General Editor Toggle allow culture variants + Toggle allow segmentation Background colour @@ -1125,7 +1180,7 @@ To manage your website, simply open the Umbraco back office and start adding con Cancel and upload another package I accept terms of use - + Path to file Absolute path to file (ie: /bin/umbraco.bin) Installed @@ -1254,6 +1309,7 @@ To manage your website, simply open the Umbraco back office and start adding con You have picked content items currently deleted or in the recycle bin + Deleted item You have picked a media item currently deleted or in the recycle bin You have picked media items currently deleted or in the recycle bin Trashed @@ -1273,6 +1329,7 @@ To manage your website, simply open the Umbraco back office and start adding con Add new crop Done Undo edits + User defined Select a version to compare with the current version @@ -1305,7 +1362,10 @@ To manage your website, simply open the Umbraco back office and start adding con Users + Tours The best Umbraco video tutorials + Visit our.umbraco.com + Visit umbraco.tv Default template @@ -1372,6 +1432,8 @@ To manage your website, simply open the Umbraco back office and start adding con User Saved User type saved User group saved + Cultures and hostnames saved + Error saving cultures and hostnames File not saved file could not be saved. Please check file permissions File saved @@ -1472,6 +1534,7 @@ To manage your website, simply open the Umbraco back office and start adding con Query builder items returned, in + copy to clipboard I want all content content of type "%0%" @@ -1539,6 +1602,11 @@ To manage your website, simply open the Umbraco back office and start adding con Choose extra Choose default are added + Warning + You are deleting the row configuration + + Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. + Compositions @@ -1553,13 +1621,14 @@ To manage your website, simply open the Umbraco back office and start adding con Allowed Templates Choose which templates editors are allowed to use on content of this type Allow as root - Allow editors to create content of this type in the root of the content tree + Allow editors to create content of this type in the root of the content tree. Allowed child node types - Allow content of the specified types to be created underneath content of this type + Allow content of the specified types to be created underneath content of this type. Choose child node Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. This content type is used in a composition, and therefore cannot be composed itself. There are no content types available to use as a composition. + Removing a composition will delete all the associated property data. Once you save the document type there's no way back. Create new Use existing Editor settings @@ -1588,13 +1657,22 @@ To manage your website, simply open the Umbraco back office and start adding con tab has no sort order Where is this composition used? This composition is currently used in the composition of the following content types: - Allow varying by culture - Allow editors to create content of this type in different languages + Allow variations + Allow vary by culture + Allow segmentation + Vary by culture + Vary by segments + Allow editors to create content of this type in different languages. + Allow editors to create content of different languages. + Allow editors to create segments of this content. Allow varying by culture + Allow segmentation Element type Is an Element type - An Element type is meant to be used for instance in Nested Content, and not in the tree + An Element type is meant to be used for instance in Nested Content, and not in the tree. + A document type cannot be changed to an Element type once it has been used to create one or more content items. This is not applicable for an Element type + You have made changes to this property. Are you sure you want to discard them? Add language @@ -1607,6 +1685,7 @@ To manage your website, simply open the Umbraco back office and start adding con No fall back language To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. Fall back language + none @@ -1784,6 +1863,7 @@ To manage your website, simply open the Umbraco back office and start adding con Password Reset password Your password has been changed! + Password changed Please confirm the new password Enter your new password Your new password cannot be blank! @@ -1936,6 +2016,7 @@ To manage your website, simply open the Umbraco back office and start adding con Newest Oldest Last login + No user groups have been added Validation @@ -1945,7 +2026,9 @@ To manage your website, simply open the Umbraco back office and start adding con Validate as a URL ...or enter a custom validation Field is mandatory + Enter a custom validation error message (optional) Enter a regular expression + Enter a custom validation error message (optional) You need to add at least You can only have items @@ -1954,6 +2037,8 @@ To manage your website, simply open the Umbraco back office and start adding con Not a number Invalid email Custom validation + %1% more.]]> + %1% too many.]]> -
          - - - - - - - - - - - - - - -
          -
          -
          - - - - -
          - - - - -
          -

          - Password reset requested -

          -

          - Your username to login to the Umbraco back-office is: %0% -

          -

          - - - - - - -
          - - Click this link to reset your password - -
          -

          -

          If you cannot click on the link, copy and paste this URL into your browser window:

          - - - - -
          - - %1% - -
          -

          -
          -
          -


          -
          -
          - - - ]]> - - - Dashboard - Sections - Content - - - Choose page above... - %0% has been copied to %1% - Select where the document %0% should be copied to below - %0% has been moved to %1% - Select where the document %0% should be moved to below - has been selected as the root of your new content, click 'ok' below. - No node selected yet, please select a node in the list above before clicking 'ok' - The current node is not allowed under the chosen node because of its type - The current node cannot be moved to one of its subpages - The current node cannot exist at the root - The action isn't allowed since you have insufficient permissions on 1 or more child documents. - Relate copied items to original - - - %0%]]> - Notification settings saved for - - The following languages have been modified %0% - - - - - - - - - - - -
          - - - - - -
          - -
          - -
          -
          - - - - - - -
          -
          -
          - - - - -
          - - - - -
          -

          - Hi %0%, -

          -

          - This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' -

          - - - - - - -
          - -
          - EDIT
          -
          -

          -

          Update summary:

          - %6% -

          -

          - Have a nice day!

          - Cheers from the Umbraco robot -

          -
          -
          -


          -
          -
          - - - ]]>
          - The following languages have been modified:

          - %0% - ]]>
          - [%0%] Notification about %1% performed on %2% - Notifications - - - Actions - Created - Create package - - button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. - ]]> - This will delete the package - Drop to upload - Include all child nodes - or click here to choose package file - Upload package - Install a local package by selecting it from your machine. Only install packages from sources you know and trust - Upload another package - Cancel and upload another package - I accept - terms of use - Path to file - Absolute path to file (ie: /bin/umbraco.bin) - Installed - Installed packages - Install local - Finish - This package has no configuration view - No packages have been created yet - You don’t have any packages installed - 'Packages' icon in the top right of your screen]]> - Package Actions - Author URL - Package Content - Package Files - Icon URL - Install package - License - License URL - Package Properties - Search for packages - Results for - We couldn’t find anything for - Please try searching for another package or browse through the categories - Popular - New releases - has - karma points - Information - Owner - Contributors - Created - Current version - .NET version - Downloads - Likes - Compatibility - This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% - External sources - Author - Documentation - Package meta data - Package name - Package doesn't contain any items -
          - You can safely remove this from the system by clicking "uninstall package" below.]]>
          - Package options - Package readme - Package repository - Confirm package uninstall - Package was uninstalled - The package was successfully uninstalled - Uninstall package - - Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, - so uninstall with caution. If in doubt, contact the package author.]]> - Package version - Upgrading from version - Package already installed - This package cannot be installed, it requires a minimum Umbraco version of - Uninstalling... - Downloading... - Importing... - Installing... - Restarting, please wait... - All done, your browser will now refresh, please wait... - Please click 'Finish' to complete installation and reload the page. - Uploading package... - - - Paste with full formatting (Not recommended) - The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. - Paste as raw text without any formatting at all - Paste, but remove formatting (Recommended) - - - Group based protection - If you want to grant access to all members of specific member groups - You need to create a member group before you can use group based authentication - Error Page - Used when people are logged on, but do not have access - %0%]]> - %0% is now protected]]> - %0%]]> - Login Page - Choose the page that contains the login form - Remove protection... - %0%?]]> - Select the pages that contain login form and error messages - %0%]]> - %0%]]> - Specific members protection - If you wish to grant access to specific members - - - Insufficient user permissions to publish all descendant documents - - - - - - - - Validation failed for required language '%0%'. This language was saved but not published. - Publishing in progress - please wait... - %0% out of %1% pages have been published... - %0% has been published - %0% and subpages have been published - Publish %0% and all its subpages - Publish to publish %0% and thereby making its content publicly available.

          - You can publish this page and all its subpages by checking Include unpublished subpages below. - ]]>
          - - - You have not configured any approved colors - - - You can only select items of type(s): %0% - You have picked a content item currently deleted or in the recycle bin - You have picked content items currently deleted or in the recycle bin - - - You have picked a media item currently deleted or in the recycle bin - You have picked media items currently deleted or in the recycle bin - Trashed - - - enter external link - choose internal page - Caption - Link - Open in new window - enter the display caption - Enter the link - - - Reset crop - Save crop - Add new crop - Done - Undo edits - - - Select a version to compare with the current version - Current version - Red text will not be shown in the selected version. , green means added]]> - Document has been rolled back - This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view - Rollback to - Select version - View - - - Edit script file - - - Content - Forms - Media - Members - Packages - Settings - Translation - Users - - - The best Umbraco video tutorials - - - Default template - To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) - New Tab Title - Node type - Type - Stylesheet - Script - Tab - Tab Title - Tabs - Master Content Type enabled - This Content Type uses - as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself - No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. - Create matching template - Add icon - - - Sort order - Creation date - Sorting complete. - Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items - - This node has no child nodes to sort - - - Validation - Validation errors must be fixed before the item can be saved - Failed - Saved - Insufficient user permissions, could not complete the operation - Cancelled - Operation was cancelled by a 3rd party add-in - Property type already exists - Property type created - DataType: %1%]]> - Propertytype deleted - Document Type saved - Tab created - Tab deleted - Tab with id: %0% deleted - Stylesheet not saved - Stylesheet saved - Stylesheet saved without any errors - Datatype saved - Dictionary item saved - Content published - and is visible on the website - %0% documents published and visible on the website - %0% published and visible on the website - %0% documents published for languages %1% and visible on the website - Content saved - Remember to publish to make changes visible - A schedule for publishing has been updated - %0% saved - Sent For Approval - Changes have been sent for approval - %0% changes have been sent for approval - Media saved - Media saved without any errors - Member saved - Member group saved - Stylesheet Property Saved - Stylesheet saved - Template saved - Error saving user (check log) - User Saved - User type saved - User group saved - File not saved - file could not be saved. Please check file permissions - File saved - File saved without any errors - Language saved - Media Type saved - Member Type saved - Member Group saved - Template not saved - Please make sure that you do not have 2 templates with the same alias - Template saved - Template saved without any errors! - Content unpublished - Content variation %0% unpublished - The mandatory language '%0%' was unpublished. All languages for this content item are now unpublished. - Partial view saved - Partial view saved without any errors! - Partial view not saved - An error occurred saving the file. - Permissions saved for - Deleted %0% user groups - %0% was deleted - Enabled %0% users - Disabled %0% users - %0% is now enabled - %0% is now disabled - User groups have been set - Unlocked %0% users - %0% is now unlocked - Member was exported to file - An error occurred while exporting the member - User %0% was deleted - Invite user - Invitation has been re-sent to %0% - Cannot publish the document since the required '%0%' is not published - Validation failed for language '%0%' - Document type was exported to file - An error occurred while exporting the document type - The release date cannot be in the past - Cannot schedule the document for publishing since the required '%0%' is not published - Cannot schedule the document for publishing since the required '%0%' has a publish date later than a non mandatory language - The expire date cannot be in the past - The expire date cannot be before the release date - - - Add style - Edit style - Rich text editor styles - Define the styles that should be available in the rich text editor for this stylesheet - Edit stylesheet - Edit stylesheet property - The name displayed in the editor style selector - Preview - How the text will look like in the rich text editor. - Selector - Uses CSS syntax, e.g. "h1" or ".redHeader" - Styles - The CSS that should be applied in the rich text editor, e.g. "color:red;" - Code - Rich Text Editor - - - Failed to delete template with ID %0% - Edit template - Sections - Insert content area - Insert content area placeholder - Insert - Choose what to insert into your template - Dictionary item - A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. - Macro - - A Macro is a configurable component which is great for - reusable parts of your design, where you need the option to provide parameters, - such as galleries, forms and lists. - - Value - Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. - Partial view - - A partial view is a separate template file which can be rendered inside another - template, it's great for reusing markup or for separating complex templates into separate files. - - Master template - No master - Render child template - @RenderBody() placeholder. - ]]> - Define a named section - @section { ... }. This can be rendered in a - specific area of the parent of this template, by using @RenderSection. - ]]> - Render a named section - @RenderSection(name) placeholder. - This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. - ]]> - Section Name - Section is mandatory - - If mandatory, the child template must contain a @section definition, otherwise an error is shown. - - Query builder - items returned, in - I want - all content - content of type "%0%" - from - my website - where - and - is - is not - before - before (including selected date) - after - after (including selected date) - equals - does not equal - contains - does not contain - greater than - greater than or equal to - less than - less than or equal to - Id - Name - Created Date - Last Updated Date - order by - ascending - descending - Template - - - Image - Macro - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied - This content is not allowed here - This content is allowed here - Click to embed - Click to insert image - Image caption... - Write here... - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells - Columns - Total combined number of columns in the grid layout - Settings - Configure what settings editors can change - Styles - Configure what styling editors can change - Allow all editors - Allow all row configurations - Maximum items - Leave blank or set to 0 for unlimited - Set as default - Choose extra - Choose default - are added - - - Compositions - Group - You have not added any groups - Add group - Inherited from - Add property - Required label - Enable list view - Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree - Allowed Templates - Choose which templates editors are allowed to use on content of this type - Allow as root - Allow editors to create content of this type in the root of the content tree - Allowed child node types - Allow content of the specified types to be created underneath content of this type - Choose child node - Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. - This content type is used in a composition, and therefore cannot be composed itself. - There are no content types available to use as a composition. - Create new - Use existing - Editor settings - Configuration - Yes, delete - was moved underneath - was copied underneath - Select the folder to move - Select the folder to copy - to in the tree structure below - All Document types - All Documents - All media items - using this document type will be deleted permanently, please confirm you want to delete these as well. - using this media type will be deleted permanently, please confirm you want to delete these as well. - using this member type will be deleted permanently, please confirm you want to delete these as well - and all documents using this type - and all media items using this type - and all members using this type - Member can edit - Allow this property value to be edited by the member on their profile page - Is sensitive data - Hide this property value from content editors that don't have access to view sensitive information - Show on member profile - Allow this property value to be displayed on the member profile page - tab has no sort order - Where is this composition used? - This composition is currently used in the composition of the following content types: - Allow varying by culture - Allow editors to create content of this type in different languages - Allow varying by culture - Element type - Is an Element type - An Element type is meant to be used for instance in Nested Content, and not in the tree - This is not applicable for an Element type - - - Add language - Mandatory language - Properties on this language have to be filled out before the node can be published. - Default language - An Umbraco site can only have one default language set. - Switching default language may result in default content missing. - Falls back to - No fall back language - To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. - Fall back language - - - Add parameter - Edit parameter - Enter macro name - Parameters - Define the parameters that should be available when using this macro. - Select partial view macro file - - - Building models - this can take a bit of time, don't worry - Models generated - Models could not be generated - Models generation has failed, see exception in U log - - - Add fallback field - Fallback field - Add default value - Default value - Fallback field - Default value - Casing - Encoding - Choose field - Convert line breaks - Yes, convert line breaks - Replaces line breaks with 'br' html tag - Custom Fields - Date only - Format and encoding - Format as date - Format the value as a date, or a date with time, according to the active culture - HTML encode - Will replace special characters by their HTML equivalent. - Will be inserted after the field value - Will be inserted before the field value - Lowercase - Modify output - None - Output sample - Insert after field - Insert before field - Recursive - Yes, make it recursive - Separator - Standard Fields - Uppercase - URL encode - Will format special characters in URLs - Will only be used when the field values above are empty - This field will only be used if the primary field is empty - Date and time - - - Translation details - Download XML DTD - Fields - Include subpages - - No translator users found. Please create a translator user before you start sending content to translation - The page '%0%' has been send to translation - Send the page '%0%' to translation - Total words - Translate to - Translation completed. - You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. - Translation failed, the XML file might be corrupt - Translation options - Translator - Upload translation XML - - - Content - Content Templates - Media - Cache Browser - Recycle Bin - Created packages - Data Types - Dictionary - Installed packages - Install skin - Install starter kit - Languages - Install local package - Macros - Media Types - Members - Member Groups - Member Roles - Member Types - Document Types - Relation Types - Packages - Packages - Partial Views - Partial View Macro Files - Install from repository - Install Runway - Runway modules - Scripting Files - Scripts - Stylesheets - Templates - Log Viewer - Users - Settings - Templating - Third Party - - - New update ready - %0% is ready, click here for download - No connection to server - Error checking for update. Please review trace-stack for further information - - - Access - Based on the assigned groups and start nodes, the user has access to the following nodes - Assign access - Administrator - Category field - User created - Change Your Password - Change photo - New password - hasn't been locked out - The password hasn't been changed - Confirm new password - You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button - Content Channel - Create another user - Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. - Description field - Disable User - Document Type - Editor - Excerpt field - Failed login attempts - Go to user profile - Add groups to assign access and permissions - Invite another user - Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours. - Language - Set the language you will see in menus and dialogs - Last lockout date - Last login - Password last changed - Username - Media start node - Limit the media library to a specific start node - Media start nodes - Limit the media library to specific start nodes - Sections - Disable Umbraco Access - has not logged in yet - Old password - Password - Reset password - Your password has been changed! - Please confirm the new password - Enter your new password - Your new password cannot be blank! - Current password - Invalid current password - There was a difference between the new password and the confirmed password. Please try again! - The confirmed password doesn't match the new password! - Replace child node permissions - You are currently modifying permissions for the pages: - Select pages to modify their permissions - Remove photo - Default permissions - Granular permissions - Set permissions for specific nodes - Profile - Search all children - Add sections to give users access - Select user groups - No start node selected - No start nodes selected - Content start node - Limit the content tree to a specific start node - Content start nodes - Limit the content tree to specific start nodes - User last updated - has been created - The new user has successfully been created. To log in to Umbraco use the password below. - User management - Name - User permissions - User group - has been invited - An invitation has been sent to the new user with details about how to log in to Umbraco. - Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. - Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it. - Uploading a photo of yourself will make it easy for other users to recognize you. Click the circle above to upload your photo. - Writer - Change - Your profile - Your recent history - Session expires in - Invite user - Create user - Send invite - Back to users - Umbraco: Invitation - - - - - - - - - - - -
          - - - - - -
          - -
          - -
          -
          - - - - - - -
          -
          -
          - - - - -
          - - - - -
          -

          - Hi %0%, -

          -

          - You have been invited by %1% to the Umbraco Back Office. -

          -

          - Message from %1%: -
          - %2% -

          - - - - - - -
          - - - - - - -
          - - Click this link to accept the invite - -
          -
          -

          If you cannot click on the link, copy and paste this URL into your browser window:

          - - - - -
          - - %3% - -
          -

          -
          -
          -


          -
          -
          - - ]]>
          - Invite - Resending invitation... - Delete User - Are you sure you wish to delete this user account? - All - Active - Disabled - Locked out - Invited - Inactive - Name (A-Z) - Name (Z-A) - Newest - Oldest - Last login - - - Validation - Validate as an email address - Validate as a number - Validate as a URL - ...or enter a custom validation - Field is mandatory - Enter a regular expression - You need to add at least - You can only have - items - items selected - Invalid date - Not a number - Invalid email - Value cannot be null - Value cannot be empty - Value is invalid, it does not match the correct pattern - Custom validation - - - - Value is set to the recommended value: '%0%'. - Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. - Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. - Found unexpected value '%0%' for '%2%' in configuration file '%3%'. - - Custom errors are set to '%0%'. - Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. - Custom errors successfully set to '%0%'. - MacroErrors are set to '%0%'. - MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. - MacroErrors are now set to '%0%'. - - Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. - Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). - Try Skip IIS Custom Errors successfully set to '%0%'. - - File does not exist: '%0%'. - '%0%' in config file '%1%'.]]> - There was an error, check log for full error: %0%. - Database - The database schema is correct for this version of Umbraco - %0% problems were detected with your database schema (Check the log for details) - Some errors were detected while validating the database schema against the current version of Umbraco. - Your website's certificate is valid. - Certificate validation error: '%0%' - Your website's SSL certificate has expired. - Your website's SSL certificate is expiring in %0% days. - Error pinging the URL %0% - '%1%' - You are currently %0% viewing the site using the HTTPS scheme. - The appSetting 'Umbraco.Core.UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. - The appSetting 'Umbraco.Core.UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. - Could not update the 'Umbraco.Core.UseHttps' setting in your web.config file. Error: %0% - - Enable HTTPS - Sets umbracoSSL setting to true in the appSettings of the web.config file. - The appSetting 'Umbraco.Core.UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. - Fix - Cannot fix a check with a value comparison type of 'ShouldNotEqual'. - Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. - Value to fix check not provided. - Debug compilation mode is disabled. - Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. - Debug compilation mode successfully disabled. - Trace mode is disabled. - Trace mode is currently enabled. It is recommended to disable this setting before go live. - Trace mode successfully disabled. - All folders have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - All files have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> - Set Header in Config - Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. - A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. - Could not update web.config file. Error: %0% - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> - Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. - A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. - Strict-Transport-Security, also known as the HSTS-header, was found.]]> - Strict-Transport-Security was not found.]]> - Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400; preload' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). - The HSTS header has been added to your web.config file. - X-XSS-Protection was found.]]> - X-XSS-Protection was not found.]]> - Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. - The X-XSS-Protection header has been added to your web.config file. - - %0%.]]> - No headers revealing information about the website technology were found. - In the Web.config file, system.net/mailsettings could not be found. - In the Web.config file system.net/mailsettings section, the host is not configured. - SMTP settings are configured correctly and the service is operating as expected. - The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. - %0%.]]> - %0%.]]> -

          Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

          %2%]]>
          - Umbraco Health Check Status: %0% - - - Disable URL tracker - Enable URL tracker - Culture - Original URL - Redirected To - Redirect Url Management - The following URLs redirect to this content item: - No redirects have been made - When a published page gets renamed or moved a redirect will automatically be made to the new page. - Are you sure you want to remove the redirect from '%0%' to '%1%'? - Redirect URL removed. - Error removing redirect URL. - This will remove the redirect - Are you sure you want to disable the URL tracker? - URL tracker has now been disabled. - Error disabling the URL tracker, more information can be found in your log file. - URL tracker has now been enabled. - Error enabling the URL tracker, more information can be found in your log file. - - - No Dictionary items to choose from - - - %0% characters left.]]> - %1% too many.]]> - - - Trashed content with Id: {0} related to original parent content with Id: {1} - Trashed media with Id: {0} related to original parent media item with Id: {1} - Cannot automatically restore this item - There is no location where this item can be automatically restored. You can move the item manually using the tree below. - was restored under - - - Direction - Parent to child - Bidirectional - Parent - Child - Count - Relations - Created - Comment - Name - No relations for this relation type. - Relation Type - Relations - - - Getting Started - Redirect URL Management - Content - Welcome - Examine Management - Published Status - Models Builder - Health Check - Getting Started - Install Umbraco Forms - - + Paste from clipboard + This item is in the Recycle Bin + + + Create a new Content Template from '%0%' + Blank + Select a Content Template + Content Template created + A Content Template was created from '%0%' + Another Content Template with the same name already exists + A Content Template is predefined content that an editor can select to use as the basis for creating new content + + + Click to upload + or click here to choose files + You can drag files here to upload. + Cannot upload this file, it does not have an approved file type + Max file size is + Media root + Failed to move media + Parent and destination folders cannot be the same + Failed to copy media + Failed to create a folder under parent id %0% + Failed to rename the folder with id %0% + Drag and drop your file(s) into the area + + + Create a new member + All Members + Member groups have no additional properties for editing. + + + Where do you want to create the new %0% + Create an item under + Select the document type you want to make a content template for + Enter a folder name + Choose a type and a title + Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Document Types within the Settings section.]]> + The selected page in the content tree doesn't allow for any pages to be created below it. + Edit permissions for this document type + Create a new document type + Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> + Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + The selected media in the tree doesn't allow for any other media to be created below it. + Edit permissions for this media type Document Type without a template + New folder + New data type + New JavaScript file + New empty partial view + New partial view macro + New partial view from snippet + New partial view macro from snippet + New partial view macro (without macro) + New style sheet file + New Rich Text Editor style sheet file + + + Browse your website + - Hide + If Umbraco isn't opening, you might need to allow popups from this site + has opened in a new window + Restart + Visit + Welcome + + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Publishing will make the selected items visible on the site. + Unpublishing will remove the selected items and all their descendants from the site. + Unpublishing will remove this page and all its descendants from the site. + You have unsaved changes. Making changes to the Document Type will discard the changes. + + + Done + Deleted %0% item + Deleted %0% items + Deleted %0% out of %1% item + Deleted %0% out of %1% items + Published %0% item + Published %0% items + Published %0% out of %1% item + Published %0% out of %1% items + Unpublished %0% item + Unpublished %0% items + Unpublished %0% out of %1% item + Unpublished %0% out of %1% items + Moved %0% item + Moved %0% items + Moved %0% out of %1% item + Moved %0% out of %1% items + Copied %0% item + Copied %0% items + Copied %0% out of %1% item + Copied %0% out of %1% items + + + Link title + Link + Anchor / querystring + Name + Close this window + Are you sure you want to delete + Are you sure you want to disable + Are you sure? + Are you sure? + Cut + Edit Dictionary Item + Edit Language + Edit selected media + Insert local link + Insert character + Insert graphic headline + Insert picture + Insert link + Click to add a Macro + Insert table + This will delete the language + Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt + Last Edited + Link + Internal link: + When using local links, insert "#" in front of link + Open in new window? + Macro Settings + This macro does not contain any properties you can edit + Paste + Edit permissions for + Set permissions for + Set permissions for %0% for user group %1% + Select the users groups you want to set permissions for + The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place + The recycle bin is now empty + When items are deleted from the recycle bin, they will be gone forever + regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> + Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' + Remove Macro + Required Field + Site is reindexed + The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished + The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. + Number of columns + Number of rows + Click on the image to see full size + Pick item + View Cache Item + Relate to original + Include descendants + The friendliest community + Link to page + Opens the linked document in a new window or tab + Link to media + Select content start node + Select media + Select media type + Select icon + Select item + Select link + Select macro + Select content + Select content type + Select media start node + Select member + Select member group + Select member type + Select node + Select sections + Select users + No icons were found + There are no parameters for this macro + There are no macros available to insert + External login providers + Exception Details + Stacktrace + Inner Exception + Link your + Un-link your + account + Select editor + Select configuration + Select snippet + This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. + + + There are no dictionary items. + + + %0%' below + ]]> + Culture Name + + Dictionary overview + + + Configured Searchers + Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher) + Field values + Health status + The health status of the index and if it can be read + Indexers + Index info + Lists the properties of the index + Manage Examine's indexes + Allows you to view the details of each index and provides some tools for managing the indexes + Rebuild index + + Depending on how much content there is in your site this could take a while.
          + It is not recommended to rebuild an index during times of high website traffic or when editors are editing content. + ]]> +
          + Searchers + Search the index and view the results + Tools + Tools to manage the index + fields + The index cannot be read and will need to be rebuilt + The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation + This index cannot be rebuilt because it has no assigned + IIndexPopulator + + + Enter your username + Enter your password + Confirm your password + Name the %0%... + Enter a name... + Enter an email... + Enter a username... + Label... + Enter a description... + Type to search... + Type to filter... + Type to add tags (press enter after each tag)... + Enter your email + Enter a message... + Your username is usually your email + #value or ?key=value + Enter alias... + Generating alias... + + + Create custom list view + Remove custom list view + A content type, media type or member type with this alias already exists + + + Renamed + Enter a new folder name here + %0% was renamed to %1% + + + Add prevalue + Database datatype + Property editor GUID + Property editor + Buttons + Enable advanced settings for + Enable context menu + Maximum default size of inserted images + Related stylesheets + Show label + Width and height + All property types & property data + using this data type will be deleted permanently, please confirm you want to delete these as well + Yes, delete + and all property types & property data using this data type + Select the folder to move + to in the tree structure below + was moved underneath + %0% will delete the properties and their data from the following items]]> + I understand this action will delete the properties and data based on this Data Type + + + Your data has been saved, but before you can publish this page there are some errors you need to fix first: + The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) + %0% already exists + There were errors: + There were errors: + The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) + %0% must be an integer + The %0% field in the %1% tab is mandatory + %0% is a mandatory field + %0% at %1% is not in a correct format + %0% is not in a correct format + + + Received an error from the server + The specified file type has been disallowed by the administrator + NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. + Please fill both alias and name on the new property type! + There is a problem with read/write access to a specific file or folder + Error loading Partial View script (file: %0%) + Please enter a title + Please choose a type + You're about to make the picture larger than the original size. Are you sure that you want to proceed? + Startnode deleted, please contact your administrator + Please mark content before changing style + No active styles available + Please place cursor at the left of the two cells you wish to merge + You cannot split a cell that hasn't been merged. + This property is invalid + + + Options + About + Action + Actions + Add + Alias + All + Are you sure? + Back + Back to overview + Border + by + Cancel + Cell margin + Choose + Clear + Close + Close Window + Comment + Confirm + Constrain + Constrain proportions + Content + Continue + Copy + Create + Database + Date + Default + Delete + Deleted + Deleting... + Design + Dictionary + Dimensions + Down + Download + Edit + Edited + Elements + Email + Error + Field + Find + First + Focal point + General + Groups + Group + Height + Help + Hide + History + Icon + Id + Import + Search only this folder + Info + Inner margin + Insert + Install + Invalid + Justify + Label + Language + Last + Layout + Links + Loading + Locked + Login + Log off + Logout + Macro + Mandatory + Message + Move + Name + New + Next + No + of + Off + OK + Open + On + or + Order by + Password + Path + One moment please... + Previous + Properties + Rebuild + Email to receive form data + Recycle Bin + Your recycle bin is empty + Reload + Remaining + Remove + Rename + Renew + Required + Retrieve + Retry + Permissions + Scheduled Publishing + Search + Sorry, we can not find what you are looking for. + No items have been added + Server + Settings + Show + Show page on Send + Size + Sort + Status + Submit + Success + Type + Type to search... + under + Up + Update + Upgrade + Upload + Url + User + Username + Value + View + Welcome... + Width + Yes + Folder + Search results + Reorder + I am done reordering + Preview + Change password + to + List view + Saving... + current + Embed + selected + Other + Articles + Videos + Clear + Installing + + + Blue + + + Add group + Add property + Add editor + Add template + Add child node + Add child + Edit data type + Navigate sections + Shortcuts + show shortcuts + Toggle list view + Toggle allow as root + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + General + Editor + Toggle allow culture variants + Toggle allow segmentation + + + Background color + Bold + Text color + Font + Text + + + Page + + + The installer cannot connect to the database. + Could not save the web.config file. Please modify the connection string manually. + Your database has been found and is identified as + Database configuration + install button to install the Umbraco %0% database + ]]> + Next to proceed.]]> + Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

          +

          To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

          +

          + Click the retry button when + done.
          + More information on editing web.config here.

          ]]>
          + + Please contact your ISP if necessary. + If you're installing on a local machine or server you might need information from your system administrator.]]> + + Press the upgrade button to upgrade your database to Umbraco %0%

          +

          + Don't worry - no content will be deleted and everything will continue working afterwards! +

          + ]]>
          + Press Next to + proceed. ]]> + next to continue the configuration wizard]]> + The Default users' password needs to be changed!]]> + The Default user has been disabled or has no access to Umbraco!

          No further actions needs to be taken. Click Next to proceed.]]> + The Default user's password has been successfully changed since the installation!

          No further actions needs to be taken. Click Next to proceed.]]> + The password is changed! + Get a great start, watch our introduction videos + By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. + Not installed yet. + Affected files and folders + More information on setting up permissions for Umbraco here + You need to grant ASP.NET modify permissions to the following files/folders + Your permission settings are almost perfect!

          + You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
          + How to Resolve + Click here to read the text version + video tutorial on setting up folder permissions for Umbraco or read the text version.]]> + Your permission settings might be an issue! +

          + You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
          + Your permission settings are not ready for Umbraco! +

          + In order to run Umbraco, you'll need to update your permission settings.]]>
          + Your permission settings are perfect!

          + You are ready to run Umbraco and install packages!]]>
          + Resolving folder issue + Follow this link for more information on problems with ASP.NET and creating folders + Setting up folder permissions + + I want to start from scratch + learn how) + You can still choose to install Runway later on. Please go to the Developer section and choose Packages. + ]]> + You've just set up a clean Umbraco platform. What do you want to do next? + Runway is installed + + This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules + ]]> + Only recommended for experienced users + I want to start with a simple website + + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, + but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, + Runway offers an easy foundation based on best practices to get you started faster than ever. + If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. +

          + + Included with Runway: Home page, Getting Started page, Installing Modules page.
          + Optional Modules: Top Navigation, Sitemap, Contact, Gallery. +
          + ]]>
          + What is Runway + Step 1/5 Accept license + Step 2/5: Database configuration + Step 3/5: Validating File Permissions + Step 4/5: Check Umbraco security + Step 5/5: Umbraco is ready to get you started + Thank you for choosing Umbraco + Browse your new site +You installed Runway, so why not see how your new website looks.]]> + Further help and information +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + Umbraco %0% is installed and ready for use + /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> + started instantly by clicking the "Launch Umbraco" button below.
          If you are new to Umbraco, +you can find plenty of resources on our getting started pages.]]>
          + Launch Umbraco +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + Connection to database failed. + Umbraco Version 3 + Umbraco Version 4 + Watch + Umbraco %0% for a fresh install or upgrading from version 3.0. +

          + Press "next" to start the wizard.]]>
          + + + Culture Code + Culture Name + + + You've been idle and logout will automatically occur in + Renew now to save your work + + + Happy super Sunday + Happy manic Monday + Happy tubular Tuesday + Happy wonderful Wednesday + Happy thunderous Thursday + Happy funky Friday + Happy Caturday + Log in below + Sign in with + Session timed out + © 2001 - %0%
          Umbraco.com

          ]]>
          + Forgotten password? + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records + Show password + Hide password + Return to login form + Please provide a new password + Your Password has been updated + The link you have clicked on is invalid or has expired + Umbraco: Reset Password + + + + + + + + + + + +
          + + + + + +
          + +
          + +
          +
          + + + + + + +
          +
          +
          + + + + +
          + + + + +
          +

          + Password reset requested +

          +

          + Your username to login to the Umbraco back-office is: %0% +

          +

          + + + + + + +
          + + Click this link to reset your password + +
          +

          +

          If you cannot click on the link, copy and paste this URL into your browser window:

          + + + + +
          + + %1% + +
          +

          +
          +
          +


          +
          +
          + + + ]]>
          + + + Dashboard + Sections + Content + + + Choose page above... + %0% has been copied to %1% + Select where the document %0% should be copied to below + %0% has been moved to %1% + Select where the document %0% should be moved to below + has been selected as the root of your new content, click 'ok' below. + No node selected yet, please select a node in the list above before clicking 'ok' + The current node is not allowed under the chosen node because of its type + The current node cannot be moved to one of its subpages + The current node cannot exist at the root + The action isn't allowed since you have insufficient permissions on 1 or more child documents. + Relate copied items to original + + + %0%]]> + Notification settings saved for + + The following languages have been modified %0% + + + + + + + + + + + +
          + + + + + +
          + +
          + +
          +
          + + + + + + +
          +
          +
          + + + + +
          + + + + +
          +

          + Hi %0%, +

          +

          + This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' +

          + + + + + + +
          + +
          + EDIT
          +
          +

          +

          Update summary:

          + %6% +

          +

          + Have a nice day!

          + Cheers from the Umbraco robot +

          +
          +
          +


          +
          +
          + + + ]]>
          + The following languages have been modified:

          + %0% + ]]>
          + [%0%] Notification about %1% performed on %2% + Notifications + + + Actions + Created + Create package + + button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. + ]]> + This will delete the package + Drop to upload + Include all child nodes + or click here to choose package file + Upload package + Install a local package by selecting it from your machine. Only install packages from sources you know and trust + Upload another package + Cancel and upload another package + I accept + terms of use + Path to file + Absolute path to file (ie: /bin/umbraco.bin) + Installed + Installed packages + Install local + Finish + This package has no configuration view + No packages have been created yet + You don’t have any packages installed + 'Packages' icon in the top right of your screen]]> + Package Actions + Author URL + Package Content + Package Files + Icon URL + Install package + License + License URL + Package Properties + Search for packages + Results for + We couldn’t find anything for + Please try searching for another package or browse through the categories + Popular + New releases + has + karma points + Information + Owner + Contributors + Created + Current version + .NET version + Downloads + Likes + Compatibility + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% + External sources + Author + Documentation + Package meta data + Package name + Package doesn't contain any items +
          + You can safely remove this from the system by clicking "uninstall package" below.]]>
          + Package options + Package readme + Package repository + Confirm package uninstall + Package was uninstalled + The package was successfully uninstalled + Uninstall package + + Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, + so uninstall with caution. If in doubt, contact the package author.]]> + Package version + Upgrading from version + Package already installed + This package cannot be installed, it requires a minimum Umbraco version of + Uninstalling... + Downloading... + Importing... + Installing... + Restarting, please wait... + All done, your browser will now refresh, please wait... + Please click 'Finish' to complete installation and reload the page. + Uploading package... + + + Paste with full formatting (Not recommended) + The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. + Paste as raw text without any formatting at all + Paste, but remove formatting (Recommended) + + + Group based protection + If you want to grant access to all members of specific member groups + You need to create a member group before you can use group based authentication + Error Page + Used when people are logged on, but do not have access + %0%]]> + %0% is now protected]]> + %0%]]> + Login Page + Choose the page that contains the login form + Remove protection... + %0%?]]> + Select the pages that contain login form and error messages + %0%]]> + %0%]]> + Specific members protection + If you wish to grant access to specific members + + + Insufficient user permissions to publish all descendant documents + + + + + + + + Validation failed for required language '%0%'. This language was saved but not published. + Publishing in progress - please wait... + %0% out of %1% pages have been published... + %0% has been published + %0% and subpages have been published + Publish %0% and all its subpages + Publish to publish %0% and thereby making its content publicly available.

          + You can publish this page and all its subpages by checking Include unpublished subpages below. + ]]>
          + + + You have not configured any approved colors + + + You can only select items of type(s): %0% + You have picked a content item currently deleted or in the recycle bin + You have picked content items currently deleted or in the recycle bin + + + Deleted item + You have picked a media item currently deleted or in the recycle bin + You have picked media items currently deleted or in the recycle bin + Trashed + + + enter external link + choose internal page + Caption + Link + Open in new window + enter the display caption + Enter the link + + + Reset crop + Save crop + Add new crop + Done + Undo edits + User defined + + + Select a version to compare with the current version + Current version + Red text will not be shown in the selected version. , green means added]]> + Document has been rolled back + This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view + Rollback to + Select version + View + + + Edit script file + + + Content + Forms + Media + Members + Packages + Settings + Translation + Users + + + Tours + The best Umbraco video tutorials + Visit our.umbraco.com + Visit umbraco.tv + + + Default template + To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + New Tab Title + Node type + Type + Stylesheet + Script + Tab + Tab Title + Tabs + Master Content Type enabled + This Content Type uses + as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself + No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Create matching template + Add icon + + + Sort order + Creation date + Sorting complete. + Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items + + This node has no child nodes to sort + + + Validation + Validation errors must be fixed before the item can be saved + Failed + Saved + Insufficient user permissions, could not complete the operation + Cancelled + Operation was cancelled by a 3rd party add-in + Property type already exists + Property type created + DataType: %1%]]> + Propertytype deleted + Document Type saved + Tab created + Tab deleted + Tab with id: %0% deleted + Stylesheet not saved + Stylesheet saved + Stylesheet saved without any errors + Datatype saved + Dictionary item saved + Content published + and is visible on the website + %0% documents published and visible on the website + %0% published and visible on the website + %0% documents published for languages %1% and visible on the website + Content saved + Remember to publish to make changes visible + A schedule for publishing has been updated + %0% saved + Sent For Approval + Changes have been sent for approval + %0% changes have been sent for approval + Media saved + Media saved without any errors + Member saved + Member group saved + Stylesheet Property Saved + Stylesheet saved + Template saved + Error saving user (check log) + User Saved + User type saved + User group saved + Cultures and hostnames saved + Error saving cultures and hostnames + File not saved + file could not be saved. Please check file permissions + File saved + File saved without any errors + Language saved + Media Type saved + Member Type saved + Member Group saved + Template not saved + Please make sure that you do not have 2 templates with the same alias + Template saved + Template saved without any errors! + Content unpublished + Content variation %0% unpublished + The mandatory language '%0%' was unpublished. All languages for this content item are now unpublished. + Partial view saved + Partial view saved without any errors! + Partial view not saved + An error occurred saving the file. + Permissions saved for + Deleted %0% user groups + %0% was deleted + Enabled %0% users + Disabled %0% users + %0% is now enabled + %0% is now disabled + User groups have been set + Unlocked %0% users + %0% is now unlocked + Member was exported to file + An error occurred while exporting the member + User %0% was deleted + Invite user + Invitation has been re-sent to %0% + Cannot publish the document since the required '%0%' is not published + Validation failed for language '%0%' + Document type was exported to file + An error occurred while exporting the document type + The release date cannot be in the past + Cannot schedule the document for publishing since the required '%0%' is not published + Cannot schedule the document for publishing since the required '%0%' has a publish date later than a non mandatory language + The expire date cannot be in the past + The expire date cannot be before the release date + + + Add style + Edit style + Rich text editor styles + Define the styles that should be available in the rich text editor for this stylesheet + Edit stylesheet + Edit stylesheet property + The name displayed in the editor style selector + Preview + How the text will look like in the rich text editor. + Selector + Uses CSS syntax, e.g. "h1" or ".redHeader" + Styles + The CSS that should be applied in the rich text editor, e.g. "color:red;" + Code + Rich Text Editor + + + Failed to delete template with ID %0% + Edit template + Sections + Insert content area + Insert content area placeholder + Insert + Choose what to insert into your template + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + Master template + No master + Render child template + @RenderBody() placeholder. + ]]> + Define a named section + @section { ... }. This can be rendered in a + specific area of the parent of this template, by using @RenderSection. + ]]> + Render a named section + @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. + ]]> + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + Query builder + items returned, in + copy to clipboard + I want + all content + content of type "%0%" + from + my website + where + and + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + Id + Name + Created Date + Last Updated Date + order by + ascending + descending + Template + + + Image + Macro + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + This content is not allowed here + This content is allowed here + Click to embed + Click to insert image + Image caption... + Write here... + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + Columns + Total combined number of columns in the grid layout + Settings + Configure what settings editors can change + Styles + Configure what styling editors can change + Allow all editors + Allow all row configurations + Maximum items + Leave blank or set to 0 for unlimited + Set as default + Choose extra + Choose default + are added + Warning + You are deleting the row configuration + + Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. + + + + Compositions + Group + You have not added any groups + Add group + Inherited from + Add property + Required label + Enable list view + Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree + Allowed Templates + Choose which templates editors are allowed to use on content of this type + Allow as root + Allow editors to create content of this type in the root of the content tree. + Allowed child node types + Allow content of the specified types to be created underneath content of this type. + Choose child node + Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. + This content type is used in a composition, and therefore cannot be composed itself. + There are no content types available to use as a composition. + Removing a composition will delete all the associated property data. Once you save the document type there's no way back. + Create new + Use existing + Editor settings + Available configurations + Create a new configuration + Configuration + Yes, delete + was moved underneath + was copied underneath + Select the folder to move + Select the folder to copy + to in the tree structure below + All Document types + All Documents + All media items + using this document type will be deleted permanently, please confirm you want to delete these as well. + using this media type will be deleted permanently, please confirm you want to delete these as well. + using this member type will be deleted permanently, please confirm you want to delete these as well + and all documents using this type + and all media items using this type + and all members using this type + Member can edit + Allow this property value to be edited by the member on their profile page + Is sensitive data + Hide this property value from content editors that don't have access to view sensitive information + Show on member profile + Allow this property value to be displayed on the member profile page + tab has no sort order + Where is this composition used? + This composition is currently used in the composition of the following content types: + Allow variations + Allow vary by culture + Allow segmentation + Vary by culture + Vary by segments + Allow editors to create content of this type in different languages. + Allow editors to create content of different languages. + Allow editors to create segments of this content. + Allow varying by culture + Allow segmentation + Element type + Is an element type + An element type is meant to be used for instance in Nested Content, and not in the tree. + A document type cannot be changed to an element type once it has been used to create one or more content items. + This is not applicable for an element type + You have made changes to this property. Are you sure you want to discard them? + + + Add language + Mandatory language + Properties on this language have to be filled out before the node can be published. + Default language + An Umbraco site can only have one default language set. + Switching default language may result in default content missing. + Falls back to + No fall back language + To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. + Fall back language + none + + + Add parameter + Edit parameter + Enter macro name + Parameters + Define the parameters that should be available when using this macro. + Select partial view macro file + + + Building models + this can take a bit of time, don't worry + Models generated + Models could not be generated + Models generation has failed, see exception in U log + + + Add fallback field + Fallback field + Add default value + Default value + Fallback field + Default value + Casing + Encoding + Choose field + Convert line breaks + Yes, convert line breaks + Replaces line breaks with 'br' html tag + Custom Fields + Date only + Format and encoding + Format as date + Format the value as a date, or a date with time, according to the active culture + HTML encode + Will replace special characters by their HTML equivalent. + Will be inserted after the field value + Will be inserted before the field value + Lowercase + Modify output + None + Output sample + Insert after field + Insert before field + Recursive + Yes, make it recursive + Separator + Standard Fields + Uppercase + URL encode + Will format special characters in URLs + Will only be used when the field values above are empty + This field will only be used if the primary field is empty + Date and time + + + Translation details + Download XML DTD + Fields + Include subpages + + No translator users found. Please create a translator user before you start sending content to translation + The page '%0%' has been send to translation + Send the page '%0%' to translation + Total words + Translate to + Translation completed. + You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. + Translation failed, the XML file might be corrupt + Translation options + Translator + Upload translation XML + + + Content + Content Templates + Media + Cache Browser + Recycle Bin + Created packages + Data Types + Dictionary + Installed packages + Install skin + Install starter kit + Languages + Install local package + Macros + Media Types + Members + Member Groups + Member Roles + Member Types + Document Types + Relation Types + Packages + Packages + Partial Views + Partial View Macro Files + Install from repository + Install Runway + Runway modules + Scripting Files + Scripts + Stylesheets + Templates + Log Viewer + Users + Settings + Templating + Third Party + + + New update ready + %0% is ready, click here for download + No connection to server + Error checking for update. Please review trace-stack for further information + + + Access + Based on the assigned groups and start nodes, the user has access to the following nodes + Assign access + Administrator + Category field + User created + Change Your Password + Change photo + New password + hasn't been locked out + The password hasn't been changed + Confirm new password + You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button + Content Channel + Create another user + Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. + Description field + Disable User + Document Type + Editor + Excerpt field + Failed login attempts + Go to user profile + Add groups to assign access and permissions + Invite another user + Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours. + Language + Set the language you will see in menus and dialogs + Last lockout date + Last login + Password last changed + Username + Media start node + Limit the media library to a specific start node + Media start nodes + Limit the media library to specific start nodes + Sections + Disable Umbraco Access + has not logged in yet + Old password + Password + Reset password + Your password has been changed! + Password changed + Please confirm the new password + Enter your new password + Your new password cannot be blank! + Current password + Invalid current password + There was a difference between the new password and the confirmed password. Please try again! + The confirmed password doesn't match the new password! + Replace child node permissions + You are currently modifying permissions for the pages: + Select pages to modify their permissions + Remove photo + Default permissions + Granular permissions + Set permissions for specific nodes + Profile + Search all children + Add sections to give users access + Select user groups + No start node selected + No start nodes selected + Content start node + Limit the content tree to a specific start node + Content start nodes + Limit the content tree to specific start nodes + User last updated + has been created + The new user has successfully been created. To log in to Umbraco use the password below. + User management + Name + User permissions + User group + has been invited + An invitation has been sent to the new user with details about how to log in to Umbraco. + Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. + Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it. + Uploading a photo of yourself will make it easy for other users to recognize you. Click the circle above to upload your photo. + Writer + Change + Your profile + Your recent history + Session expires in + Invite user + Create user + Send invite + Back to users + Umbraco: Invitation + + + + + + + + + + + +
          + + + + + +
          + +
          + +
          +
          + + + + + + +
          +
          +
          + + + + +
          + + + + +
          +

          + Hi %0%, +

          +

          + You have been invited by %1% to the Umbraco Back Office. +

          +

          + Message from %1%: +
          + %2% +

          + + + + + + +
          + + + + + + +
          + + Click this link to accept the invite + +
          +
          +

          If you cannot click on the link, copy and paste this URL into your browser window:

          + + + + +
          + + %3% + +
          +

          +
          +
          +


          +
          +
          + + ]]>
          + Invite + Resending invitation... + Delete User + Are you sure you wish to delete this user account? + All + Active + Disabled + Locked out + Invited + Inactive + Name (A-Z) + Name (Z-A) + Newest + Oldest + Last login + No user groups have been added + + + Validation + Validate as an email address + Validate as a number + Validate as a URL + ...or enter a custom validation + Field is mandatory + Enter a custom validation error message (optional) + Enter a regular expression + Enter a custom validation error message (optional) + You need to add at least + You can only have + items + items selected + Invalid date + Not a number + Invalid email + Value cannot be null + Value cannot be empty + Value is invalid, it does not match the correct pattern + Custom validation + %1% more.]]> + %1% too many.]]> + + + + Value is set to the recommended value: '%0%'. + Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. + Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. + Found unexpected value '%0%' for '%2%' in configuration file '%3%'. + + Custom errors are set to '%0%'. + Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. + Custom errors successfully set to '%0%'. + MacroErrors are set to '%0%'. + MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. + MacroErrors are now set to '%0%'. + + Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. + Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). + Try Skip IIS Custom Errors successfully set to '%0%'. + + File does not exist: '%0%'. + '%0%' in config file '%1%'.]]> + There was an error, check log for full error: %0%. + Database - The database schema is correct for this version of Umbraco + %0% problems were detected with your database schema (Check the log for details) + Some errors were detected while validating the database schema against the current version of Umbraco. + Your website's certificate is valid. + Certificate validation error: '%0%' + Your website's SSL certificate has expired. + Your website's SSL certificate is expiring in %0% days. + Error pinging the URL %0% - '%1%' + You are currently %0% viewing the site using the HTTPS scheme. + The appSetting 'Umbraco.Core.UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. + The appSetting 'Umbraco.Core.UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. + Could not update the 'Umbraco.Core.UseHttps' setting in your web.config file. Error: %0% + + Enable HTTPS + Sets umbracoSSL setting to true in the appSettings of the web.config file. + The appSetting 'Umbraco.Core.UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. + Fix + Cannot fix a check with a value comparison type of 'ShouldNotEqual'. + Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. + Value to fix check not provided. + Debug compilation mode is disabled. + Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Debug compilation mode successfully disabled. + Trace mode is disabled. + Trace mode is currently enabled. It is recommended to disable this setting before go live. + Trace mode successfully disabled. + All folders have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + All files have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + Set Header in Config + Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. + A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. + Could not update web.config file. Error: %0% + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> + Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. + A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. + Strict-Transport-Security, also known as the HSTS-header, was found.]]> + Strict-Transport-Security was not found.]]> + Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). + The HSTS header has been added to your web.config file. + X-XSS-Protection was found.]]> + X-XSS-Protection was not found.]]> + Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. + The X-XSS-Protection header has been added to your web.config file. + + %0%.]]> + No headers revealing information about the website technology were found. + In the Web.config file, system.net/mailsettings could not be found. + In the Web.config file system.net/mailsettings section, the host is not configured. + SMTP settings are configured correctly and the service is operating as expected. + The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. + %0%.]]> + %0%.]]> +

          Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

          %2%]]>
          + Umbraco Health Check Status: %0% + Check All Groups + Check group + + The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. + You can add your own health checks, have a look at the documentation for more information about custom health checks.

          + ]]> +
          + + + Disable URL tracker + Enable URL tracker + Culture + Original URL + Redirected To + Redirect Url Management + The following URLs redirect to this content item: + No redirects have been made + When a published page gets renamed or moved a redirect will automatically be made to the new page. + Are you sure you want to remove the redirect from '%0%' to '%1%'? + Redirect URL removed. + Error removing redirect URL. + This will remove the redirect + Are you sure you want to disable the URL tracker? + URL tracker has now been disabled. + Error disabling the URL tracker, more information can be found in your log file. + URL tracker has now been enabled. + Error enabling the URL tracker, more information can be found in your log file. + + + No Dictionary items to choose from + + + %0% characters left.]]> + %1% too many.]]> + + + Trashed content with Id: {0} related to original parent content with Id: {1} + Trashed media with Id: {0} related to original parent media item with Id: {1} + Cannot automatically restore this item + There is no location where this item can be automatically restored. You can move the item manually using the tree below. + was restored under + + + Direction + Parent to child + Bidirectional + Parent + Child + Count + Relations + Created + Comment + Name + No relations for this relation type. + Relation Type + Relations + + + Getting Started + Redirect URL Management + Content + Welcome + Examine Management + Published Status + Models Builder + Health Check + Profiling + Getting Started + Install Umbraco Forms + + + Go back + Active layout: + Jump to + group + passed + warning + failed + suggestion + Check passed + Check failed + Open backoffice search + Open/Close backoffice help + Open/Close your profile options + Open context menu for + Current language + Switch language to + Create new folder + Partial View + Partial View Macro + Member + Data type + Search the redirect dashboard + Search the user group section + Search the users section + Create item + Create + Edit + Name + Add new row + View more options + + + References + This Data Type has no references. + Used in Document Types + No references to Document Types. + Used in Media Types + No references to Media Types. + Used in Member Types + No references to Member Types. + Used by + Used in Documents + Used in Members + Used in Media + + + Log Levels + Saved Searches + Total Items + Timestamp + Level + Machine + Message + Exception + Properties + Search With Google + Search this message with Google + Search With Bing + Search this message with Bing + Search Our Umbraco + Search this message on Our Umbraco forums and docs + Search Our Umbraco with Google + Search Our Umbraco forums using Google + Search Umbraco Source + Search within Umbraco source code on Github + Search Umbraco Issues + Search Umbraco Issues on Github + Delete this search + Find Logs with Request ID + Find Logs with Namespace + Find Logs with Machine Name + Open + + + Copy %0% + %0% from %1% + Remove all items + + + Open Property Actions + + + Wait + Refresh status + Memory Cache + + + + Reload + Database Cache + + Rebuilding can be expensive. + Use it when reloading is not enough, and you think that the database cache has not been + properly generated—which would indicate some critical Umbraco issue. + ]]> + + Rebuild + Internals + + not need to use it. + ]]> + + Collect + Published Cache Status + Caches + + + Performance profiling + + + Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. +

          +

          + If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. +

          +

          + If you want the profiler to be activated by default for all page renderings, you can use the toggle below. + It will set a cookie in your browser, which then activates the profiler automatically. + In other words, the profiler will only be active by default in your browser - not everyone else's. +

          + ]]> +
          + Activate the profiler by default + Friendly reminder + + + You should never let a production site run in debug mode. Debug mode is turned off by setting debug="false" on the <compilation /> element in web.config. +

          + ]]> +
          + + + Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site. +

          +

          + Debug mode is turned on by setting debug="true" on the <compilation /> element in web.config. +

          + ]]> +
          + + + Hours of Umbraco training videos are only a click away + + Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

          + ]]> +
          + To get you started + + + Start here + This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section + Find out more + + in the Documentation section of Our Umbraco + ]]> + + + Community Forum + ]]> + + + tutorial videos (some are free, some require a subscription) + ]]> + + + productivity boosting tools and commercial support + ]]> + + + training and certification opportunities + ]]> + + + + Welcome to The Friendly CMS + Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. + + + Umbraco Forms + Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml index 66cad5fd26..c35c84ebdc 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml @@ -124,7 +124,7 @@ Guardar y publicar Guardar y enviar para aprobación Guardar vista de lista - Previsualizar + Previsualizar La previsualización está deshabilitada porque no hay ninguna plantilla asignada Elegir estilo Mostrar estilos @@ -311,7 +311,6 @@ Enlace interno Al usar enlaces locales, insertar "#" delante del enlace ¿Abrir en nueva ventana? - Ajustes para la Macro Esta macro no contiene ninguna propiedad que pueda editar Pegar Editar permisos para @@ -366,7 +365,7 @@ Selecciona snippet - Editar las diferentes versiones lingüísticas para la entrada en el diccionario '% 0%' debajo añadir otros idiomas en el menú de 'idiomas' en el menú de la izquierda + Editar las diferentes versiones lingüísticas para la entrada en el diccionario '% 0%' debajo Sauver et planifier Sauver et envoyer pour approbation Sauver la mise en page de la liste - Prévisualiser + Prévisualiser La prévisualisation est désactivée car aucun modèle n'a été assigné. Choisir un style Afficher les styles @@ -324,7 +324,6 @@ Lien interne : Si vous utilisez des ancres, insérez # au début du lien Ouvrir dans une nouvelle fenêtre? - Paramètres de macro Cette macro ne contient aucune propriété éditable Coller Editer les permissions pour @@ -380,7 +379,7 @@ %0%' ci-dessous.
          Vous pouvez ajouter d'autres langues depuis le menu ci-dessous "Langues". + Editez les différentes versions de langues pour l'élément de dictionaire '%0%' ci-dessous. ]]>
          Nom de Culture Type de propriété créé Type de données : %1%]]> Type de propriété supprimé - Type de documet sauvegardé + Type de document sauvegardé Onglet créé Onglet supprimé Onglet avec l'ID : %0% supprimé @@ -1813,7 +1812,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Une configuration a été ajoutée dans votre fichier web.config pour créer un header protégeant contre les vulnérabilités de MIME sniffing. Strict-Transport-Security, aussi connu sous le nom de HSTS-header, a été trouvé.]]> Strict-Transport-Security, aussi connu sous le nom de HSTS-header, n'a pas été trouvé.]]> - Ajoute l'en-tête 'Strict-Transport-Security' avec la valeur 'max-age=10886400; preload' à la section httpProtocol/customHeaders du fichier web.config. Utilisez cette correction uniquement si vos domaines vont fonctionner en https pour les 18 prochaines semaines (minimum). + Ajoute l'en-tête 'Strict-Transport-Security' avec la valeur 'max-age=10886400' à la section httpProtocol/customHeaders du fichier web.config. Utilisez cette correction uniquement si vos domaines vont fonctionner en https pour les 18 prochaines semaines (minimum). L'en-tête HSTS a été ajouté dans votre fichier web.config. X-XSS-Protection a été trouvé.]]> X-XSS-Protection n'a pas été trouvé.]]> diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml index ece1810ef5..e100cb4301 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml @@ -68,7 +68,7 @@ שמור שמור ופרסם שמור ושלח לאישור - תצוגה מקדימה + תצוגה מקדימה בחר עיצוב הצג עיצוב הוספת טבלה @@ -175,7 +175,6 @@ קישור פנימי: בעת שימוש בקישוריים פנימיים, הוסף "#" בתחילת הקישור לפתוח בחלון חדש? - הגדרות מאקרו המאקרו לא מכיל מאפיינים שניתן לערוך הדבק ערוך הרשאות עבור diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml index 3ae078252a..4866fff843 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml @@ -69,7 +69,7 @@ Salva Salva e pubblica Salva e invia per approvazione - Anteprima + Anteprima Scegli lo stile Mostra gli stili @@ -180,7 +180,6 @@ - Impostazioni Macro Incolla Modifica il Permesso per @@ -324,8 +323,7 @@ Mostra la pagina inviata Dimensione Ordina - Submit - + Invia Tipo Su @@ -340,8 +338,8 @@ Benvenuto... Larghezza Si - Reorder - I am done reordering + Riordina + Ho finito di ordinare Colore di sfondo @@ -619,8 +617,8 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i Tabs - Sort order - Creation date + Ordinamento + Data creazione @@ -854,6 +852,6 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i - Writer + Autore diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml index 303d0f0427..21559f915a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml @@ -85,7 +85,7 @@ 保存及び公開 保存して承認に送る リスト ビューの保存 - プレビュー + プレビュー テンプレートが指定されていないのでプレビューは無効になっています スタイルの選択 スタイルの表示 @@ -240,7 +240,6 @@ 内部リンク: 内部リンクを使うときは、リンクの前に "#" を挿入してください。 新規ウィンドウで開きますか? - マクロの設定 このマクロは編集できるプロパティがありません 貼り付け 許可の編集 diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml index 512d192237..a87f6f1410 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml @@ -67,7 +67,7 @@ 저장 저장 후 발행 저장 후 승인을 위해 전송 - 미리보기 + 미리보기 스타일 선택 스타일 보기 테이블 삽입 @@ -174,7 +174,6 @@ 내부링크: 내부링크를 사용하실 때 링크앞에 "#"를 넣어주세요 새 창으로 여시겠습니까? - 매크로 세팅 이 매크로에는 편집할 수 있는 항목이 포함되어 있지 않습니다. 붙여넣기 권한 편집 diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml index cd6878a196..731aea4a20 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml @@ -2,7 +2,7 @@ The Umbraco community - http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files Angi domene @@ -83,7 +83,7 @@ Lagre og publiser Lagre og planlegge Lagre og send til publisering - Forhåndsvis + Forhåndsvis Forhåndsvisning er deaktivert siden det ikke er angitt noen mal Velg formattering Vis stiler @@ -234,7 +234,6 @@ Intern link: Ved lokal link, sett inn "#" foran link Åpne i nytt vindu? - Makroinnstillinger Denne makroen har ingen egenskaper du kan endre Lim inn Endre rettigheter for @@ -437,7 +436,7 @@ Databasekonfigurasjon installer-knappen for å installere Umbraco %0% databasen]]> Neste for å fortsette.]]> - Databasen ble ikke funnet! Vennligst sjekk at informasjonen i "connection string" i "web.config"-filen er korrekt.

          For å fortsette, vennligst rediger "web.config"-filen (bruk Visual Studio eller din favoritteditor), rull ned til bunnen, og legg til tilkoblingsstrengen for din database i nøkkelen "umbracoDbDSN" og lagre filen.

          Klikk prøv på nytt når du er ferdig.
          Mer informasjon om redigering av web.config her.

          ]]>
          + Databasen ble ikke funnet! Vennligst sjekk at informasjonen i "connection string" i "web.config"-filen er korrekt.

          For å fortsette, vennligst rediger "web.config"-filen (bruk Visual Studio eller din favoritteditor), rull ned til bunnen, og legg til tilkoblingsstrengen for din database i nøkkelen "umbracoDbDSN" og lagre filen.

          Klikk prøv på nytt når du er ferdig.
          Mer informasjon om redigering av web.config her.

          ]]>
          Vennligst kontakt din ISP om nødvendig. Hvis du installerer på en lokal maskin eller server, må du kanskje skaffe informasjonen fra din systemadministrator.]]> Trykk på knappen oppgrader for å oppgradere databasen din til Umbraco %0%

          Ikke vær urolig - intet innhold vil bli slettet og alt vil fortsette å virke etterpå!

          ]]>
          Trykk Neste for å fortsette.]]> diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml index 556a8c4d48..601626d896 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml @@ -90,7 +90,7 @@ Opslaan en publiceren Opslaan en verzenden voor goedkeuring Sla list view op - voorbeeld bekijken + voorbeeld bekijken Voorbeeld bekijken is uitgeschakeld omdat er geen template is geselecteerd Stijl kiezen Stijlen tonen @@ -263,7 +263,6 @@ Interne link: Plaats een hekje (“#”) voor voor interne links. In nieuw venster openen? - Macro Settings Deze macro heeft geen eigenschappen die u kunt bewerken Plakken Bewerk rechten voor diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml index 055d9eddcf..fd806041c5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml @@ -121,7 +121,7 @@ Zapisz i publikuj Zapisz i wyślij do zaakceptowania Zapisz widok listy - Podgląd + Podgląd Podgląd jest wyłączony, ponieważ żaden szablon nie został przydzielony Wybierz styl Pokaż style @@ -312,7 +312,6 @@ Link wewnętrzny: Kiedy używasz odnośników lokalnych, wstaw znak "#" na początku linku Otworzyć w nowym oknie? - Ustawienia Makro To makro nie posiada żadnych właściwości, które można edytować Wklej Edytuj Uprawnienia dla diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml index 767617a2d5..9fd4696c28 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml @@ -67,7 +67,7 @@ Salvar Salvar e publicar Salvar e mandar para aprovação - Prévia + Prévia Escolha estilo Mostrar estilos Inserir tabela @@ -174,7 +174,6 @@ Link interno: Ao usar links locais insira "#" na frente do link Abrir em nova janela? - Configurações de Macro Este macro não contém nenhuma propriedade que possa ser editada Colar Editar Permissões para diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml index 7698453b4a..7a3e099262 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml @@ -155,7 +155,7 @@ Направить на публикацию Сохранить список Выбрать - Предварительный просмотр + Предварительный просмотр Предварительный просмотр запрещен, так как документу не сопоставлен шаблон Другие действия Выбрать стиль @@ -360,7 +360,6 @@ Внутренняя ссылка: Для того чтобы определить локальную ссылку, используйте "#" первым символом Открыть в новом окне? - Свойства макроса Этот макрос не имеет редактируемых свойств Заголовок ссылки Ни одной пиктограммы не найдено @@ -743,7 +742,7 @@ Значение, добавляющее заголовок, препятствующий использованию MIME-уязвимостей, успешно добавлено в файл web.config. Strict-Transport-Security, известный также как HSTS-header, обнаружен.]]> Strict-Transport-Security не найден.]]> - Добавляет заголовок 'Strict-Transport-Security' и его значение 'max-age=10886400; preload' в секцию httpProtocol/customHeaders файла web.config. Применяйте этот способ только в случае, если доступ к Вашим сайтам будет осуществляться по протоколу https как минимум ближайшие 18 недель. + Добавляет заголовок 'Strict-Transport-Security' и его значение 'max-age=10886400' в секцию httpProtocol/customHeaders файла web.config. Применяйте этот способ только в случае, если доступ к Вашим сайтам будет осуществляться по протоколу https как минимум ближайшие 18 недель. Заголовок HSTS-header успешно добавлен в файл web.config. X-XSS-Protection обнаружен.]]> X-XSS-Protection не найден.]]> diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml index bd847a605a..e7e7abe2cd 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml @@ -4,6 +4,9 @@ The Umbraco community https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files + + Innehåll + Hantera domännamn Hantera versioner @@ -11,7 +14,9 @@ Ändra dokumenttyp Kopiera Skapa + Skapa grupp Skapa paket + Skapa innehållsmall Standardvärde Ta bort Avaktivera @@ -37,6 +42,12 @@ Avpublicera Uppdatera + + Innehåll + Administration + Struktur + Övrigt + Lägg till nytt domännamn Domännamn @@ -59,8 +70,45 @@ Visar för + Innehållet raderat + Innehållet avpublicerat + Innehållet avpublicerat för språk: %0% + Spara och publicera utförd av användaren + Innehåll publicerat för språk: %0% + Innehåll sparat + Innehåll sparat för språk: %0% + Innehåll flyttat + Innehåll kopierat + Innehållet rullades tillbaka + Innehåll skickat för publicering + Innehåll skickat för publicering för språk: %0% + Sortering av underliggande objekt utfört av användaren + Kopiera + Publicera + Publicera + Flytta + Spara + Spara + Ta bort + Avpublicera + Avpublicera + Rulla tillbaka + Skicka till publicering + Skicka till publicering + Sortera + Historik (alla varianter) + + + Skapa en ny innehållsmall för '%0%' + Tom + Välj en innehållsmall + Innehållsmall skapad + En innehållsmall skapades från '%0%' + En annan innehållsmall med samma namn finns redan + En innehållsmall är fördefinierat innehåll som en redaktör kan välja att använda som grund för att skapa nytt innehåll + Rensa urval Fetstil Minska indrag Infoga formulärfält @@ -77,14 +125,16 @@ Numrerad lista Infoga macro Infoga bild + Publicera och stäng Ändra relation Återvänd till lista Spara Spara och publicera Spara och schemalägg Spara och skicka för godkännande + Schemaläggning Välj - Förhandsgranska + Förhandsgranska Förhandsgranskning är avstängt på grund av att det inte finns någon mall tilldelad Ångra Välj stil @@ -142,14 +192,18 @@ Medlemstyp Inget datum valt Sidnamn + Inget innehåll kan läggas till för det här objektet Ej medlem av grupp(er) Egenskaper Detta dokument är publicerat men syns inte eftersom den överordnade sidan %0% inte är publicerad Oops: detta dokument är publicerat men finns inte i cacheminnet (internt fel) Publicera + Publicerad Publiceringsstatus Publiceringsdatum Rensa datum + Välj datum och tid för att publicera och / eller avpublicera innehållsobjektet. + Välj datum Sorteringsordningen har uppdaterats För att sortera noderna, dra i dem eller klicka på någon av kolumnrubrikerna. Du kan markera flera noder samtidigt genom att hålla nere SHIFT eller CONTROL medan du klickar Statistik @@ -180,6 +234,14 @@ Välkommen Besök + + Komma igång + URL-omdirigeringshantering + Innehåll + Välkommen + Komma igång + Installera Umbraco Forms + Stay Discard changes @@ -187,27 +249,27 @@ Are you sure you want to navigate away from this page? - you have unsaved changes - Done - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items + Klar + Tog bort %0% objekt + Tog bort %0% objekt + Tog bort %0% av %1% objekt + Tog bort %0% av %1% objekt + Publicerade %0% objekt + Publicerade %0% objekt + Publicerade %0% av %1% objekt + Publicerade %0% av %1% objekt + Avpublicerade %0% objekt + Avpublicerade %0% objekt + Avpublicerade %0% av %1% objekt + Avpublicerade %0% av %1% objekt + Flyttade %0% objekt + Flyttade %0% objekt + Flyttade %0% av %1% objekt + Flyttade %0% av %1% objekt + Kopierade %0% objekt + Kopierade %0% objekt + Kopierade %0% av %1% objekt + Kopierade %0% av %1% objekt Namn @@ -232,7 +294,6 @@ Intern länk: När du använder lokala länkar, lägg till "#" framför länken Öppna i nytt fönster? - Makroinställningar Detta makro innehåller inga egenskaper som du kan redigera Klistra in Redigera rättigheter för @@ -246,6 +307,12 @@ Webbplatsen har indexerats Cache för webbplatsen har uppdaterats. Allt publicerat innehåll är nu uppdaterat. Innehåll som inte har publicerats är fortfarande opublicerat. Webbplatsens cache kommer att uppdateras. Allt innehåll som är publicerat kommer att uppdateras. Innehåll som inte är publicerat kommer att förbli opublicerat. + Välj startnod för innehåll + Välj ikon + Välj startnod för media + Välj användargrupper + Välj sektioner + Välj användare Antal kolumner Antal rader Klicka på förhandsgranskningsbilden för att se bilden i full storlek @@ -312,11 +379,13 @@ Avbryt Cellmarginal Välj + Rensa Stäng Stäng fönstret Kommentar Bekräfta Begränsa proportioner + Innehåll Fortsätt Kopiera Skapa @@ -337,8 +406,11 @@ Fel Hitta Mapp + Generella + Grupper Höjd Hjälp + Historik Ikon Importera Innermarginal @@ -347,6 +419,7 @@ Justera Språk Layout + Länkar Laddar Låst Logga in @@ -362,6 +435,7 @@ OK Öppna eller + Sortering Lösenord Sökväg Ett ögonblick... @@ -369,13 +443,17 @@ Egenskaper E-postadress för formulärsdata Papperskorg + Din papperskorg är tom Återstående + Ta bort Döp om Förnya Obligatorisk Försök igen Rättigheter + Schemalagd publicering Sök + Tyvärr kan vi inte hitta det du söker. Sökresultat Server Visa @@ -508,6 +586,8 @@ Klicka för att ladda upp + eller klicka här för att välja filer + Drag och släpp dina filer i denna yta Välj sida ovan... @@ -541,6 +621,7 @@ Paketalternativ Paket läsmig Paketvalv + Sök efter paket Bekräfta avinstallation Paketet har avinstallerats Paketet har avinstallerats utan problem @@ -561,6 +642,7 @@ Fyll i ditt lösenord Skriv för att söka... Fyll i ditt lösenord + Välj alias... Skriv för att lägga till taggar (och tryck enter efter varje tagg)... @@ -631,6 +713,7 @@ Media Medlemmar Nyhetsbrev + Paket Inställningar Statistik Översättning @@ -714,6 +797,7 @@ Stilar + Skapad Redigera sidmall Lägg in innehållsyta Lägg in platshållare för innehållsyta @@ -725,7 +809,7 @@ Sidmall - Image + Bild Macro Lägg till Choose layout @@ -805,6 +889,7 @@ Cacha webbläsare + Innehåll Papperskorg Skapade paket Datatyper @@ -841,23 +926,52 @@ Fel vid kontroll av uppdatering. Se trace-stack för mer information. + Åtkomst + Baserat på tilldelade grupper och startnod så har användaren åtkomst till följande noder + Tilldela åtkomst Administratör Kategorifält + Användare skapad Ändra lösenord - Du kan byta ditt lösenord för Umbraco Back Office genom att fylla i nedanstående formulär och klicka på knappen "Ändra lösenord". + Ändra bild Bekräfta det nya lösenordet + Du kan byta ditt lösenord för Umbraco Back Office genom att fylla i nedanstående formulär och klicka på knappen "Ändra lösenord". Innehållskanal + Skapa en till användare + Skapa nya användare för att ge dom åtkomst till Umbraco. När en ny användare skapas kommer ett lösenord genereras som du kan dela med användaren. + Skapa användare + Ta bort användare User + Är du säker på att du vill ta bort användarens konto? Fält för beskrivning Avaktivera användare Dokumenttyp Redaktör Fält för utdrag + Misslyckade inloggningsförsök + Gå till användarens profil + Lägg till grupper för att tilldela åtkomst och rättigheter + Bjud in + Bjud in en till användare + Bjud in nya användare för att ge dom åtkomst till Umbraco. Ett e-postmeddelande kommer skikcas till användaren med information om hur man loggar in i Umbraco. Inbjudningar är giltiga i 72 timmar. Språk + Välj de språk som kommer visas i meny och dialoger + Senast utlåst + Senast inloggad + Lösenordet ändrades Login Startnod i mediabiblioteket + Begränsa media sectionen till en specifik startnod + Media startnoder + Begränsa media sectionen till specifika startnoder Sektioner Byt ditt lösenord + har inte blivit utlåst + har inte loggat in ännu Inaktivera tillgång till Umbraco + Lösenordet har inte ändrats + Ingen startnod vald + Inga startnoder valda + Gammalt lösenord Lösenord Ditt lösenord är nu ändrat! Vänligen bekräfta ditt nya lösenord @@ -871,15 +985,36 @@ Du redigerar nu rättigheterna för sidorna: Välj de sidor vars rättigheter du vill redigera Återställ lösenord + Ta bort bild + Standard rättigheter + Granulära rättigheter Sök igenom alla undernoder + Sätt rättigheter för specifika noder + Profil Sessionen går ut - Startnod i innehåll + Välj sektioner för användaråtkomst + Alla + Aktiv + Utlåst + Inbjuden + Inaktiv + Namn (A-Z) + Namn (Z-A) + Startnod för innehåll + Begränsa sidträdet till en specifik startnod + Startnoder för innehåll + Begränsa sidträdet till specifika startnoder + Användare ändrad Användarens namn + Användarhantering Användarrättigheter Användartyp Användartyper Skribent Din nuvarande historik Din profil + Nyast + Äldst + Senaste login
          diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml index 347f205a1f..f998080b06 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml @@ -85,7 +85,7 @@ Kaydet Kaydet ve Yayınla Kaydet ve Onay için gönder - Önizle + Önizle Önizleme kapalı, Atanmış şablon yok Stili seçin Stilleri Göster @@ -203,7 +203,6 @@ İç Bağlantı: Yerel bağlantıları kullanırken, bağlantının önündeki "# " insert Yeni pencerede aç - Makro ayarları Düzenleyebileceğiniz Bu makro herhangi bir özellikleri içermiyor Yapıştır İzinleri düzenle diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml index 40a81413e5..5210b46bcc 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml @@ -90,7 +90,7 @@ 保存并发布 保存并提交审核 保存列表视图 - 预览 + 预览 因未设置模板无法预览 选择样式 显示样式 @@ -251,7 +251,6 @@ 内部链接: 本地链接请用“#”号开头 在新窗口中打开? - 宏设置 粘贴 编辑权限 diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml index a212826bbc..320c3f63d8 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml @@ -2,7 +2,7 @@ The Umbraco community - http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files 管理主機名稱 @@ -87,7 +87,7 @@ 保存並發佈 保存並提交審核 保存清單檢視 - 預覽 + 預覽 因未設置範本無法預覽 選擇樣式 顯示樣式 @@ -166,7 +166,7 @@ 子項目 目標 預計發表的時間(伺服器端) - 這是什麼意思?]]> + 這是什麼意思?]]> 點選以便上傳 @@ -248,7 +248,6 @@ 內部連結: 本地連結請用“#”號開頭 在新視窗中打開? - 巨集設置 本巨集沒有包含您可以編輯的屬性 粘貼 編輯許可權 @@ -508,7 +507,7 @@

          請編輯檔案"web.config" (例如使用Visual Studio或您喜歡的編輯器),移動到檔案底部,並在名稱為"UmbracoDbDSN"的字串中設定資料庫連結資訊,並存檔。

          點選重試按鈕當上述步驟完成。
          - + 在此查詢更多編輯web.config的資訊。

          ]]> 若需要時,請聯繫您的網路公司。如果您在本地機器或伺服器安裝的話,您也許需要聯絡系統管理者。]]> diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml index defe59d808..131b0515ae 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml @@ -2,31 +2,31 @@ @using Umbraco.Web.Templates @using Newtonsoft.Json.Linq -@* +@* Razor helpers located at the bottom of this file *@ @if (Model != null && Model.sections != null) { var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; - +
          @if (oneColumn) { foreach (var section in Model.sections) {
          @foreach (var row in section.rows) { - @renderRow(row); + @renderRow(row) }
          - } - }else { + } + }else {
          @foreach (var s in Model.sections) {
          @foreach (var row in s.rows) { - @renderRow(row); + @renderRow(row) }
          @@ -85,4 +85,4 @@ return new MvcHtmlString(string.Join(" ", attrs)); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml index 9333628ed6..23fee33043 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml @@ -12,7 +12,7 @@ foreach (var section in Model.sections) {
          @foreach (var row in section.rows) { - @renderRow(row, true); + @renderRow(row, true) }
          } @@ -23,7 +23,7 @@
          @foreach (var row in s.rows) { - @renderRow(row, false); + @renderRow(row, false) }
          diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml index 393157bcf8..4a915a444b 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml @@ -1,7 +1,10 @@ @model dynamic @using Umbraco.Web.Templates - +@{ + string embedValue = Convert.ToString(Model.value); + embedValue = embedValue.DetectIsJson() ? Model.value.preview : Model.value; +}
          - @Html.Raw(Model.value) + @Html.Raw(embedValue)
          diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml index ea79ce41ad..e20b717ed5 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml @@ -2,20 +2,24 @@ @using Umbraco.Web.Templates @if (Model.value != null) -{ +{ var url = Model.value.image; if(Model.editor.config != null && Model.editor.config.size != null){ - url += "?width=" + Model.editor.config.size.width; - url += "&height=" + Model.editor.config.size.height; - - if(Model.value.focalPoint != null){ - url += "¢er=" + Model.value.focalPoint.top +"," + Model.value.focalPoint.left; - url += "&mode=crop"; - } + url = ImageCropperTemplateExtensions.GetCropUrl(url, + width: Model.editor.config.size.width, + height: Model.editor.config.size.height, + cropDataSet: Model.value.focalPoint == null ? null : new Umbraco.Core.PropertyEditors.ValueConverters.ImageCropperValue + { + FocalPoint = new Umbraco.Core.PropertyEditors.ValueConverters.ImageCropperValue.ImageCropperFocalPoint + { + Top = Model.value.focalPoint.top, + Left = Model.value.focalPoint.left + } + }); } var altText = Model.value.altText ?? Model.value.caption ?? string.Empty; - + @altText if (Model.value.caption != null) diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Rte.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Rte.cshtml index b7e293eed8..34bb744596 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Rte.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Rte.cshtml @@ -1,5 +1,9 @@ @model dynamic @using Umbraco.Web.Composing @using Umbraco.Web.Templates - -@Html.Raw(TemplateUtilities.ParseInternalLinks(Model.value.ToString(), Current.UmbracoContext.UrlProvider)) +@{ + var value = TemplateUtilities.ParseInternalLinks(Model.value.ToString(), Current.UmbracoContext.UrlProvider); + value = TemplateUtilities.ResolveUrlsFromTextString(value); + value = TemplateUtilities.ResolveMediaFromTextString(value); +} +@Html.Raw(value) diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index e300e6562e..7b3f2a2184 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -1,4 +1,22 @@ [ + { + "name": "Email Marketing", + "alias": "umbEmailMarketing", + "group": "Email Marketing", + "groupOrder": 10, + "hidden": true, + "requiredSections": [ + "content" + ], + "steps": [ + { + "title": "Do you want to stay updated on everything Umbraco?", + "content": "

          Thank you for using Umbraco! Would you like to stay up-to-date with Umbraco product updates, security advisories, community news and special offers? Sign up for our newsletter and never miss out on the latest Umbraco news.

          By signing up, you agree that we can use your info according to our privacy policy.

          ", + "view": "emails", + "type": "promotion" + } + ] + }, { "name": "Introduction", "alias": "umbIntroIntroduction", diff --git a/src/Umbraco.Web.UI/config/grid.editors.config.js b/src/Umbraco.Web.UI/config/grid.editors.config.js index 12fa726f21..210d167f9e 100644 --- a/src/Umbraco.Web.UI/config/grid.editors.config.js +++ b/src/Umbraco.Web.UI/config/grid.editors.config.js @@ -7,12 +7,14 @@ }, { "name": "Image", + "nameTemplate": "{{ value && value.udi ? (value.udi | ncNodeName) : '' }}", "alias": "media", "view": "media", "icon": "icon-picture" }, { "name": "Macro", + "nameTemplate": "{{ value && value.macroAlias ? value.macroAlias : '' }}", "alias": "macro", "view": "macro", "icon": "icon-settings-alt" @@ -25,6 +27,7 @@ }, { "name": "Headline", + "nameTemplate": "{{ value }}", "alias": "headline", "view": "textstring", "icon": "icon-coin", @@ -35,6 +38,7 @@ }, { "name": "Quote", + "nameTemplate": "{{ value ? value.substring(0,32) + (value.length > 32 ? '...' : '') : '' }}", "alias": "quote", "view": "textstring", "icon": "icon-quote", @@ -43,4 +47,4 @@ "markup": "
          #value#
          " } } -] \ No newline at end of file +] diff --git a/src/Umbraco.Web.UI/config/logviewer.searches.config.js b/src/Umbraco.Web.UI/config/logviewer.searches.config.js index 25ee9b2242..345fe23764 100644 --- a/src/Umbraco.Web.UI/config/logviewer.searches.config.js +++ b/src/Umbraco.Web.UI/config/logviewer.searches.config.js @@ -4,7 +4,7 @@ "query": "Not(@Level='Verbose') and Not(@Level='Debug')" }, { - "name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)", + "name": "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", "query": "Has(@Exception)" }, { diff --git a/src/Umbraco.Web.UI/config/serilog.user.Release.config b/src/Umbraco.Web.UI/config/serilog.user.Release.config index 24e5e4e4be..8f207406e3 100644 --- a/src/Umbraco.Web.UI/config/serilog.user.Release.config +++ b/src/Umbraco.Web.UI/config/serilog.user.Release.config @@ -26,12 +26,13 @@ --> + diff --git a/src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.cs b/src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.cs index 38f6793fbe..51653d54ca 100644 --- a/src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.cs +++ b/src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.UI.Config.Splashes { base.OnInit(e); - var store = Current.UmbracoContext.ContentCache; + var store = Current.UmbracoContext.Content; if (store.HasContent()) { //if there is actually content, go to the root diff --git a/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config b/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config index 40630b7eb6..f6a26ee89a 100644 --- a/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config +++ b/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@ - + diff --git a/src/Umbraco.Web.UI/config/tinyMceConfig.config b/src/Umbraco.Web.UI/config/tinyMceConfig.config index d01c0ca7aa..7f7cb657e6 100644 --- a/src/Umbraco.Web.UI/config/tinyMceConfig.config +++ b/src/Umbraco.Web.UI/config/tinyMceConfig.config @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@ - + @@ -33,7 +33,8 @@ - + + paste @@ -47,6 +48,7 @@ directionality tabfocus searchreplace + fullscreen - - - - - - - - - - - - 1 - - - - - - - - your@email.here - - - - - In Preview Mode - click to end - ]]> - - - throw - - - ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,php,htaccess - - - assets/img/login.jpg - - - - - - false - - true - - false - - - - - - - - - - - - + + + + + + + + + + + + + 1 + + + + + + + + your@email.here + + + + + + Preview mode + + … + + + Click to end + +
          + + ]]> + + + + throw + + + ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,php,htaccess + + + assets/img/login.jpg + + + + + + false + + true + + false + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index f647e1ae1e..c3df6cfb41 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -38,9 +38,140 @@ - In Preview Mode - click to end - ]]> + + Preview mode + + … + + + Click to end + +
          + + ]]> + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index ff42f098f7..0026f23514 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -21,6 +21,11 @@ + + + + + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index f0abbfde52..b85c892e70 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -4,7 +4,7 @@ diff --git a/src/Umbraco.Web/Actions/ActionCollection.cs b/src/Umbraco.Web/Actions/ActionCollection.cs index 89ac8a59f4..95d7b02e90 100644 --- a/src/Umbraco.Web/Actions/ActionCollection.cs +++ b/src/Umbraco.Web/Actions/ActionCollection.cs @@ -28,15 +28,5 @@ namespace Umbraco.Web.Actions .WhereNotNull() .ToList(); } - - internal IReadOnlyList FromEntityPermission(EntityPermission entityPermission) - { - var actions = this.ToArray(); // no worry: internally, it's already an array - return entityPermission.AssignedPermissions - .Where(x => x.Length == 1) - .SelectMany(x => actions.Where(y => y.Letter == x[0])) - .WhereNotNull() - .ToList(); - } } } diff --git a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs index e4d2c2e4da..6abad820c9 100644 --- a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; @@ -49,9 +50,12 @@ namespace Umbraco.Web.Cache var idsRemoved = new HashSet(); var isolatedCache = AppCaches.IsolatedCaches.GetOrCreate(); - foreach (var payload in payloads) + foreach (var payload in payloads.Where(x => x.Id != default)) { + //By INT Id isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + //By GUID Key + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); _idkMap.ClearCache(payload.Id); @@ -97,13 +101,7 @@ namespace Umbraco.Web.Cache //if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) // ... - _publishedSnapshotService.Notify(payloads, out _, out var publishedChanged); - - if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged) - { - // when a public version changes - AppCaches.ClearPartialViewCache(); - } + NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads); base.Refresh(payloads); } @@ -111,40 +109,54 @@ namespace Umbraco.Web.Cache // these events should never trigger // everything should be PAYLOAD/JSON - public override void RefreshAll() - { - throw new NotSupportedException(); - } + public override void RefreshAll() => throw new NotSupportedException(); - public override void Refresh(int id) - { - throw new NotSupportedException(); - } + public override void Refresh(int id) => throw new NotSupportedException(); - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } + public override void Refresh(Guid id) => throw new NotSupportedException(); - public override void Remove(int id) - { - throw new NotSupportedException(); - } + public override void Remove(int id) => throw new NotSupportedException(); #endregion #region Json + /// + /// Refreshes the publish snapshot service and if there are published changes ensures that partial view caches are refreshed too + /// + /// + /// + /// + internal static void NotifyPublishedSnapshotService(IPublishedSnapshotService service, AppCaches appCaches, JsonPayload[] payloads) + { + service.Notify(payloads, out _, out var publishedChanged); + + if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged) + { + // when a public version changes + appCaches.ClearPartialViewCache(); + } + } + public class JsonPayload { + [Obsolete("Use the constructor specifying a GUID instead, using this constructor will result in not refreshing all caches")] public JsonPayload(int id, TreeChangeTypes changeTypes) { Id = id; ChangeTypes = changeTypes; } - public int Id { get; } + [JsonConstructor] + public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) + { + Id = id; + Key = key; + ChangeTypes = changeTypes; + } + public int Id { get; } + public Guid? Key { get; } public TreeChangeTypes ChangeTypes { get; } } diff --git a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index 86ff709541..63cfe0df9e 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -5,7 +5,6 @@ using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Web.PublishedCache; @@ -85,17 +84,15 @@ namespace Umbraco.Web.Cache // don't try to be clever - refresh all MemberCacheRefresher.RefreshMemberTypes(AppCaches); - // we have to refresh models before we notify the published snapshot - // service of changes, else factories may try to rebuild models while - // we are using the database to load content into caches - - _publishedModelFactory.WithSafeLiveFactory(() => + // refresh the models and cache + _publishedModelFactory.WithSafeLiveFactoryReset(() => _publishedSnapshotService.Notify(payloads)); // now we can trigger the event base.Refresh(payloads); } + public override void RefreshAll() { throw new NotSupportedException(); diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index 1d41c0578b..c9caf5aaa7 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -62,11 +62,9 @@ namespace Umbraco.Web.Cache TagsValueConverter.ClearCaches(); SliderValueConverter.ClearCaches(); - // we have to refresh models before we notify the published snapshot - // service of changes, else factories may try to rebuild models while - // we are using the database to load content into caches + // refresh the models and cache - _publishedModelFactory.WithSafeLiveFactory(() => + _publishedModelFactory.WithSafeLiveFactoryReset(() => _publishedSnapshotService.Notify(payloads)); base.Refresh(payloads); diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 80c49e279c..b00a4818f6 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -106,7 +106,7 @@ namespace Umbraco.Web.Cache public static void RefreshAllContentCache(this DistributedCache dc) { - var payloads = new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }; + var payloads = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; // note: refresh all content cache does refresh content types too dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); @@ -117,7 +117,7 @@ namespace Umbraco.Web.Cache if (changes.Length == 0) return; var payloads = changes - .Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.ChangeTypes)); + .Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); } @@ -157,7 +157,7 @@ namespace Umbraco.Web.Cache public static void RefreshAllMediaCache(this DistributedCache dc) { - var payloads = new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }; + var payloads = new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; // note: refresh all media cache does refresh content types too dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); @@ -168,7 +168,7 @@ namespace Umbraco.Web.Cache if (changes.Length == 0) return; var payloads = changes - .Select(x => new MediaCacheRefresher.JsonPayload(x.Item.Id, x.ChangeTypes)); + .Select(x => new MediaCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); } @@ -266,13 +266,21 @@ namespace Umbraco.Web.Cache public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) { if (language == null) return; - dc.Refresh(LanguageCacheRefresher.UniqueId, language.Id); + + var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, + language.WasPropertyDirty(nameof(ILanguage.IsoCode)) + ? LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture + : LanguageCacheRefresher.JsonPayload.LanguageChangeType.Update); + + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); } public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) { if (language == null) return; - dc.Remove(LanguageCacheRefresher.UniqueId, language.Id); + + var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); } #endregion diff --git a/src/Umbraco.Web/Cache/DomainCacheRefresher.cs b/src/Umbraco.Web/Cache/DomainCacheRefresher.cs index 37b0a483a6..4ffe4e2717 100644 --- a/src/Umbraco.Web/Cache/DomainCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DomainCacheRefresher.cs @@ -47,25 +47,13 @@ namespace Umbraco.Web.Cache // these events should never trigger // everything should be PAYLOAD/JSON - public override void RefreshAll() - { - throw new NotSupportedException(); - } + public override void RefreshAll() => throw new NotSupportedException(); - public override void Refresh(int id) - { - throw new NotSupportedException(); - } + public override void Refresh(int id) => throw new NotSupportedException(); - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } + public override void Refresh(Guid id) => throw new NotSupportedException(); - public override void Remove(int id) - { - throw new NotSupportedException(); - } + public override void Remove(int id) => throw new NotSupportedException(); #endregion diff --git a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs index 9093124609..7c4dab0e17 100644 --- a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs @@ -5,16 +5,18 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Web.PublishedCache; +using static Umbraco.Web.Cache.LanguageCacheRefresher.JsonPayload; namespace Umbraco.Web.Cache { - public sealed class LanguageCacheRefresher : CacheRefresherBase + public sealed class LanguageCacheRefresher : PayloadCacheRefresherBase + + //CacheRefresherBase { - public LanguageCacheRefresher(AppCaches appCaches, IPublishedSnapshotService publishedSnapshotService, IDomainService domainService) + public LanguageCacheRefresher(AppCaches appCaches, IPublishedSnapshotService publishedSnapshotService) : base(appCaches) { _publishedSnapshotService = publishedSnapshotService; - _domainService = domainService; } #region Define @@ -23,7 +25,6 @@ namespace Umbraco.Web.Cache public static readonly Guid UniqueId = Guid.Parse("3E0F95D8-0BE5-44B8-8394-2B8750B62654"); private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IDomainService _domainService; public override Guid RefresherUniqueId => UniqueId; @@ -33,41 +34,118 @@ namespace Umbraco.Web.Cache #region Refresher - public override void Refresh(int id) + public override void Refresh(JsonPayload[] payloads) { + if (payloads.Length == 0) return; + + var clearDictionary = false; + var clearContent = false; + + //clear all no matter what type of payload ClearAllIsolatedCacheByEntityType(); - RefreshDomains(id); - base.Refresh(id); + + foreach (var payload in payloads) + { + switch (payload.ChangeType) + { + case LanguageChangeType.Update: + clearDictionary = true; + break; + case LanguageChangeType.Remove: + case LanguageChangeType.ChangeCulture: + clearDictionary = true; + clearContent = true; + break; + } + } + + if (clearDictionary) + { + ClearAllIsolatedCacheByEntityType(); + } + + //if this flag is set, we will tell the published snapshot service to refresh ALL content and evict ALL IContent items + if (clearContent) + { + //clear all domain caches + RefreshDomains(); + ContentCacheRefresher.RefreshContentTypes(AppCaches); // we need to evict all IContent items + //now refresh all nucache + var clearContentPayload = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; + ContentCacheRefresher.NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, clearContentPayload); + } + + // then trigger event + base.Refresh(payloads); } - public override void Remove(int id) - { - ClearAllIsolatedCacheByEntityType(); - //if a language is removed, then all dictionary cache needs to be removed - ClearAllIsolatedCacheByEntityType(); - RefreshDomains(id); - base.Remove(id); - } + // these events should never trigger + // everything should be PAYLOAD/JSON + + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); #endregion - private void RefreshDomains(int langId) + /// + /// Clears all domain caches + /// + private void RefreshDomains() { - var assignedDomains = _domainService.GetAll(true).Where(x => x.LanguageId.HasValue && x.LanguageId.Value == langId).ToList(); + ClearAllIsolatedCacheByEntityType(); - if (assignedDomains.Count > 0) + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + + var payloads = new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }; + _publishedSnapshotService.Notify(payloads); + } + + #region Json + + public class JsonPayload + { + public JsonPayload(int id, string isoCode, LanguageChangeType changeType) { - // TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container, - // and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the - // DomainCacheRefresher? + Id = id; + IsoCode = isoCode; + ChangeType = changeType; + } - ClearAllIsolatedCacheByEntityType(); - // note: must do what's above FIRST else the repositories still have the old cached - // content and when the PublishedCachesService is notified of changes it does not see - // the new content... - // notify - _publishedSnapshotService.Notify(assignedDomains.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray()); + public int Id { get; } + public string IsoCode { get; } + public LanguageChangeType ChangeType { get; } + + public enum LanguageChangeType + { + /// + /// A new languages has been added + /// + Add = 0, + + /// + /// A language has been deleted + /// + Remove = 1, + + /// + /// A language has been updated - but it's culture remains the same + /// + Update = 2, + + /// + /// A language has been updated - it's culture has changed + /// + ChangeCulture = 3 } } + + #endregion } } diff --git a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs index a5187db211..0cecba7b7b 100644 --- a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs @@ -53,7 +53,7 @@ namespace Umbraco.Web.Cache { macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); } - }; + } base.Refresh(json); } @@ -99,6 +99,7 @@ namespace Umbraco.Web.Cache return new[] { CacheKeys.MacroContentCacheKey, // macro render cache + CacheKeys.MacroFromAliasCacheKey, // lookup macro by alias }; } diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index fc62d38e62..1f54b62c5b 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -62,6 +62,7 @@ namespace Umbraco.Web.Cache // it *was* done for each pathId but really that does not make sense // only need to do it for the current media mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Key)); // remove those that are in the branch if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) @@ -104,14 +105,15 @@ namespace Umbraco.Web.Cache public class JsonPayload { - public JsonPayload(int id, TreeChangeTypes changeTypes) + public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) { Id = id; + Key = key; ChangeTypes = changeTypes; } public int Id { get; } - + public Guid? Key { get; } public TreeChangeTypes ChangeTypes { get; } } diff --git a/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs b/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs index eda3b6eef4..2c93a6f754 100644 --- a/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs @@ -1,6 +1,7 @@ using System; using Umbraco.Core.Cache; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Services; namespace Umbraco.Web.Cache @@ -8,11 +9,13 @@ namespace Umbraco.Web.Cache public sealed class TemplateCacheRefresher : CacheRefresherBase { private readonly IdkMap _idkMap; + private readonly IContentTypeCommonRepository _contentTypeCommonRepository; - public TemplateCacheRefresher(AppCaches appCaches, IdkMap idkMap) + public TemplateCacheRefresher(AppCaches appCaches, IdkMap idkMap, IContentTypeCommonRepository contentTypeCommonRepository) : base(appCaches) { _idkMap = idkMap; + _contentTypeCommonRepository = contentTypeCommonRepository; } #region Define @@ -45,6 +48,7 @@ namespace Umbraco.Web.Cache // it has an associated template. ClearAllIsolatedCacheByEntityType(); ClearAllIsolatedCacheByEntityType(); + _contentTypeCommonRepository.ClearCache(); base.Remove(id); } diff --git a/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs b/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs index 9bb1889b68..4244d575af 100644 --- a/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs +++ b/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs @@ -1,5 +1,6 @@ using System; using Umbraco.Core; +using Umbraco.Core.Compose; using Umbraco.Core.Composing; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -39,7 +40,7 @@ namespace Umbraco.Web.Compose private IUser GetPerformingUser(int userId) { var found = userId >= 0 ? _userService.GetUserById(userId) : null; - return found ?? new User { Id = 0, Name = "SYSTEM", Email = "" }; + return found ?? AuditEventsComponent.UnknownUser; } private static string FormatEmail(IMembershipUser user) diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index a68e137665..6dfad8c45b 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -1,12 +1,8 @@ using System; using System.Threading; using Umbraco.Core; -using Umbraco.Core.Compose; using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Sync; @@ -58,8 +54,8 @@ namespace Umbraco.Web.Compose // note: refresh all content & media caches does refresh content types too var svc = Current.PublishedSnapshotService; svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }); - svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _, out _); - svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _); + svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _); + svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _); }, //rebuild indexes if the server is not synced diff --git a/src/Umbraco.Web/Compose/NotificationsComponent.cs b/src/Umbraco.Web/Compose/NotificationsComponent.cs index db602b3db9..6fcc1269cb 100644 --- a/src/Umbraco.Web/Compose/NotificationsComponent.cs +++ b/src/Umbraco.Web/Compose/NotificationsComponent.cs @@ -204,14 +204,15 @@ namespace Umbraco.Web.Compose private void SendNotification(IUser sender, IEnumerable entities, IAction action, Uri siteUri) { if (sender == null) throw new ArgumentNullException(nameof(sender)); - if (siteUri == null) throw new ArgumentNullException(nameof(siteUri)); + if (siteUri == null) + { + _logger.Warn(typeof(Notifier), "Notifications can not be sent, no site url is set (might be during boot process?)"); + return; + } //group by the content type variation since the emails will be different foreach(var contentVariantGroup in entities.GroupBy(x => x.ContentType.Variations)) { - if (contentVariantGroup.Key == ContentVariation.CultureAndSegment || contentVariantGroup.Key == ContentVariation.Segment) - throw new NotSupportedException("Segments are not yet supported in Umbraco"); - _notificationService.SendNotifications( sender, contentVariantGroup, diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 5be5e45ecd..2419eaa6d4 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -88,6 +88,12 @@ namespace Umbraco.Web.Composing public static UmbracoHelper UmbracoHelper => Factory.GetInstance(); + public static IUmbracoComponentRenderer UmbracoComponentRenderer + => Factory.GetInstance(); + public static ITagQuery TagQuery + => Factory.GetInstance(); + public static IPublishedContentQuery PublishedContentQuery + => Factory.GetInstance(); public static DistributedCache DistributedCache => Factory.GetInstance(); @@ -182,6 +188,8 @@ namespace Umbraco.Web.Composing public static DataEditorCollection DataEditors => CoreCurrent.DataEditors; + public static DataValueReferenceFactoryCollection DataValueReferenceFactories => CoreCurrent.DataValueReferenceFactories; + public static PropertyEditorCollection PropertyEditors => CoreCurrent.PropertyEditors; public static ParameterEditorCollection ParameterEditors => CoreCurrent.ParameterEditors; diff --git a/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs b/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs index ea2dc1b2d6..add7e2f16a 100644 --- a/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs +++ b/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.Models.Membership; @@ -13,6 +14,7 @@ namespace Umbraco.Web.ContentApps private ContentApp _contentApp; private ContentApp _mediaApp; + private ContentApp _memberApp; public ContentApp GetContentAppFor(object o, IEnumerable userGroups) { @@ -23,7 +25,7 @@ namespace Umbraco.Web.ContentApps { Alias = "umbContent", Name = "Content", - Icon = "icon-document", + Icon = Constants.Icons.Content, View = "views/content/apps/content/content.html", Weight = Weight }); @@ -36,7 +38,7 @@ namespace Umbraco.Web.ContentApps { Alias = "umbContent", Name = "Content", - Icon = "icon-document", + Icon = Constants.Icons.Content, View = "views/media/apps/content/content.html", Weight = Weight }); @@ -44,6 +46,16 @@ namespace Umbraco.Web.ContentApps case IMedia _: return null; + case IMember _: + return _memberApp ?? (_memberApp = new ContentApp + { + Alias = "umbContent", + Name = "Content", + Icon = Constants.Icons.Content, + View = "views/member/apps/content/content.html", + Weight = Weight + }); + default: throw new NotSupportedException($"Object type {o.GetType()} is not supported here."); } diff --git a/src/Umbraco.Web/ContentApps/ContentInfoContentAppFactory.cs b/src/Umbraco.Web/ContentApps/ContentInfoContentAppFactory.cs index 49be194349..fac03c43d0 100644 --- a/src/Umbraco.Web/ContentApps/ContentInfoContentAppFactory.cs +++ b/src/Umbraco.Web/ContentApps/ContentInfoContentAppFactory.cs @@ -13,6 +13,7 @@ namespace Umbraco.Web.ContentApps private ContentApp _contentApp; private ContentApp _mediaApp; + private ContentApp _memberApp; public ContentApp GetContentAppFor(object o, IEnumerable userGroups) { @@ -37,6 +38,15 @@ namespace Umbraco.Web.ContentApps View = "views/media/apps/info/info.html", Weight = Weight }); + case IMember _: + return _memberApp ?? (_memberApp = new ContentApp + { + Alias = "umbInfo", + Name = "Info", + Icon = "icon-info", + View = "views/member/apps/info/info.html", + Weight = Weight + }); default: throw new NotSupportedException($"Object type {o.GetType()} is not supported here."); diff --git a/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs b/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs index ab605c7014..bf6184197f 100644 --- a/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs +++ b/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs @@ -45,6 +45,8 @@ namespace Umbraco.Web.ContentApps entityType = "media"; dtdId = Core.Constants.DataTypes.DefaultMediaListView; break; + case IMember member: + return null; default: throw new NotSupportedException($"Object type {o.GetType()} is not supported here."); } @@ -100,6 +102,15 @@ namespace Umbraco.Web.ContentApps if (configTabName != null && String.IsNullOrWhiteSpace(configTabName.ToString()) == false) contentApp.Name = configTabName.ToString(); } + + //Override Icon if icon is provided + if (listViewConfig.ContainsKey("icon")) + { + var configIcon = listViewConfig["icon"]; + if (configIcon != null && String.IsNullOrWhiteSpace(configIcon.ToString()) == false) + contentApp.Icon = configIcon.ToString(); + } + // if the list view is configured to show umbContent first, update the list view content app weight accordingly if(listViewConfig.ContainsKey("showContentFirst") && listViewConfig["showContentFirst"]?.ToString().TryConvertTo().Result == true) diff --git a/src/Umbraco.Web/Controllers/TagsController.cs b/src/Umbraco.Web/Controllers/TagsController.cs deleted file mode 100644 index b65d4de531..0000000000 --- a/src/Umbraco.Web/Controllers/TagsController.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Core.Services; -using Umbraco.Web.Models; -using Umbraco.Web.WebApi; - -namespace Umbraco.Web.Controllers -{ - /// - /// A public web service for querying tags - /// - /// - /// This controller does not contain methods to query for content, media or members based on tags, those methods would require - /// authentication and should not be exposed publicly. - /// - // TODO: This controller should be moved to a more suitable place. - public class TagsController : UmbracoApiController - { - public TagsController() - { - } - - public TagsController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) - { - } - - /// - /// Get every tag stored in the database (with optional group) - /// - public IEnumerable GetAllTags(string group = null) - { - return Umbraco.TagQuery.GetAllTags(group); - } - - /// - /// Get all tags for content items (with optional group) - /// - /// - /// - public IEnumerable GetAllContentTags(string group = null) - { - return Umbraco.TagQuery.GetAllContentTags(group); - } - - /// - /// Get all tags for media items (with optional group) - /// - /// - /// - public IEnumerable GetAllMediaTags(string group = null) - { - return Umbraco.TagQuery.GetAllMediaTags(group); - } - - /// - /// Get all tags for member items (with optional group) - /// - /// - /// - public IEnumerable GetAllMemberTags(string group = null) - { - return Umbraco.TagQuery.GetAllMemberTags(group); - } - - /// - /// Returns all tags attached to a property by entity id - /// - /// - /// - /// - /// - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null) - { - return Umbraco.TagQuery.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup); - } - - /// - /// Returns all tags attached to an entity (content, media or member) by entity id - /// - /// - /// - /// - public IEnumerable GetTagsForEntity(int contentId, string tagGroup = null) - { - return Umbraco.TagQuery.GetTagsForEntity(contentId, tagGroup); - } - - } -} diff --git a/src/Umbraco.Web/Controllers/UmbLoginController.cs b/src/Umbraco.Web/Controllers/UmbLoginController.cs index 2980b4a4c0..88bc17abff 100644 --- a/src/Umbraco.Web/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web/Controllers/UmbLoginController.cs @@ -22,6 +22,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleLogin([Bind(Prefix = "loginModel")]LoginModel model) { if (ModelState.IsValid == false) @@ -45,7 +46,7 @@ namespace Umbraco.Web.Controllers // if it's not a local url we'll redirect to the root of the current site return Redirect(Url.IsLocalUrl(model.RedirectUrl) ? model.RedirectUrl - : CurrentPage.AncestorOrSelf(1).Url); + : CurrentPage.AncestorOrSelf(1).Url()); } //redirect to current page by default diff --git a/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs b/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs index fdc2de8c5f..8f572404fc 100644 --- a/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs +++ b/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs @@ -24,6 +24,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleLogout([Bind(Prefix = "logoutModel")]PostRedirectModel model) { if (ModelState.IsValid == false) diff --git a/src/Umbraco.Web/Controllers/UmbProfileController.cs b/src/Umbraco.Web/Controllers/UmbProfileController.cs index b14652ad2e..d9333e8e65 100644 --- a/src/Umbraco.Web/Controllers/UmbProfileController.cs +++ b/src/Umbraco.Web/Controllers/UmbProfileController.cs @@ -23,6 +23,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleUpdateProfile([Bind(Prefix = "profileModel")] ProfileModel model) { var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); diff --git a/src/Umbraco.Web/Controllers/UmbRegisterController.cs b/src/Umbraco.Web/Controllers/UmbRegisterController.cs index b0187d6127..4f4173a67d 100644 --- a/src/Umbraco.Web/Controllers/UmbRegisterController.cs +++ b/src/Umbraco.Web/Controllers/UmbRegisterController.cs @@ -24,6 +24,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleRegisterMember([Bind(Prefix = "registerModel")]RegisterModel model) { if (ModelState.IsValid == false) diff --git a/src/Umbraco.Web/Dashboards/ProfilerDashboard.cs b/src/Umbraco.Web/Dashboards/ProfilerDashboard.cs new file mode 100644 index 0000000000..a4f51398e8 --- /dev/null +++ b/src/Umbraco.Web/Dashboards/ProfilerDashboard.cs @@ -0,0 +1,18 @@ +using System; +using Umbraco.Core.Composing; +using Umbraco.Core.Dashboards; + +namespace Umbraco.Web.Dashboards +{ + [Weight(60)] + public class ProfilerDashboardDashboard : IDashboard + { + public string Alias => "settingsProfiler"; + + public string[] Sections => new [] { "settings" }; + + public string View => "views/dashboard/settings/profiler.html"; + + public IAccessRule[] AccessRules => Array.Empty(); + } +} diff --git a/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs b/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs index 6e1c35352a..92c1412638 100644 --- a/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs +++ b/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs @@ -122,7 +122,20 @@ namespace Umbraco.Web.Dictionary //ensure it's stored/retrieved from request cache //NOTE: This is no longer necessary since these are cached at the runtime level, but we can leave it here for now. return _requestCache.GetCacheItem(typeof (DefaultCultureDictionary).Name + "Culture" + Culture.Name, - () => _localizationService.GetLanguageByIsoCode(Culture.Name)); + () => { + // find a language that matches the current culture or any of its parent cultures + var culture = Culture; + while(culture != CultureInfo.InvariantCulture) + { + var language = _localizationService.GetLanguageByIsoCode(culture.Name); + if(language != null) + { + return language; + } + culture = culture.Parent; + } + return null; + }); } } } diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 7ed87c44dd..e77a1b70f2 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -145,6 +145,7 @@ namespace Umbraco.Web.Editors ///
/// [HttpGet] + [StatusCodeResult(System.Net.HttpStatusCode.ServiceUnavailable)] public async Task AuthorizeUpgrade() { return await RenderDefaultOrProcessExternalLoginAsync( @@ -170,12 +171,26 @@ namespace Umbraco.Web.Editors : CultureInfo.GetCultureInfo(GlobalSettings.DefaultUILanguage) : CultureInfo.GetCultureInfo(culture); - var textForCulture = Services.TextService.GetAllStoredValues(cultureInfo) - //the dictionary returned is fine but the delimiter between an 'area' and a 'value' is a '/' but the javascript - // in the back office requires the delimiter to be a '_' so we'll just replace it - .ToDictionary(key => key.Key.Replace("/", "_"), val => val.Value); + var allValues = Services.TextService.GetAllStoredValues(cultureInfo); + var pathedValues = allValues.Select(kv => + { + var slashIndex = kv.Key.IndexOf('/'); + var areaAlias = kv.Key.Substring(0, slashIndex); + var valueAlias = kv.Key.Substring(slashIndex+1); + return new + { + areaAlias, + valueAlias, + value = kv.Value + }; + }); - return new JsonNetResult { Data = textForCulture, Formatting = Formatting.Indented }; + Dictionary> nestedDictionary = pathedValues + .GroupBy(pv => pv.areaAlias) + .ToDictionary(pv => pv.Key, pv => + pv.ToDictionary(pve => pve.valueAlias, pve => pve.value)); + + return new JsonNetResult { Data = nestedDictionary, Formatting = Formatting.None }; } /// @@ -222,7 +237,7 @@ namespace Umbraco.Web.Editors GetAssetList, new TimeSpan(0, 2, 0)); - return new JsonNetResult { Data = result, Formatting = Formatting.Indented }; + return new JsonNetResult { Data = result, Formatting = Formatting.None }; } [UmbracoAuthorize(Order = 0)] @@ -230,7 +245,7 @@ namespace Umbraco.Web.Editors public JsonNetResult GetGridConfig() { var gridConfig = Current.Configs.Grids(); - return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.Indented }; + return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.None }; } diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 5f3e84916b..53865830d8 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -20,6 +20,7 @@ using Umbraco.Web.Features; using Umbraco.Web.HealthCheck; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; +using Umbraco.Web.Profiling; using Umbraco.Web.PropertyEditors; using Umbraco.Web.Trees; using Constants = Umbraco.Core.Constants; @@ -184,7 +185,7 @@ namespace Umbraco.Web.Editors { "currentUserApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.PostChangePassword(null)) - }, + }, { "entityApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetById(0, UmbracoEntityTypes.Media)) @@ -237,10 +238,6 @@ namespace Umbraco.Web.Editors "updateCheckApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetCheck()) }, - { - "tagApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( - controller => controller.GetAllTags(null)) - }, { "templateApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetById(0)) @@ -307,8 +304,20 @@ namespace Umbraco.Web.Editors }, { "logViewerApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( - controller => controller.GetNumberOfErrors()) - } + controller => controller.GetNumberOfErrors(null, null)) + }, + { + "webProfilingBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.GetStatus()) + }, + { + "tinyMceApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.UploadImage()) + }, + { + "imageUrlGeneratorApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.GetCropUrl(null, null, null, null, null)) + }, } }, { @@ -340,6 +349,7 @@ namespace Umbraco.Web.Editors {"loginBackgroundImage", Current.Configs.Settings().Content.LoginBackgroundImage}, {"showUserInvite", EmailSender.CanSendRequiredEmail}, {"canSendRequiredEmail", EmailSender.CanSendRequiredEmail}, + {"showAllowSegmentationForDocumentTypes", false}, } }, { diff --git a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs index 11a876345d..a6cba6ce41 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs @@ -49,18 +49,15 @@ namespace Umbraco.Web.Editors.Binders { foreach (var variant in model.Variants) { - if (variant.Culture.IsNullOrWhiteSpace()) - { - //map the property dto collection (no culture is passed to the mapping context so it will be invariant) - variant.PropertyCollectionDto = Current.Mapper.Map(model.PersistedContent); - } - else - { - //map the property dto collection with the culture of the current variant - variant.PropertyCollectionDto = Current.Mapper.Map( - model.PersistedContent, - context => context.SetCulture(variant.Culture)); - } + //map the property dto collection with the culture of the current variant + variant.PropertyCollectionDto = Current.Mapper.Map( + model.PersistedContent, + context => + { + // either of these may be null and that is ok, if it's invariant they will be null which is what is expected + context.SetCulture(variant.Culture); + context.SetSegment(variant.Segment); + }); //now map all of the saved values to the dto _modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); @@ -87,6 +84,5 @@ namespace Umbraco.Web.Editors.Binders model.ParentId, contentType); } - } } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index de65689353..691d6afa3a 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -303,7 +303,7 @@ namespace Umbraco.Web.Editors } /// - /// Gets the content json for the content id + /// Gets the content json for the content guid /// /// /// @@ -323,7 +323,7 @@ namespace Umbraco.Web.Editors } /// - /// Gets the content json for the content id + /// Gets the content json for the content udi /// /// /// @@ -341,7 +341,7 @@ namespace Umbraco.Web.Editors } /// - /// Gets an empty content item for the + /// Gets an empty content item for the document type. /// /// /// @@ -576,8 +576,13 @@ namespace Umbraco.Web.Editors Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); //we need to reuse the underlying logic so return the result that it wants return OperationResult.Succeed(new EventMessages()); + }, + content => + { + var display = MapToDisplay(content); + SetupBlueprint(display, content); + return display; }); - SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); return contentItemDisplay; } @@ -591,11 +596,15 @@ namespace Umbraco.Web.Editors [OutgoingEditorModelEvent] public ContentItemDisplay PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { - var contentItemDisplay = PostSaveInternal(contentItem, content => Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id)); + var contentItemDisplay = PostSaveInternal( + contentItem, + content => Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id), + MapToDisplay); + return contentItemDisplay; } - private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func saveMethod) + private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func saveMethod, Func mapToDisplay) { //Recent versions of IE/Edge may send in the full client side file path instead of just the file name. //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all @@ -626,7 +635,7 @@ namespace Umbraco.Web.Editors { //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the model state to the outgoing object and throw a validation message - var forDisplay = MapToDisplay(contentItem.PersistedContent); + var forDisplay = mapToDisplay(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } @@ -717,11 +726,7 @@ namespace Umbraco.Web.Editors case ContentSaveAction.PublishNew: { var publishStatus = PublishInternal(contentItem, defaultCulture, cultureForInvariantErrors, out wasCancelled, out var successfulCultures); - //global notifications - AddMessageForPublishStatus(new[] { publishStatus }, globalNotifications, successfulCultures); - //variant specific notifications - foreach (var c in successfulCultures) - AddMessageForPublishStatus(new[] { publishStatus }, notifications.GetOrCreate(c), successfulCultures); + AddPublishStatusNotifications(new[] { publishStatus }, globalNotifications, notifications, successfulCultures); } break; case ContentSaveAction.PublishWithDescendants: @@ -737,12 +742,7 @@ namespace Umbraco.Web.Editors } var publishStatus = PublishBranchInternal(contentItem, false, cultureForInvariantErrors, out wasCancelled, out var successfulCultures).ToList(); - - //global notifications - AddMessageForPublishStatus(publishStatus, globalNotifications, successfulCultures); - //variant specific notifications - foreach (var c in successfulCultures) - AddMessageForPublishStatus(publishStatus, notifications.GetOrCreate(c), successfulCultures); + AddPublishStatusNotifications(publishStatus, globalNotifications, notifications, successfulCultures); } break; case ContentSaveAction.PublishWithDescendantsForce: @@ -758,12 +758,7 @@ namespace Umbraco.Web.Editors } var publishStatus = PublishBranchInternal(contentItem, true, cultureForInvariantErrors, out wasCancelled, out var successfulCultures).ToList(); - - //global notifications - AddMessageForPublishStatus(publishStatus, globalNotifications, successfulCultures); - //variant specific notifications - foreach (var c in successfulCultures) - AddMessageForPublishStatus(publishStatus, notifications.GetOrCreate(c), successfulCultures); + AddPublishStatusNotifications(publishStatus, globalNotifications, notifications, successfulCultures); } break; default: @@ -771,7 +766,7 @@ namespace Umbraco.Web.Editors } //get the updated model - var display = MapToDisplay(contentItem.PersistedContent); + var display = mapToDisplay(contentItem.PersistedContent); //merge the tracked success messages with the outgoing model display.Notifications.AddRange(globalNotifications.Notifications); @@ -801,6 +796,15 @@ namespace Umbraco.Web.Editors return display; } + private void AddPublishStatusNotifications(IReadOnlyCollection publishStatus, SimpleNotificationModel globalNotifications, Dictionary variantNotifications, string[] successfulCultures) + { + //global notifications + AddMessageForPublishStatus(publishStatus, globalNotifications, successfulCultures); + //variant specific notifications + foreach (var c in successfulCultures ?? Array.Empty()) + AddMessageForPublishStatus(publishStatus, variantNotifications.GetOrCreate(c), successfulCultures); + } + /// /// Validates critical data for persistence and updates the ModelState and result accordingly /// @@ -865,7 +869,7 @@ namespace Umbraco.Web.Editors return true; } - + /// /// Helper method to perform the saving of the content and add the notifications to the result @@ -893,7 +897,13 @@ namespace Umbraco.Web.Editors if (variantCount > 1) { var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService, cultureForInvariantErrors); - foreach (var c in contentItem.Variants.Where(x => x.Save && !cultureErrors.Contains(x.Culture)).Select(x => x.Culture).ToArray()) + + var savedWithoutErrors = contentItem.Variants + .Where(x => x.Save && !cultureErrors.Contains(x.Culture) && x.Culture != null) + .Select(x => x.Culture) + .ToArray(); + + foreach (var c in savedWithoutErrors) { AddSuccessNotification(notifications, c, Services.TextService.Localize("speechBubbles/editContentSavedHeader"), @@ -1143,7 +1153,7 @@ namespace Umbraco.Web.Editors var publishStatus = Services.ContentService.SaveAndPublishBranch(contentItem.PersistedContent, force, userId: Security.CurrentUser.Id); // TODO: Deal with multiple cancellations wasCancelled = publishStatus.Any(x => x.Result == PublishResultType.FailedPublishCancelledByEvent); - successfulCultures = Array.Empty(); + successfulCultures = null; //must be null! this implies invariant return publishStatus; } @@ -1157,14 +1167,14 @@ namespace Umbraco.Web.Editors //validate if we can publish based on the mandatory language requirements var canPublish = ValidatePublishingMandatoryLanguages( cultureErrors, - contentItem, cultureVariants, mandatoryCultures, + contentItem, cultureVariants, mandatoryCultures, mandatoryVariant => mandatoryVariant.Publish); //Now check if there are validation errors on each variant. //If validation errors are detected on a variant and it's state is set to 'publish', then we //need to change it to 'save'. //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. - + foreach (var variant in contentItem.Variants) { if (cultureErrors.Contains(variant.Culture)) @@ -1215,7 +1225,7 @@ namespace Umbraco.Web.Editors //its invariant, proceed normally var publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, userId: Security.CurrentUser.Id); wasCancelled = publishStatus.Result == PublishResultType.FailedPublishCancelledByEvent; - successfulCultures = Array.Empty(); + successfulCultures = null; //must be null! this implies invariant return publishStatus; } @@ -1652,14 +1662,14 @@ namespace Umbraco.Web.Editors [HttpPost] public DomainSave PostSaveLanguageAndDomains(DomainSave model) { - foreach(var domain in model.Domains) + foreach (var domain in model.Domains) { try { - var uri = DomainHelper.ParseUriFromDomainName(domain.Name, Request.RequestUri); + var uri = DomainUtilities.ParseUriFromDomainName(domain.Name, Request.RequestUri); } catch (UriFormatException) - { + { var response = Request.CreateValidationErrorResponse(Services.TextService.Localize("assignDomain/invalidDomain")); throw new HttpResponseException(response); } @@ -1675,9 +1685,9 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(response); } - var permission = Services.UserService.GetPermissions(Security.CurrentUser, node.Path); + var assignedPermissions = Services.UserService.GetAssignedPermissions(Security.CurrentUser, node.Id); - if (permission.AssignedPermissions.Contains(ActionAssignDomain.ActionLetter.ToString(), StringComparer.Ordinal) == false) + if (assignedPermissions.Contains(ActionAssignDomain.ActionLetter.ToString(), StringComparer.Ordinal) == false) { var response = Request.CreateResponse(HttpStatusCode.BadRequest); response.Content = new StringContent("You do not have permission to assign domains on that node."); @@ -1825,15 +1835,20 @@ namespace Umbraco.Web.Editors base.HandleInvalidModelState(display); } - + /// /// Maps the dto property values and names to the persisted model /// /// private void MapValuesForPersistence(ContentItemSave contentSave) { - // inline method to determine if a property type varies - bool Varies(Property property) => property.PropertyType.VariesByCulture(); + // inline method to determine the culture and segment to persist the property + (string culture, string segment) PropertyCultureAndSegment(Property property, ContentVariantSave variant) + { + var culture = property.PropertyType.VariesByCulture() ? variant.Culture : null; + var segment = property.PropertyType.VariesBySegment() ? variant.Segment : null; + return (culture, segment); + } var variantIndex = 0; @@ -1865,15 +1880,26 @@ namespace Umbraco.Web.Editors ? variant.PropertyCollectionDto : new ContentPropertyCollectionDto { - Properties = variant.PropertyCollectionDto.Properties.Where(x => !x.Culture.IsNullOrWhiteSpace()) + Properties = variant.PropertyCollectionDto.Properties.Where( + x => !x.Culture.IsNullOrWhiteSpace() || !x.Segment.IsNullOrWhiteSpace()) }; //for each variant, map the property values MapPropertyValuesForPersistence( contentSave, propertyCollection, - (save, property) => Varies(property) ? property.GetValue(variant.Culture) : property.GetValue(), //get prop val - (save, property, v) => { if (Varies(property)) property.SetValue(v, variant.Culture); else property.SetValue(v); }, //set prop val + (save, property) => + { + // Get property value + (var culture, var segment) = PropertyCultureAndSegment(property, variant); + return property.GetValue(culture, segment); + }, + (save, property, v) => + { + // Set property value + (var culture, var segment) = PropertyCultureAndSegment(property, variant); + property.SetValue(v, culture, segment); + }, variant.Culture); variantIndex++; @@ -1922,10 +1948,8 @@ namespace Umbraco.Web.Editors } if (model.ParentId < 0) { - //cannot move if the content item is not allowed at the root unless there are - //none allowed at root (in which case all should be allowed at root) - var contentTypeService = Services.ContentTypeService; - if (toMove.ContentType.AllowedAsRoot == false && contentTypeService.GetAll().Any(ct => ct.AllowedAsRoot)) + //cannot move if the content item is not allowed at the root + if (toMove.ContentType.AllowedAsRoot == false) { throw new HttpResponseException( Request.CreateNotificationValidationErrorResponse( @@ -2128,7 +2152,7 @@ namespace Umbraco.Web.Editors { foreach (var c in successfulCultures) { - var names = string.Join(", ", status.Select(x => $"'{x.Content.GetCultureName(c)}'")); + var names = string.Join(", ", status.Select(x => $"'{(x.Content.ContentType.VariesByCulture() ? x.Content.GetCultureName(c) : x.Content.Name)}'")); display.AddWarningNotification( Services.TextService.Localize("publish"), Services.TextService.Localize("publish/contentPublishedFailedInvalid", @@ -2155,7 +2179,10 @@ namespace Umbraco.Web.Editors /// private ContentItemDisplay MapToDisplay(IContent content) { - var display = Mapper.Map(content); + var display = Mapper.Map(content, context => + { + context.Items["CurrentUser"] = Security.CurrentUser; + }); display.AllowPreview = display.AllowPreview && content.Trashed == false && content.ContentType.IsElement == false; return display; } @@ -2293,7 +2320,7 @@ namespace Umbraco.Web.Editors } var entry = Services.PublicAccessService.GetEntryForContent(content); - if (entry == null) + if (entry == null || entry.ProtectedNodeId != content.Id) { return Request.CreateResponse(HttpStatusCode.OK); } @@ -2375,7 +2402,7 @@ namespace Umbraco.Web.Editors var entry = Services.PublicAccessService.GetEntryForContent(content); - if (entry == null) + if (entry == null || entry.ProtectedNodeId != content.Id) { entry = new PublicAccessEntry(content, loginPage, errorPage, new List()); diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index e2a1e54571..9c248f186b 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -17,9 +17,11 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Packaging; using Umbraco.Core.Persistence; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; @@ -46,6 +48,7 @@ namespace Umbraco.Web.Editors { private readonly IEntityXmlSerializer _serializer; private readonly PropertyEditorCollection _propertyEditors; + private readonly IScopeProvider _scopeProvider; public ContentTypeController(IEntityXmlSerializer serializer, ICultureDictionaryFactory cultureDictionaryFactory, @@ -53,11 +56,13 @@ namespace Umbraco.Web.Editors IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, PropertyEditorCollection propertyEditors, ServiceContext services, AppCaches appCaches, - IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) + IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper, + IScopeProvider scopeProvider) : base(cultureDictionaryFactory, globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { _serializer = serializer; _propertyEditors = propertyEditors; + _scopeProvider = scopeProvider; } public int GetCount() @@ -65,6 +70,13 @@ namespace Umbraco.Web.Editors return Services.ContentTypeService.Count(); } + [HttpGet] + [UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)] + public bool HasContentNodes(int id) + { + return Services.ContentTypeService.HasContentNodes(id); + } + public DocumentTypeDisplay GetById(int id) { var ct = Services.ContentTypeService.Get(id); @@ -132,7 +144,7 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage GetAvailableCompositeContentTypes(GetAvailableCompositionsFilter filter) { - var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.DocumentType, filter.FilterContentTypes, filter.FilterPropertyTypes) + var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.DocumentType, filter.FilterContentTypes, filter.FilterPropertyTypes, filter.IsElement) .Select(x => new { contentType = x.Item1, @@ -373,7 +385,7 @@ namespace Umbraco.Web.Editors else ct = new ContentType(parentId); - ct.Icon = "icon-document"; + ct.Icon = Constants.Icons.Content; var dto = Mapper.Map(ct); return dto; @@ -409,11 +421,7 @@ namespace Umbraco.Web.Editors IEnumerable types; if (contentId == Constants.System.Root) { - var allContentTypes = Services.ContentTypeService.GetAll().ToList(); - bool AllowedAsRoot(IContentType x) => x.AllowedAsRoot; - types = allContentTypes.Any(AllowedAsRoot) - ? allContentTypes.Where(AllowedAsRoot).ToList() - : allContentTypes; + types = Services.ContentTypeService.GetAll().Where(x => x.AllowedAsRoot).ToList(); } else { @@ -424,11 +432,11 @@ namespace Umbraco.Web.Editors } var contentType = Services.ContentTypeBaseServices.GetContentTypeOf(contentItem); - var ids = contentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray(); + var ids = contentType.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value).ToArray(); if (ids.Any() == false) return Enumerable.Empty(); - types = Services.ContentTypeService.GetAll(ids).ToList(); + types = Services.ContentTypeService.GetAll(ids).OrderBy(c => ids.IndexOf(c.Id)).ToList(); } var basics = types.Where(type => type.IsElement == false).Select(Mapper.Map).ToList(); @@ -451,7 +459,7 @@ namespace Umbraco.Web.Editors } } - return basics; + return basics.OrderBy(c => contentId == Constants.System.Root ? c.Name : string.Empty); } /// @@ -520,7 +528,7 @@ namespace Umbraco.Web.Editors } var dataInstaller = new PackageDataInstallation(Logger, Services.FileService, Services.MacroService, Services.LocalizationService, - Services.DataTypeService, Services.EntityService, Services.ContentTypeService, Services.ContentService, _propertyEditors); + Services.DataTypeService, Services.EntityService, Services.ContentTypeService, Services.ContentService, _propertyEditors, _scopeProvider); var xd = new XmlDocument {XmlResolver = null}; xd.Load(filePath); diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index e62438cc6f..2b3990c7a1 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -53,11 +53,13 @@ namespace Umbraco.Web.Editors /// be looked up via the db, they need to be passed in. /// /// + /// Wether the composite content types should be applicable for an element type /// protected IEnumerable> PerformGetAvailableCompositeContentTypes(int contentTypeId, UmbracoObjectTypes type, string[] filterContentTypes, - string[] filterPropertyTypes) + string[] filterPropertyTypes, + bool isElement) { IContentTypeComposition source = null; @@ -98,7 +100,7 @@ namespace Umbraco.Web.Editors throw new ArgumentOutOfRangeException("The entity type was not a content type"); } - var availableCompositions = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, filterContentTypes, filterPropertyTypes); + var availableCompositions = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, filterContentTypes, filterPropertyTypes, isElement); @@ -304,9 +306,12 @@ namespace Umbraco.Web.Editors //check if the type is trying to allow type 0 below itself - id zero refers to the currently unsaved type //always filter these 0 types out var allowItselfAsChild = false; + var allowIfselfAsChildSortOrder = -1; if (contentTypeSave.AllowedContentTypes != null) { + allowIfselfAsChildSortOrder = contentTypeSave.AllowedContentTypes.IndexOf(0); allowItselfAsChild = contentTypeSave.AllowedContentTypes.Any(x => x == 0); + contentTypeSave.AllowedContentTypes = contentTypeSave.AllowedContentTypes.Where(x => x > 0).ToList(); } @@ -335,10 +340,12 @@ namespace Umbraco.Web.Editors saveContentType(newCt); //we need to save it twice to allow itself under itself. - if (allowItselfAsChild) + if (allowItselfAsChild && newCt != null) { - //NOTE: This will throw if the composition isn't right... but it shouldn't be at this stage - newCt.AddContentType(newCt); + newCt.AllowedContentTypes = + newCt.AllowedContentTypes.Union( + new []{ new ContentTypeSort(newCt.Id, allowIfselfAsChildSortOrder) } + ); saveContentType(newCt); } return newCt; diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index 3c8aed247e..46151f2a9e 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -135,6 +135,8 @@ namespace Umbraco.Web.Editors //They've successfully set their password, we can now update their user account to be approved Security.CurrentUser.IsApproved = true; + //They've successfully set their password, and will now get fully logged into the back office, so the lastlogindate is set so the backoffice shows they have logged in + Security.CurrentUser.LastLoginDate = DateTime.UtcNow; Services.UserService.Save(Security.CurrentUser); //now we can return their full object since they are now really logged into the back office diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index 75fa8476e7..eef0b5df93 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -140,7 +140,7 @@ namespace Umbraco.Web.Editors break; case "OUR": - urlPrefix = "https://our.umbraco.org/"; + urlPrefix = "https://our.umbraco.com/"; break; case "COM": diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 30c4162252..ad92d40ecf 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -20,6 +20,7 @@ using Umbraco.Web.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; +using System.Web.Http.Controllers; namespace Umbraco.Web.Editors { @@ -34,6 +35,7 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] [UmbracoTreeAuthorize(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)] [EnableOverrideAuthorization] + [DataTypeControllerConfiguration] public class DataTypeController : BackOfficeNotificationsController { private readonly PropertyEditorCollection _propertyEditors; @@ -44,6 +46,19 @@ namespace Umbraco.Web.Editors _propertyEditors = propertyEditors; } + /// + /// Configures this controller with a custom action selector + /// + private class DataTypeControllerConfigurationAttribute : Attribute, IControllerConfiguration + { + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) + )); + } + } + /// /// Gets data type by name /// @@ -70,6 +85,40 @@ namespace Umbraco.Web.Editors return Mapper.Map(dataType); } + /// + /// Gets the datatype json for the datatype guid + /// + /// + /// + public DataTypeDisplay GetById(Guid id) + { + var dataType = Services.DataTypeService.GetDataType(id); + if (dataType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + return Mapper.Map(dataType); + } + + /// + /// Gets the datatype json for the datatype udi + /// + /// + /// + public DataTypeDisplay GetById(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + var dataType = Services.DataTypeService.GetDataType(guidUdi.Guid); + if (dataType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + return Mapper.Map(dataType); + } + /// /// Deletes a data type with a given ID /// @@ -283,6 +332,60 @@ namespace Umbraco.Web.Editors : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); } + /// + /// Returns the references (usages) for the data type + /// + /// + /// + public DataTypeReferences GetReferences(int id) + { + var result = new DataTypeReferences(); + var usages = Services.DataTypeService.GetReferences(id); + + foreach(var groupOfEntityType in usages.GroupBy(x => x.Key.EntityType)) + { + //get all the GUIDs for the content types to find + var guidsAndPropertyAliases = groupOfEntityType.ToDictionary(i => ((GuidUdi)i.Key).Guid, i => i.Value); + + if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.DocumentType)) + result.DocumentTypes = GetContentTypeUsages(Services.ContentTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); + else if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.MediaType)) + result.MediaTypes = GetContentTypeUsages(Services.MediaTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); + else if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.MemberType)) + result.MemberTypes = GetContentTypeUsages(Services.MemberTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); + } + + return result; + } + + /// + /// Maps the found content types and usages to the resulting model + /// + /// + /// + /// + private IEnumerable GetContentTypeUsages( + IEnumerable cts, + IReadOnlyDictionary> usages) + { + return cts.Select(x => new DataTypeReferences.ContentTypeReferences + { + Id = x.Id, + Key = x.Key, + Alias = x.Alias, + Icon = x.Icon, + Name = x.Name, + Udi = new GuidUdi(ObjectTypes.GetUdiType(UmbracoObjectTypes.DocumentType), x.Key), + //only select matching properties + Properties = x.PropertyTypes.Where(p => usages[x.Key].InvariantContains(p.Alias)) + .Select(p => new DataTypeReferences.ContentTypeReferences.PropertyTypeReferences + { + Alias = p.Alias, + Name = p.Name + }) + }); + } + #region ReadOnly actions to return basic data - allow access for: content ,media, members, settings, developer /// /// Gets the content json for all data types @@ -324,7 +427,7 @@ namespace Umbraco.Web.Editors { var propertyEditor = propertyEditors.SingleOrDefault(x => x.Alias == dataType.Alias); if (propertyEditor != null) - dataType.HasPrevalues = propertyEditor.GetConfigurationEditor().Fields.Any(); ; + dataType.HasPrevalues = propertyEditor.GetConfigurationEditor().Fields.Any(); } var grouped = dataTypes diff --git a/src/Umbraco.Web/Editors/DictionaryController.cs b/src/Umbraco.Web/Editors/DictionaryController.cs index 120ebeded7..d132fdc201 100644 --- a/src/Umbraco.Web/Editors/DictionaryController.cs +++ b/src/Umbraco.Web/Editors/DictionaryController.cs @@ -52,6 +52,13 @@ namespace Umbraco.Web.Editors if (foundDictionary == null) throw new HttpResponseException(HttpStatusCode.NotFound); + var foundDictionaryDescendants = Services.LocalizationService.GetDictionaryItemDescendants(foundDictionary.Key); + + foreach (var dictionaryItem in foundDictionaryDescendants) + { + Services.LocalizationService.Delete(dictionaryItem, Security.CurrentUser.Id); + } + Services.LocalizationService.Delete(foundDictionary, Security.CurrentUser.Id); return Request.CreateResponse(HttpStatusCode.OK); diff --git a/src/Umbraco.Web/Editors/EditorValidatorCollection.cs b/src/Umbraco.Web/Editors/EditorValidatorCollection.cs index 6fc6bb5de2..0e42b0027c 100644 --- a/src/Umbraco.Web/Editors/EditorValidatorCollection.cs +++ b/src/Umbraco.Web/Editors/EditorValidatorCollection.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Composing; namespace Umbraco.Web.Editors { - internal class EditorValidatorCollection : BuilderCollectionBase + public class EditorValidatorCollection : BuilderCollectionBase { public EditorValidatorCollection(IEnumerable items) : base(items) diff --git a/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs b/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs index b3b7bab1a5..994a813cf4 100644 --- a/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs +++ b/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs @@ -2,7 +2,7 @@ namespace Umbraco.Web.Editors { - internal class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase + public class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase { protected override EditorValidatorCollectionBuilder This => this; } diff --git a/src/Umbraco.Web/Editors/EditorValidatorOfT.cs b/src/Umbraco.Web/Editors/EditorValidatorOfT.cs index 4ca008cf0d..715a49179e 100644 --- a/src/Umbraco.Web/Editors/EditorValidatorOfT.cs +++ b/src/Umbraco.Web/Editors/EditorValidatorOfT.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.Editors /// Provides a base class for implementations. /// /// The validated object type. - internal abstract class EditorValidator : IEditorValidator + public abstract class EditorValidator : IEditorValidator { public Type ModelType => typeof (T); @@ -16,4 +16,4 @@ namespace Umbraco.Web.Editors protected abstract IEnumerable Validate(T model); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 05f7b6525c..3938ae5ab8 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -21,6 +21,7 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Core.Xml; +using Umbraco.Web.Models; using Umbraco.Web.Models.Mapping; using Umbraco.Web.Models.TemplateQuery; using Umbraco.Web.Search; @@ -36,7 +37,12 @@ namespace Umbraco.Web.Editors /// The API controller used for getting entity objects, basic name, icon, id representation of umbraco objects that are based on CMSNode /// /// - /// Some objects such as macros are not based on CMSNode + /// + /// This controller allows resolving basic entity data for various entities without placing the hard restrictions on users that may not have access + /// to the sections these entities entities exist in. This is to allow pickers, etc... of data to work for all users. In some cases such as accessing + /// Members, more explicit security checks are done. + /// + /// Some objects such as macros are not based on CMSNode /// [EntityControllerConfiguration] [PluginController("UmbracoApi")] @@ -68,6 +74,7 @@ namespace Umbraco.Web.Editors //id is passed in eventually we'll probably want to support GUID + Udi too new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetPagedChildren", "id", typeof(int), typeof(string)), new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetPath", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetUrlAndAnchors", "id", typeof(int), typeof(Guid), typeof(Udi)), new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)), new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetByIds", "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[])))); } @@ -98,36 +105,20 @@ namespace Umbraco.Web.Editors /// /// A starting point for the search, generally a node id, but for members this is a member type alias /// - /// - [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] - [HttpGet] - public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null) - { - return Search(query, type, ignoreUserStartNodes: false, searchFrom); - } - - /// - /// Searches for results based on the entity type - /// - /// - /// - /// - /// A starting point for the search, generally a node id, but for members this is a member type alias - /// - /// If set to true, user and group start node permissions will be ignored. + /// If set used to look up whether user and group start node permissions will be ignored. /// [HttpGet] - public IEnumerable Search(string query, UmbracoEntityTypes type, bool? ignoreUserStartNodes, string searchFrom = null) + public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null, Guid? dataTypeKey = null) { - // TODO: Should we restrict search results based on what app the user has access to? - // - Theoretically you shouldn't be able to see member data if you don't have access to members right? + // NOTE: Theoretically you shouldn't be able to see member data if you don't have access to members right? ... but there is a member picker, so can't really do that if (string.IsNullOrEmpty(query)) return Enumerable.Empty(); //TODO: This uses the internal UmbracoTreeSearcher, this instead should delgate to the ISearchableTree implementation for the type - return ExamineSearch(query, type, searchFrom, ignoreUserStartNodes != null && ignoreUserStartNodes.Value); + var ignoreUserStartNodes = dataTypeKey.HasValue && Services.DataTypeService.IsDataTypeIgnoringUserStartNodes(dataTypeKey.Value); + return ExamineSearch(query, type, searchFrom, ignoreUserStartNodes); } /// @@ -153,22 +144,20 @@ namespace Umbraco.Web.Editors var allowedSections = Security.CurrentUser.AllowedSections.ToArray(); - foreach (var searchableTree in _searchableTreeCollection.SearchableApplicationTrees) + foreach (var searchableTree in _searchableTreeCollection.SearchableApplicationTrees.OrderBy(t => t.Value.SortOrder)) { if (allowedSections.Contains(searchableTree.Value.AppAlias)) { var tree = _treeService.GetByAlias(searchableTree.Key); if (tree == null) continue; //shouldn't occur - var searchableTreeAttribute = searchableTree.Value.SearchableTree.GetType().GetCustomAttribute(false); - result[Tree.GetRootNodeDisplayName(tree, Services.TextService)] = new TreeSearchResult { Results = searchableTree.Value.SearchableTree.Search(query, 200, 0, out var total), TreeAlias = searchableTree.Key, AppAlias = searchableTree.Value.AppAlias, - JsFormatterService = searchableTreeAttribute == null ? "" : searchableTreeAttribute.ServiceName, - JsFormatterMethod = searchableTreeAttribute == null ? "" : searchableTreeAttribute.MethodName + JsFormatterService = searchableTree.Value.FormatterService, + JsFormatterMethod = searchableTree.Value.FormatterMethod }; } } @@ -217,19 +206,54 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } + /// + /// Gets the url of an entity + /// + /// UDI of the entity to fetch URL for + /// The culture to fetch the URL for + /// The URL or path to the item + public HttpResponseMessage GetUrl(Udi udi, string culture = "*") + { + var intId = Services.EntityService.GetId(udi); + if (!intId.Success) + throw new HttpResponseException(HttpStatusCode.NotFound); + UmbracoEntityTypes entityType; + switch(udi.EntityType) + { + case Constants.UdiEntityType.Document: + entityType = UmbracoEntityTypes.Document; + break; + case Constants.UdiEntityType.Media: + entityType = UmbracoEntityTypes.Media; + break; + case Constants.UdiEntityType.Member: + entityType = UmbracoEntityTypes.Member; + break; + default: + throw new HttpResponseException(HttpStatusCode.NotFound); + } + return GetUrl(intId.Result, entityType, culture); + } + /// /// Gets the url of an entity /// /// Int id of the entity to fetch URL for /// The type of entity such as Document, Media, Member + /// The culture to fetch the URL for /// The URL or path to the item - public HttpResponseMessage GetUrl(int id, UmbracoEntityTypes type) + /// + /// We are not restricting this with security because there is no sensitive data + /// + public HttpResponseMessage GetUrl(int id, UmbracoEntityTypes type, string culture = null) { + culture = culture ?? ClientCulture(); + var returnUrl = string.Empty; if (type == UmbracoEntityTypes.Document) { - var foundUrl = UmbracoContext.Url(id); + var foundUrl = UmbracoContext.Url(id, culture); if (string.IsNullOrEmpty(foundUrl) == false && foundUrl != "#") { returnUrl = foundUrl; @@ -296,6 +320,34 @@ namespace Umbraco.Web.Editors publishedContentExists: i => Umbraco.Content(i) != null); } + [HttpGet] + public UrlAndAnchors GetUrlAndAnchors(Udi id, string culture = "*") + { + var intId = Services.EntityService.GetId(id); + if (!intId.Success) + throw new HttpResponseException(HttpStatusCode.NotFound); + + return GetUrlAndAnchors(intId.Result, culture); + } + [HttpGet] + public UrlAndAnchors GetUrlAndAnchors(int id, string culture = "*") + { + culture = culture ?? ClientCulture(); + + var url = UmbracoContext.UrlProvider.GetUrl(id, culture: culture); + var anchorValues = Services.ContentService.GetAnchorValuesFromRTEs(id, culture); + return new UrlAndAnchors(url, anchorValues); + } + + [HttpGet] + [HttpPost] + public IEnumerable GetAnchors(AnchorsModel model) + { + var anchorValues = Services.ContentService.GetAnchorValuesFromRTEContent(model.RteContent); + return anchorValues; + } + + #region GetById /// @@ -416,9 +468,43 @@ namespace Umbraco.Web.Editors } #endregion - public IEnumerable GetChildren(int id, UmbracoEntityTypes type) + public IEnumerable GetChildren(int id, UmbracoEntityTypes type, Guid? dataTypeKey = null) { - return GetResultForChildren(id, type); + var objectType = ConvertToObjectType(type); + if (objectType.HasValue) + { + //TODO: Need to check for Object types that support hierarchy here, some might not. + + var startNodes = GetStartNodes(type); + + var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(dataTypeKey); + + // root is special: we reduce it to start nodes if the user's start node is not the default, then we need to return their start nodes + if (id == Constants.System.Root && startNodes.Length > 0 && startNodes.Contains(Constants.System.Root) == false && !ignoreUserStartNodes) + { + var nodes = Services.EntityService.GetAll(objectType.Value, startNodes).ToArray(); + if (nodes.Length == 0) + return Enumerable.Empty(); + var pr = new List(nodes.Select(Mapper.Map)); + return pr; + } + + // else proceed as usual + + return Services.EntityService.GetChildren(id, objectType.Value) + .WhereNotNull() + .Select(Mapper.Map); + } + //now we need to convert the unknown ones + switch (type) + { + case UmbracoEntityTypes.Domain: + case UmbracoEntityTypes.Language: + case UmbracoEntityTypes.User: + case UmbracoEntityTypes.Macro: + default: + throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + type); + } } /// @@ -439,7 +525,8 @@ namespace Umbraco.Web.Editors int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, - string filter = "") + string filter = "", + Guid? dataTypeKey = null) { if (int.TryParse(id, out var intId)) { @@ -464,7 +551,7 @@ namespace Umbraco.Web.Editors //the EntityService can search paged members from the root intId = -1; - return GetPagedChildren(intId, type, pageNumber, pageSize, orderBy, orderDirection, filter); + return GetPagedChildren(intId, type, pageNumber, pageSize, orderBy, orderDirection, filter, dataTypeKey); } //the EntityService cannot search members of a certain type, this is currently not supported and would require @@ -499,7 +586,8 @@ namespace Umbraco.Web.Editors int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, - string filter = "") + string filter = "", + Guid? dataTypeKey = null) { if (pageNumber <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); @@ -509,12 +597,37 @@ namespace Umbraco.Web.Editors var objectType = ConvertToObjectType(type); if (objectType.HasValue) { - var entities = Services.EntityService.GetPagedChildren(id, objectType.Value, pageNumber - 1, pageSize, out var totalRecords, + IEnumerable entities; + long totalRecords; + + var startNodes = GetStartNodes(type); + + var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(dataTypeKey); + + // root is special: we reduce it to start nodes if the user's start node is not the default, then we need to return their start nodes + if (id == Constants.System.Root && startNodes.Length > 0 && startNodes.Contains(Constants.System.Root) == false && !ignoreUserStartNodes) + { + if (pageNumber > 0) + return new PagedResult(0, 0, 0); + var nodes = Services.EntityService.GetAll(objectType.Value, startNodes).ToArray(); + if (nodes.Length == 0) + return new PagedResult(0, 0, 0); + if (pageSize < nodes.Length) pageSize = nodes.Length; // bah + var pr = new PagedResult(nodes.Length, pageNumber, pageSize) + { + Items = nodes.Select(Mapper.Map) + }; + return pr; + } + + // else proceed as usual + entities = Services.EntityService.GetPagedChildren(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, filter.IsNullOrWhiteSpace() ? null : SqlContext.Query().Where(x => x.Name.Contains(filter)), Ordering.By(orderBy, orderDirection)); + if (totalRecords == 0) { return new PagedResult(0, 0, 0); @@ -529,6 +642,7 @@ namespace Umbraco.Web.Editors { context.SetCulture(culture); }); + //TODO: Why is this here and not in the mapping? target.AdditionalData["hasChildren"] = source.HasChildren; return target; }) @@ -551,18 +665,17 @@ namespace Umbraco.Web.Editors } } - [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] - public PagedResult GetPagedDescendants( - int id, - UmbracoEntityTypes type, - int pageNumber, - int pageSize, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - string filter = "") + private int[] GetStartNodes(UmbracoEntityTypes type) { - return GetPagedDescendants(id, type, pageNumber, pageSize, - ignoreUserStartNodes: false, orderBy, orderDirection, filter); + switch (type) + { + case UmbracoEntityTypes.Document: + return Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + case UmbracoEntityTypes.Media: + return Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + default: + return Array.Empty(); + } } public PagedResult GetPagedDescendants( @@ -570,16 +683,19 @@ namespace Umbraco.Web.Editors UmbracoEntityTypes type, int pageNumber, int pageSize, - bool ignoreUserStartNodes, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, - string filter = "") + string filter = "", + Guid? dataTypeKey = null) { if (pageNumber <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); if (pageSize <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); + // re-normalize since NULL can be passed in + filter = filter ?? string.Empty; + var objectType = ConvertToObjectType(type); if (objectType.HasValue) { @@ -590,17 +706,9 @@ namespace Umbraco.Web.Editors { // root is special: we reduce it to start nodes - int[] aids = null; - switch (type) - { - case UmbracoEntityTypes.Document: - aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); - break; - case UmbracoEntityTypes.Media: - aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); - break; - } + int[] aids = GetStartNodes(type); + var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(dataTypeKey); entities = aids == null || aids.Contains(Constants.System.Root) || ignoreUserStartNodes ? Services.EntityService.GetPagedDescendants(objectType.Value, pageNumber - 1, pageSize, out totalRecords, SqlContext.Query().Where(x => x.Name.Contains(filter)), @@ -643,15 +751,11 @@ namespace Umbraco.Web.Editors } } - [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] - public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormDataCollection queryStrings) - { - return GetResultForAncestors(id, type, queryStrings, ignoreUserStartNodes: false); - } + private bool IsDataTypeIgnoringUserStartNodes(Guid? dataTypeKey) => dataTypeKey.HasValue && Services.DataTypeService.IsDataTypeIgnoringUserStartNodes(dataTypeKey.Value); - public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormDataCollection queryStrings, bool ignoreUserStartNodes) + public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormDataCollection queryStrings) { - return GetResultForAncestors(id, type, queryStrings, ignoreUserStartNodes); + return GetResultForAncestors(id, type, queryStrings); } /// @@ -664,7 +768,8 @@ namespace Umbraco.Web.Editors /// private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null, bool ignoreUserStartNodes = false) { - return _treeSearcher.ExamineSearch(query, entityType, 200, 0, out _, ignoreUserStartNodes, searchFrom); + var culture = ClientCulture(); + return _treeSearcher.ExamineSearch(query, entityType, 200, 0, culture, out _, searchFrom, ignoreUserStartNodes); } private IEnumerable GetResultForChildren(int id, UmbracoEntityTypes entityType) @@ -690,7 +795,7 @@ namespace Umbraco.Web.Editors } } - private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType, FormDataCollection queryStrings = null, bool ignoreUserStartNodes = false) + private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType, FormDataCollection queryStrings = null) { var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) @@ -699,6 +804,7 @@ namespace Umbraco.Web.Editors var ids = Services.EntityService.Get(id).Path.Split(',').Select(int.Parse).Distinct().ToArray(); + var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(queryStrings?.GetValue("dataTypeId")); if (ignoreUserStartNodes == false) { int[] aids = null; @@ -893,7 +999,7 @@ namespace Umbraco.Web.Editors case UmbracoEntityTypes.Media: return UmbracoObjectTypes.Media; case UmbracoEntityTypes.MemberType: - return UmbracoObjectTypes.MediaType; + return UmbracoObjectTypes.MemberType; case UmbracoEntityTypes.MemberGroup: return UmbracoObjectTypes.MemberGroup; case UmbracoEntityTypes.MediaType: diff --git a/src/Umbraco.Web/Editors/ExamineManagementController.cs b/src/Umbraco.Web/Editors/ExamineManagementController.cs index 5f969cdd80..cf1dfd5d5d 100644 --- a/src/Umbraco.Web/Editors/ExamineManagementController.cs +++ b/src/Umbraco.Web/Editors/ExamineManagementController.cs @@ -85,7 +85,7 @@ namespace Umbraco.Web.Editors }; } - + /// /// Check if the index has been rebuilt @@ -141,9 +141,6 @@ namespace Umbraco.Web.Editors try { - //clear and replace - index.CreateIndex(); - var cacheKey = "temp_indexing_op_" + index.Name; //put temp val in cache which is used as a rudimentary way to know when the indexing is done AppCaches.RuntimeCache.Insert(cacheKey, () => "tempValue", TimeSpan.FromMinutes(5)); @@ -177,8 +174,12 @@ namespace Umbraco.Web.Editors var indexName = index.Name; if (!(index is IIndexDiagnostics indexDiag)) - indexDiag = new GenericIndexDiagnostics(index); - + { + if (index is LuceneIndex luceneIndex) + indexDiag = new LuceneIndexDiagnostics(luceneIndex, Logger); + else + indexDiag = new GenericIndexDiagnostics(index); + } var isHealth = indexDiag.IsHealthy(); var properties = new Dictionary @@ -250,7 +251,7 @@ namespace Umbraco.Web.Editors private void Indexer_IndexOperationComplete(object sender, EventArgs e) { - var indexer = (LuceneIndex)sender; + var indexer = (IIndex)sender; _logger.Debug("Logging operation completed for index {IndexName}", indexer.Name); @@ -259,7 +260,7 @@ namespace Umbraco.Web.Editors _logger .Info($"Rebuilding index '{indexer.Name}' done, {indexer.CommitCount} items committed (can differ from the number of items in the index)"); + >($"Rebuilding index '{indexer.Name}' done."); var cacheKey = "temp_indexing_op_" + indexer.Name; _runtimeCache.Clear(cacheKey); diff --git a/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs b/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs index 4acf0c948e..810c2d1bea 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs @@ -5,10 +5,10 @@ using System.Net; using System.Net.Http; using System.Web.Http.Controllers; using System.Web.Http.ModelBinding; -using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Editors.Filters @@ -43,8 +43,11 @@ namespace Umbraco.Web.Editors.Filters where TModelSave: IContentSave where TModelWithProperties : IContentProperties { - protected ContentModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) : base(logger, umbracoContextAccessor) + private readonly ILocalizedTextService _textService; + + protected ContentModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor) { + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); } /// @@ -122,6 +125,18 @@ namespace Umbraco.Web.Editors.Filters { var properties = modelWithProperties.Properties.ToDictionary(x => x.Alias, x => x); + // Retrieve default messages used for required and regex validatation. We'll replace these + // if set with custom ones if they've been provided for a given property. + var requiredDefaultMessages = new[] + { + _textService.Localize("validation", "invalidNull"), + _textService.Localize("validation", "invalidEmpty") + }; + var formatDefaultMessages = new[] + { + _textService.Localize("validation", "invalidPattern"), + }; + foreach (var p in dto.Properties) { var editor = p.PropertyEditor; @@ -141,7 +156,7 @@ namespace Umbraco.Web.Editors.Filters var postedValue = postedProp.Value; - ValidatePropertyValue(model, modelWithProperties, editor, p, postedValue, modelState); + ValidatePropertyValue(model, modelWithProperties, editor, p, postedValue, modelState, requiredDefaultMessages, formatDefaultMessages); } @@ -157,22 +172,34 @@ namespace Umbraco.Web.Editors.Filters /// /// /// + /// + /// protected virtual void ValidatePropertyValue( TModelSave model, TModelWithProperties modelWithProperties, IDataEditor editor, ContentPropertyDto property, object postedValue, - ModelStateDictionary modelState) + ModelStateDictionary modelState, + string[] requiredDefaultMessages, + string[] formatDefaultMessages) { - // validate var valueEditor = editor.GetValueEditor(property.DataType.Configuration); foreach (var r in valueEditor.Validate(postedValue, property.IsRequired, property.ValidationRegExp)) { - modelState.AddPropertyError(r, property.Alias, property.Culture); + // If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate(). + if (property.IsRequired && !string.IsNullOrWhiteSpace(property.IsRequiredMessage) && requiredDefaultMessages.Contains(r.ErrorMessage, StringComparer.OrdinalIgnoreCase)) + { + r.ErrorMessage = property.IsRequiredMessage; + } + + if (!string.IsNullOrWhiteSpace(property.ValidationRegExp) && !string.IsNullOrWhiteSpace(property.ValidationRegExpMessage) && formatDefaultMessages.Contains(r.ErrorMessage, StringComparer.OrdinalIgnoreCase)) + { + r.ErrorMessage = property.ValidationRegExpMessage; + } + + modelState.AddPropertyError(r, property.Alias, property.Culture, property.Segment); } } - - } } diff --git a/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs b/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs index b74cd71f11..39bd6ab0f4 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs @@ -1,8 +1,6 @@ -using System.Web.Http.ModelBinding; -using Umbraco.Core; -using Umbraco.Core.Logging; +using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Editors.Filters @@ -12,7 +10,7 @@ namespace Umbraco.Web.Editors.Filters /// internal class ContentSaveModelValidator : ContentModelValidator { - public ContentSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) : base(logger, umbracoContextAccessor) + public ContentSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor, textService) { } } diff --git a/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs index 18fd334b1c..fbce2d0414 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs @@ -23,28 +23,30 @@ namespace Umbraco.Web.Editors.Filters /// internal sealed class ContentSaveValidationAttribute : ActionFilterAttribute { - public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService) + private readonly ILogger _logger; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILocalizedTextService _textService; + private readonly IContentService _contentService; + private readonly IUserService _userService; + private readonly IEntityService _entityService; + + public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService) { } - public ContentSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, IContentService contentService, IUserService userService, IEntityService entityService) + public ContentSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService) { - _logger = logger; - _umbracoContextAccessor = umbracoContextAccessor; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); } - private readonly ILogger _logger; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IContentService _contentService; - private readonly IUserService _userService; - private readonly IEntityService _entityService; - public override void OnActionExecuting(HttpActionContext actionContext) { var model = (ContentItemSave)actionContext.ActionArguments["contentItem"]; - var contentItemValidator = new ContentSaveModelValidator(_logger, _umbracoContextAccessor); + var contentItemValidator = new ContentSaveModelValidator(_logger, _umbracoContextAccessor, _textService); if (!ValidateAtLeastOneVariantIsBeingSaved(model, actionContext)) return; if (!contentItemValidator.ValidateExistingContent(model, actionContext)) return; diff --git a/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs index 7351c476e4..449ef95675 100644 --- a/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs @@ -1,7 +1,6 @@ -using System.Linq; +using System; using System.Net; using System.Net.Http; -using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; using Umbraco.Core; @@ -21,25 +20,27 @@ namespace Umbraco.Web.Editors.Filters { private readonly ILogger _logger; private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILocalizedTextService _textService; private readonly IMediaService _mediaService; private readonly IEntityService _entityService; - public MediaItemSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.MediaService, Current.Services.EntityService) + public MediaItemSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.MediaService, Current.Services.EntityService) { } - public MediaItemSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, IMediaService mediaService, IEntityService entityService) + public MediaItemSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IMediaService mediaService, IEntityService entityService) { - _logger = logger; - _umbracoContextAccessor = umbracoContextAccessor; - _mediaService = mediaService; - _entityService = entityService; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); + _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); + _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); } public override void OnActionExecuting(HttpActionContext actionContext) { var model = (MediaItemSave)actionContext.ActionArguments["contentItem"]; - var contentItemValidator = new MediaSaveModelValidator(_logger, _umbracoContextAccessor); + var contentItemValidator = new MediaSaveModelValidator(_logger, _umbracoContextAccessor, _textService); if (ValidateUserAccess(model, actionContext)) { diff --git a/src/Umbraco.Web/Editors/Filters/MediaSaveModelValidator.cs b/src/Umbraco.Web/Editors/Filters/MediaSaveModelValidator.cs index 83c0885d62..87b55fea76 100644 --- a/src/Umbraco.Web/Editors/Filters/MediaSaveModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/MediaSaveModelValidator.cs @@ -1,5 +1,6 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Editors.Filters @@ -9,8 +10,8 @@ namespace Umbraco.Web.Editors.Filters /// internal class MediaSaveModelValidator : ContentModelValidator> { - public MediaSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) : base(logger, umbracoContextAccessor) + public MediaSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor, textService) { } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Editors/Filters/MemberSaveModelValidator.cs b/src/Umbraco.Web/Editors/Filters/MemberSaveModelValidator.cs index 7fc2b1e648..9b47965493 100644 --- a/src/Umbraco.Web/Editors/Filters/MemberSaveModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/MemberSaveModelValidator.cs @@ -21,10 +21,10 @@ namespace Umbraco.Web.Editors.Filters { private readonly IMemberTypeService _memberTypeService; - public MemberSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, IMemberTypeService memberTypeService) - : base(logger, umbracoContextAccessor) + public MemberSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IMemberTypeService memberTypeService) + : base(logger, umbracoContextAccessor, textService) { - _memberTypeService = memberTypeService; + _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); } /// diff --git a/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs index 04b0112d56..7e2c204596 100644 --- a/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs @@ -1,4 +1,5 @@ -using System.Web.Http.Controllers; +using System; +using System.Web.Http.Controllers; using System.Web.Http.Filters; using Umbraco.Core.Logging; using Umbraco.Core.Services; @@ -14,23 +15,25 @@ namespace Umbraco.Web.Editors.Filters { private readonly ILogger _logger; private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILocalizedTextService _textService; private readonly IMemberTypeService _memberTypeService; public MemberSaveValidationAttribute() - : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.MemberTypeService) + : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.MemberTypeService) { } - public MemberSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, IMemberTypeService memberTypeService) + public MemberSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IMemberTypeService memberTypeService) { - _logger = logger; - _umbracoContextAccessor = umbracoContextAccessor; - _memberTypeService = memberTypeService; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); + _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); } public override void OnActionExecuting(HttpActionContext actionContext) { var model = (MemberSave)actionContext.ActionArguments["contentItem"]; - var contentItemValidator = new MemberSaveModelValidator(_logger, _umbracoContextAccessor, _memberTypeService); + var contentItemValidator = new MemberSaveModelValidator(_logger, _umbracoContextAccessor, _textService, _memberTypeService); //now do each validation step if (contentItemValidator.ValidateExistingContent(model, actionContext)) if (contentItemValidator.ValidateProperties(model, model, actionContext)) diff --git a/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs b/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs index 1bd7f766d4..78cd8e6a4d 100644 --- a/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs @@ -50,6 +50,14 @@ namespace Umbraco.Web.Editors.Filters actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message); return; } + + if (persisted.Alias != userGroupSave.Alias && persisted.IsSystemUserGroup()) + { + var message = $"User group with alias: {persisted.Alias} cannot be changed"; + actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, message); + return; + } + //map the model to the persisted instance Mapper.Map(userGroupSave, persisted); break; diff --git a/src/Umbraco.Web/Editors/HelpController.cs b/src/Umbraco.Web/Editors/HelpController.cs index ccbbcaeee8..39dbbc435c 100644 --- a/src/Umbraco.Web/Editors/HelpController.cs +++ b/src/Umbraco.Web/Editors/HelpController.cs @@ -13,14 +13,23 @@ namespace Umbraco.Web.Editors { var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree); - if (_httpClient == null) - _httpClient = new HttpClient(); + try + { - //fetch dashboard json and parse to JObject - var json = await _httpClient.GetStringAsync(url); - var result = JsonConvert.DeserializeObject>(json); - if (result != null) - return result; + if (_httpClient == null) + _httpClient = new HttpClient(); + + //fetch dashboard json and parse to JObject + var json = await _httpClient.GetStringAsync(url); + var result = JsonConvert.DeserializeObject>(json); + if (result != null) + return result; + + } + catch (HttpRequestException rex) + { + Logger.Info(GetType(), $"Check your network connection, exception: {rex.Message}"); + } return new List(); } diff --git a/src/Umbraco.Web/Editors/IEditorValidator.cs b/src/Umbraco.Web/Editors/IEditorValidator.cs index bec978788c..2d655e3506 100644 --- a/src/Umbraco.Web/Editors/IEditorValidator.cs +++ b/src/Umbraco.Web/Editors/IEditorValidator.cs @@ -14,14 +14,12 @@ namespace Umbraco.Web.Editors // initialized with all IEditorValidator instances // // validation is used exclusively in ContentTypeControllerBase - // the whole thing is internal at the moment, never released - // and, there are no IEditorValidator implementation in Core - // so... this all mechanism is basically useless + // currently the only implementations are for Models Builder. /// /// Provides a general object validator. /// - internal interface IEditorValidator : IDiscoverable + public interface IEditorValidator : IDiscoverable { /// /// Gets the object type validated by this validator. diff --git a/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs b/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs new file mode 100644 index 0000000000..87d7e29619 --- /dev/null +++ b/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Web.Models; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Editors +{ + /// + /// The API controller used for getting URLs for images with parameters + /// + /// + /// + /// This controller allows for retrieving URLs for processed images, such as resized, cropped, + /// or otherwise altered. These can be different based on the IImageUrlGenerator + /// implementation in use, and so the BackOffice could should not rely on hard-coded string + /// building to generate correct URLs + /// + /// + public class ImageUrlGeneratorController : UmbracoAuthorizedJsonController + { + private readonly IImageUrlGenerator _imageUrlGenerator; + + public ImageUrlGeneratorController(IImageUrlGenerator imageUrlGenerator) + { + _imageUrlGenerator = imageUrlGenerator; + } + + public string GetCropUrl(string mediaPath, int? width = null, int? height = null, ImageCropMode? imageCropMode = null, string animationProcessMode = null) + { + return mediaPath.GetCropUrl(_imageUrlGenerator, null, width: width, height: height, imageCropMode: imageCropMode, animationProcessMode: animationProcessMode); + } + } +} diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index 0eb0f54882..f3682a5b43 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -2,8 +2,10 @@ using System.IO; using System.Net; using System.Net.Http; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; +using Umbraco.Core.Models; using Umbraco.Web.Media; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; @@ -19,11 +21,17 @@ namespace Umbraco.Web.Editors { private readonly IMediaFileSystem _mediaFileSystem; private readonly IContentSection _contentSection; + private readonly IImageUrlGenerator _imageUrlGenerator; - public ImagesController(IMediaFileSystem mediaFileSystem, IContentSection contentSection) + [Obsolete("This constructor will be removed in a future release. Please use the constructor with the IImageUrlGenerator overload")] + public ImagesController(IMediaFileSystem mediaFileSystem, IContentSection contentSection) : this (mediaFileSystem, contentSection, Current.ImageUrlGenerator) + { + } + public ImagesController(IMediaFileSystem mediaFileSystem, IContentSection contentSection, IImageUrlGenerator imageUrlGenerator) { _mediaFileSystem = mediaFileSystem; _contentSection = contentSection; + _imageUrlGenerator = imageUrlGenerator; } /// @@ -60,8 +68,25 @@ namespace Umbraco.Web.Editors //redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file var response = Request.CreateResponse(HttpStatusCode.Found); - var imageLastModified = _mediaFileSystem.GetLastModified(imagePath); - response.Headers.Location = new Uri($"{imagePath}?rnd={imageLastModified:yyyyMMddHHmmss}&upscale=false&width={width}", UriKind.Relative); + + DateTimeOffset? imageLastModified = null; + try + { + imageLastModified = _mediaFileSystem.GetLastModified(imagePath); + + } + catch (Exception) + { + // if we get an exception here it's probably because the image path being requested is an image that doesn't exist + // in the local media file system. This can happen if someone is storing an absolute path to an image online, which + // is perfectly legal but in that case the media file system isn't going to resolve it. + // so ignore and we won't set a last modified date. + } + + var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : null; + var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(imagePath) { UpScale = false, Width = width, AnimationProcessMode = "first", ImageCropMode = "max", CacheBusterValue = rnd }); + + response.Headers.Location = new Uri(imageUrl, UriKind.RelativeOrAbsolute); return response; } diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 2ee77ca418..cb7fde23db 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -46,7 +46,7 @@ namespace Umbraco.Web.Editors { var allLanguages = Services.LocalizationService.GetAllLanguages(); - return Mapper.MapEnumerable(allLanguages); + return Mapper.Map, IEnumerable>(allLanguages); } [HttpGet] @@ -99,24 +99,28 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); // this is prone to race conditions but the service will not let us proceed anyways - var existing = Services.LocalizationService.GetLanguageByIsoCode(language.IsoCode); + var existingByCulture = Services.LocalizationService.GetLanguageByIsoCode(language.IsoCode); // the localization service might return the generic language even when queried for specific ones (e.g. "da" when queried for "da-DK") // - we need to handle that explicitly - if (existing?.IsoCode != language.IsoCode) + if (existingByCulture?.IsoCode != language.IsoCode) { - existing = null; + existingByCulture = null; } - if (existing != null && language.Id != existing.Id) + if (existingByCulture != null && language.Id != existingByCulture.Id) { //someone is trying to create a language that already exist ModelState.AddModelError("IsoCode", "The language " + language.IsoCode + " already exists"); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - if (existing == null) + var existingById = language.Id != default ? Services.LocalizationService.GetLanguageById(language.Id) : null; + + if (existingById == null) { + //Creating a new lang... + CultureInfo culture; try { @@ -141,38 +145,39 @@ namespace Umbraco.Web.Editors return Mapper.Map(newLang); } - existing.IsMandatory = language.IsMandatory; + existingById.IsMandatory = language.IsMandatory; // note that the service will prevent the default language from being "un-defaulted" // but does not hurt to test here - though the UI should prevent it too - if (existing.IsDefault && !language.IsDefault) + if (existingById.IsDefault && !language.IsDefault) { ModelState.AddModelError("IsDefault", "Cannot un-default the default language."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - existing.IsDefault = language.IsDefault; - existing.FallbackLanguageId = language.FallbackLanguageId; + existingById.IsDefault = language.IsDefault; + existingById.FallbackLanguageId = language.FallbackLanguageId; + existingById.IsoCode = language.IsoCode; // modifying an existing language can create a fallback, verify // note that the service will check again, dealing with race conditions - if (existing.FallbackLanguageId.HasValue) + if (existingById.FallbackLanguageId.HasValue) { var languages = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); - if (!languages.ContainsKey(existing.FallbackLanguageId.Value)) + if (!languages.ContainsKey(existingById.FallbackLanguageId.Value)) { ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - if (CreatesCycle(existing, languages)) + if (CreatesCycle(existingById, languages)) { - ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existing.FallbackLanguageId.Value].IsoCode} would create a circular path."); + ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existingById.FallbackLanguageId.Value].IsoCode} would create a circular path."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } } - Services.LocalizationService.Save(existing); - return Mapper.Map(existing); + Services.LocalizationService.Save(existingById); + return Mapper.Map(existingById); } // see LocalizationService diff --git a/src/Umbraco.Web/Editors/LogViewerController.cs b/src/Umbraco.Web/Editors/LogViewerController.cs index e5c75926f6..d48de8a00a 100644 --- a/src/Umbraco.Web/Editors/LogViewerController.cs +++ b/src/Umbraco.Web/Editors/LogViewerController.cs @@ -15,76 +15,102 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class LogViewerController : UmbracoAuthorizedJsonController { - private ILogViewer _logViewer; + private readonly ILogViewer _logViewer; public LogViewerController(ILogViewer logViewer) { - _logViewer = logViewer; + _logViewer = logViewer ?? throw new ArgumentNullException(nameof(logViewer)); } - private bool CanViewLogs() + private bool CanViewLogs(LogTimePeriod logTimePeriod) { //Can the interface deal with Large Files if (_logViewer.CanHandleLargeLogs) return true; //Interface CheckCanOpenLogs - return _logViewer.CheckCanOpenLogs(startDate: DateTime.Now.AddDays(-1), endDate: DateTime.Now); + return _logViewer.CheckCanOpenLogs(logTimePeriod); } [HttpGet] - public bool GetCanViewLogs() + public bool GetCanViewLogs([FromUri] DateTime? startDate = null,[FromUri] DateTime? endDate = null) { - return CanViewLogs(); + var logTimePeriod = GetTimePeriod(startDate, endDate); + return CanViewLogs(logTimePeriod); } [HttpGet] - public int GetNumberOfErrors() + public int GetNumberOfErrors([FromUri] DateTime? startDate = null,[FromUri] DateTime? endDate = null) { + var logTimePeriod = GetTimePeriod(startDate, endDate); //We will need to stop the request if trying to do this on a 1GB file - if (CanViewLogs() == false) + if (CanViewLogs(logTimePeriod) == false) { throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size")); } - return _logViewer.GetNumberOfErrors(startDate: DateTime.Now.AddDays(-1), endDate: DateTime.Now); + return _logViewer.GetNumberOfErrors(logTimePeriod); } [HttpGet] - public LogLevelCounts GetLogLevelCounts() + public LogLevelCounts GetLogLevelCounts([FromUri] DateTime? startDate = null,[FromUri] DateTime? endDate = null) { + var logTimePeriod = GetTimePeriod(startDate, endDate); //We will need to stop the request if trying to do this on a 1GB file - if (CanViewLogs() == false) + if (CanViewLogs(logTimePeriod) == false) { throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size")); } - return _logViewer.GetLogLevelCounts(startDate: DateTime.Now.AddDays(-1), endDate: DateTime.Now); + return _logViewer.GetLogLevelCounts(logTimePeriod); } [HttpGet] - public IEnumerable GetMessageTemplates() + public IEnumerable GetMessageTemplates([FromUri] DateTime? startDate = null,[FromUri] DateTime? endDate = null) { + var logTimePeriod = GetTimePeriod(startDate, endDate); //We will need to stop the request if trying to do this on a 1GB file - if (CanViewLogs() == false) + if (CanViewLogs(logTimePeriod) == false) { throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size")); } - return _logViewer.GetMessageTemplates(startDate: DateTime.Now.AddDays(-1), endDate: DateTime.Now); + return _logViewer.GetMessageTemplates(logTimePeriod); } [HttpGet] - public PagedResult GetLogs(string orderDirection = "Descending", int pageNumber = 1, string filterExpression = null, [FromUri]string[] logLevels = null) + public PagedResult GetLogs(string orderDirection = "Descending", int pageNumber = 1, string filterExpression = null, [FromUri]string[] logLevels = null, [FromUri] DateTime? startDate = null,[FromUri] DateTime? endDate = null) { + var logTimePeriod = GetTimePeriod(startDate, endDate); + //We will need to stop the request if trying to do this on a 1GB file - if (CanViewLogs() == false) + if (CanViewLogs(logTimePeriod) == false) { throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Unable to view logs, due to size")); } var direction = orderDirection == "Descending" ? Direction.Descending : Direction.Ascending; - return _logViewer.GetLogs(startDate: DateTime.Now.AddDays(-1), endDate: DateTime.Now, filterExpression: filterExpression, pageNumber: pageNumber, orderDirection: direction, logLevels: logLevels); + + return _logViewer.GetLogs(logTimePeriod, filterExpression: filterExpression, pageNumber: pageNumber, orderDirection: direction, logLevels: logLevels); + } + + private static LogTimePeriod GetTimePeriod(DateTime? startDate, DateTime? endDate) + { + if (startDate == null || endDate == null) + { + var now = DateTime.Now; + if (startDate == null) + { + startDate = now.AddDays(-1); + } + + if (endDate == null) + { + endDate = now; + } + } + + return new LogTimePeriod(startDate.Value, endDate.Value); } [HttpGet] @@ -104,5 +130,11 @@ namespace Umbraco.Web.Editors { return _logViewer.DeleteSavedSearch(item.Name, item.Query); } + + [HttpGet] + public string GetLogLevel() + { + return _logViewer.GetLogLevel(); + } } } diff --git a/src/Umbraco.Web/Editors/MacroRenderingController.cs b/src/Umbraco.Web/Editors/MacroRenderingController.cs index a2bbfe1dfd..7aabff6822 100644 --- a/src/Umbraco.Web/Editors/MacroRenderingController.cs +++ b/src/Umbraco.Web/Editors/MacroRenderingController.cs @@ -104,11 +104,11 @@ namespace Umbraco.Web.Editors if (m == null) throw new HttpResponseException(HttpStatusCode.NotFound); - var publishedContent = UmbracoContext.ContentCache.GetById(true, pageId); + var publishedContent = UmbracoContext.Content.GetById(true, pageId); //if it isn't supposed to be rendered in the editor then return an empty string //currently we cannot render a macro if the page doesn't yet exist - if (pageId == -1 || publishedContent == null || !m.UseInEditor) + if (pageId == -1 || publishedContent == null || m.DontRender) { var response = Request.CreateResponse(); //need to create a specific content result formatted as HTML since this controller has been configured @@ -123,22 +123,31 @@ namespace Umbraco.Web.Editors // Since a Macro might contain thing thats related to the culture of the "IPublishedContent" (ie Dictionary keys) we want // to set the current culture to the culture related to the content item. This is hacky but it works. - var culture = publishedContent.GetCulture(); - _variationContextAccessor.VariationContext = new VariationContext(); //must have an active variation context! - if (culture != null) - { - Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture.Culture); - _variationContextAccessor.VariationContext = new VariationContext(Thread.CurrentThread.CurrentCulture.Name); - } + // fixme + // in a 1:1 situation we do not handle the language being edited + // so the macro renders in the wrong language - var result = Request.CreateResponse(); - //need to create a specific content result formatted as HTML since this controller has been configured - //with only json formatters. - result.Content = new StringContent( - _componentRenderer.RenderMacro(pageId, m.Alias, macroParams).ToString(), - Encoding.UTF8, - "text/html"); - return result; + var culture = publishedContent.GetCultureFromDomains(); + + if (culture != null) + Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture); + + // must have an active variation context! + _variationContextAccessor.VariationContext = new VariationContext(culture); + + using (UmbracoContext.ForcedPreview(true)) + { + + var result = Request.CreateResponse(); + //need to create a specific content result formatted as HTML since this controller has been configured + //with only json formatters. + result.Content = new StringContent( + _componentRenderer.RenderMacro(pageId, m.Alias, macroParams).ToString(), + Encoding.UTF8, + "text/html"); + + return result; + } } [HttpPost] diff --git a/src/Umbraco.Web/Editors/MacrosController.cs b/src/Umbraco.Web/Editors/MacrosController.cs index d7d50236d5..38103400d9 100644 --- a/src/Umbraco.Web/Editors/MacrosController.cs +++ b/src/Umbraco.Web/Editors/MacrosController.cs @@ -19,6 +19,7 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; +using System.Web.Http.Controllers; namespace Umbraco.Web.Editors { @@ -28,6 +29,7 @@ namespace Umbraco.Web.Editors /// [PluginController("UmbracoApi")] [UmbracoTreeAuthorize(Constants.Trees.Macros)] + [MacrosControllerConfiguration] public class MacrosController : BackOfficeNotificationsController { private readonly IMacroService _macroService; @@ -38,6 +40,19 @@ namespace Umbraco.Web.Editors _macroService = Services.MacroService; } + /// + /// Configures this controller with a custom action selector + /// + private class MacrosControllerConfigurationAttribute : Attribute, IControllerConfiguration + { + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) + )); + } + } + /// /// Creates a new macro /// @@ -62,6 +77,11 @@ namespace Umbraco.Web.Editors return this.ReturnErrorResponse("Macro with this alias already exists"); } + if (name == null || name.Length > 255) + { + return this.ReturnErrorResponse("Name cannnot be more than 255 characters in length."); + } + try { var macro = new Macro @@ -92,39 +112,43 @@ namespace Umbraco.Web.Editors return this.ReturnErrorResponse($"Macro with id {id} does not exist"); } - var macroDisplay = new MacroDisplay - { - Alias = macro.Alias, - Id = macro.Id, - Key = macro.Key, - Name = macro.Name, - CacheByPage = macro.CacheByPage, - CacheByUser = macro.CacheByMember, - CachePeriod = macro.CacheDuration, - View = macro.MacroSource, - RenderInEditor = !macro.DontRender, - UseInEditor = macro.UseInEditor, - Path = $"-1,{macro.Id}" - }; - - var parameters = new List(); - - foreach (var param in macro.Properties.Values.OrderBy(x => x.SortOrder)) - { - parameters.Add(new MacroParameterDisplay - { - Editor = param.EditorAlias, - Key = param.Alias, - Label = param.Name, - Id = param.Id - }); - } - - macroDisplay.Parameters = parameters; + var macroDisplay = MapToDisplay(macro); return this.Request.CreateResponse(HttpStatusCode.OK, macroDisplay); } + [HttpGet] + public HttpResponseMessage GetById(Guid id) + { + var macro = _macroService.GetById(id); + + if (macro == null) + { + return this.ReturnErrorResponse($"Macro with id {id} does not exist"); + } + + var macroDisplay = MapToDisplay(macro); + + return this.Request.CreateResponse(HttpStatusCode.OK, macroDisplay); + } + + [HttpGet] + public HttpResponseMessage GetById(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi == null) + this.ReturnErrorResponse($"Macro with id {id} does not exist"); + + var macro = _macroService.GetById(guidUdi.Guid); + if (macro == null) + { + return this.ReturnErrorResponse($"Macro with id {id} does not exist"); + } + + var macroDisplay = MapToDisplay(macro); + + return this.Request.CreateResponse(HttpStatusCode.OK, macroDisplay); + } [HttpPost] public HttpResponseMessage DeleteById(int id) @@ -149,6 +173,11 @@ namespace Umbraco.Web.Editors return this.ReturnErrorResponse($"No macro data found in request"); } + if (macroDisplay.Name == null || macroDisplay.Name.Length > 255) + { + return this.ReturnErrorResponse("Name cannnot be more than 255 characters in length."); + } + var macro = _macroService.GetById(int.Parse(macroDisplay.Id.ToString())); if (macro == null) @@ -219,6 +248,39 @@ namespace Umbraco.Web.Editors return this.Request.CreateResponse(HttpStatusCode.OK, Current.ParameterEditors); } + /// + /// Gets the available parameter editors grouped by their group. + /// + /// + /// The . + /// + public HttpResponseMessage GetGroupedParameterEditors() + { + var parameterEditors = Current.ParameterEditors.ToArray(); + + var grouped = parameterEditors + .GroupBy(x => x.Group.IsNullOrWhiteSpace() ? "" : x.Group.ToLower()) + .OrderBy(x => x.Key) + .ToDictionary(group => group.Key, group => group.OrderBy(d => d.Name).AsEnumerable()); + + return this.Request.CreateResponse(HttpStatusCode.OK, grouped); + } + + /// + /// Get parameter editor by alias. + /// + /// + /// The . + /// + public HttpResponseMessage GetParameterEditorByAlias(string alias) + { + var parameterEditors = Current.ParameterEditors.ToArray(); + + var parameterEditor = parameterEditors.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + + return this.Request.CreateResponse(HttpStatusCode.OK, parameterEditor); + } + /// /// Returns a error response and optionally logs it /// @@ -280,7 +342,6 @@ namespace Umbraco.Web.Editors /// Finds partial view files in app plugin folders. /// /// - /// The . /// private IEnumerable FindPartialViewFilesInPluginFolders() { @@ -342,5 +403,29 @@ namespace Umbraco.Web.Editors return files; } + + /// + /// Used to map an instance to a + /// + /// + /// + private MacroDisplay MapToDisplay(IMacro macro) + { + var display = Mapper.Map(macro); + + var parameters = macro.Properties.Values + .OrderBy(x => x.SortOrder) + .Select(x => new MacroParameterDisplay() + { + Editor = x.EditorAlias, + Key = x.Alias, + Label = x.Name, + Id = x.Id + }); + + display.Parameters = parameters; + + return display; + } } } diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 1097646830..c2561e334e 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -36,6 +36,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Web.ContentApps; using Umbraco.Web.Editors.Binders; using Umbraco.Web.Editors.Filters; +using Umbraco.Core.Models.Entities; namespace Umbraco.Web.Editors { @@ -246,7 +247,6 @@ namespace Umbraco.Web.Editors /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(int id, - bool ignoreUserStartNodes, int pageNumber = 0, int pageSize = 0, string orderBy = "SortOrder", @@ -256,7 +256,7 @@ namespace Umbraco.Web.Editors { //if a request is made for the root node data but the user's start node is not the default, then // we need to return their start nodes - if (id == Constants.System.Root && UserStartNodes.Length > 0 && (UserStartNodes.Contains(Constants.System.Root) == false && ignoreUserStartNodes == false)) + if (id == Constants.System.Root && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) { if (pageNumber > 0) return new PagedResult>(0, 0, 0); @@ -312,7 +312,6 @@ namespace Umbraco.Web.Editors } /// - /// This method is obsolete, use the overload with ignoreUserStartNodes instead /// Returns the child media objects - using the entity GUID id /// /// @@ -323,34 +322,8 @@ namespace Umbraco.Web.Editors /// /// /// - [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(Guid id, - int pageNumber = 0, - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - return GetChildren(id, ignoreUserStartNodes: false, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); - } - - /// - /// Returns the child media objects - using the entity GUID id - /// - /// - /// /// If set to true, user and group start node permissions will be ignored. - /// - /// - /// - /// - /// - /// - /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren(Guid id, - bool ignoreUserStartNodes, int pageNumber = 0, int pageSize = 0, string orderBy = "SortOrder", @@ -361,13 +334,12 @@ namespace Umbraco.Web.Editors var entity = Services.EntityService.Get(id); if (entity != null) { - return GetChildren(entity.Id, ignoreUserStartNodes, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); } throw new HttpResponseException(HttpStatusCode.NotFound); } /// - /// This method is obsolete, use the overload with ignoreUserStartNodes instead /// Returns the child media objects - using the entity UDI id /// /// @@ -378,7 +350,6 @@ namespace Umbraco.Web.Editors /// /// /// - [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(Udi id, int pageNumber = 0, @@ -387,31 +358,6 @@ namespace Umbraco.Web.Editors Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") - { - return GetChildren(id, ignoreUserStartNodes: false, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); - } - - /// - /// Returns the child media objects - using the entity UDI id - /// - /// - /// If set to true, user and group start node permissions will be ignored. - /// - /// - /// - /// - /// - /// - /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren(Udi id, - bool ignoreUserStartNodes, - int pageNumber = 0, - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") { var guidUdi = id as GuidUdi; if (guidUdi != null) @@ -419,7 +365,7 @@ namespace Umbraco.Web.Editors var entity = Services.EntityService.Get(guidUdi.Guid); if (entity != null) { - return GetChildren(entity.Id, ignoreUserStartNodes, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); } } @@ -870,7 +816,7 @@ namespace Umbraco.Web.Editors } else { - throw new EntityNotFoundException(parentId, "The passed id doesn't exist"); + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.NotFound, "The passed id doesn't exist")); } } else @@ -998,5 +944,31 @@ namespace Umbraco.Web.Editors return hasPathAccess; } + + public PagedResult GetPagedReferences(int id, string entityType, int pageNumber = 1, int pageSize = 100) + { + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } + + var objectType = ObjectTypes.GetUmbracoObjectType(entityType); + var udiType = ObjectTypes.GetUdiType(objectType); + + var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out var totalRecords, objectType); + + return new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = relations.Cast().Select(rel => new EntityBasic + { + Id = rel.Id, + Key = rel.Key, + Udi = Udi.Create(udiType, rel.Key), + Icon = rel.ContentTypeIcon, + Name = rel.Name, + Alias = rel.ContentTypeAlias + }) + }; + } } } diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 391f3b8cf9..3a4026423a 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -19,6 +19,7 @@ using Umbraco.Core.Dictionary; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Web.Composing; +using IMediaType = Umbraco.Core.Models.IMediaType; namespace Umbraco.Web.Editors { @@ -109,7 +110,7 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage GetAvailableCompositeMediaTypes(GetAvailableCompositionsFilter filter) { - var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.MediaType, filter.FilterContentTypes, filter.FilterPropertyTypes) + var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.MediaType, filter.FilterContentTypes, filter.FilterPropertyTypes, filter.IsElement) .Select(x => new { contentType = x.Item1, @@ -135,9 +136,18 @@ namespace Umbraco.Web.Editors } public MediaTypeDisplay GetEmpty(int parentId) { - var ct = new MediaType(parentId) {Icon = "icon-picture"}; + IMediaType mt; + if (parentId != Constants.System.Root) + { + var parent = Services.MediaTypeService.Get(parentId); + mt = parent != null ? new MediaType(parent, string.Empty) : new MediaType(parentId); + } + else + mt = new MediaType(parentId); - var dto = Mapper.Map(ct); + mt.Icon = Constants.Icons.MediaImage; + + var dto = Mapper.Map(mt); return dto; } @@ -230,11 +240,11 @@ namespace Umbraco.Web.Editors } var contentType = Services.MediaTypeService.Get(contentItem.ContentTypeId); - var ids = contentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray(); + var ids = contentType.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value).ToArray(); if (ids.Any() == false) return Enumerable.Empty(); - types = Services.MediaTypeService.GetAll(ids).ToList(); + types = Services.MediaTypeService.GetAll(ids).OrderBy(c => ids.IndexOf(c.Id)).ToList(); } var basics = types.Select(Mapper.Map).ToList(); @@ -245,7 +255,7 @@ namespace Umbraco.Web.Editors basic.Description = TranslateItem(basic.Description); } - return basics.OrderBy(x => x.Name); + return basics.OrderBy(c => contentId == Constants.System.Root ? c.Name : string.Empty); } /// diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index df7faeccf4..fffead9155 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -89,7 +89,7 @@ namespace Umbraco.Web.Editors [FromUri]string[] filterContentTypes, [FromUri]string[] filterPropertyTypes) { - var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType, filterContentTypes, filterPropertyTypes) + var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType, filterContentTypes, filterPropertyTypes, false) .Select(x => new { contentType = x.Item1, @@ -102,7 +102,7 @@ namespace Umbraco.Web.Editors public MemberTypeDisplay GetEmpty() { var ct = new MemberType(-1); - ct.Icon = "icon-user"; + ct.Icon = Constants.Icons.Member; var dto = Mapper.Map(ct); return dto; diff --git a/src/Umbraco.Web/Editors/PackageInstallController.cs b/src/Umbraco.Web/Editors/PackageInstallController.cs index 6b2bd07fbd..1030498734 100644 --- a/src/Umbraco.Web/Editors/PackageInstallController.cs +++ b/src/Umbraco.Web/Editors/PackageInstallController.cs @@ -102,8 +102,8 @@ namespace Umbraco.Web.Editors model.LicenseUrl = ins.LicenseUrl; model.Readme = ins.Readme; model.ConflictingMacroAliases = ins.Warnings.ConflictingMacros.ToDictionary(x => x.Name, x => x.Alias); - model.ConflictingStyleSheetNames = ins.Warnings.ConflictingStylesheets.ToDictionary(x => x.Name, x => x.Alias); ; - model.ConflictingTemplateAliases = ins.Warnings.ConflictingTemplates.ToDictionary(x => x.Name, x => x.Alias); ; + model.ConflictingStyleSheetNames = ins.Warnings.ConflictingStylesheets.ToDictionary(x => x.Name, x => x.Alias); + model.ConflictingTemplateAliases = ins.Warnings.ConflictingTemplates.ToDictionary(x => x.Name, x => x.Alias); model.ContainsUnsecureFiles = ins.Warnings.UnsecureFiles.Any(); model.Url = ins.Url; model.Version = ins.Version; diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index e124f3a9a4..2698a68b40 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -151,6 +151,8 @@ namespace Umbraco.Web.Editors /// public Attempt ChangePasswordWithMembershipProvider(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider) { + var umbracoBaseProvider = membershipProvider as MembershipProviderBase; + // YES! It is completely insane how many options you have to take into account based on the membership provider. yikes! if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel)); @@ -179,7 +181,7 @@ namespace Umbraco.Web.Editors //this is only possible when using a membership provider if the membership provider supports AllowManuallyChangingPassword if (passwordModel.NewPassword.IsNullOrWhiteSpace() == false) { - if (membershipProvider is MembershipProviderBase umbracoBaseProvider && umbracoBaseProvider.AllowManuallyChangingPassword) + if (umbracoBaseProvider !=null && umbracoBaseProvider.AllowManuallyChangingPassword) { //this provider allows manually changing the password without the old password, so we can just do it try diff --git a/src/Umbraco.Web/Editors/RelationTypeController.cs b/src/Umbraco.Web/Editors/RelationTypeController.cs index faafbb79f1..f12faf77cc 100644 --- a/src/Umbraco.Web/Editors/RelationTypeController.cs +++ b/src/Umbraco.Web/Editors/RelationTypeController.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Web.Http; +using System.Linq; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; @@ -45,14 +47,28 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var relations = Services.RelationService.GetByRelationTypeId(relationType.Id); - var display = Mapper.Map(relationType); - display.Relations = Mapper.MapEnumerable(relations); - + return display; } + public PagedResult GetPagedResults(int id, int pageNumber = 1, int pageSize = 100) + { + + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } + + // Ordering do we need to pass through? + var relations = Services.RelationService.GetPagedByRelationTypeId(id, pageNumber -1, pageSize, out long totalRecords); + + return new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = relations.Select(x => Mapper.Map(x)) + }; + } + /// /// Gets a list of object types which can be associated via relations. /// @@ -84,11 +100,7 @@ namespace Umbraco.Web.Editors /// A containing the persisted relation type's ID. public HttpResponseMessage PostCreate(RelationTypeSave relationType) { - var relationTypePersisted = new RelationType(relationType.ChildObjectType, relationType.ParentObjectType, relationType.Name.ToSafeAlias(true)) - { - Name = relationType.Name, - IsBidirectional = relationType.IsBidirectional - }; + var relationTypePersisted = new RelationType(relationType.Name, relationType.Name.ToSafeAlias(true), relationType.IsBidirectional, relationType.ChildObjectType, relationType.ParentObjectType); try { diff --git a/src/Umbraco.Web/Editors/TemplateController.cs b/src/Umbraco.Web/Editors/TemplateController.cs index 8da5e80f2e..903446d23c 100644 --- a/src/Umbraco.Web/Editors/TemplateController.cs +++ b/src/Umbraco.Web/Editors/TemplateController.cs @@ -194,7 +194,9 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var template = Services.FileService.CreateTemplateWithIdentity(display.Name, display.Alias, display.Content, master); + // we need to pass the template name as alias to keep the template file casing consistent with templates created with content + // - see comment in FileService.CreateTemplateForContentType for additional details + var template = Services.FileService.CreateTemplateWithIdentity(display.Name, display.Name, display.Content, master); Mapper.Map(template, display); } diff --git a/src/Umbraco.Web/Editors/TinyMceController.cs b/src/Umbraco.Web/Editors/TinyMceController.cs new file mode 100644 index 0000000000..c6c1acd048 --- /dev/null +++ b/src/Umbraco.Web/Editors/TinyMceController.cs @@ -0,0 +1,110 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using System.Web.Http; +using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Services; +using Umbraco.Web.Composing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Editors +{ + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorize( + Constants.Applications.Content, + Constants.Applications.Media, + Constants.Applications.Members)] + public class TinyMceController : UmbracoAuthorizedApiController + { + private IMediaService _mediaService; + private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + + + public TinyMceController(IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) + { + _mediaService = mediaService; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + } + + [HttpPost] + public async Task UploadImage() + { + + if (Request.Content.IsMimeMultipartContent() == false) + { + throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); + } + + // Create an unique folder path to help with concurrent users to avoid filename clash + var imageTempPath = IOHelper.MapPath(SystemDirectories.TempImageUploads + "/" + Guid.NewGuid().ToString()); + + // Temp folderpath (Files come in as bodypart & will need to move/saved into imgTempPath + var folderPath = IOHelper.MapPath(SystemDirectories.TempFileUploads); + + // Ensure image temp path exists + if(Directory.Exists(imageTempPath) == false) + { + Directory.CreateDirectory(imageTempPath); + } + + // File uploaded will be saved as bodypart into TEMP folder + var provider = new MultipartFormDataStreamProvider(folderPath); + var result = await Request.Content.ReadAsMultipartAsync(provider); + + // Must have a file + if (result.FileData.Count == 0) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + // Should only have one file + if (result.FileData.Count > 1) + { + return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Only one file can be uploaded at a time"); + } + + // Really we should only have one file per request to this endpoint + var file = result.FileData[0]; + var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }).TrimEnd(); + var safeFileName = fileName.ToSafeFileName(); + var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); + + if (Current.Configs.Settings().Content.IsFileAllowedForUpload(ext) == false || Current.Configs.Settings().Content.ImageFileTypes.Contains(ext) == false) + { + // Throw some error - to say can't upload this IMG type + return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "This is not an image filetype extension that is approved"); + } + + //var mediaItemName = fileName.ToFriendlyName(); + var currentFile = file.LocalFileName; + var newFilePath = imageTempPath + IOHelper.DirSepChar + safeFileName; + var relativeNewFilePath = IOHelper.GetRelativePath(newFilePath); + + try + { + // Move the file from bodypart to a real filename + // This is what we return from this API so RTE updates img src path + // Until we fully persist & save the media item when persisting + // If we find data attribute + File.Move(currentFile, newFilePath); + } + catch (Exception ex) + { + // IOException, PathTooLong, DirectoryNotFound, UnathorizedAccess + Logger.Error(ex, "Error when trying to move {CurrentFilePath} to {NewFilePath}", currentFile, newFilePath); + return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, $"Error when trying to move {currentFile} to {newFilePath}", ex); + } + + return Request.CreateResponse(HttpStatusCode.OK, new { tmpLocation = relativeNewFilePath }); + } + } +} diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs index a61926740a..8991bcdd6a 100644 --- a/src/Umbraco.Web/Editors/TourController.cs +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -30,6 +30,10 @@ namespace Umbraco.Web.Editors if (Current.Configs.Settings().BackOffice.Tours.EnableTours == false) return result; + var user = Composing.Current.UmbracoContext.Security.CurrentUser; + if (user == null) + return result; + //get all filters that will be applied to all tour aliases var aliasOnlyFilters = _filters.Where(x => x.PluginName == null && x.TourFileName == null).ToList(); @@ -47,31 +51,37 @@ namespace Umbraco.Web.Editors } //collect all tour files in packages - foreach (var plugin in Directory.EnumerateDirectories(IOHelper.MapPath(SystemDirectories.AppPlugins))) + var appPlugins = IOHelper.MapPath(SystemDirectories.AppPlugins); + if (Directory.Exists(appPlugins)) { - var pluginName = Path.GetFileName(plugin.TrimEnd('\\')); - var pluginFilters = _filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName)).ToList(); - - //If there is any filter applied to match the plugin only (no file or tour alias) then ignore the plugin entirely - var isPluginFiltered = pluginFilters.Any(x => x.TourFileName == null && x.TourAlias == null); - if (isPluginFiltered) continue; - - //combine matched package filters with filters not specific to a package - var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList(); - - foreach (var backofficeDir in Directory.EnumerateDirectories(plugin, "backoffice")) + foreach (var plugin in Directory.EnumerateDirectories(appPlugins)) { - foreach (var tourDir in Directory.EnumerateDirectories(backofficeDir, "tours")) + var pluginName = Path.GetFileName(plugin.TrimEnd('\\')); + var pluginFilters = _filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName)) + .ToList(); + + //If there is any filter applied to match the plugin only (no file or tour alias) then ignore the plugin entirely + var isPluginFiltered = pluginFilters.Any(x => x.TourFileName == null && x.TourAlias == null); + if (isPluginFiltered) continue; + + //combine matched package filters with filters not specific to a package + var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList(); + + foreach (var backofficeDir in Directory.EnumerateDirectories(plugin, "backoffice")) { - foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json")) + foreach (var tourDir in Directory.EnumerateDirectories(backofficeDir, "tours")) { - TryParseTourFile(tourFile, result, combinedFilters, aliasOnlyFilters, pluginName); + foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json")) + { + TryParseTourFile(tourFile, result, combinedFilters, aliasOnlyFilters, pluginName); + } } } } } + //Get all allowed sections for the current user - var allowedSections = Composing.Current.UmbracoContext.Security.CurrentUser.AllowedSections.ToList(); + var allowedSections = user.AllowedSections.ToList(); var toursToBeRemoved = new List(); @@ -100,6 +110,39 @@ namespace Umbraco.Web.Editors return result.Except(toursToBeRemoved).OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase); } + /// + /// Gets a tours for a specific doctype + /// + /// The documenttype alias + /// A + public IEnumerable GetToursForDoctype(string doctypeAlias) + { + var tourFiles = this.GetTours(); + + var doctypeAliasWithCompositions = new List + { + doctypeAlias + }; + + var contentType = this.Services.ContentTypeService.Get(doctypeAlias); + + if (contentType != null) + { + doctypeAliasWithCompositions.AddRange(contentType.CompositionAliases()); + } + + return tourFiles.SelectMany(x => x.Tours) + .Where(x => + { + if (string.IsNullOrEmpty(x.ContentType)) + { + return false; + } + var contentTypes = x.ContentType.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(ct => ct.Trim()); + return contentTypes.Intersect(doctypeAliasWithCompositions).Any(); + }); + } + private void TryParseTourFile(string tourFile, ICollection result, List filters, diff --git a/src/Umbraco.Web/Editors/UpdateCheckController.cs b/src/Umbraco.Web/Editors/UpdateCheckController.cs index cd11382d13..480298fea7 100644 --- a/src/Umbraco.Web/Editors/UpdateCheckController.cs +++ b/src/Umbraco.Web/Editors/UpdateCheckController.cs @@ -2,11 +2,14 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading.Tasks; using System.Web.Http.Filters; +using Semver; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Web.Models; using Umbraco.Web.Mvc; @@ -15,8 +18,17 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class UpdateCheckController : UmbracoAuthorizedJsonController { + private readonly IUpgradeService _upgradeService; + + public UpdateCheckController() { } + + public UpdateCheckController(IUpgradeService upgradeService) + { + _upgradeService = upgradeService; + } + [UpdateCheckResponseFilter] - public UpgradeCheckResponse GetCheck() + public async Task GetCheck() { var updChkCookie = Request.Headers.GetCookies("UMB_UPDCHK").FirstOrDefault(); var updateCheckCookie = updChkCookie != null ? updChkCookie["UMB_UPDCHK"].Value : ""; @@ -24,19 +36,22 @@ namespace Umbraco.Web.Editors { try { - var check = new org.umbraco.update.CheckForUpgrade(); - var result = check.CheckUpgrade(UmbracoVersion.Current.Major, - UmbracoVersion.Current.Minor, - UmbracoVersion.Current.Build, - UmbracoVersion.Comment); + var version = new SemVersion(UmbracoVersion.Current.Major, UmbracoVersion.Current.Minor, + UmbracoVersion.Current.Build, UmbracoVersion.Comment); + var result = await _upgradeService.CheckUpgrade(version); - return new UpgradeCheckResponse(result.UpgradeType.ToString(), result.Comment, result.UpgradeUrl); + return new UpgradeCheckResponse(result.UpgradeType, result.Comment, result.UpgradeUrl); } catch (System.Net.WebException) { //this occurs if the server is down or cannot be reached return null; } + catch (System.Web.Services.Protocols.SoapException) + { + //this occurs if the server has a timeout + return null; + } } return null; } diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index e79cfd625c..b081ca6137 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -50,13 +50,8 @@ namespace Umbraco.Web.Editors if (isAuthorized == false) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, isAuthorized.Result)); - //current user needs to be added to a new group if not an admin (possibly only if no other users are added?) to avoid a 401 - if(!Security.CurrentUser.IsAdmin() && (userGroupSave.Id == null || Convert.ToInt32(userGroupSave.Id) >= 0)/* && !userGroupSave.Users.Any() */) - { - var userIds = userGroupSave.Users.ToList(); - userIds.Add(Security.CurrentUser.Id); - userGroupSave.Users = userIds; - } + //need to ensure current user is in a group if not an admin to avoid a 401 + EnsureNonAdminUserIsInSavedUserGroup(userGroupSave); //save the group Services.UserService.Save(userGroupSave.PersistedUserGroup, userGroupSave.Users.ToArray()); @@ -87,6 +82,23 @@ namespace Umbraco.Web.Editors return display; } + private void EnsureNonAdminUserIsInSavedUserGroup(UserGroupSave userGroupSave) + { + if (Security.CurrentUser.IsAdmin()) + { + return; + } + + var userIds = userGroupSave.Users.ToList(); + if (userIds.Contains(Security.CurrentUser.Id)) + { + return; + } + + userIds.Add(Security.CurrentUser.Id); + userGroupSave.Users = userIds; + } + /// /// Returns the scaffold for creating a new user group /// @@ -142,8 +154,8 @@ namespace Umbraco.Web.Editors public HttpResponseMessage PostDeleteUserGroups([FromUri] int[] userGroupIds) { var userGroups = Services.UserService.GetAllUserGroups(userGroupIds) - //never delete the admin group or translators group - .Where(x => x.Alias != Constants.Security.AdminGroupAlias && x.Alias != Constants.Security.TranslatorGroupAlias) + //never delete the admin group, sensitive data or translators group + .Where(x => !x.IsSystemUserGroup()) .ToArray(); foreach (var userGroup in userGroups) { diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index e133d2e6b0..c27d7c9f48 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Runtime.Serialization; +using System.Security.Cryptography; using System.Threading.Tasks; using System.Web; using System.Web.Http; @@ -25,6 +26,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Editors.Filters; +using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; @@ -105,7 +107,7 @@ namespace Umbraco.Web.Editors if (Current.Configs.Settings().Content.DisallowedUploadFiles.Contains(ext) == false) { //generate a path of known data, we don't want this path to be guessable - user.Avatar = "UserAvatars/" + (user.Id + safeFileName).ToSHA1() + "." + ext; + user.Avatar = "UserAvatars/" + (user.Id + safeFileName).GenerateHash() + "." + ext; using (var fs = System.IO.File.OpenRead(file.LocalFileName)) { @@ -543,34 +545,12 @@ namespace Umbraco.Web.Editors hasErrors = true; } - // if the found user has his email for username, we want to keep this synced when changing the email. + // if the found user has their email for username, we want to keep this synced when changing the email. // we have already cross-checked above that the email isn't colliding with anything, so we can safely assign it here. if (Current.Configs.Settings().Security.UsernameIsEmail && found.Username == found.Email && userSave.Username != userSave.Email) { userSave.Username = userSave.Email; - } - - if (userSave.ChangePassword != null) - { - var passwordChanger = new PasswordChanger(Logger, Services.UserService, UmbracoContext.HttpContext); - - //this will change the password and raise appropriate events - var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, found, userSave.ChangePassword, UserManager); - if (passwordChangeResult.Success) - { - //need to re-get the user - found = Services.UserService.GetUserById(intId.Result); - } - else - { - hasErrors = true; - - foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames) - { - ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage); - } - } - } + } if (hasErrors) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); @@ -586,6 +566,51 @@ namespace Umbraco.Web.Editors return display; } + /// + /// + /// + /// + /// + public async Task> PostChangePassword(ChangingPasswordModel changingPasswordModel) + { + changingPasswordModel = changingPasswordModel ?? throw new ArgumentNullException(nameof(changingPasswordModel)); + + if (ModelState.IsValid == false) + { + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + var intId = changingPasswordModel.Id.TryConvertTo(); + if (intId.Success == false) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var found = Services.UserService.GetUserById(intId.Result); + if (found == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var passwordChanger = new PasswordChanger(Logger, Services.UserService, UmbracoContext.HttpContext); + var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, found, changingPasswordModel, UserManager); + + if (passwordChangeResult.Success) + { + var result = new ModelWithNotifications(passwordChangeResult.Result.ResetPassword); + result.AddSuccessNotification(Services.TextService.Localize("general/success"), Services.TextService.Localize("user/passwordChangedGeneric")); + return result; + } + + foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames) + { + ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage); + } + + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + + /// /// Disables the users with the given user ids /// diff --git a/src/Umbraco.Web/ExamineExtensions.cs b/src/Umbraco.Web/ExamineExtensions.cs index 9a9fa98d95..421993f8fd 100644 --- a/src/Umbraco.Web/ExamineExtensions.cs +++ b/src/Umbraco.Web/ExamineExtensions.cs @@ -2,32 +2,95 @@ using System.Collections.Generic; using System.Linq; using Examine; -using Umbraco.Core; +using Examine.LuceneEngine.Providers; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Examine; using Umbraco.Web.PublishedCache; namespace Umbraco.Web { /// - /// Extension methods for Examine + /// Extension methods for Examine. /// public static class ExamineExtensions { + /// + /// Creates an containing all content from the . + /// + /// The search results. + /// The cache to fetch the content from. + /// + /// An containing all content. + /// + /// cache + /// + /// Search results are skipped if it can't be fetched from the by its integer id. + /// public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedCache cache) { - var list = new List(); + if (cache == null) throw new ArgumentNullException(nameof(cache)); - foreach (var result in results.OrderByDescending(x => x.Score)) + var publishedSearchResults = new List(); + + foreach (var result in results) { - if (!int.TryParse(result.Id, out var intId)) continue; //invalid - var content = cache.GetById(intId); - if (content == null) continue; // skip if this doesn't exist in the cache - - list.Add(new PublishedSearchResult(content, result.Score)); - + if (int.TryParse(result.Id, out var contentId) && + cache.GetById(contentId) is IPublishedContent content) + { + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); + } } - return list; + return publishedSearchResults; + } + + /// + /// Creates an containing all content, media or members from the . + /// + /// The search results. + /// The snapshot. + /// + /// An containing all content, media or members. + /// + /// snapshot + /// + /// Search results are skipped if it can't be fetched from the respective cache by its integer id. + /// + public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedSnapshot snapshot) + { + if (snapshot == null) throw new ArgumentNullException(nameof(snapshot)); + + var publishedSearchResults = new List(); + + foreach (var result in results) + { + if (int.TryParse(result.Id, out var contentId) && + result.Values.TryGetValue(LuceneIndex.CategoryFieldName, out var indexType)) + { + IPublishedContent content; + switch (indexType) + { + case IndexTypes.Content: + content = snapshot.Content.GetById(contentId); + break; + case IndexTypes.Media: + content = snapshot.Media.GetById(contentId); + break; + case IndexTypes.Member: + content = snapshot.Members.GetById(contentId); + break; + default: + continue; + } + + if (content != null) + { + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); + } + } + } + + return publishedSearchResults; } } } diff --git a/src/Umbraco.Web/GridTemplateExtensions.cs b/src/Umbraco.Web/GridTemplateExtensions.cs index 2c6e66c68b..81dc33d2c6 100644 --- a/src/Umbraco.Web/GridTemplateExtensions.cs +++ b/src/Umbraco.Web/GridTemplateExtensions.cs @@ -1,13 +1,10 @@ using System; -using System.Web.Mvc.Html; using System.Web.Mvc; -using System.IO; -using Umbraco.Core.Exceptions; +using System.Web.Mvc.Html; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web { - public static class GridTemplateExtensions { public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedProperty property, string framework = "bootstrap3") @@ -26,18 +23,20 @@ namespace Umbraco.Web public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias) { - if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentNullOrEmptyException(nameof(propertyAlias)); + if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); + if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); return html.GetGridHtml(contentItem, propertyAlias, "bootstrap3"); } public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias, string framework) { - if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentNullOrEmptyException(nameof(propertyAlias)); + if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); + if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); var view = "Grid/" + framework; var prop = contentItem.GetProperty(propertyAlias); - if (prop == null) throw new NullReferenceException("No property type found with alias " + propertyAlias); + if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); var model = prop.GetValue(); var asString = model as string; @@ -46,6 +45,34 @@ namespace Umbraco.Web return html.Partial(view, model); } + public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedElement contentItem) + { + return html.GetGridHtml(contentItem, "bodyText", "bootstrap3"); + } + + public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedElement contentItem, string propertyAlias) + { + if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); + if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); + + return html.GetGridHtml(contentItem, propertyAlias, "bootstrap3"); + } + + public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedElement contentItem, string propertyAlias, string framework) + { + if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); + if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); + + var view = "Grid/" + framework; + var prop = contentItem.GetProperty(propertyAlias); + if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); + var model = prop.GetValue(); + + var asString = model as string; + if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty); + + return html.Partial(view, model); + } public static MvcHtmlString GetGridHtml(this IPublishedProperty property, HtmlHelper html, string framework = "bootstrap3") { var asString = property.GetValue() as string; @@ -54,23 +81,28 @@ namespace Umbraco.Web var view = "Grid/" + framework; return html.Partial(view, property.GetValue()); } + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html) { return GetGridHtml(contentItem, html, "bodyText", "bootstrap3"); } + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias) { - if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentNullOrEmptyException(nameof(propertyAlias)); + if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); + if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); return GetGridHtml(contentItem, html, propertyAlias, "bootstrap3"); } + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias, string framework) { - if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentNullOrEmptyException(nameof(propertyAlias)); + if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); + if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); var view = "Grid/" + framework; var prop = contentItem.GetProperty(propertyAlias); - if (prop == null) throw new NullReferenceException("No property type found with alias " + propertyAlias); + if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); var model = prop.GetValue(); var asString = model as string; @@ -78,12 +110,5 @@ namespace Umbraco.Web return html.Partial(view, model); } - - private class FakeView : IView - { - public void Render(ViewContext viewContext, TextWriter writer) - { - } - } } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs new file mode 100644 index 0000000000..0c3e2f3d91 --- /dev/null +++ b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Serilog.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Web.HealthCheck.Checks.Data +{ + [HealthCheck( + "73DD0C1C-E0CA-4C31-9564-1DCA509788AF", + "Database data integrity check", + Description = "Checks for various data integrity issues in the Umbraco database.", + Group = "Data Integrity")] + public class DatabaseIntegrityCheck : HealthCheck + { + private readonly IContentService _contentService; + private readonly IMediaService _mediaService; + private const string _fixMediaPaths = "fixMediaPaths"; + private const string _fixContentPaths = "fixContentPaths"; + private const string _fixMediaPathsTitle = "Fix media paths"; + private const string _fixContentPathsTitle = "Fix content paths"; + + public DatabaseIntegrityCheck(IContentService contentService, IMediaService mediaService) + { + _contentService = contentService; + _mediaService = mediaService; + } + + /// + /// Get the status for this health check + /// + /// + public override IEnumerable GetStatus() + { + //return the statuses + return new[] + { + CheckDocuments(false), + CheckMedia(false) + }; + } + + private HealthCheckStatus CheckMedia(bool fix) + { + return CheckPaths(_fixMediaPaths, _fixMediaPathsTitle, Core.Constants.UdiEntityType.Media, fix, + () => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix})); + } + + private HealthCheckStatus CheckDocuments(bool fix) + { + return CheckPaths(_fixContentPaths, _fixContentPathsTitle, Core.Constants.UdiEntityType.Document, fix, + () => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix})); + } + + private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, bool detailedReport, Func doCheck) + { + var report = doCheck(); + + var actions = new List(); + if (!report.Ok) + { + actions.Add(new HealthCheckAction(actionAlias, Id) + { + Name = actionName + }); + } + + return new HealthCheckStatus(GetReport(report, entityType, detailedReport)) + { + ResultType = report.Ok ? StatusResultType.Success : StatusResultType.Error, + Actions = actions + }; + } + + private static string GetReport(ContentDataIntegrityReport report, string entityType, bool detailed) + { + var sb = new StringBuilder(); + + if (report.Ok) + { + sb.AppendLine($"

All {entityType} paths are valid

"); + + if (!detailed) + return sb.ToString(); + } + else + { + sb.AppendLine($"

{report.DetectedIssues.Count} invalid {entityType} paths detected.

"); + } + + if (detailed && report.DetectedIssues.Count > 0) + { + sb.AppendLine("
    "); + foreach (var issueGroup in report.DetectedIssues.GroupBy(x => x.Value.IssueType)) + { + var countByGroup = issueGroup.Count(); + var fixedByGroup = issueGroup.Count(x => x.Value.Fixed); + sb.AppendLine("
  • "); + sb.AppendLine($"{countByGroup} issues of type {issueGroup.Key} ... {fixedByGroup} fixed"); + sb.AppendLine("
  • "); + } + sb.AppendLine("
"); + } + + return sb.ToString(); + } + + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + { + switch (action.Alias) + { + case _fixContentPaths: + return CheckDocuments(true); + case _fixMediaPaths: + return CheckMedia(true); + default: + throw new InvalidOperationException("Action not supported"); + } + } + } +} diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs index 7d644f8cf1..5e66bc47b1 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs @@ -78,7 +78,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Security var response = request.GetResponse(); // Check first for header - success = DoHttpHeadersContainHeader(response); + success = HasMatchingHeader(response.Headers.AllKeys); // If not found, and available, check for meta-tag if (success == false && _metaTagOptionAvailable) @@ -113,9 +113,9 @@ namespace Umbraco.Web.HealthCheck.Checks.Security }; } - private bool DoHttpHeadersContainHeader(WebResponse response) + private bool HasMatchingHeader(IEnumerable headerKeys) { - return response.Headers.AllKeys.Contains(_header); + return headerKeys.Contains(_header, StringComparer.InvariantCultureIgnoreCase); } private bool DoMetaTagsContainKeyForHeader(WebResponse response) @@ -127,7 +127,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Security { var html = reader.ReadToEnd(); var metaTags = ParseMetaTags(html); - return metaTags.ContainsKey(_header); + return HasMatchingHeader(metaTags.Keys); } } } @@ -186,24 +186,22 @@ namespace Umbraco.Web.HealthCheck.Checks.Security } var removeHeaderElement = customHeadersElement.Elements("remove") - .SingleOrDefault(x => x.Attribute("name") != null && - x.Attribute("name")?.Value == _value); + .SingleOrDefault(x => x.Attribute("name")?.Value.Equals(_value, StringComparison.InvariantCultureIgnoreCase) == true); if (removeHeaderElement == null) { - removeHeaderElement = new XElement("remove"); - removeHeaderElement.Add(new XAttribute("name", _header)); - customHeadersElement.Add(removeHeaderElement); + customHeadersElement.Add( + new XElement("remove", + new XAttribute("name", _header))); } var addHeaderElement = customHeadersElement.Elements("add") - .SingleOrDefault(x => x.Attribute("name") != null && - x.Attribute("name").Value == _header); + .SingleOrDefault(x => x.Attribute("name")?.Value.Equals(_header, StringComparison.InvariantCultureIgnoreCase) == true); if (addHeaderElement == null) { - addHeaderElement = new XElement("add"); - addHeaderElement.Add(new XAttribute("name", _header)); - addHeaderElement.Add(new XAttribute("value", _value)); - customHeadersElement.Add(addHeaderElement); + customHeadersElement.Add( + new XElement("add", + new XAttribute("name", _header), + new XAttribute("value", _value))); } doc.Save(configFile); diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/HstsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/HstsCheck.cs index 18827b0c81..d0da243ced 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/HstsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/HstsCheck.cs @@ -6,17 +6,17 @@ namespace Umbraco.Web.HealthCheck.Checks.Security [HealthCheck( "E2048C48-21C5-4BE1-A80B-8062162DF124", "Cookie hijacking and protocol downgrade attacks Protection (Strict-Transport-Security Header (HSTS))", - Description = "Checks if your site, when running with HTTPS, contains the Strict-Transport-Security Header (HSTS). If not, it adds with a default of 100 days.", + Description = "Checks if your site, when running with HTTPS, contains the Strict-Transport-Security Header (HSTS). If not, it adds with a default of 18 weeks.", Group = "Security")] public class HstsCheck : BaseHttpHeaderCheck { // The check is mostly based on the instructions in the OWASP CheatSheet - // (https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet) + // (https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.md) // and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) - // If you want do to it perfectly, you have to submit it https://hstspreload.appspot.com/, + // If you want do to it perfectly, you have to submit it https://hstspreload.org/, // but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. public HstsCheck(IRuntimeState runtime, ILocalizedTextService textService) - : base(runtime, textService, "Strict-Transport-Security", "max-age=10886400; preload", "hSTS", true) + : base(runtime, textService, "Strict-Transport-Security", "max-age=10886400", "hSTS", true) { } } diff --git a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs index 87c0e4f46d..873b356214 100644 --- a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods public EmailNotificationMethod(ILocalizedTextService textService, IRuntimeState runtimeState, ILogger logger) { - var recipientEmail = Settings["recipientEmail"]?.Value; + var recipientEmail = Settings?["recipientEmail"]?.Value; if (string.IsNullOrWhiteSpace(recipientEmail)) { Enabled = false; diff --git a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs index 26a1d97f67..1d518fa1d3 100644 --- a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs @@ -13,6 +13,7 @@ using Umbraco.Web.Models; namespace Umbraco.Web { using Core.Configuration; + using Umbraco.Web.JavaScript; /// /// HtmlHelper extensions for the back office @@ -118,6 +119,21 @@ namespace Umbraco.Web sb.AppendLine(JsonConvert.SerializeObject(resetCodeModel)); sb.AppendLine(@"});"); + return html.Raw(sb.ToString()); + } + + public static IHtmlString AngularValueTinyMceAssets(this HtmlHelper html) + { + var ctx = new HttpContextWrapper(HttpContext.Current); + var files = JsInitialization.OptimizeTinyMceScriptFiles(ctx); + + var sb = new StringBuilder(); + + sb.AppendLine(@"app.value(""tinyMceAssets"","); + sb.AppendLine(JsonConvert.SerializeObject(files)); + sb.AppendLine(@");"); + + return html.Raw(sb.ToString()); } } diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 99adf71742..ebe5e08f89 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -8,11 +8,7 @@ using System.Web.Mvc; using System.Web.Mvc.Html; using System.Web.Routing; using Umbraco.Core; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Exceptions; using Umbraco.Core.IO; -using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Web.Security; using Current = Umbraco.Web.Composing.Current; @@ -68,7 +64,8 @@ namespace Umbraco.Web var htmlBadge = String.Format(Current.Configs.Settings().Content.PreviewBadge, IOHelper.ResolveUrl(SystemDirectories.Umbraco), - Current.UmbracoContext.HttpContext.Server.UrlEncode(Current.UmbracoContext.HttpContext.Request.Path)); + Current.UmbracoContext.HttpContext.Server.UrlEncode(Current.UmbracoContext.HttpContext.Request.Path), + Current.UmbracoContext.PublishedRequest.PublishedContent.Id); return new MvcHtmlString(htmlBadge); } return new MvcHtmlString(""); @@ -169,8 +166,9 @@ namespace Umbraco.Web /// public static IHtmlString Action(this HtmlHelper htmlHelper, string actionName, Type surfaceType) { + if (actionName == null) throw new ArgumentNullException(nameof(actionName)); + if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); if (surfaceType == null) throw new ArgumentNullException(nameof(surfaceType)); - if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); var routeVals = new RouteValueDictionary(new {area = ""}); @@ -221,34 +219,34 @@ namespace Umbraco.Web { _viewContext = viewContext; _method = method; - _controllerName = controllerName; + _controllerName = controllerName; _encryptedString = UrlHelperRenderExtensions.CreateEncryptedRouteString(controllerName, controllerAction, area, additionalRouteVals); } + private readonly ViewContext _viewContext; private readonly FormMethod _method; private bool _disposed; private readonly string _encryptedString; - private readonly string _controllerName; + private readonly string _controllerName; protected override void Dispose(bool disposing) { if (this._disposed) return; this._disposed = true; - //Detect if the call is targeting UmbRegisterController/UmbProfileController/UmbLoginStatusController/UmbLoginController and if it is we automatically output a AntiForgeryToken() // We have a controllerName and area so we can match if (_controllerName == "UmbRegister" || _controllerName == "UmbProfile" || _controllerName == "UmbLoginStatus" || _controllerName == "UmbLogin") - { - _viewContext.Writer.Write(AntiForgery.GetHtml().ToString()); - } + { + _viewContext.Writer.Write(AntiForgery.GetHtml().ToString()); + } //write out the hidden surface form routes - _viewContext.Writer.Write(""); + _viewContext.Writer.Write(""); base.Dispose(disposing); } @@ -355,8 +353,10 @@ namespace Umbraco.Web IDictionary htmlAttributes, FormMethod method) { - if (string.IsNullOrWhiteSpace(action)) throw new ArgumentNullOrEmptyException(nameof(action)); - if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); + if (action == null) throw new ArgumentNullException(nameof(action)); + if (string.IsNullOrWhiteSpace(action)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(action)); + if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); + if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); return html.BeginUmbracoForm(action, controllerName, "", additionalRouteVals, htmlAttributes, method); } @@ -374,8 +374,10 @@ namespace Umbraco.Web object additionalRouteVals, IDictionary htmlAttributes) { - if (string.IsNullOrWhiteSpace(action)) throw new ArgumentNullOrEmptyException(nameof(action)); - if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); + if (action == null) throw new ArgumentNullException(nameof(action)); + if (string.IsNullOrWhiteSpace(action)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(action)); + if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); + if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); return html.BeginUmbracoForm(action, controllerName, "", additionalRouteVals, htmlAttributes); } @@ -575,7 +577,9 @@ namespace Umbraco.Web IDictionary htmlAttributes, FormMethod method) { - if (string.IsNullOrWhiteSpace(action)) throw new ArgumentNullOrEmptyException(nameof(action)); + + if (action == null) throw new ArgumentNullException(nameof(action)); + if (string.IsNullOrWhiteSpace(action)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(action)); if (surfaceType == null) throw new ArgumentNullException(nameof(surfaceType)); var area = ""; @@ -687,8 +691,10 @@ namespace Umbraco.Web IDictionary htmlAttributes, FormMethod method) { - if (string.IsNullOrEmpty(action)) throw new ArgumentNullOrEmptyException(nameof(action)); - if (string.IsNullOrEmpty(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); + if (action == null) throw new ArgumentNullException(nameof(action)); + if (string.IsNullOrEmpty(action)) throw new ArgumentException("Value can't be empty.", nameof(action)); + if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); + if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); var formAction = Current.UmbracoContext.OriginalRequestUrl.PathAndQuery; return html.RenderForm(formAction, method, htmlAttributes, controllerName, action, area, additionalRouteVals); diff --git a/src/Umbraco.Web/HtmlStringUtilities.cs b/src/Umbraco.Web/HtmlStringUtilities.cs index 6cf5092bb0..a8cbb70019 100644 --- a/src/Umbraco.Web/HtmlStringUtilities.cs +++ b/src/Umbraco.Web/HtmlStringUtilities.cs @@ -19,10 +19,26 @@ namespace Umbraco.Web /// Replaces text line breaks with HTML line breaks /// /// The text. - /// The text with text line breaks replaced with HTML line breaks (
)
+ /// The text with text line breaks replaced with HTML line breaks (<br />). + [Obsolete("This method doesn't HTML encode the text. Use ReplaceLineBreaks instead.")] public HtmlString ReplaceLineBreaksForHtml(string text) { - return new HtmlString(text.Replace("\r\n", @"
").Replace("\n", @"
").Replace("\r", @"
")); + return new HtmlString(text.Replace("\r\n", @"
").Replace("\n", @"
").Replace("\r", @"
")); + } + + /// + /// HTML encodes the text and replaces text line breaks with HTML line breaks. + /// + /// The text. + /// The HTML encoded text with text line breaks replaced with HTML line breaks (<br />). + public IHtmlString ReplaceLineBreaks(string text) + { + var value = HttpUtility.HtmlEncode(text)? + .Replace("\r\n", "
") + .Replace("\r", "
") + .Replace("\n", "
"); + + return new HtmlString(value); } public HtmlString StripHtmlTags(string html, params string[] tags) @@ -227,21 +243,27 @@ namespace Umbraco.Web } } - if (!lengthReached && currentTextLength >= length) + if (!lengthReached) { - // if the last character added was the first of a two character unicode pair, add the second character - if (Char.IsHighSurrogate((char)ic)) + if (currentTextLength == length) { - var lowSurrogate = tr.Read(); - outputtw.Write((char)lowSurrogate); - } + // if the last character added was the first of a two character unicode pair, add the second character + if (char.IsHighSurrogate((char)ic)) + { + var lowSurrogate = tr.Read(); + outputtw.Write((char)lowSurrogate); + } - // Reached truncate limit. - if (addElipsis) - { - outputtw.Write(hellip); } - lengthReached = true; + // only add elipsis if current length greater than original length + if (currentTextLength > length) + { + if (addElipsis) + { + outputtw.Write(hellip); + } + lengthReached = true; + } } } diff --git a/src/Umbraco.Web/HttpUrlHelperExtensions.cs b/src/Umbraco.Web/HttpUrlHelperExtensions.cs index 2eb45465cf..4e5533d327 100644 --- a/src/Umbraco.Web/HttpUrlHelperExtensions.cs +++ b/src/Umbraco.Web/HttpUrlHelperExtensions.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Linq.Expressions; using System.Web.Http.Routing; using Umbraco.Core; -using Umbraco.Core.Exceptions; using Umbraco.Web.Composing; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; @@ -53,7 +52,8 @@ namespace Umbraco.Web /// public static string GetUmbracoApiService(this UrlHelper url, string actionName, Type apiControllerType, object id = null) { - if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); + if (actionName == null) throw new ArgumentNullException(nameof(actionName)); + if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); if (apiControllerType == null) throw new ArgumentNullException(nameof(apiControllerType)); var area = ""; @@ -95,8 +95,10 @@ namespace Umbraco.Web /// public static string GetUmbracoApiService(this UrlHelper url, string actionName, string controllerName, string area, object id = null) { - if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); - if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); + if (actionName == null) throw new ArgumentNullException(nameof(actionName)); + if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); + if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); + if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); string routeName; if (area.IsNullOrWhiteSpace()) diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 76e7be5e97..7066475dc9 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -35,44 +35,63 @@ namespace Umbraco.Web /// /// Searches content. /// - /// Term to search. - /// Optional culture. - /// Optional index name. + /// The term to search. + /// The culture (defaults to a culture insensitive search). + /// The name of the index to search (defaults to ). + /// + /// The search results. + /// /// - /// When the is not specified, all cultures are searched. + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. + /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, string culture = null, string indexName = null); + IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName); /// /// Searches content. /// - /// Term to search. - /// Numbers of items to skip. - /// Numbers of items to return. - /// Total number of matching items. - /// Optional culture. - /// Optional index name. + /// The term to search. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// The culture (defaults to a culture insensitive search). + /// The name of the index to search (defaults to ). + /// + /// The search results. + /// /// - /// When the is not specified, all cultures are searched. + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. + /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = null, string indexName = null); + IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName); /// - /// Executes the query and converts the results to PublishedSearchResult. + /// Executes the query and converts the results to . /// - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// + /// The query. + /// + /// The search results. + /// IEnumerable Search(IQueryExecutor query); /// - /// Executes the query and converts the results to PublishedSearchResult. + /// Executes the query and converts the results to . /// - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// + /// The query. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// + /// The search results. + /// IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); } } diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs new file mode 100644 index 0000000000..7ac5578d75 --- /dev/null +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -0,0 +1,379 @@ +using System; +using Newtonsoft.Json.Linq; +using System.Globalization; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.Models; + +namespace Umbraco.Web +{ + public static class ImageCropperTemplateCoreExtensions + { + /// + /// Gets the ImageProcessor Url by the crop alias (from the "umbracoFile" property alias) on the IPublishedContent item + /// + /// + /// The IPublishedContent item. + /// + /// + /// The crop alias e.g. thumbnail + /// + /// + /// The ImageProcessor.Web Url. + /// + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, IImageUrlGenerator imageUrlGenerator) + { + return mediaItem.GetCropUrl(imageUrlGenerator, cropAlias: cropAlias, useCropDimensions: true); + } + + /// + /// Gets the ImageProcessor Url by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. + /// + /// + /// The IPublishedContent item. + /// + /// + /// The property alias of the property containing the Json data e.g. umbracoFile + /// + /// + /// The crop alias e.g. thumbnail + /// + /// + /// The ImageProcessor.Web Url. + /// + public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias, IImageUrlGenerator imageUrlGenerator) + { + return mediaItem.GetCropUrl(imageUrlGenerator, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + } + + /// + /// Gets the ImageProcessor Url from the IPublishedContent item. + /// + /// + /// The IPublishedContent item. + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// Property alias of the property containing the Json data. + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point, to generate an output image using the focal point instead of the predefined crop + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// The . + /// + public static string GetCropUrl( + this IPublishedContent mediaItem, + IImageUrlGenerator imageUrlGenerator, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) + { + if (mediaItem == null) throw new ArgumentNullException("mediaItem"); + + var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; + + if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) + return string.Empty; + + var mediaItemUrl = mediaItem.MediaUrl(propertyAlias: propertyAlias); + + //get the default obj from the value converter + var cropperValue = mediaItem.Value(propertyAlias); + + //is it strongly typed? + var stronglyTyped = cropperValue as ImageCropperValue; + if (stronglyTyped != null) + { + return GetCropUrl( + mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + //this shouldn't be the case but we'll check + var jobj = cropperValue as JObject; + if (jobj != null) + { + stronglyTyped = jobj.ToObject(); + return GetCropUrl( + mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + //it's a single string + return GetCropUrl( + mediaItemUrl, imageUrlGenerator, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + /// + /// Gets the ImageProcessor Url from the image path. + /// + /// + /// The image url. + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// The Json data from the Umbraco Core Image Cropper property editor + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// The . + /// + public static string GetCropUrl( + this string imageUrl, + IImageUrlGenerator imageUrlGenerator, + int? width = null, + int? height = null, + string imageCropperValue = null, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) + { + if (string.IsNullOrEmpty(imageUrl)) return string.Empty; + + ImageCropperValue cropDataSet = null; + if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + { + cropDataSet = imageCropperValue.DeserializeImageCropperValue(); + } + return GetCropUrl( + imageUrl, imageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + /// + /// Gets the ImageProcessor Url from the image path. + /// + /// + /// The image url. + /// + /// + /// The generator that will process all the options and the image URL to return a full image urls with all processing options appended + /// + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// The . + /// + public static string GetCropUrl( + this string imageUrl, + IImageUrlGenerator imageUrlGenerator, + ImageCropperValue cropDataSet, + int? width = null, + int? height = null, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true, + string animationProcessMode = null) + { + if (string.IsNullOrEmpty(imageUrl)) return string.Empty; + + ImageUrlGenerationOptions options; + + if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + { + var crop = cropDataSet.GetCrop(cropAlias); + + // if a crop was specified, but not found, return null + if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) + return null; + + options = cropDataSet.GetCropBaseOptions(imageUrl, crop, string.IsNullOrWhiteSpace(cropAlias), preferFocalPoint); + + if (crop != null & useCropDimensions) + { + width = crop.Width; + height = crop.Height; + } + + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) + { + options.HeightRatio = (decimal)crop.Height / crop.Width; + } + + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) + { + options.WidthRatio = (decimal)crop.Width / crop.Height; + } + } + else + { + options = new ImageUrlGenerationOptions (imageUrl) + { + ImageCropMode = (imageCropMode ?? ImageCropMode.Pad).ToString().ToLowerInvariant(), + ImageCropAnchor = imageCropAnchor?.ToString().ToLowerInvariant() + }; + } + + options.Quality = quality; + options.Width = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Width ? null : width; + options.Height = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Height ? null : height; + options.AnimationProcessMode = animationProcessMode; + + if (ratioMode == ImageCropRatioMode.Width && height != null) + { + // if only height specified then assume a square + if (width == null) width = height; + options.WidthRatio = (decimal)width / (decimal)height; + } + + if (ratioMode == ImageCropRatioMode.Height && width != null) + { + // if only width specified then assume a square + if (height == null) height = width; + options.HeightRatio = (decimal)height / (decimal)width; + } + + options.UpScale = upScale; + options.FurtherOptions = furtherOptions; + options.CacheBusterValue = cacheBusterValue; + + return imageUrlGenerator.GetImageUrl(options); + } + } +} diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index d7f457287a..f3833ba42a 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -29,10 +29,7 @@ namespace Umbraco.Web /// /// The ImageProcessor.Web Url. /// - public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) - { - return mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true); - } + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator); /// /// Gets the ImageProcessor Url by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. @@ -49,10 +46,7 @@ namespace Umbraco.Web /// /// The ImageProcessor.Web Url. /// - public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias) - { - return mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); - } + public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, propertyAlias, cropAlias, Current.ImageUrlGenerator); /// /// Gets the ImageProcessor Url from the IPublishedContent item. @@ -121,44 +115,7 @@ namespace Umbraco.Web bool cacheBuster = true, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) - { - if (mediaItem == null) throw new ArgumentNullException("mediaItem"); - - var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; - - if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) - return string.Empty; - - var mediaItemUrl = mediaItem.MediaUrl(propertyAlias); - - //get the default obj from the value converter - var cropperValue = mediaItem.Value(propertyAlias); - - //is it strongly typed? - var stronglyTyped = cropperValue as ImageCropperValue; - if (stronglyTyped != null) - { - return GetCropUrl( - mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); - } - - //this shouldn't be the case but we'll check - var jobj = cropperValue as JObject; - if (jobj != null) - { - stronglyTyped = jobj.ToObject(); - return GetCropUrl( - mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); - } - - //it's a single string - return GetCropUrl( - mediaItemUrl, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); - } + bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, Current.ImageUrlGenerator, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); /// /// Gets the ImageProcessor Url from the image path. @@ -227,19 +184,7 @@ namespace Umbraco.Web string cacheBusterValue = null, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) - { - if (string.IsNullOrEmpty(imageUrl)) return string.Empty; - - ImageCropperValue cropDataSet = null; - if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) - { - cropDataSet = imageCropperValue.DeserializeImageCropperValue(); - } - return GetCropUrl( - imageUrl, cropDataSet, width, height, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); - } + bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(imageUrl, Current.ImageUrlGenerator, width, height, imageCropperValue, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); /// /// Gets the ImageProcessor Url from the image path. @@ -306,129 +251,8 @@ namespace Umbraco.Web string cacheBusterValue = null, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) - { - if (string.IsNullOrEmpty(imageUrl) == false) - { - var imageProcessorUrl = new StringBuilder(); + bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(imageUrl, Current.ImageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); - if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) - { - var crop = cropDataSet.GetCrop(cropAlias); - - // if a crop was specified, but not found, return null - if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) - return null; - - imageProcessorUrl.Append(imageUrl); - cropDataSet.AppendCropBaseUrl(imageProcessorUrl, crop, string.IsNullOrWhiteSpace(cropAlias), preferFocalPoint); - - if (crop != null & useCropDimensions) - { - width = crop.Width; - height = crop.Height; - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) - { - var heightRatio = (decimal)crop.Height / (decimal)crop.Width; - imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) - { - var widthRatio = (decimal)crop.Width / (decimal)crop.Height; - imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); - } - } - else - { - imageProcessorUrl.Append(imageUrl); - - if (imageCropMode == null) - { - imageCropMode = ImageCropMode.Pad; - } - - imageProcessorUrl.Append("?mode=" + imageCropMode.ToString().ToLower()); - - if (imageCropAnchor != null) - { - imageProcessorUrl.Append("&anchor=" + imageCropAnchor.ToString().ToLower()); - } - } - - var hasFormat = furtherOptions != null && furtherOptions.InvariantContains("&format="); - - //Only put quality here, if we don't have a format specified. - //Otherwise we need to put quality at the end to avoid it being overridden by the format. - if (quality != null && hasFormat == false) - { - imageProcessorUrl.Append("&quality=" + quality); - } - - if (width != null && ratioMode != ImageCropRatioMode.Width) - { - imageProcessorUrl.Append("&width=" + width); - } - - if (height != null && ratioMode != ImageCropRatioMode.Height) - { - imageProcessorUrl.Append("&height=" + height); - } - - if (ratioMode == ImageCropRatioMode.Width && height != null) - { - // if only height specified then assume a square - if (width == null) - { - width = height; - } - - var widthRatio = (decimal)width / (decimal)height; - imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); - } - - if (ratioMode == ImageCropRatioMode.Height && width != null) - { - // if only width specified then assume a square - if (height == null) - { - height = width; - } - - var heightRatio = (decimal)height / (decimal)width; - imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); - } - - if (upScale == false) - { - imageProcessorUrl.Append("&upscale=false"); - } - - if (furtherOptions != null) - { - imageProcessorUrl.Append(furtherOptions); - } - - //If furtherOptions contains a format, we need to put the quality after the format. - if (quality != null && hasFormat) - { - imageProcessorUrl.Append("&quality=" + quality); - } - - if (cacheBusterValue != null) - { - imageProcessorUrl.Append("&rnd=").Append(cacheBusterValue); - } - - return imageProcessorUrl.ToString(); - } - - return string.Empty; - } internal static ImageCropperValue DeserializeImageCropperValue(this string json) { diff --git a/src/Umbraco.Web/Install/Controllers/InstallController.cs b/src/Umbraco.Web/Install/Controllers/InstallController.cs index f45e7a0ae0..902ebbcd53 100644 --- a/src/Umbraco.Web/Install/Controllers/InstallController.cs +++ b/src/Umbraco.Web/Install/Controllers/InstallController.cs @@ -6,6 +6,7 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; using Umbraco.Web.JavaScript; +using Umbraco.Web.Mvc; using Umbraco.Web.Security; namespace Umbraco.Web.Install.Controllers @@ -35,6 +36,7 @@ namespace Umbraco.Web.Install.Controllers } [HttpGet] + [StatusCodeResult(System.Net.HttpStatusCode.ServiceUnavailable)] public ActionResult Index() { if (_runtime.Level == RuntimeLevel.Run) diff --git a/src/Umbraco.Web/Install/FilePermissionHelper.cs b/src/Umbraco.Web/Install/FilePermissionHelper.cs index ede9008514..af62b4fd9e 100644 --- a/src/Umbraco.Web/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Web/Install/FilePermissionHelper.cs @@ -45,8 +45,8 @@ namespace Umbraco.Web.Install /// /// This will test the directories for write access /// - /// - /// + /// + /// /// /// If this is false, the easiest way to test for write access is to write a temp file, however some folder will cause /// an App Domain restart if a file is written to the folder, so in that case we need to use the ACL APIs which aren't as diff --git a/src/Umbraco.Web/Install/InstallException.cs b/src/Umbraco.Web/Install/InstallException.cs index a9f254e921..3dc297e6b2 100644 --- a/src/Umbraco.Web/Install/InstallException.cs +++ b/src/Umbraco.Web/Install/InstallException.cs @@ -1,44 +1,106 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Runtime.Serialization; namespace Umbraco.Web.Install { /// - /// Used for steps to be able to return a json structure back to the UI + /// Used for steps to be able to return a JSON structure back to the UI. /// - internal class InstallException : Exception + /// + [Serializable] + public class InstallException : Exception { - private readonly string _message; + /// + /// Gets the view. + /// + /// + /// The view. + /// public string View { get; private set; } + + /// + /// Gets the view model. + /// + /// + /// The view model. + /// + /// + /// This object is not included when serializing. + /// public object ViewModel { get; private set; } - public override string Message - { - get { return _message; } - } + /// + /// Initializes a new instance of the class. + /// + public InstallException() + { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public InstallException(string message) + : this(message, "error", null) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The view model. + public InstallException(string message, object viewModel) + : this(message, "error", viewModel) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The view. + /// The view model. public InstallException(string message, string view, object viewModel) + : base(message) { - _message = message; View = view; ViewModel = viewModel; } - public InstallException(string message, object viewModel) + /// + /// 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 InstallException(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 InstallException(SerializationInfo info, StreamingContext context) + : base(info, context) { - _message = message; - View = "error"; - ViewModel = viewModel; + View = info.GetString(nameof(View)); } - public InstallException(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) { - _message = message; - View = "error"; - ViewModel = null; + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + info.AddValue(nameof(View), View); + + base.GetObjectData(info, context); } } } diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index effb46c9b7..b74a20c27c 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -4,14 +4,17 @@ using System.Configuration; using System.IO; using System.Linq; using System.Net.Http; +using System.Threading.Tasks; using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web.Install.Models; @@ -24,16 +27,18 @@ namespace Umbraco.Web.Install private readonly HttpContextBase _httpContext; private readonly ILogger _logger; private readonly IGlobalSettings _globalSettings; + private readonly IInstallationService _installationService; private InstallationType? _installationType; public InstallHelper(IUmbracoContextAccessor umbracoContextAccessor, DatabaseBuilder databaseBuilder, - ILogger logger, IGlobalSettings globalSettings) + ILogger logger, IGlobalSettings globalSettings, IInstallationService installationService) { _httpContext = umbracoContextAccessor.UmbracoContext.HttpContext; _logger = logger; _globalSettings = globalSettings; _databaseBuilder = databaseBuilder; + _installationService = installationService; } public InstallationType GetInstallationType() @@ -41,7 +46,7 @@ namespace Umbraco.Web.Install return _installationType ?? (_installationType = IsBrandNewInstall ? InstallationType.NewInstall : InstallationType.Upgrade).Value; } - internal void InstallStatus(bool isCompleted, string errorMsg) + internal async Task InstallStatus(bool isCompleted, string errorMsg) { try { @@ -59,8 +64,12 @@ namespace Umbraco.Web.Install if (installId == Guid.Empty) installId = Guid.NewGuid(); } + else + { + installId = Guid.NewGuid(); // Guid.TryParse will have reset installId to Guid.Empty + } } - _httpContext.Response.Cookies.Set(new HttpCookie(Constants.Web.InstallerCookieName, "1")); + _httpContext.Response.Cookies.Set(new HttpCookie(Constants.Web.InstallerCookieName, installId.ToString())); var dbProvider = string.Empty; if (IsBrandNewInstall == false) @@ -70,18 +79,13 @@ namespace Umbraco.Web.Install dbProvider = GetDbProviderString(Current.SqlContext); } - var check = new org.umbraco.update.CheckForUpgrade(); - check.Install(installId, - IsBrandNewInstall == false, - isCompleted, - DateTime.Now, - UmbracoVersion.Current.Major, - UmbracoVersion.Current.Minor, - UmbracoVersion.Current.Build, - UmbracoVersion.Comment, - errorMsg, - userAgent, - dbProvider); + var installLog = new InstallLog(installId: installId, isUpgrade: IsBrandNewInstall == false, + installCompleted: isCompleted, timestamp: DateTime.Now, versionMajor: UmbracoVersion.Current.Major, + versionMinor: UmbracoVersion.Current.Minor, versionPatch: UmbracoVersion.Current.Build, + versionComment: UmbracoVersion.Comment, error: errorMsg, userAgent: userAgent, + dbProvider: dbProvider); + + await _installationService.LogInstall(installLog); } catch (Exception ex) { diff --git a/src/Umbraco.Web/Install/InstallSteps/ConfigureMachineKey.cs b/src/Umbraco.Web/Install/InstallSteps/ConfigureMachineKey.cs index 4d64d30e9d..c273d642ed 100644 --- a/src/Umbraco.Web/Install/InstallSteps/ConfigureMachineKey.cs +++ b/src/Umbraco.Web/Install/InstallSteps/ConfigureMachineKey.cs @@ -39,7 +39,8 @@ namespace Umbraco.Web.Install.InstallSteps var fileName = IOHelper.MapPath($"{SystemDirectories.Root}/web.config"); var xml = XDocument.Load(fileName, LoadOptions.PreserveWhitespace); - var systemWeb = xml.Root.DescendantsAndSelf("system.web").Single(); + // we only want to get the element that is under the root, (there may be more under tags we don't want them) + var systemWeb = xml.Root.Element("system.web"); // Update appSetting if it exists, or else create a new appSetting for the given key and value var machineKey = systemWeb.Descendants("machineKey").FirstOrDefault(); diff --git a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs index c1268ca675..17191d2521 100644 --- a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs @@ -13,7 +13,7 @@ using Umbraco.Web.Security; namespace Umbraco.Web.Install.InstallSteps { [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, - "UmbracoVersion", 50, "Installation is complete!, get ready to be redirected to your new CMS.", + "UmbracoVersion", 50, "Installation is complete! Get ready to be redirected to your new CMS.", PerformsAppRestart = true)] internal class SetUmbracoVersionStep : InstallSetupStep { @@ -34,18 +34,32 @@ namespace Umbraco.Web.Install.InstallSteps public override Task ExecuteAsync(object model) { - //During a new install we'll log the default user in (which is id = 0). - // During an upgrade, the user will already need to be logged in order to run the installer. - var security = new WebSecurity(_httpContext, _userService, _globalSettings); - //we do this check here because for upgrades the user will already be logged in, for brand new installs, - // they will not be logged in, however we cannot check the current installation status because it will tell - // us that it is in 'upgrade' because we already have a database conn configured and a database. + if (security.IsAuthenticated() == false && _globalSettings.ConfigurationStatus.IsNullOrWhiteSpace()) { security.PerformLogin(-1); } + if (security.IsAuthenticated()) + { + // when a user is already logged in, we need to check whether it's user 'zero' + // which is the legacy super user from v7 - and then we need to actually log the + // true super user in - but before that we need to log off, else audit events + // will try to reference user zero and fail + var userIdAttempt = security.GetUserId(); + if (userIdAttempt && userIdAttempt.Result == 0) + { + security.ClearCurrentLogin(); + security.PerformLogin(Constants.Security.SuperUserId); + } + } + else if (_globalSettings.ConfigurationStatus.IsNullOrWhiteSpace()) + { + // for installs, we need to log the super user in + security.PerformLogin(Constants.Security.SuperUserId); + } + // Update configurationStatus _globalSettings.ConfigurationStatus = UmbracoVersion.SemanticVersion.ToSemanticString(); diff --git a/src/Umbraco.Web/JavaScript/AssetInitialization.cs b/src/Umbraco.Web/JavaScript/AssetInitialization.cs index 00605ece1f..42c750ffd3 100644 --- a/src/Umbraco.Web/JavaScript/AssetInitialization.cs +++ b/src/Umbraco.Web/JavaScript/AssetInitialization.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Web; using ClientDependency.Core; using ClientDependency.Core.Config; +using Umbraco.Core; using Umbraco.Web.Composing; using Umbraco.Web.PropertyEditors; @@ -35,7 +36,7 @@ namespace Umbraco.Web.JavaScript var requestUrl = httpContext.Request.Url; if (requestUrl == null) throw new ArgumentException("HttpContext.Request.Url is null.", nameof(httpContext)); - var dependencies = assets.Select(x => + var dependencies = assets.Where(x => x.IsNullOrWhiteSpace() == false).Select(x => { // most declarations with be made relative to the /umbraco folder, so things // like lib/blah/blah.js so we need to turn them into absolutes here diff --git a/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs b/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs index 777786675f..2bf069b06d 100644 --- a/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs +++ b/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs @@ -106,8 +106,10 @@ namespace Umbraco.Web.JavaScript } try - { - var fullPath = currentHttpContext.Server.MapPath(XmlFileMapper.FileMapDefaultFolder); + { + var fullPath = XmlFileMapper.FileMapDefaultFolder.StartsWith("~/") + ? currentHttpContext.Server.MapPath(XmlFileMapper.FileMapDefaultFolder) + : XmlFileMapper.FileMapDefaultFolder; if (fullPath != null) { cdfTempDirectories.Add(fullPath); @@ -122,13 +124,12 @@ namespace Umbraco.Web.JavaScript var success = true; foreach (var directory in cdfTempDirectories) { - var directoryInfo = new DirectoryInfo(directory); - if (directoryInfo.Exists == false) - continue; - try { - directoryInfo.Delete(true); + if (!Directory.Exists(directory)) + continue; + + Directory.Delete(directory, true); } catch (Exception ex) { diff --git a/src/Umbraco.Web/JavaScript/JsInitialization.cs b/src/Umbraco.Web/JavaScript/JsInitialization.cs index 638b1bf5b4..17e4f7b094 100644 --- a/src/Umbraco.Web/JavaScript/JsInitialization.cs +++ b/src/Umbraco.Web/JavaScript/JsInitialization.cs @@ -131,6 +131,17 @@ namespace Umbraco.Web.JavaScript return resources.Where(x => x.Type == JTokenType.String).Select(x => x.ToString()); } + internal static IEnumerable GetTinyMceInitialization() + { + var resources = JsonConvert.DeserializeObject(Resources.TinyMceInitialize); + return resources.Where(x => x.Type == JTokenType.String).Select(x => x.ToString()); + } + + internal static IEnumerable OptimizeTinyMceScriptFiles(HttpContextBase httpContext) + { + return OptimizeScriptFiles(httpContext, GetTinyMceInitialization()); + } + /// /// Parses the JsResources.Main and replaces the replacement tokens accordingly. /// diff --git a/src/Umbraco.Web/JavaScript/JsInitialize.js b/src/Umbraco.Web/JavaScript/JsInitialize.js index 1ef6e74dfe..4b0a457b64 100644 --- a/src/Umbraco.Web/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/JavaScript/JsInitialize.js @@ -13,6 +13,7 @@ 'lib/angular-route/angular-route.js', 'lib/angular-cookies/angular-cookies.js', + 'lib/angular-aria/angular-aria.min.js', 'lib/angular-touch/angular-touch.js', 'lib/angular-sanitize/angular-sanitize.js', 'lib/angular-animate/angular-animate.js', diff --git a/src/Umbraco.Web/JavaScript/Resources.Designer.cs b/src/Umbraco.Web/JavaScript/Resources.Designer.cs index ca571b1186..f11839f6ca 100644 --- a/src/Umbraco.Web/JavaScript/Resources.Designer.cs +++ b/src/Umbraco.Web/JavaScript/Resources.Designer.cs @@ -146,5 +146,17 @@ namespace Umbraco.Web.JavaScript { return ResourceManager.GetString("ServerVariables", resourceCulture); } } + + /// + /// Looks up a localized string similar to [ + /// '../lib/tinymce/tinymce.min.js', + ///] + ///. + /// + internal static string TinyMceInitialize { + get { + return ResourceManager.GetString("TinyMceInitialize", resourceCulture); + } + } } } diff --git a/src/Umbraco.Web/JavaScript/Resources.resx b/src/Umbraco.Web/JavaScript/Resources.resx index 95526f51e7..1adcaeeb4a 100644 --- a/src/Umbraco.Web/JavaScript/Resources.resx +++ b/src/Umbraco.Web/JavaScript/Resources.resx @@ -130,4 +130,7 @@ servervariables.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - + + TinyMceInitialize.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + \ No newline at end of file diff --git a/src/Umbraco.Web/JavaScript/TinyMceInitialize.js b/src/Umbraco.Web/JavaScript/TinyMceInitialize.js new file mode 100644 index 0000000000..6cb9cf6277 --- /dev/null +++ b/src/Umbraco.Web/JavaScript/TinyMceInitialize.js @@ -0,0 +1,17 @@ +[ + 'lib/tinymce/tinymce.min.js', + + 'lib/tinymce/plugins/paste/plugin.min.js', + 'lib/tinymce/plugins/anchor/plugin.min.js', + 'lib/tinymce/plugins/charmap/plugin.min.js', + 'lib/tinymce/plugins/table/plugin.min.js', + 'lib/tinymce/plugins/lists/plugin.min.js', + 'lib/tinymce/plugins/advlist/plugin.min.js', + 'lib/tinymce/plugins/hr/plugin.min.js', + 'lib/tinymce/plugins/autolink/plugin.min.js', + 'lib/tinymce/plugins/directionality/plugin.min.js', + 'lib/tinymce/plugins/tabfocus/plugin.min.js', + 'lib/tinymce/plugins/searchreplace/plugin.min.js', + 'lib/tinymce/plugins/fullscreen/plugin.min.js', + 'lib/tinymce/plugins/noneditable/plugin.min.js' +] diff --git a/src/Umbraco.Web/Logging/WebProfiler.cs b/src/Umbraco.Web/Logging/WebProfiler.cs index 14c1bb065f..512edb2296 100755 --- a/src/Umbraco.Web/Logging/WebProfiler.cs +++ b/src/Umbraco.Web/Logging/WebProfiler.cs @@ -68,6 +68,7 @@ namespace Umbraco.Web.Logging if (request.Result.Url.IsClientSideRequest()) return false; if (bool.TryParse(request.Result.QueryString["umbDebug"], out var umbDebug)) return umbDebug; if (bool.TryParse(request.Result.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug; + if (bool.TryParse(request.Result.Cookies["UMB-DEBUG"]?.Value, out var cUmbDebug)) return cUmbDebug; return false; } diff --git a/src/Umbraco.Web/Macros/MacroRenderer.cs b/src/Umbraco.Web/Macros/MacroRenderer.cs index 9736aa283b..b4fd8c0d86 100755 --- a/src/Umbraco.Web/Macros/MacroRenderer.cs +++ b/src/Umbraco.Web/Macros/MacroRenderer.cs @@ -40,12 +40,18 @@ namespace Umbraco.Web.Macros #region MacroContent cache // gets this macro content cache identifier - private string GetContentCacheIdentifier(MacroModel model, int pageId) + private string GetContentCacheIdentifier(MacroModel model, int pageId, string cultureName) { var id = new StringBuilder(); var alias = model.Alias; id.AppendFormat("{0}-", alias); + //always add current culture to the key to allow variants to have different cache results + if (!string.IsNullOrEmpty(cultureName)) + { + // are there any unusual culture formats we'd need to handle? + id.AppendFormat("{0}-", cultureName); + } if (model.CacheByPage) id.AppendFormat("{0}-", pageId); @@ -186,18 +192,19 @@ namespace Umbraco.Web.Macros foreach (var prop in model.Properties) { var key = prop.Key.ToLowerInvariant(); - prop.Value = macroParams.ContainsKey(key) + prop.Value = macroParams != null && macroParams.ContainsKey(key) ? macroParams[key]?.ToString() ?? string.Empty : string.Empty; } - } + } #endregion #region Render/Execute public MacroContent Render(string macroAlias, IPublishedContent content, IDictionary macroParams) { - var m = _macroService.GetByAlias(macroAlias); + var m = _appCaches.RuntimeCache.GetCacheItem(CacheKeys.MacroFromAliasCacheKey + macroAlias, () => _macroService.GetByAlias(macroAlias)); + if (m == null) throw new InvalidOperationException("No macro found by alias " + macroAlias); @@ -220,7 +227,8 @@ namespace Umbraco.Web.Macros foreach (var prop in macro.Properties) prop.Value = ParseAttribute(pageElements, prop.Value); - macro.CacheIdentifier = GetContentCacheIdentifier(macro, content.Id); + var cultureName = System.Threading.Thread.CurrentThread.CurrentUICulture.Name; + macro.CacheIdentifier = GetContentCacheIdentifier(macro, content.Id, cultureName); // get the macro from cache if it is there var macroContent = GetMacroContentFromCache(macro); @@ -316,7 +324,7 @@ namespace Umbraco.Web.Macros private Attempt ExecuteMacroOfType(MacroModel model, IPublishedContent content) { if (model == null) throw new ArgumentNullException(nameof(model)); - + // ensure that we are running against a published node (ie available in XML) // that may not be the case if the macro is embedded in a RTE of an unpublished document @@ -439,7 +447,7 @@ namespace Umbraco.Web.Macros // this was, and still is, an ugly piece of nonsense var value = string.Empty; - var cache = _umbracoContextAccessor.UmbracoContext.ContentCache; + var cache = _umbracoContextAccessor.UmbracoContext.Content; var splitpath = (string[])pageElements["splitpath"]; for (var i = splitpath.Length - 1; i > 0; i--) // at 0 we have root (-1) @@ -452,9 +460,9 @@ namespace Umbraco.Web.Macros return value; } - + #endregion - + } } diff --git a/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs b/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs index 67669fd607..3bbc4a793b 100644 --- a/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs +++ b/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs @@ -140,14 +140,14 @@ namespace Umbraco.Web.Macros private readonly object _sourceValue; private readonly IPublishedContent _content; - public PagePublishedProperty(PublishedPropertyType propertyType, IPublishedContent content) + public PagePublishedProperty(IPublishedPropertyType propertyType, IPublishedContent content) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { _sourceValue = null; _content = content; } - public PagePublishedProperty(PublishedPropertyType propertyType, IPublishedContent content, Umbraco.Core.Models.Property property) + public PagePublishedProperty(IPublishedPropertyType propertyType, IPublishedContent content, Umbraco.Core.Models.Property property) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { _sourceValue = property.GetValue(); @@ -218,7 +218,7 @@ namespace Umbraco.Web.Macros Parent = new PagePublishedContent(_inner.ParentId); } - public PublishedContentType ContentType { get; } + public IPublishedContentType ContentType { get; } public int Id { get; } @@ -230,19 +230,6 @@ namespace Umbraco.Web.Macros public string Name => _inner.Name; - public PublishedCultureInfo GetCulture(string culture = null) - { - // handle context culture - if (culture == null) - culture = _variationContextAccessor.VariationContext.Culture; - - // no invariant culture infos - if (culture == "") return null; - - // get - return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos : null; - } - public IReadOnlyDictionary Cultures { get @@ -261,8 +248,10 @@ namespace Umbraco.Web.Macros public string UrlSegment => throw new NotImplementedException(); + [Obsolete("Use WriterName(IUserService) extension instead")] public string WriterName { get; } + [Obsolete("Use CreatorName(IUserService) extension instead")] public string CreatorName { get; } public int WriterId => _inner.WriterId; @@ -279,8 +268,6 @@ namespace Umbraco.Web.Macros public string Url => throw new NotImplementedException(); - public string GetUrl(string culture = null) => throw new NotSupportedException(); - public PublishedItemType ItemType => PublishedItemType.Content; public bool IsDraft(string culture = null) @@ -297,6 +284,8 @@ namespace Umbraco.Web.Macros public IEnumerable Children => throw new NotImplementedException(); + public IEnumerable ChildrenForAllCultures => throw new NotImplementedException(); + public IEnumerable Properties => _properties; public IPublishedProperty GetProperty(string alias) diff --git a/src/Umbraco.Web/Media/EmbedProviders/Flickr.cs b/src/Umbraco.Web/Media/EmbedProviders/Flickr.cs index 2ce014c639..861e849fe2 100644 --- a/src/Umbraco.Web/Media/EmbedProviders/Flickr.cs +++ b/src/Umbraco.Web/Media/EmbedProviders/Flickr.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Media.EmbedProviders var imageHeight = GetXmlProperty(xmlDocument, "/oembed/height"); var imageTitle = GetXmlProperty(xmlDocument, "/oembed/title"); - return string.Format("\"{3}\"", imageUrl, imageWidth, imageHeight, HttpUtility.HtmlEncode(imageTitle)); + return string.Format("\"{3}\"", imageUrl, imageWidth, imageHeight, HttpUtility.HtmlEncode(imageTitle)); } } } diff --git a/src/Umbraco.Web/Media/EmbedProviders/Giphy.cs b/src/Umbraco.Web/Media/EmbedProviders/Giphy.cs new file mode 100644 index 0000000000..1069de749c --- /dev/null +++ b/src/Umbraco.Web/Media/EmbedProviders/Giphy.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Web.Media.EmbedProviders +{ + /// + /// Embed Provider for Giphy.com the popular online GIFs and animated sticker provider. + /// + public class Giphy : EmbedProviderBase + { + public override string ApiEndpoint => "https://giphy.com/services/oembed?url="; + + public override string[] UrlSchemeRegex => new string[] + { + @"giphy\.com/*", + @"gph\.is/*" + }; + + public override Dictionary RequestParams => new Dictionary(); + + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + var oembed = base.GetJsonResponse(requestUrl); + + return oembed.GetHtml(); + } + } +} diff --git a/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs b/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs index 86bd12c15e..1dcae62acd 100644 --- a/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs +++ b/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Media.Exif public ExifBitConverter(ByteOrder from, ByteOrder to) : base(from, to) { - ; + } #endregion diff --git a/src/Umbraco.Web/Media/Exif/ExifExceptions.cs b/src/Umbraco.Web/Media/Exif/ExifExceptions.cs index 040e84ff99..3d2ce61e9e 100644 --- a/src/Umbraco.Web/Media/Exif/ExifExceptions.cs +++ b/src/Umbraco.Web/Media/Exif/ExifExceptions.cs @@ -1,42 +1,47 @@ using System; +using System.Runtime.Serialization; namespace Umbraco.Web.Media.Exif { /// - /// The exception that is thrown when the format of the JPEG/Exif file - /// could not be understood. + /// The exception that is thrown when the format of the JPEG/EXIF file could not be understood. /// - internal class NotValidExifFileException : Exception + /// + [Serializable] + public class NotValidExifFileException : Exception { + /// + /// Initializes a new instance of the class. + /// public NotValidExifFileException() - : base("Not a valid JPEG/Exif file.") - { - ; - } + : base("Not a valid JPEG/EXIF file.") + { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. public NotValidExifFileException(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 NotValidExifFileException(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 NotValidExifFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } - /// - /// The exception that is thrown when an invalid enum type is given to an - /// ExifEnumProperty. - /// - internal class UnknownEnumTypeException : Exception - { - public UnknownEnumTypeException() - : base("Unknown enum type.") - { - ; - } - - public UnknownEnumTypeException(string message) - : base(message) - { - ; - } - } } diff --git a/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs b/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs index 40ba275fe4..8889a13e1d 100644 --- a/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs +++ b/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Media.Exif public ExifEnumProperty(ExifTag tag, T value) : this(tag, value, false) { - ; + } public override ExifInterOperability Interoperability @@ -64,7 +64,7 @@ namespace Umbraco.Web.Media.Exif return new ExifInterOperability(tagid, 3, 1, ExifBitConverter.GetBytes((ushort)((object)mValue), BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); } else - throw new UnknownEnumTypeException(); + throw new InvalidOperationException($"An invalid enum type ({basetype.FullName}) was provided for type {type.FullName}"); } } } @@ -210,13 +210,13 @@ namespace Umbraco.Web.Media.Exif public ExifPointSubjectArea(ExifTag tag, ushort[] value) : base(tag, value) { - ; + } public ExifPointSubjectArea(ExifTag tag, ushort x, ushort y) - : base(tag, new ushort[] { x, y }) + : base(tag, new ushort[] {x, y}) { - ; + } } @@ -239,13 +239,13 @@ namespace Umbraco.Web.Media.Exif public ExifCircularSubjectArea(ExifTag tag, ushort[] value) : base(tag, value) { - ; + } public ExifCircularSubjectArea(ExifTag tag, ushort x, ushort y, ushort d) : base(tag, new ushort[] { x, y, d }) { - ; + } } @@ -269,13 +269,13 @@ namespace Umbraco.Web.Media.Exif public ExifRectangularSubjectArea(ExifTag tag, ushort[] value) : base(tag, value) { - ; + } public ExifRectangularSubjectArea(ExifTag tag, ushort x, ushort y, ushort w, ushort h) : base(tag, new ushort[] { x, y, w, h }) { - ; + } } @@ -303,13 +303,13 @@ namespace Umbraco.Web.Media.Exif public GPSLatitudeLongitude(ExifTag tag, MathEx.UFraction32[] value) : base(tag, value) { - ; + } public GPSLatitudeLongitude(ExifTag tag, float d, float m, float s) : base(tag, new MathEx.UFraction32[] { new MathEx.UFraction32(d), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) { - ; + } } @@ -331,13 +331,13 @@ namespace Umbraco.Web.Media.Exif public GPSTimeStamp(ExifTag tag, MathEx.UFraction32[] value) : base(tag, value) { - ; + } public GPSTimeStamp(ExifTag tag, float h, float m, float s) : base(tag, new MathEx.UFraction32[] { new MathEx.UFraction32(h), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) { - ; + } } diff --git a/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs b/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs index 94b255f4d1..24b3ac74be 100644 --- a/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs +++ b/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Media.Exif public JFIFVersion(ExifTag tag, ushort value) : base(tag, value) { - ; + } public override string ToString() diff --git a/src/Umbraco.Web/Media/Exif/JPEGExceptions.cs b/src/Umbraco.Web/Media/Exif/JPEGExceptions.cs index 631c3cb1b9..40fd9f3be8 100644 --- a/src/Umbraco.Web/Media/Exif/JPEGExceptions.cs +++ b/src/Umbraco.Web/Media/Exif/JPEGExceptions.cs @@ -1,99 +1,171 @@ using System; +using System.Runtime.Serialization; namespace Umbraco.Web.Media.Exif { - /// - /// The exception that is thrown when the format of the image file - /// could not be understood. - /// - internal class NotValidImageFileException : Exception - { - public NotValidImageFileException() - : base("Not a valid image file.") - { - ; - } - - public NotValidImageFileException(string message) - : base(message) - { - ; - } - } /// - /// The exception that is thrown when the format of the JPEG file - /// could not be understood. + /// The exception that is thrown when the format of the JPEG file could not be understood. /// - internal class NotValidJPEGFileException : Exception + /// + [Serializable] + public class NotValidJPEGFileException : Exception { + /// + /// Initializes a new instance of the class. + /// public NotValidJPEGFileException() : base("Not a valid JPEG file.") - { - ; - } + { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. public NotValidJPEGFileException(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 NotValidJPEGFileException(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 NotValidJPEGFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } /// - /// The exception that is thrown when the format of the TIFF file - /// could not be understood. + /// The exception that is thrown when the format of the TIFF file could not be understood. /// - internal class NotValidTIFFileException : Exception + /// + [Serializable] + public class NotValidTIFFileException : Exception { + /// + /// Initializes a new instance of the class. + /// public NotValidTIFFileException() : base("Not a valid TIFF file.") - { - ; - } + { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. public NotValidTIFFileException(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 NotValidTIFFileException(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 NotValidTIFFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } /// - /// The exception that is thrown when the format of the TIFF header - /// could not be understood. + /// The exception that is thrown when the format of the TIFF header could not be understood. /// + /// + [Serializable] internal class NotValidTIFFHeader : Exception { + /// + /// Initializes a new instance of the class. + /// public NotValidTIFFHeader() : base("Not a valid TIFF header.") - { - ; - } + { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. public NotValidTIFFHeader(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 NotValidTIFFHeader(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 NotValidTIFFHeader(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } /// /// The exception that is thrown when the length of a section exceeds 64 kB. /// - internal class SectionExceeds64KBException : Exception + /// + [Serializable] + public class SectionExceeds64KBException : Exception { + /// + /// Initializes a new instance of the class. + /// public SectionExceeds64KBException() : base("Section length exceeds 64 kB.") - { - ; - } + { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. public SectionExceeds64KBException(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 SectionExceeds64KBException(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 SectionExceeds64KBException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } } diff --git a/src/Umbraco.Web/Media/Exif/JPEGSection.cs b/src/Umbraco.Web/Media/Exif/JPEGSection.cs index a1bc420fe4..78565d2bfa 100644 --- a/src/Umbraco.Web/Media/Exif/JPEGSection.cs +++ b/src/Umbraco.Web/Media/Exif/JPEGSection.cs @@ -45,7 +45,7 @@ public JPEGSection(JPEGMarker marker) : this(marker, new byte[0], new byte[0]) { - ; + } #endregion diff --git a/src/Umbraco.Web/Media/Exif/MathEx.cs b/src/Umbraco.Web/Media/Exif/MathEx.cs index 735358c40a..94cbccfbda 100644 --- a/src/Umbraco.Web/Media/Exif/MathEx.cs +++ b/src/Umbraco.Web/Media/Exif/MathEx.cs @@ -403,37 +403,37 @@ namespace Umbraco.Web.Media.Exif public Fraction32(int numerator, int denominator) : this(numerator, denominator, 0) { - ; + } public Fraction32(int numerator) : this(numerator, (int)1) { - ; + } public Fraction32(Fraction32 f) : this(f.Numerator, f.Denominator, f.Error) { - ; + } public Fraction32(float value) : this((double)value) { - ; + } public Fraction32(double value) : this(FromDouble(value)) { - ; + } public Fraction32(string s) : this(FromString(s)) { - ; + } #endregion @@ -1033,37 +1033,37 @@ namespace Umbraco.Web.Media.Exif public UFraction32(uint numerator, uint denominator) : this(numerator, denominator, 0) { - ; + } public UFraction32(uint numerator) : this(numerator, (uint)1) { - ; + } public UFraction32(UFraction32 f) : this(f.Numerator, f.Denominator, f.Error) { - ; + } public UFraction32(float value) : this((double)value) { - ; + } public UFraction32(double value) : this(FromDouble(value)) { - ; + } public UFraction32(string s) : this(FromString(s)) { - ; + } #endregion diff --git a/src/Umbraco.Web/Media/TypeDetector/SvgDetector.cs b/src/Umbraco.Web/Media/TypeDetector/SvgDetector.cs index 621e2237bc..fbd1b2c74c 100644 --- a/src/Umbraco.Web/Media/TypeDetector/SvgDetector.cs +++ b/src/Umbraco.Web/Media/TypeDetector/SvgDetector.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.Media.TypeDetector { document = XDocument.Load(fileStream); } - catch (System.Exception ex) + catch (System.Exception) { return false; } diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index 3bbcdfb0ac..6144d6f9c9 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -49,13 +49,15 @@ namespace Umbraco.Web /// /// The culture for the property, if the property is invariant than this is empty internal static void AddPropertyError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, - ValidationResult result, string propertyAlias, string culture = "") + ValidationResult result, string propertyAlias, string culture = "", string segment = "") { if (culture == null) culture = ""; modelState.AddValidationError(result, "_Properties", propertyAlias, //if the culture is null, we'll add the term 'invariant' as part of the key - culture.IsNullOrWhiteSpace() ? "invariant" : culture); + culture.IsNullOrWhiteSpace() ? "invariant" : culture, + // if the segment is null, we'll add the term 'null' as part of the key + segment.IsNullOrWhiteSpace() ? "null" : segment); } /// @@ -73,7 +75,7 @@ namespace Umbraco.Web } /// - /// Returns a list of cultures that have property validation errors errors + /// Returns a list of cultures that have property validation errors /// /// /// @@ -148,11 +150,33 @@ namespace Umbraco.Web var delimitedParts = string.Join(".", parts); foreach (var memberName in result.MemberNames) { - modelState.AddModelError($"{delimitedParts}.{memberName}", result.ErrorMessage); + modelState.TryAddModelError($"{delimitedParts}.{memberName}", result.ErrorMessage); withNames = true; } if (!withNames) - modelState.AddModelError($"{delimitedParts}", result.ErrorMessage); + { + modelState.TryAddModelError($"{delimitedParts}", result.ErrorMessage); + } + + } + + /// + /// Will add an error to model state for a key if that key and error don't already exist + /// + /// + /// + /// + /// + private static bool TryAddModelError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, string key, string errorMsg) + { + if (modelState.TryGetValue(key, out var errs)) + { + foreach(var e in errs.Errors) + if (e.ErrorMessage == errorMsg) return false; //if this same error message exists for the same key, just exit + } + + modelState.AddModelError(key, errorMsg); + return true; } public static IDictionary ToErrorDictionary(this System.Web.Http.ModelBinding.ModelStateDictionary modelState) diff --git a/src/Umbraco.Web/Models/AnchorsModel.cs b/src/Umbraco.Web/Models/AnchorsModel.cs new file mode 100644 index 0000000000..9edcf3466b --- /dev/null +++ b/src/Umbraco.Web/Models/AnchorsModel.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Web.Models +{ + public class AnchorsModel + { + public string RteContent { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/BackOfficeTour.cs b/src/Umbraco.Web/Models/BackOfficeTour.cs index d5987ec5bc..7396d3d00d 100644 --- a/src/Umbraco.Web/Models/BackOfficeTour.cs +++ b/src/Umbraco.Web/Models/BackOfficeTour.cs @@ -16,20 +16,32 @@ namespace Umbraco.Web.Models [DataMember(Name = "name")] public string Name { get; set; } + [DataMember(Name = "alias")] public string Alias { get; set; } + [DataMember(Name = "group")] public string Group { get; set; } + [DataMember(Name = "groupOrder")] public int GroupOrder { get; set; } + + [DataMember(Name = "hidden")] + public bool Hidden { get; set; } + [DataMember(Name = "allowDisable")] public bool AllowDisable { get; set; } + [DataMember(Name = "requiredSections")] public List RequiredSections { get; set; } + [DataMember(Name = "steps")] public BackOfficeTourStep[] Steps { get; set; } [DataMember(Name = "culture")] public string Culture { get; set; } + + [DataMember(Name = "contentType")] + public string ContentType { get; set; } } } diff --git a/src/Umbraco.Web/Models/ChangingPasswordModel.cs b/src/Umbraco.Web/Models/ChangingPasswordModel.cs index 355b358049..633c42a056 100644 --- a/src/Umbraco.Web/Models/ChangingPasswordModel.cs +++ b/src/Umbraco.Web/Models/ChangingPasswordModel.cs @@ -49,5 +49,11 @@ namespace Umbraco.Web.Models /// [DataMember(Name = "generatedPassword")] public string GeneratedPassword { get; set; } + + /// + /// The id of the user - required to allow changing password without the entire UserSave model + /// + [DataMember(Name = "id")] + public int Id { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs index dc72b0a81f..d4156d6db5 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs @@ -38,6 +38,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "updater")] public UserProfile Updater { get; set; } + public int ContentTypeId { get; set; } + [DataMember(Name = "contentTypeAlias", IsRequired = true)] [Required(AllowEmptyStrings = false)] public string ContentTypeAlias { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index 80358bfc7a..b6a90a93c3 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -108,6 +108,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "treeNodeUrl")] public string TreeNodeUrl { get; set; } + [DataMember(Name = "contentTypeId")] + public int ContentTypeId { get; set; } + [DataMember(Name = "contentTypeAlias", IsRequired = true)] [Required(AllowEmptyStrings = false)] public string ContentTypeAlias { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs index d9d730956a..2b70a63035 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Core.PropertyEditors; @@ -21,6 +22,10 @@ namespace Umbraco.Web.Models.ContentEditing [Required] public int Id { get; set; } + [DataMember(Name = "dataTypeKey", IsRequired = false)] + [ReadOnly(true)] + public Guid DataTypeKey { get; set; } + [DataMember(Name = "value")] public object Value { get; set; } @@ -48,11 +53,21 @@ namespace Umbraco.Web.Models.ContentEditing [ReadOnly(true)] public string Culture { get; set; } + /// + /// The segment of the property + /// + /// + /// The segment value of a property can always be null but can only have a non-null value + /// when the property can be varied by segment. + /// + [DataMember(Name = "segment")] + [ReadOnly(true)] + public string Segment { get; set; } + /// /// Used internally during model mapping /// [IgnoreDataMember] internal IDataEditor PropertyEditor { get; set; } - } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDto.cs b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDto.cs index 1f9d0e9b77..83e5e2a9b2 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDto.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDto.cs @@ -11,10 +11,17 @@ namespace Umbraco.Web.Models.ContentEditing internal class ContentPropertyDto : ContentPropertyBasic { public IDataType DataType { get; set; } + public string Label { get; set; } + public string Description { get; set; } + public bool IsRequired { get; set; } + + public string IsRequiredMessage { get; set; } + public string ValidationRegExp { get; set; } + public string ValidationRegExpMessage { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs index b1d24c5fd2..a93be56846 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs @@ -69,6 +69,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "allowCultureVariant")] public bool AllowCultureVariant { get; set; } + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } + //Tabs [DataMember(Name = "groups")] public IEnumerable> Groups { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs index 61dacfb2aa..9a7555ad92 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Core.Models; using Umbraco.Core.Models.Validation; @@ -16,6 +17,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "name", IsRequired = true)] [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] + [MaxLength(255, ErrorMessage ="Name must be less than 255 characters")] public string Name { get; set; } [DataMember(Name = "properties")] @@ -27,6 +29,12 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "culture")] public string Culture { get; set; } + /// + /// The segment of this variant, if this is invariant than this is null or empty + /// + [DataMember(Name = "segment")] + public string Segment { get; set; } + /// /// Indicates if the variant should be updated /// diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs index aff79d7b9d..2a2ec49002 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs @@ -24,6 +24,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "name", IsRequired = true)] public string Name { get; set; } + [DataMember(Name = "displayName")] + public string DisplayName { get; set; } + /// /// Defines the tabs containing display properties /// diff --git a/src/Umbraco.Web/Models/ContentEditing/DataTypeReferences.cs b/src/Umbraco.Web/Models/ContentEditing/DataTypeReferences.cs new file mode 100644 index 0000000000..d9cd93e7c8 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DataTypeReferences.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "dataTypeReferences", Namespace = "")] + public class DataTypeReferences + { + [DataMember(Name = "documentTypes")] + public IEnumerable DocumentTypes { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "mediaTypes")] + public IEnumerable MediaTypes { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "memberTypes")] + public IEnumerable MemberTypes { get; set; } = Enumerable.Empty(); + + [DataContract(Name = "contentType", Namespace = "")] + public class ContentTypeReferences : EntityBasic + { + [DataMember(Name = "properties")] + public object Properties { get; set; } + + [DataContract(Name = "property", Namespace = "")] + public class PropertyTypeReferences + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "alias")] + public string Alias { get; set; } + } + } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs index 6b82f74ca7..9467033aec 100644 --- a/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs @@ -20,9 +20,12 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "defaultTemplate")] public EntityBasic DefaultTemplate { get; set; } - + [DataMember(Name = "allowCultureVariant")] public bool AllowCultureVariant { get; set; } + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } + } } diff --git a/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs index 38442f6da5..b1528db697 100644 --- a/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs +++ b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs @@ -16,5 +16,10 @@ /// along with any content types that have matching property types that are included in the filtered content types /// public string[] FilterContentTypes { get; set; } + + /// + /// Wether the content type is currently marked as an element type + /// + public bool IsElement { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MediaTypeDisplay.cs index 40227184db..ea0393336c 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MediaTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MediaTypeDisplay.cs @@ -5,6 +5,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "contentType", Namespace = "")] public class MediaTypeDisplay : ContentTypeCompositionDisplay { - + [DataMember(Name = "isSystemMediaType")] + public bool IsSystemMediaType { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs index fd1c1ed5b8..3857731671 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.Models.Membership; namespace Umbraco.Web.Models.ContentEditing @@ -15,6 +16,7 @@ namespace Umbraco.Web.Models.ContentEditing public MemberDisplay() { MemberProviderFieldMapping = new Dictionary(); + ContentApps = new List(); } [DataMember(Name = "username")] @@ -34,5 +36,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "fieldConfig")] public IDictionary MemberProviderFieldMapping { get; set; } + [DataMember(Name = "apps")] + public IEnumerable ContentApps { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/MessagesExtensions.cs b/src/Umbraco.Web/Models/ContentEditing/MessagesExtensions.cs index 3a8496ac3f..1f526a50f3 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MessagesExtensions.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MessagesExtensions.cs @@ -1,10 +1,15 @@  +using System.Linq; +using Umbraco.Core; + namespace Umbraco.Web.Models.ContentEditing { public static class MessagesExtensions { public static void AddNotification(this INotificationModel model, string header, string msg, NotificationStyle type) { + if (model.Exists(header, msg, type)) return; + model.Notifications.Add(new Notification() { Header = header, @@ -15,6 +20,8 @@ namespace Umbraco.Web.Models.ContentEditing public static void AddSuccessNotification(this INotificationModel model, string header, string msg) { + if (model.Exists(header, msg, NotificationStyle.Success)) return; + model.Notifications.Add(new Notification() { Header = header, @@ -25,6 +32,8 @@ namespace Umbraco.Web.Models.ContentEditing public static void AddErrorNotification(this INotificationModel model, string header, string msg) { + if (model.Exists(header, msg, NotificationStyle.Error)) return; + model.Notifications.Add(new Notification() { Header = header, @@ -35,6 +44,8 @@ namespace Umbraco.Web.Models.ContentEditing public static void AddWarningNotification(this INotificationModel model, string header, string msg) { + if (model.Exists(header, msg, NotificationStyle.Warning)) return; + model.Notifications.Add(new Notification() { Header = header, @@ -45,6 +56,8 @@ namespace Umbraco.Web.Models.ContentEditing public static void AddInfoNotification(this INotificationModel model, string header, string msg) { + if (model.Exists(header, msg, NotificationStyle.Info)) return; + model.Notifications.Add(new Notification() { Header = header, @@ -52,5 +65,7 @@ namespace Umbraco.Web.Models.ContentEditing NotificationType = NotificationStyle.Info }); } + + private static bool Exists(this INotificationModel model, string header, string message, NotificationStyle notificationType) => model.Notifications.Any(x => x.Header.InvariantEquals(header) && x.Message.InvariantEquals(message) && x.NotificationType == notificationType); } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs index cde9d0dabc..5ff1744ea2 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; @@ -43,11 +44,18 @@ namespace Umbraco.Web.Models.ContentEditing [Required] public int DataTypeId { get; set; } + [DataMember(Name = "dataTypeKey")] + [ReadOnly(true)] + public Guid DataTypeKey { get; set; } + //SD: Is this really needed ? [DataMember(Name = "groupId")] public int GroupId { get; set; } [DataMember(Name = "allowCultureVariant")] public bool AllowCultureVariant { get; set; } + + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeValidation.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeValidation.cs index 929c7ce324..a45af12341 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeValidation.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeValidation.cs @@ -11,7 +11,13 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "mandatory")] public bool Mandatory { get; set; } + [DataMember(Name = "mandatoryMessage")] + public string MandatoryMessage { get; set; } + [DataMember(Name = "pattern")] public string Pattern { get; set; } + + [DataMember(Name = "patternMessage")] + public string PatternMessage { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs index 55e140d6f3..b7f209cd20 100644 --- a/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.Models.ContentEditing /// /// Corresponds to the NodeObjectType in the umbracoNode table [DataMember(Name = "parentObjectType", IsRequired = true)] - public Guid ParentObjectType { get; set; } + public Guid? ParentObjectType { get; set; } /// /// Gets or sets the Parent's object type name. @@ -38,7 +38,7 @@ namespace Umbraco.Web.Models.ContentEditing /// /// Corresponds to the NodeObjectType in the umbracoNode table [DataMember(Name = "childObjectType", IsRequired = true)] - public Guid ChildObjectType { get; set; } + public Guid? ChildObjectType { get; set; } /// /// Gets or sets the Child's object type name. @@ -47,13 +47,6 @@ namespace Umbraco.Web.Models.ContentEditing [ReadOnly(true)] public string ChildObjectTypeName { get; set; } - /// - /// Gets or sets the relations associated with this relation type. - /// - [DataMember(Name = "relations")] - [ReadOnly(true)] - public IEnumerable Relations { get; set; } - /// /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. /// diff --git a/src/Umbraco.Web/Models/ContentEditing/RelationTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/RelationTypeSave.cs index e7e8d6d2ba..434cf1de89 100644 --- a/src/Umbraco.Web/Models/ContentEditing/RelationTypeSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/RelationTypeSave.cs @@ -16,12 +16,12 @@ namespace Umbraco.Web.Models.ContentEditing /// Gets or sets the parent object type ID. /// [DataMember(Name = "parentObjectType", IsRequired = false)] - public Guid ParentObjectType { get; set; } + public Guid? ParentObjectType { get; set; } /// /// Gets or sets the child object type ID. /// [DataMember(Name = "childObjectType", IsRequired = false)] - public Guid ChildObjectType { get; set; } + public Guid? ChildObjectType { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/UrlAndAnchors.cs b/src/Umbraco.Web/Models/ContentEditing/UrlAndAnchors.cs new file mode 100644 index 0000000000..86642a8d65 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/UrlAndAnchors.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "urlAndAnchors", Namespace = "")] + public class UrlAndAnchors + { + public UrlAndAnchors(string url, IEnumerable anchorValues) + { + Url = url; + AnchorValues = anchorValues; + } + + [DataMember(Name = "url")] + public string Url { get; } + + [DataMember(Name = "anchorValues")] + public IEnumerable AnchorValues { get; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/UserGroupBasic.cs b/src/Umbraco.Web/Models/ContentEditing/UserGroupBasic.cs index 0457711c6c..0446ed6d34 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserGroupBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserGroupBasic.cs @@ -33,5 +33,11 @@ namespace Umbraco.Web.Models.ContentEditing /// [DataMember(Name = "userCount")] public int UserCount { get; set; } + + /// + /// Is the user group a system group e.g. "Administrators", "Sensitive data" or "Translators" + /// + [DataMember(Name = "isSystemUserGroup")] + public bool IsSystemUserGroup { get; set; } } } diff --git a/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs b/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs new file mode 100644 index 0000000000..e471e9aa4a --- /dev/null +++ b/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs @@ -0,0 +1,64 @@ +using System.Globalization; +using System.Text; +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models +{ + internal class ImageProcessorImageUrlGenerator : IImageUrlGenerator + { + public string GetImageUrl(ImageUrlGenerationOptions options) + { + if (options == null) return null; + + var imageProcessorUrl = new StringBuilder(options.ImageUrl ?? string.Empty); + + if (options.FocalPoint != null) AppendFocalPoint(imageProcessorUrl, options); + else if (options.Crop != null) AppendCrop(imageProcessorUrl, options); + else if (options.DefaultCrop) imageProcessorUrl.Append("?anchor=center&mode=crop"); + else + { + imageProcessorUrl.Append("?mode=").Append((options.ImageCropMode ?? "crop").ToLower()); + + if (options.ImageCropAnchor != null) imageProcessorUrl.Append("&anchor=").Append(options.ImageCropAnchor.ToLower()); + } + + var hasFormat = options.FurtherOptions != null && options.FurtherOptions.InvariantContains("&format="); + + //Only put quality here, if we don't have a format specified. + //Otherwise we need to put quality at the end to avoid it being overridden by the format. + if (options.Quality != null && hasFormat == false) imageProcessorUrl.Append("&quality=").Append(options.Quality); + if (options.HeightRatio != null) imageProcessorUrl.Append("&heightratio=").Append(options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.WidthRatio != null) imageProcessorUrl.Append("&widthratio=").Append(options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.Width != null) imageProcessorUrl.Append("&width=").Append(options.Width); + if (options.Height != null) imageProcessorUrl.Append("&height=").Append(options.Height); + if (options.UpScale == false) imageProcessorUrl.Append("&upscale=false"); + if (options.AnimationProcessMode != null) imageProcessorUrl.Append("&animationprocessmode=").Append(options.AnimationProcessMode); + if (options.FurtherOptions != null) imageProcessorUrl.Append(options.FurtherOptions); + + //If furtherOptions contains a format, we need to put the quality after the format. + if (options.Quality != null && hasFormat) imageProcessorUrl.Append("&quality=").Append(options.Quality); + if (options.CacheBusterValue != null) imageProcessorUrl.Append("&rnd=").Append(options.CacheBusterValue); + + return imageProcessorUrl.ToString(); + } + + private void AppendFocalPoint(StringBuilder imageProcessorUrl, ImageUrlGenerationOptions options) + { + imageProcessorUrl.Append("?center="); + imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append("&mode=crop"); + } + + private void AppendCrop(StringBuilder imageProcessorUrl, ImageUrlGenerationOptions options) + { + imageProcessorUrl.Append("?crop="); + imageProcessorUrl.Append(options.Crop.X1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.X2.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y2.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append("&cropmode=percentage"); + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index 2b30b0ac5a..865057ba24 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -5,6 +5,7 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Routing; @@ -27,6 +28,7 @@ namespace Umbraco.Web.Models.Mapping private readonly ILocalizationService _localizationService; private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IEntityService _entityService; private readonly TabsAndPropertiesMapper _tabsAndPropertiesMapper; private readonly ContentSavedStateMapper _stateMapper; private readonly ContentBasicSavedStateMapper _basicStateMapper; @@ -34,7 +36,7 @@ namespace Umbraco.Web.Models.Mapping public ContentMapDefinition(CommonMapper commonMapper, ILocalizedTextService localizedTextService, IContentService contentService, IContentTypeService contentTypeService, IFileService fileService, IUmbracoContextAccessor umbracoContextAccessor, IPublishedRouter publishedRouter, ILocalizationService localizationService, ILogger logger, - IUserService userService) + IUserService userService, IEntityService entityService) { _commonMapper = commonMapper; _localizedTextService = localizedTextService; @@ -46,11 +48,11 @@ namespace Umbraco.Web.Models.Mapping _localizationService = localizationService; _logger = logger; _userService = userService; - + _entityService = entityService; _tabsAndPropertiesMapper = new TabsAndPropertiesMapper(localizedTextService); _stateMapper = new ContentSavedStateMapper(); _basicStateMapper = new ContentBasicSavedStateMapper(); - _contentVariantMapper = new ContentVariantMapper(_localizationService); + _contentVariantMapper = new ContentVariantMapper(_localizationService, localizedTextService); } public void DefineMaps(UmbracoMapper mapper) @@ -73,13 +75,14 @@ namespace Umbraco.Web.Models.Mapping target.AllowedActions = GetActions(source); target.AllowedTemplates = GetAllowedTemplates(source); target.ContentApps = _commonMapper.GetContentApps(source); + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = _localizedTextService.UmbracoDictionaryTranslate(source.ContentType.Name); target.DocumentType = _commonMapper.GetContentType(source, context); target.Icon = source.ContentType.Icon; target.Id = source.Id; target.IsBlueprint = source.Blueprint; - target.IsChildOfListView = DermineIsChildOfListView(source); + target.IsChildOfListView = DetermineIsChildOfListView(source, context); target.IsContainer = source.ContentType.IsContainer; target.IsElement = source.ContentType.IsElement; target.Key = source.Key; @@ -101,7 +104,7 @@ namespace Umbraco.Web.Models.Mapping target.ContentDto.Properties = context.MapEnumerable(source.Properties); } - // Umbraco.Code.MapAll -Segment -Language + // Umbraco.Code.MapAll -Segment -Language -DisplayName private void Map(IContent source, ContentVariantDisplay target, MapperContext context) { target.CreateDate = source.CreateDate; @@ -117,6 +120,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Alias private void Map(IContent source, ContentItemBasic target, MapperContext context) { + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.CreateDate = source.CreateDate; target.Edited = source.Edited; @@ -209,13 +213,66 @@ namespace Umbraco.Web.Models.Mapping return source.CultureInfos.TryGetValue(culture, out var name) && !name.Name.IsNullOrWhiteSpace() ? name.Name : $"({source.Name})"; } - private bool DermineIsChildOfListView(IContent source) + /// + /// Checks if the content item is a descendant of a list view + /// + /// + /// + /// + /// Returns true if the content item is a descendant of a list view and where the content is + /// not a current user's start node. + /// + /// + /// We must check if it's the current user's start node because in that case we will actually be + /// rendering the tree node underneath the list view to visually show context. In this case we return + /// false because the item is technically not being rendered as part of a list view but instead as a + /// real tree node. If we didn't perform this check then tree syncing wouldn't work correctly. + /// + private bool DetermineIsChildOfListView(IContent source, MapperContext context) { - // map the IsChildOfListView (this is actually if it is a descendant of a list view!) + var userStartNodes = Array.Empty(); + + // In cases where a user's start node is below a list view, we will actually render + // out the tree to that start node and in that case for that start node, we want to return + // false here. + if (context.HasItems && context.Items.TryGetValue("CurrentUser", out var usr) && usr is IUser currentUser) + { + userStartNodes = currentUser.CalculateContentStartNodeIds(_entityService); + if (!userStartNodes.Contains(Constants.System.Root)) + { + // return false if this is the user's actual start node, the node will be rendered in the tree + // regardless of if it's a list view or not + if (userStartNodes.Contains(source.Id)) + return false; + } + } + var parent = _contentService.GetParent(source); - return parent != null && (parent.ContentType.IsContainer || _contentTypeService.HasContainerInPath(parent.Path)); + + if (parent == null) + return false; + + var pathParts = parent.Path.Split(',').Select(x => int.TryParse(x, out var i) ? i : 0).ToList(); + + // reduce the path parts so we exclude top level content items that + // are higher up than a user's start nodes + foreach (var n in userStartNodes) + { + var index = pathParts.IndexOf(n); + if (index != -1) + { + // now trim all top level start nodes to the found index + for (var i = 0; i < index; i++) + { + pathParts.RemoveAt(0); + } + } + } + + return parent.ContentType.IsContainer || _contentTypeService.HasContainerInPath(pathParts.ToArray()); } + private DateTime? GetScheduledDate(IContent source, ContentScheduleAction action, MapperContext context) { var culture = context.GetCulture() ?? string.Empty; diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs index e7c53f5728..cf1bc3c253 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs @@ -16,15 +16,17 @@ namespace Umbraco.Web.Models.Mapping internal class ContentPropertyBasicMapper where TDestination : ContentPropertyBasic, new() { + private readonly IEntityService _entityService; private readonly ILogger _logger; private readonly PropertyEditorCollection _propertyEditors; protected IDataTypeService DataTypeService { get; } - public ContentPropertyBasicMapper(IDataTypeService dataTypeService, ILogger logger, PropertyEditorCollection propertyEditors) + public ContentPropertyBasicMapper(IDataTypeService dataTypeService, IEntityService entityService, ILogger logger, PropertyEditorCollection propertyEditors) { _logger = logger; _propertyEditors = propertyEditors; DataTypeService = dataTypeService; + _entityService = entityService; } /// @@ -48,6 +50,7 @@ namespace Umbraco.Web.Models.Mapping dest.Alias = property.Alias; dest.PropertyEditor = editor; dest.Editor = editor.Alias; + dest.DataTypeKey = property.PropertyType.DataTypeKey; // if there's a set of property aliases specified, we will check if the current property's value should be mapped. // if it isn't one of the ones specified in 'includeProperties', we will just return the result without mapping the Value. @@ -67,8 +70,13 @@ namespace Umbraco.Web.Models.Mapping dest.Culture = culture; + // Get the segment, which is always allowed to be null even if the propertyType *can* be varied by segment. + // There is therefore no need to perform the null check like with culture above. + var segment = !property.PropertyType.VariesBySegment() ? null : context.GetSegment(); + dest.Segment = segment; + // if no 'IncludeProperties' were specified or this property is set to be included - we will map the value and return. - dest.Value = editor.GetValueEditor().ToEditor(property, DataTypeService, culture); + dest.Value = editor.GetValueEditor().ToEditor(property, DataTypeService, culture, segment); } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs index 8a45548e9c..d81f81abe2 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs @@ -14,8 +14,8 @@ namespace Umbraco.Web.Models.Mapping { private readonly ILocalizedTextService _textService; - public ContentPropertyDisplayMapper(IDataTypeService dataTypeService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors) - : base(dataTypeService, logger, propertyEditors) + public ContentPropertyDisplayMapper(IDataTypeService dataTypeService, IEntityService entityService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors) + : base(dataTypeService, entityService, logger, propertyEditors) { _textService = textService; } @@ -42,7 +42,9 @@ namespace Umbraco.Web.Models.Mapping //add the validation information dest.Validation.Mandatory = originalProp.PropertyType.Mandatory; + dest.Validation.MandatoryMessage = originalProp.PropertyType.MandatoryMessage; dest.Validation.Pattern = originalProp.PropertyType.ValidationRegExp; + dest.Validation.PatternMessage = originalProp.PropertyType.ValidationRegExpMessage; if (dest.PropertyEditor == null) { diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs index f192cd32ce..fd57e4dd2e 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs @@ -12,8 +12,8 @@ namespace Umbraco.Web.Models.Mapping /// internal class ContentPropertyDtoMapper : ContentPropertyBasicMapper { - public ContentPropertyDtoMapper(IDataTypeService dataTypeService, ILogger logger, PropertyEditorCollection propertyEditors) - : base(dataTypeService, logger, propertyEditors) + public ContentPropertyDtoMapper(IDataTypeService dataTypeService, IEntityService entityService, ILogger logger, PropertyEditorCollection propertyEditors) + : base(dataTypeService, entityService, logger, propertyEditors) { } public override void Map(Property property, ContentPropertyDto dest, MapperContext context) @@ -21,7 +21,9 @@ namespace Umbraco.Web.Models.Mapping base.Map(property, dest, context); dest.IsRequired = property.PropertyType.Mandatory; + dest.IsRequiredMessage = property.PropertyType.MandatoryMessage; dest.ValidationRegExp = property.PropertyType.ValidationRegExp; + dest.ValidationRegExpMessage = property.PropertyType.ValidationRegExpMessage; dest.Description = property.PropertyType.Description; dest.Label = property.PropertyType.Name; dest.DataType = DataTypeService.GetDataType(property.PropertyType.DataTypeId); diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyMapDefinition.cs index 226560c516..e6290cc19e 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyMapDefinition.cs @@ -17,11 +17,11 @@ namespace Umbraco.Web.Models.Mapping private readonly ContentPropertyDtoMapper _contentPropertyDtoConverter; private readonly ContentPropertyDisplayMapper _contentPropertyDisplayMapper; - public ContentPropertyMapDefinition(IDataTypeService dataTypeService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors) + public ContentPropertyMapDefinition(IDataTypeService dataTypeService, IEntityService entityService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors) { - _contentPropertyBasicConverter = new ContentPropertyBasicMapper(dataTypeService, logger, propertyEditors); - _contentPropertyDtoConverter = new ContentPropertyDtoMapper(dataTypeService, logger, propertyEditors); - _contentPropertyDisplayMapper = new ContentPropertyDisplayMapper(dataTypeService, textService, logger, propertyEditors); + _contentPropertyBasicConverter = new ContentPropertyBasicMapper(dataTypeService, entityService, logger, propertyEditors); + _contentPropertyDtoConverter = new ContentPropertyDtoMapper(dataTypeService, entityService, logger, propertyEditors); + _contentPropertyDisplayMapper = new ContentPropertyDisplayMapper(dataTypeService, entityService, textService, logger, propertyEditors); } public void DefineMaps(UmbracoMapper mapper) diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index a438f04781..c767e9aaf3 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Web.Models.ContentEditing; using Umbraco.Core.Services; +using Umbraco.Core.Exceptions; namespace Umbraco.Web.Models.Mapping { @@ -120,6 +121,7 @@ namespace Umbraco.Web.Models.Mapping MapTypeToDisplayBase(source, target); target.AllowCultureVariant = source.VariesByCulture(); + target.AllowSegmentVariant = source.VariesBySegment(); //sync templates target.AllowedTemplates = context.MapEnumerable(source.AllowedTemplates); @@ -144,6 +146,7 @@ namespace Umbraco.Web.Models.Mapping //default listview target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; + target.IsSystemMediaType = source.IsSystemMediaType(); if (string.IsNullOrEmpty(source.Name)) return; @@ -214,14 +217,18 @@ namespace Umbraco.Web.Models.Mapping } // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate - // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType + // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType -Variations private static void Map(PropertyTypeBasic source, PropertyType target, MapperContext context) { target.Name = source.Label; target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; target.Mandatory = source.Validation.Mandatory; + target.MandatoryMessage = source.Validation.MandatoryMessage; target.ValidationRegExp = source.Validation.Pattern; - target.Variations = source.AllowCultureVariant ? ContentVariation.Culture : ContentVariation.Nothing; + target.ValidationRegExpMessage = source.Validation.PatternMessage; + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); + target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); if (source.Id > 0) target.Id = source.Id; @@ -333,7 +340,9 @@ namespace Umbraco.Web.Models.Mapping { target.Alias = source.Alias; target.AllowCultureVariant = source.AllowCultureVariant; + target.AllowSegmentVariant = source.AllowSegmentVariant; target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; target.Description = source.Description; target.GroupId = source.GroupId; target.Id = source.Id; @@ -348,7 +357,9 @@ namespace Umbraco.Web.Models.Mapping { target.Alias = source.Alias; target.AllowCultureVariant = source.AllowCultureVariant; + target.AllowSegmentVariant = source.AllowSegmentVariant; target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; target.Description = source.Description; target.GroupId = source.GroupId; target.Id = source.Id; @@ -361,7 +372,7 @@ namespace Umbraco.Web.Models.Mapping target.Validation = source.Validation; } - // Umbraco.Code.MapAll -CreatorId -Level -SortOrder + // Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate // Umbraco.Code.MapAll -ContentTypeComposition (done by AfterMapSaveToType) private static void MapSaveToTypeBase(TSource source, IContentTypeComposition target, MapperContext context) @@ -391,9 +402,8 @@ namespace Umbraco.Web.Models.Mapping if (!(target is IMemberType)) { - target.Variations = ContentVariation.Nothing; - if (source.AllowCultureVariant) - target.Variations |= ContentVariation.Culture; + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); + target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); } // handle property groups and property types @@ -484,7 +494,7 @@ namespace Umbraco.Web.Models.Mapping target.Udi = MapContentTypeUdi(source); target.UpdateDate = source.UpdateDate; - target.AllowedContentTypes = source.AllowedContentTypes.Select(x => x.Id.Value); + target.AllowedContentTypes = source.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value); target.CompositeContentTypes = source.ContentTypeComposition.Select(x => x.Alias); target.LockedCompositeContentTypes = MapLockedCompositions(source); } @@ -574,7 +584,7 @@ namespace Umbraco.Web.Models.Mapping udiType = Constants.UdiEntityType.DocumentType; break; default: - throw new Exception("panic"); + throw new PanicException($"Source is of type {source.GetType()} which isn't supported here"); } return Udi.Create(udiType, source.Key); diff --git a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs index 560d398a2c..0b6be53045 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Umbraco.Core; using Umbraco.Core.Mapping; @@ -13,60 +14,158 @@ namespace Umbraco.Web.Models.Mapping internal class ContentVariantMapper { private readonly ILocalizationService _localizationService; + private readonly ILocalizedTextService _localizedTextService; - public ContentVariantMapper(ILocalizationService localizationService) + public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService) { _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); } public IEnumerable Map(IContent source, MapperContext context) { - var result = new List(); - if (!source.ContentType.VariesByCulture()) + var variesByCulture = source.ContentType.VariesByCulture(); + var variesBySegment = source.ContentType.VariesBySegment(); + + IList variants = new List(); + + if (!variesByCulture && !variesBySegment) { - //this is invariant so just map the IContent instance to ContentVariationDisplay - result.Add(context.Map(source)); + // this is invariant so just map the IContent instance to ContentVariationDisplay + var variantDisplay = context.Map(source); + variants.Add(variantDisplay); + } + else if (variesByCulture && !variesBySegment) + { + var languages = GetLanguages(context); + variants = languages + .Select(language => CreateVariantDisplay(context, source, language, null)) + .ToList(); + } + else if (variesBySegment && !variesByCulture) + { + // Segment only + var segments = GetSegments(source); + variants = segments + .Select(segment => CreateVariantDisplay(context, source, null, segment)) + .ToList(); } else { - var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); - if (allLanguages.Count == 0) return Enumerable.Empty(); //this should never happen + // Culture and segment + var languages = GetLanguages(context).ToList(); + var segments = GetSegments(source).ToList(); - var langs = context.MapEnumerable(allLanguages).ToList(); - - //create a variant for each language, then we'll populate the values - var variants = langs.Select(x => + if (languages.Count == 0 || segments.Count == 0) { - //We need to set the culture in the mapping context since this is needed to ensure that the correct property values - //are resolved during the mapping - context.SetCulture(x.IsoCode); - return context.Map(source); - }).ToList(); - - for (int i = 0; i < langs.Count; i++) - { - var x = langs[i]; - var variant = variants[i]; - - variant.Language = x; - variant.Name = source.GetCultureName(x.IsoCode); + // This should not happen + throw new InvalidOperationException("No languages or segments available"); } - //Put the default language first in the list & then sort rest by a-z - var defaultLang = variants.SingleOrDefault(x => x.Language.IsDefault); + variants = languages + .SelectMany(language => segments + .Select(segment => CreateVariantDisplay(context, source, language, segment))) + .ToList(); + } - //Remove the default language from the list for now - variants.Remove(defaultLang); - - //Sort the remaining languages a-z - variants = variants.OrderBy(x => x.Name).ToList(); - - //Insert the default language as the first item - variants.Insert(0, defaultLang); + return SortVariants(variants); + } + private IList SortVariants(IList variants) + { + if (variants == null || variants.Count <= 1) + { return variants; } - return result; + + // Default variant first, then order by language, segment. + return variants + .OrderBy(v => IsDefaultLanguage(v) ? 0 : 1) + .ThenBy(v => IsDefaultSegment(v) ? 0 : 1) + .ThenBy(v => v?.Language?.Name) + .ThenBy(v => v.Segment) + .ToList(); + } + + private static bool IsDefaultSegment(ContentVariantDisplay variant) + { + return variant.Segment == null; + } + + private static bool IsDefaultLanguage(ContentVariantDisplay variant) + { + return variant.Language == null || variant.Language.IsDefault; + } + + private IEnumerable GetLanguages(MapperContext context) + { + var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); + if (allLanguages.Count == 0) + { + // This should never happen + return Enumerable.Empty(); + } + else + { + return context.MapEnumerable(allLanguages).ToList(); + } + } + + /// + /// Returns all segments assigned to the content + /// + /// + /// + /// Returns all segments assigned to the content including the default `null` segment. + /// + private IEnumerable GetSegments(IContent content) + { + // The default segment (null) is always there, + // even when there is no property data at all yet + var segments = new List { null }; + + // Add actual segments based on the property values + segments.AddRange(content.Properties.SelectMany(p => p.Values.Select(v => v.Segment))); + + // Do not return a segment more than once + return segments.Distinct(); + } + + private ContentVariantDisplay CreateVariantDisplay(MapperContext context, IContent content, Language language, string segment) + { + context.SetCulture(language?.IsoCode); + context.SetSegment(segment); + + var variantDisplay = context.Map(content); + + variantDisplay.Segment = segment; + variantDisplay.Language = language; + variantDisplay.Name = content.GetCultureName(language?.IsoCode); + variantDisplay.DisplayName = GetDisplayName(language, segment); + + return variantDisplay; + } + + private string GetDisplayName(Language language, string segment) + { + var isCultureVariant = language != null; + var isSegmentVariant = !segment.IsNullOrWhiteSpace(); + + if(!isCultureVariant && !isSegmentVariant) + { + return _localizedTextService.Localize("general/default"); + } + + var parts = new List(); + + if (isSegmentVariant) + parts.Add(segment); + + if (isCultureVariant) + parts.Add(language.Name); + + return string.Join(" — ", parts); + } } } diff --git a/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs index 4c4a03939d..6b5797e05b 100644 --- a/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs @@ -146,19 +146,26 @@ namespace Umbraco.Web.Models.Mapping var fields = context.MapEnumerable(configurationEditor.Fields); var configurationDictionary = configurationEditor.ToConfigurationEditor(dataType.Configuration); - MapConfigurationFields(fields, configurationDictionary); + MapConfigurationFields(dataType, fields, configurationDictionary); return fields; } - - private void MapConfigurationFields(List fields, IDictionary configuration) + + private void MapConfigurationFields(IDataType dataType, List fields, IDictionary configuration) { if (fields == null) throw new ArgumentNullException(nameof(fields)); if (configuration == null) throw new ArgumentNullException(nameof(configuration)); // now we need to wire up the pre-values values with the actual fields defined - foreach (var field in fields) + foreach (var field in fields.ToList()) { + //filter out the not-supported pre-values for built-in data types + if (dataType != null && dataType.IsBuildInDataType() && field.Key.InvariantEquals(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes)) + { + fields.Remove(field); + continue; + } + if (configuration.TryGetValue(field.Key, out var value)) { field.Value = value; @@ -194,7 +201,7 @@ namespace Umbraco.Web.Models.Mapping var defaultConfiguration = configurationEditor.DefaultConfiguration; if (defaultConfiguration != null) - MapConfigurationFields(fields, defaultConfiguration); + MapConfigurationFields(null, fields, defaultConfiguration); return fields; } diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index 94698f84bd..ddff093aab 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -42,8 +42,20 @@ namespace Umbraco.Web.Models.Mapping target.Trashed = source.Trashed; target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); - if (source.NodeObjectType == Constants.ObjectTypes.Member && target.Icon.IsNullOrWhiteSpace()) - target.Icon = "icon-user"; + if (source is IContentEntitySlim contentSlim) + { + source.AdditionalData["ContentTypeAlias"] = contentSlim.ContentTypeAlias; + } + + if (source is IDocumentEntitySlim documentSlim) + { + source.AdditionalData["IsPublished"] = documentSlim.Published; + } + + if (source is IMediaEntitySlim mediaSlim) + { + source.AdditionalData["MediaPath"] = mediaSlim.MediaPath; + } // NOTE: we're mapping the objects in AdditionalData by object reference here. // it works fine for now, but it's something to keep in mind in the future @@ -83,7 +95,7 @@ namespace Umbraco.Web.Models.Mapping private static void Map(IUser source, EntityBasic target, MapperContext context) { target.Alias = source.Username; - target.Icon = "icon-user"; + target.Icon = Constants.Icons.User; target.Id = source.Id; target.Key = source.Key; target.Name = source.Name; @@ -95,7 +107,7 @@ namespace Umbraco.Web.Models.Mapping private static void Map(ITemplate source, EntityBasic target, MapperContext context) { target.Alias = source.Alias; - target.Icon = "icon-layout"; + target.Icon = Constants.Icons.Template; target.Id = source.Id; target.Key = source.Key; target.Name = source.Name; @@ -138,15 +150,15 @@ namespace Umbraco.Web.Models.Mapping if (target.Icon.IsNullOrWhiteSpace()) { if (source.NodeObjectType == Constants.ObjectTypes.Member) - target.Icon = "icon-user"; + target.Icon = Constants.Icons.Member; else if (source.NodeObjectType == Constants.ObjectTypes.DataType) - target.Icon = "icon-autofill"; + target.Icon = Constants.Icons.DataType; else if (source.NodeObjectType == Constants.ObjectTypes.DocumentType) - target.Icon = "icon-item-arrangement"; + target.Icon = Constants.Icons.ContentType; else if (source.NodeObjectType == Constants.ObjectTypes.MediaType) - target.Icon = "icon-thumbnails"; + target.Icon = Constants.Icons.MediaType; else if (source.NodeObjectType == Constants.ObjectTypes.TemplateType) - target.Icon = "icon-newspaper-alt"; + target.Icon = Constants.Icons.Template; } } @@ -161,10 +173,24 @@ namespace Umbraco.Web.Models.Mapping //get the icon if there is one target.Icon = source.Values.ContainsKey(UmbracoExamineIndex.IconFieldName) ? source.Values[UmbracoExamineIndex.IconFieldName] - : "icon-document"; + : Constants.Icons.DefaultIcon; target.Name = source.Values.ContainsKey("nodeName") ? source.Values["nodeName"] : "[no name]"; + var culture = context.GetCulture(); + if(culture.IsNullOrWhiteSpace() == false) + { + target.Name = source.Values.ContainsKey($"nodeName_{culture}") ? source.Values[$"nodeName_{culture}"] : target.Name; + } + + if (source.Values.TryGetValue(UmbracoExamineIndex.UmbracoFileFieldName, out var umbracoFile)) + { + if (umbracoFile != null) + { + target.Name = $"{target.Name} ({umbracoFile})"; + } + } + if (source.Values.ContainsKey(UmbracoExamineIndex.NodeKeyFieldName)) { if (Guid.TryParse(source.Values[UmbracoExamineIndex.NodeKeyFieldName], out var key)) @@ -211,7 +237,18 @@ namespace Umbraco.Web.Models.Mapping } private static string MapContentTypeIcon(IEntitySlim entity) - => entity is ContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + { + switch (entity) + { + case IMemberEntitySlim memberEntity: + return memberEntity.ContentTypeIcon.IfNullOrWhiteSpace(Constants.Icons.Member); + case IContentEntitySlim contentEntity: + // NOTE: this case covers both content and media entities + return contentEntity.ContentTypeIcon; + } + + return null; + } private static string MapName(IEntitySlim source, MapperContext context) { diff --git a/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs index 089f5d5d71..e654fc16a1 100644 --- a/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Logging; @@ -23,6 +24,7 @@ namespace Umbraco.Web.Models.Mapping public void DefineMaps(UmbracoMapper mapper) { mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new MacroDisplay(), Map); mapper.Define>((source, context) => context.MapEnumerable(source.Properties.Values)); mapper.Define((source, context) => new MacroParameter(), Map); } @@ -31,7 +33,7 @@ namespace Umbraco.Web.Models.Mapping private static void Map(IMacro source, EntityBasic target, MapperContext context) { target.Alias = source.Alias; - target.Icon = "icon-settings-alt"; + target.Icon = Constants.Icons.Macro; target.Id = source.Id; target.Key = source.Key; target.Name = source.Name; @@ -40,6 +42,23 @@ namespace Umbraco.Web.Models.Mapping target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); } + private void Map(IMacro source, MacroDisplay target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = Constants.Icons.Macro; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); + target.CacheByPage = source.CacheByPage; + target.CacheByUser = source.CacheByMember; + target.CachePeriod = source.CacheDuration; + target.UseInEditor = source.UseInEditor; + target.RenderInEditor = !source.DontRender; + target.View = source.MacroSource; + } // Umbraco.Code.MapAll -Value private void Map(IMacroProperty source, MacroParameter target, MapperContext context) { diff --git a/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs b/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs index 1538f1a987..20a387c679 100644 --- a/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs @@ -8,6 +8,7 @@ namespace Umbraco.Web.Models.Mapping internal static class MapperContextExtensions { private const string CultureKey = "Map.Culture"; + private const string SegmentKey = "Map.Segment"; private const string IncludedPropertiesKey = "Map.IncludedProperties"; /// @@ -18,6 +19,14 @@ namespace Umbraco.Web.Models.Mapping return context.HasItems && context.Items.TryGetValue(CultureKey, out var obj) && obj is string s ? s : null; } + /// + /// Gets the context segment. + /// + public static string GetSegment(this MapperContext context) + { + return context.HasItems && context.Items.TryGetValue(SegmentKey, out var obj) && obj is string s ? s : null; + } + /// /// Sets a context culture. /// @@ -26,6 +35,14 @@ namespace Umbraco.Web.Models.Mapping context.Items[CultureKey] = culture; } + /// + /// Sets a context segment. + /// + public static void SetSegment(this MapperContext context, string segment) + { + context.Items[SegmentKey] = segment; + } + /// /// Get included properties. /// @@ -42,4 +59,4 @@ namespace Umbraco.Web.Models.Mapping context.Items[IncludedPropertiesKey] = properties; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs index 3da61bc9c0..fa89496392 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs @@ -50,12 +50,13 @@ namespace Umbraco.Web.Models.Mapping { target.ContentApps = _commonMapper.GetContentApps(source); target.ContentType = _commonMapper.GetContentType(source, context); + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = source.ContentType.Name; target.CreateDate = source.CreateDate; target.Icon = source.ContentType.Icon; target.Id = source.Id; - target.IsChildOfListView = DermineIsChildOfListView(source); + target.IsChildOfListView = DetermineIsChildOfListView(source); target.Key = source.Key; target.MediaLink = string.Join(",", source.GetUrls(Current.Configs.Settings().Content, _logger)); target.Name = source.Name; @@ -75,6 +76,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Edited -Updater -Alias private void Map(IMedia source, ContentItemBasic target, MapperContext context) { + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.CreateDate = source.CreateDate; target.Icon = source.ContentType.Icon; @@ -93,7 +95,7 @@ namespace Umbraco.Web.Models.Mapping target.VariesByCulture = source.ContentType.VariesByCulture(); } - private bool DermineIsChildOfListView(IMedia source) + private bool DetermineIsChildOfListView(IMedia source) { // map the IsChildOfListView (this is actually if it is a descendant of a list view!) var parent = _mediaService.GetParent(source); diff --git a/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs index b230dcfe15..851f452244 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs @@ -76,6 +76,8 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Trashed -IsContainer -VariesByCulture private void Map(IMember source, MemberDisplay target, MapperContext context) { + target.ContentApps = _commonMapper.GetContentApps(source); + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = source.ContentType.Name; target.CreateDate = source.CreateDate; @@ -101,6 +103,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Trashed -Edited -Updater -Alias -VariesByCulture private void Map(IMember source, MemberBasic target, MapperContext context) { + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.CreateDate = source.CreateDate; target.Email = source.Email; @@ -121,12 +124,12 @@ namespace Umbraco.Web.Models.Mapping //TODO: SD: I can't remember why this mapping is here? // Umbraco.Code.MapAll -Udi -Properties -ParentId -Path -SortOrder -Edited -Updater - // Umbraco.Code.MapAll -Trashed -Alias -ContentTypeAlias -VariesByCulture + // Umbraco.Code.MapAll -Trashed -Alias -ContentTypeId -ContentTypeAlias -VariesByCulture private void Map(MembershipUser source, MemberBasic target, MapperContext context) { target.CreateDate = source.CreationDate; target.Email = source.Email; - target.Icon = "icon-user"; + target.Icon = Constants.Icons.Member; target.Id = int.MaxValue; target.Key = source.ProviderUserKey.TryConvertTo().Result; target.Name = source.UserName; @@ -142,7 +145,7 @@ namespace Umbraco.Web.Models.Mapping target.Id = source.Id; target.Key = source.Key; target.Name = source.Name; - target.Path = "-1" + source.Id; + target.Path = $"-1,{source.Id}"; target.Udi = Udi.Create(Constants.UdiEntityType.MemberGroup, source.Key); } diff --git a/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs index a96ef24e6f..8744b068a7 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -96,7 +96,7 @@ namespace Umbraco.Web.Models.Mapping linkText = source.ContentType.Name, url = memberTypeLink, target = "_self", - icon = "icon-item-arrangement" + icon = Constants.Icons.ContentType } }; docTypeProperty.View = "urllist"; @@ -250,6 +250,7 @@ namespace Umbraco.Web.Models.Mapping var result = Roles.GetAllRoles().Distinct() // if a role starts with __umbracoRole we won't show it as it's an internal role used for public access .Where(x => x.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false) + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ToDictionary(x => x, x => false); // if user has no roles, just return the dictionary diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs index 8c5f347799..370a459279 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs @@ -223,7 +223,13 @@ namespace Umbraco.Web.Models.Mapping Alias = p.Alias, Description = p.Description, Editor = p.PropertyEditorAlias, - Validation = new PropertyTypeValidation {Mandatory = p.Mandatory, Pattern = p.ValidationRegExp}, + Validation = new PropertyTypeValidation + { + Mandatory = p.Mandatory, + MandatoryMessage = p.MandatoryMessage, + Pattern = p.ValidationRegExp, + PatternMessage = p.ValidationRegExpMessage, + }, Label = p.Name, View = propertyEditor.GetValueEditor().View, Config = config, @@ -231,10 +237,12 @@ namespace Umbraco.Web.Models.Mapping GroupId = groupId, Inherited = inherited, DataTypeId = p.DataTypeId, + DataTypeKey = p.DataTypeKey, SortOrder = p.SortOrder, ContentTypeId = contentType.Id, ContentTypeName = contentType.Name, - AllowCultureVariant = p.VariesByCulture() + AllowCultureVariant = p.VariesByCulture(), + AllowSegmentVariant = p.VariesBySegment() }); } diff --git a/src/Umbraco.Web/Models/Mapping/RedirectUrlMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/RedirectUrlMapDefinition.cs index 73123a0407..e773fcfee5 100644 --- a/src/Umbraco.Web/Models/Mapping/RedirectUrlMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/RedirectUrlMapDefinition.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Models.Mapping target.ContentId = source.ContentId; target.CreateDateUtc = source.CreateDateUtc; target.Culture = source.Culture; - target.DestinationUrl = source.ContentId > 0 ? UmbracoContext?.UrlProvider?.GetUrl(source.ContentId, source.Culture) : "#"; + target.DestinationUrl = source.ContentId > 0 ? UmbracoContext?.UrlProvider?.GetUrl(source.ContentId, culture: source.Culture) : "#"; target.OriginalUrl = UmbracoContext?.UrlProvider?.GetUrlFromRoute(source.ContentId, source.Url, source.Culture); target.RedirectId = source.Key; } diff --git a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs index 7787750e54..d26a867858 100644 --- a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs @@ -1,12 +1,23 @@ -using Umbraco.Core; +using System.Linq; +using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { internal class RelationMapDefinition : IMapDefinition { + private readonly IEntityService _entityService; + private readonly IRelationService _relationService; + + public RelationMapDefinition(IEntityService entityService, IRelationService relationService) + { + _entityService = entityService; + _relationService = relationService; + } + public void DefineMaps(UmbracoMapper mapper) { mapper.Define((source, context) => new RelationTypeDisplay(), Map); @@ -15,8 +26,8 @@ namespace Umbraco.Web.Models.Mapping } // Umbraco.Code.MapAll -Icon -Trashed -AdditionalData - // Umbraco.Code.MapAll -Relations -ParentId -Notifications - private static void Map(IRelationType source, RelationTypeDisplay target, MapperContext context) + // Umbraco.Code.MapAll -ParentId -Notifications + private void Map(IRelationType source, RelationTypeDisplay target, MapperContext context) { target.ChildObjectType = source.ChildObjectType; target.Id = source.Id; @@ -28,18 +39,32 @@ namespace Umbraco.Web.Models.Mapping target.Udi = Udi.Create(Constants.UdiEntityType.RelationType, source.Key); target.Path = "-1," + source.Id; - // Set the "friendly" names for the parent and child object types - target.ParentObjectTypeName = ObjectTypes.GetUmbracoObjectType(source.ParentObjectType).GetFriendlyName(); - target.ChildObjectTypeName = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType).GetFriendlyName(); + // Set the "friendly" and entity names for the parent and child object types + if (source.ParentObjectType.HasValue) + { + var objType = ObjectTypes.GetUmbracoObjectType(source.ParentObjectType.Value); + target.ParentObjectTypeName = objType.GetFriendlyName(); + } + + if (source.ChildObjectType.HasValue) + { + var objType = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType.Value); + target.ChildObjectTypeName = objType.GetFriendlyName(); + } } // Umbraco.Code.MapAll -ParentName -ChildName - private static void Map(IRelation source, RelationDisplay target, MapperContext context) + private void Map(IRelation source, RelationDisplay target, MapperContext context) { target.ChildId = source.ChildId; target.Comment = source.Comment; target.CreateDate = source.CreateDate; target.ParentId = source.ParentId; + + var entities = _relationService.GetEntitiesFromRelation(source); + + target.ParentName = entities.Item1.Name; + target.ChildName = entities.Item2.Name; } // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index 1b245cdce2..aa158799cb 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -138,7 +138,7 @@ namespace Umbraco.Web.Models.Mapping } // Umbraco.Code.MapAll -ContentStartNode -UserCount -MediaStartNode -Key -Sections - // Umbraco.Code.MapAll -Notifications -Udi -Trashed -AdditionalData + // Umbraco.Code.MapAll -Notifications -Udi -Trashed -AdditionalData -IsSystemUserGroup private void Map(IReadOnlyUserGroup source, UserGroupBasic target, MapperContext context) { target.Alias = source.Alias; @@ -147,11 +147,13 @@ namespace Umbraco.Web.Models.Mapping target.Name = source.Name; target.ParentId = -1; target.Path = "-1," + source.Id; + target.IsSystemUserGroup = source.IsSystemUserGroup(); + MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); } // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications - // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData + // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData -IsSystemUserGroup private void Map(IUserGroup source, UserGroupBasic target, MapperContext context) { target.Alias = source.Alias; @@ -162,6 +164,8 @@ namespace Umbraco.Web.Models.Mapping target.ParentId = -1; target.Path = "-1," + source.Id; target.UserCount = source.UserCount; + target.IsSystemUserGroup = source.IsSystemUserGroup(); + MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); } @@ -179,7 +183,7 @@ namespace Umbraco.Web.Models.Mapping target.DefaultPermissions = MapUserGroupDefaultPermissions(source); if (target.Icon.IsNullOrWhiteSpace()) - target.Icon = "icon-users"; + target.Icon = Constants.Icons.UserGroup; } // Umbraco.Code.MapAll -Trashed -Alias -AssignedPermissions @@ -194,7 +198,7 @@ namespace Umbraco.Web.Models.Mapping target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); if (source.NodeObjectType == Constants.ObjectTypes.Member && target.Icon.IsNullOrWhiteSpace()) - target.Icon = "icon-user"; + target.Icon = Constants.Icons.Member; } // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications -Udi @@ -210,6 +214,7 @@ namespace Umbraco.Web.Models.Mapping target.ParentId = -1; target.Path = "-1," + source.Id; target.UserCount = source.UserCount; + target.IsSystemUserGroup = source.IsSystemUserGroup(); MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); @@ -301,7 +306,7 @@ namespace Umbraco.Web.Models.Mapping target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache); target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); target.Email = source.Email; - target.EmailHash = source.Email.ToLowerInvariant().Trim().ToMd5(); + target.EmailHash = source.Email.ToLowerInvariant().Trim().GenerateHash(); target.Id = source.Id; target.Key = source.Key; target.LastLoginDate = source.LastLoginDate == default ? null : (DateTime?) source.LastLoginDate; @@ -350,7 +355,7 @@ namespace Umbraco.Web.Models.Mapping target.ContentStartNode = CreateRootNode(_textService.Localize("content/contentRoot")); if (target.Icon.IsNullOrWhiteSpace()) - target.Icon = "icon-users"; + target.Icon = Constants.Icons.UserGroup; } private IDictionary> MapUserGroupDefaultPermissions(IUserGroup source) @@ -375,8 +380,8 @@ namespace Umbraco.Web.Models.Mapping .ToDictionary(x => x.Key, x => (IEnumerable)x.ToArray()); } - private static string MapContentTypeIcon(EntitySlim entity) - => entity is ContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + private static string MapContentTypeIcon(IEntitySlim entity) + => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedKey, MapperContext context) { diff --git a/src/Umbraco.Web/Models/PartialViewMacroModel.cs b/src/Umbraco.Web/Models/PartialViewMacroModel.cs index 9964877cba..f3272644f4 100644 --- a/src/Umbraco.Web/Models/PartialViewMacroModel.cs +++ b/src/Umbraco.Web/Models/PartialViewMacroModel.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.Models /// /// The model used when rendering Partial View Macros /// - public class PartialViewMacroModel + public class PartialViewMacroModel : IContentModel { public PartialViewMacroModel(IPublishedContent page, diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 7b467b6d15..57c3094eaf 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -111,7 +111,7 @@ namespace Umbraco.Web.Models.PublishedContent var propertyType = content.ContentType.GetPropertyType(alias); if (propertyType != null) { - _variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment); + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); noValueProperty = content.GetProperty(alias); } @@ -168,7 +168,7 @@ namespace Umbraco.Web.Models.PublishedContent { culture = null; segment = null; - _variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment); + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); } property = content?.GetProperty(alias); diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index cdcfd8a0cd..d6a57a66d7 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.PropertyEditors.ValueConverters; namespace Umbraco.Web.Models { @@ -12,19 +11,12 @@ namespace Umbraco.Web.Models /// /// This base class does which (a) consistently resolves and caches the Url, (b) provides an implementation /// for this[alias], and (c) provides basic content set management. - [DebuggerDisplay("Content Id: {Id}, Name: {Name}")] + [DebuggerDisplay("Content Id: {Id}")] public abstract class PublishedContentBase : IPublishedContent { - protected PublishedContentBase(IUmbracoContextAccessor umbracoContextAccessor) - { - UmbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - } - - protected IUmbracoContextAccessor UmbracoContextAccessor { get; } - #region ContentType - public abstract PublishedContentType ContentType { get; } + public abstract IPublishedContentType ContentType { get; } #endregion @@ -41,10 +33,10 @@ namespace Umbraco.Web.Models public abstract int Id { get; } /// - public abstract string Name { get; } + public virtual string Name => this.Name(); /// - public abstract string UrlSegment { get; } + public virtual string UrlSegment => this.UrlSegment(); /// public abstract int SortOrder { get; } @@ -62,6 +54,7 @@ namespace Umbraco.Web.Models public abstract int CreatorId { get; } /// + [Obsolete("Use CreatorName(IUserService) extension instead")] public abstract string CreatorName { get; } /// @@ -71,40 +64,15 @@ namespace Umbraco.Web.Models public abstract int WriterId { get; } /// + [Obsolete("Use WriterName(IUserService) extension instead")] public abstract string WriterName { get; } /// public abstract DateTime UpdateDate { get; } /// - public virtual string Url => GetUrl(); - - /// - /// - /// The url of documents are computed by the document url providers. The url of medias are computed by the media url providers - /// - public virtual string GetUrl(string culture = null) // TODO: consider .GetCulture("fr-FR").Url - { - var umbracoContext = UmbracoContextAccessor.UmbracoContext; - - if (umbracoContext == null) - throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext is null."); - if (umbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext.UrlProvider is null."); - - switch (ItemType) - { - case PublishedItemType.Content: - return umbracoContext.UrlProvider.GetUrl(this, culture); - case PublishedItemType.Media: - return umbracoContext.UrlProvider.GetMediaUrl(this, Constants.Conventions.Media.File, culture); - default: - throw new NotSupportedException(); - } - } - - /// - public abstract PublishedCultureInfo GetCulture(string culture = null); + [Obsolete("Use the Url() extension instead")] + public virtual string Url => this.Url(); /// public abstract IReadOnlyDictionary Cultures { get; } @@ -126,7 +94,10 @@ namespace Umbraco.Web.Models public abstract IPublishedContent Parent { get; } /// - public abstract IEnumerable Children { get; } + public virtual IEnumerable Children => this.Children(); + + /// + public abstract IEnumerable ChildrenForAllCultures { get; } #endregion diff --git a/src/Umbraco.Web/Models/PublishedProperty.cs b/src/Umbraco.Web/Models/PublishedProperty.cs deleted file mode 100644 index d32612c54c..0000000000 --- a/src/Umbraco.Web/Models/PublishedProperty.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Composing; - -namespace Umbraco.Web.Models -{ - public static class PublishedProperty - { - /// - /// Maps a collection of Property to a collection of IPublishedProperty for a specified collection of PublishedPropertyType. - /// - /// The published property types. - /// The properties. - /// A mapping function. - /// A collection of IPublishedProperty corresponding to the collection of PublishedPropertyType - /// and taking values from the collection of Property. - /// Ensures that all conversions took place correctly. - internal static IEnumerable MapProperties( - IEnumerable propertyTypes, IEnumerable properties, - Func map) - { - var propertyEditors = Current.PropertyEditors; - var dataTypeService = Current.Services.DataTypeService; - - // TODO: not dealing with variants - // but the entire thing should die anyways - - return propertyTypes.Select(x => - { - var p = properties.SingleOrDefault(xx => xx.Alias == x.Alias); - var v = p == null || p.GetValue() == null ? null : p.GetValue(); - if (v != null) - { - var e = propertyEditors[x.EditorAlias]; - - // We are converting to string, even for database values which are integer or - // DateTime, which is not optimum. Doing differently would require that we have a way to tell - // whether the conversion to XML string changes something or not... which we don't, and we - // don't want to implement it as PropertyValueEditor.ConvertDbToXml/String should die anyway. - - // Don't think about improving the situation here: this is a corner case and the real - // thing to do is to get rig of PropertyValueEditor.ConvertDbToXml/String. - - // Use ConvertDbToString to keep it simple, although everywhere we use ConvertDbToXml and - // nothing ensures that the two methods are consistent. - - if (e != null) - v = e.GetValueEditor().ConvertDbToString(p.PropertyType, v, dataTypeService); - } - - return map(x, v); - }); - } - } -} diff --git a/src/Umbraco.Web/Models/RegisterModel.cs b/src/Umbraco.Web/Models/RegisterModel.cs index 86a5459a74..fd6cadc04e 100644 --- a/src/Umbraco.Web/Models/RegisterModel.cs +++ b/src/Umbraco.Web/Models/RegisterModel.cs @@ -5,7 +5,6 @@ using System.ComponentModel.DataAnnotations; using System.Web; using System.Web.Mvc; using Umbraco.Core; -using Umbraco.Core.Composing; using Umbraco.Web.Security; using Current = Umbraco.Web.Composing.Current; @@ -65,7 +64,7 @@ namespace Umbraco.Web.Models /// [Required] public string Password { get; set; } - + /// /// The username of the model, if UsernameIsEmail is true then this is ignored. /// diff --git a/src/Umbraco.Web/Models/Trees/TreeNode.cs b/src/Umbraco.Web/Models/Trees/TreeNode.cs index 63614b141f..dc383bd18b 100644 --- a/src/Umbraco.Web/Models/Trees/TreeNode.cs +++ b/src/Umbraco.Web/Models/Trees/TreeNode.cs @@ -1,10 +1,9 @@ -using System.Runtime.Serialization; -using Umbraco.Core.IO; +using System; using System.Collections.Generic; +using System.Runtime.Serialization; using Umbraco.Core; using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Exceptions; +using Umbraco.Core.IO; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Trees @@ -27,7 +26,8 @@ namespace Umbraco.Web.Models.Trees /// internal TreeNode(string nodeId, string parentId, string getChildNodesUrl, string menuUrl) { - if (string.IsNullOrWhiteSpace(nodeId)) throw new ArgumentNullOrEmptyException(nameof(nodeId)); + if (nodeId == null) throw new ArgumentNullException(nameof(nodeId)); + if (string.IsNullOrWhiteSpace(nodeId)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(nodeId)); Id = nodeId; ParentId = parentId; diff --git a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs index 61a659615f..493d9deed2 100644 --- a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs +++ b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs @@ -6,7 +6,6 @@ using System.Web.Routing; using System.Web.SessionState; using Umbraco.Core; using Umbraco.Core.Configuration; -using Umbraco.Core.Exceptions; using Umbraco.Web.WebApi; namespace Umbraco.Web.Mvc @@ -48,7 +47,8 @@ namespace Umbraco.Web.Mvc bool isMvc = true, string areaPathPrefix = "") { - if (string.IsNullOrEmpty(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); + if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); + if (string.IsNullOrEmpty(controllerName)) throw new ArgumentException("Value can't be empty.", nameof(controllerName)); if (controllerSuffixName == null) throw new ArgumentNullException(nameof(controllerSuffixName)); if (controllerType == null) throw new ArgumentNullException(nameof(controllerType)); diff --git a/src/Umbraco.Web/Mvc/ContentModelBinder.cs b/src/Umbraco.Web/Mvc/ContentModelBinder.cs index c6df52e007..052938807a 100644 --- a/src/Umbraco.Web/Mvc/ContentModelBinder.cs +++ b/src/Umbraco.Web/Mvc/ContentModelBinder.cs @@ -135,13 +135,16 @@ namespace Umbraco.Web.Mvc msg.Append(modelType.FullName); msg.Append("."); -// raise event, to give model factories a chance at reporting + // raise event, to give model factories a chance at reporting // the error with more details, and optionally request that // the application restarts. var args = new ModelBindingArgs(sourceType, modelType, msg); ModelBindingException?.Invoke(Instance, args); + // TODO: with all of the tests I've done i don't think restarting the app here is required anymore, + // when I don't have this code enabled and i get a model binding error and just refresh, it fixes itself. + // We'll leave this for now though. if (args.Restart) { msg.Append(" The application is restarting now."); diff --git a/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs b/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs new file mode 100644 index 0000000000..db2040665c --- /dev/null +++ b/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.Serialization; +using System.Web; + +namespace Umbraco.Web.Mvc +{ + /// + /// Exception that occurs when an Umbraco form route string is invalid + /// + /// + [Serializable] + public sealed class HttpUmbracoFormRouteStringException : HttpException + { + /// + /// Initializes a new instance of the class. + /// + public HttpUmbracoFormRouteStringException() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that holds the contextual information about the source or destination. + private HttpUmbracoFormRouteStringException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The error message displayed to the client when the exception is thrown. + public HttpUmbracoFormRouteStringException(string message) + : base(message) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The error message displayed to the client when the exception is thrown. + /// The , if any, that threw the current exception. + public HttpUmbracoFormRouteStringException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/src/Umbraco.Web/Mvc/ModelBindingException.cs b/src/Umbraco.Web/Mvc/ModelBindingException.cs index d675ae4a65..548b548d4e 100644 --- a/src/Umbraco.Web/Mvc/ModelBindingException.cs +++ b/src/Umbraco.Web/Mvc/ModelBindingException.cs @@ -1,14 +1,45 @@ using System; +using System.Runtime.Serialization; namespace Umbraco.Web.Mvc { + /// + /// The exception that is thrown when an error occurs while binding a source to a model. + /// + /// + [Serializable] public class ModelBindingException : Exception { + /// + /// Initializes a new instance of the class. + /// public ModelBindingException() { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. public ModelBindingException(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 ModelBindingException(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 ModelBindingException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } } diff --git a/src/Umbraco.Web/Mvc/ModelBindingExceptionFilter.cs b/src/Umbraco.Web/Mvc/ModelBindingExceptionFilter.cs new file mode 100644 index 0000000000..5d8fbdac2f --- /dev/null +++ b/src/Umbraco.Web/Mvc/ModelBindingExceptionFilter.cs @@ -0,0 +1,63 @@ +using System; +using System.Configuration; +using System.Net; +using System.Text.RegularExpressions; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Web.Composing; + +namespace Umbraco.Web.Mvc +{ + /// + /// An exception filter checking if we get a or with the same model. + /// In which case it returns a redirect to the same page after 1 sec if not in debug mode. + /// + /// + /// This is only enabled when running PureLive + /// + internal class ModelBindingExceptionFilter : FilterAttribute, IExceptionFilter + { + private static readonly Regex GetPublishedModelsTypesRegex = new Regex("Umbraco.Web.PublishedModels.(\\w+)", RegexOptions.Compiled); + + public void OnException(ExceptionContext filterContext) + { + if (Current.PublishedModelFactory.IsLiveFactory() + && ConfigurationManager.AppSettings["Umbraco.Web.DisableModelBindingExceptionFilter"] != "true" + && !filterContext.ExceptionHandled + && ((filterContext.Exception is ModelBindingException || filterContext.Exception is InvalidCastException) + && IsMessageAboutTheSameModelType(filterContext.Exception.Message))) + { + filterContext.HttpContext.Response.Headers.Add(HttpResponseHeader.RetryAfter.ToString(), "1"); + filterContext.Result = new RedirectResult(filterContext.HttpContext.Request.RawUrl, false); + + filterContext.ExceptionHandled = true; + } + } + + /// + /// Returns true if the message is about two models with the same name. + /// + /// + /// Message could be something like: + /// + /// InvalidCastException: + /// [A]Umbraco.Web.PublishedModels.Home cannot be cast to [B]Umbraco.Web.PublishedModels.Home. Type A originates from 'App_Web_all.generated.cs.8f9494c4.rtdigm_z, Version=0.0.0.3, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Users\User\AppData\Local\Temp\Temporary ASP.NET Files\root\c5c63f4d\c168d9d4\App_Web_all.generated.cs.8f9494c4.rtdigm_z.dll'. Type B originates from 'App_Web_all.generated.cs.8f9494c4.rbyqlplu, Version=0.0.0.5, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Users\User\AppData\Local\Temp\Temporary ASP.NET Files\root\c5c63f4d\c168d9d4\App_Web_all.generated.cs.8f9494c4.rbyqlplu.dll'. + /// + /// + /// ModelBindingException: + /// Cannot bind source content type Umbraco.Web.PublishedModels.Home to model type Umbraco.Web.PublishedModels.Home. Both view and content models are PureLive, with different versions. The application is in an unstable state and is going to be restarted. The application is restarting now. + /// + /// + private bool IsMessageAboutTheSameModelType(string exceptionMessage) + { + var matches = GetPublishedModelsTypesRegex.Matches(exceptionMessage); + + if (matches.Count >= 2) + { + return string.Equals(matches[0].Value, matches[1].Value, StringComparison.InvariantCulture); + } + + return false; + } + } +} diff --git a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs index 52d168620b..437eec4e5c 100644 --- a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs +++ b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs @@ -56,7 +56,7 @@ namespace Umbraco.Web.Mvc if (_publishedContent != null) return _publishedContent; //need to get the URL for the page - _publishedContent = Current.UmbracoContext.ContentCache.GetById(_pageId); + _publishedContent = Current.UmbracoContext.Content.GetById(_pageId); return _publishedContent; } diff --git a/src/Umbraco.Web/Mvc/RenderIndexActionSelectorAttribute.cs b/src/Umbraco.Web/Mvc/RenderIndexActionSelectorAttribute.cs index fa80107181..99aa88b3e2 100644 --- a/src/Umbraco.Web/Mvc/RenderIndexActionSelectorAttribute.cs +++ b/src/Umbraco.Web/Mvc/RenderIndexActionSelectorAttribute.cs @@ -7,7 +7,7 @@ using System.Web.Mvc; namespace Umbraco.Web.Mvc { /// - /// A custom ActionMethodSelector which will ensure that the RenderMvcController.Index(RenderModel model) action will be executed + /// A custom ActionMethodSelector which will ensure that the RenderMvcController.Index(ContentModel model) action will be executed /// if the /// internal class RenderIndexActionSelectorAttribute : ActionMethodSelectorAttribute diff --git a/src/Umbraco.Web/Mvc/RenderMvcController.cs b/src/Umbraco.Web/Mvc/RenderMvcController.cs index c2aa3bd8ed..64c9ad52c4 100644 --- a/src/Umbraco.Web/Mvc/RenderMvcController.cs +++ b/src/Umbraco.Web/Mvc/RenderMvcController.cs @@ -10,10 +10,12 @@ using Umbraco.Web.Routing; namespace Umbraco.Web.Mvc { + /// /// Represents the default front-end rendering controller. /// [PreRenderViewActionFilter] + [ModelBindingExceptionFilter] public class RenderMvcController : UmbracoController, IRenderMvcController { private PublishedRequest _publishedRequest; diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index 54215c2e8c..c30f83170b 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Mvc public class RenderRouteHandler : IRouteHandler { // Define reserved dictionary keys for controller, action and area specified in route additional values data - private static class ReservedAdditionalKeys + internal static class ReservedAdditionalKeys { internal const string Controller = "c"; internal const string Action = "a"; @@ -86,7 +86,7 @@ namespace Umbraco.Web.Mvc internal void SetupRouteDataForRequest(ContentModel contentModel, RequestContext requestContext, PublishedRequest frequest) { //put essential data into the data tokens, the 'umbraco' key is required to be there for the view engine - requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, contentModel); //required for the RenderModelBinder and view engine + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, contentModel); //required for the ContentModelBinder and view engine requestContext.RouteData.DataTokens.Add(Core.Constants.Web.PublishedDocumentRequestDataToken, frequest); //required for RenderMvcController requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, UmbracoContext); //required for UmbracoViewPage } @@ -134,36 +134,7 @@ namespace Umbraco.Web.Mvc return null; } - - string decryptedString; - try - { - decryptedString = encodedVal.DecryptWithMachineKey(); - } - catch (FormatException) - { - Current.Logger.Warn("A value was detected in the ufprt parameter but Umbraco could not decrypt the string"); - return null; - } - - var parsedQueryString = HttpUtility.ParseQueryString(decryptedString); - var decodedParts = new Dictionary(); - - foreach (var key in parsedQueryString.AllKeys) - { - decodedParts[key] = parsedQueryString[key]; - } - - //validate all required keys exist - - //the controller - if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Controller)) - return null; - //the action - if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Action)) - return null; - //the area - if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Area)) + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(encodedVal, out var decodedParts)) return null; foreach (var item in decodedParts.Where(x => new[] { @@ -184,6 +155,8 @@ namespace Umbraco.Web.Mvc }; } + + /// /// Handles a posted form to an Umbraco Url and ensures the correct controller is routed to and that /// the right DataTokens are set. @@ -260,7 +233,7 @@ namespace Umbraco.Web.Mvc } /// - /// Returns a RouteDefinition object based on the current renderModel + /// Returns a RouteDefinition object based on the current content request /// /// /// @@ -417,7 +390,7 @@ namespace Umbraco.Web.Mvc return new UmbracoMvcHandler(requestContext); } - + private SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext, string controllerName) { return _controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); diff --git a/src/Umbraco.Web/Mvc/RenderViewEngine.cs b/src/Umbraco.Web/Mvc/RenderViewEngine.cs index 0fa22616b2..f8a636c87b 100644 --- a/src/Umbraco.Web/Mvc/RenderViewEngine.cs +++ b/src/Umbraco.Web/Mvc/RenderViewEngine.cs @@ -89,7 +89,7 @@ namespace Umbraco.Web.Mvc var umbracoToken = controllerContext.GetDataTokenInViewContextHierarchy(Core.Constants.Web.UmbracoDataToken); // first check if we're rendering a partial view for the back office, or surface controller, etc... - // anything that is not IUmbracoRenderModel as this should only pertain to Umbraco views. + // anything that is not ContentModel as this should only pertain to Umbraco views. if (isPartial && !(umbracoToken is ContentModel)) return true; diff --git a/src/Umbraco.Web/Mvc/RouteDefinition.cs b/src/Umbraco.Web/Mvc/RouteDefinition.cs index 6acafabe1c..89b0467dc3 100644 --- a/src/Umbraco.Web/Mvc/RouteDefinition.cs +++ b/src/Umbraco.Web/Mvc/RouteDefinition.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Mvc /// - /// The current RenderModel found for the request + /// Everything related to the current content request including the requested content /// public PublishedRequest PublishedRequest { get; set; } diff --git a/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs b/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs new file mode 100644 index 0000000000..4a78d201ca --- /dev/null +++ b/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs @@ -0,0 +1,28 @@ +using System.Net; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Web.Composing; + +namespace Umbraco.Web.Mvc +{ + /// + /// Forces the response to have a specific http status code + /// + internal class StatusCodeResultAttribute : ActionFilterAttribute + { + private readonly HttpStatusCode _statusCode; + + public StatusCodeResultAttribute(HttpStatusCode statusCode) + { + _statusCode = statusCode; + } + + public override void OnActionExecuted(ActionExecutedContext filterContext) + { + base.OnActionExecuted(filterContext); + + filterContext.HttpContext.Response.StatusCode = (int)_statusCode; + filterContext.HttpContext.Response.TrySkipIisCustomErrors = Current.Configs.Settings().WebRouting.TrySkipIisCustomErrors; + } + } +} diff --git a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs index 5ac4037fdb..0adb654d45 100644 --- a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs +++ b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs @@ -215,7 +215,8 @@ namespace Umbraco.Web.Mvc markupToInject = string.Format(Current.Configs.Settings().Content.PreviewBadge, IOHelper.ResolveUrl(SystemDirectories.Umbraco), - Server.UrlEncode(Current.UmbracoContext.HttpContext.Request.Url?.PathAndQuery)); + Server.UrlEncode(Current.UmbracoContext.HttpContext.Request.Url?.PathAndQuery), + Current.UmbracoContext.PublishedRequest.PublishedContent.Id); } else { diff --git a/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByIdRouteHandler.cs b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByIdRouteHandler.cs index 304f59b64b..0ada310b12 100644 --- a/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByIdRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByIdRouteHandler.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.Mvc protected sealed override IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext) { - var byId = umbracoContext.ContentCache.GetById(_realNodeId); + var byId = umbracoContext.Content.GetById(_realNodeId); return byId == null ? null : FindContent(requestContext, umbracoContext, byId); } diff --git a/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByUdiRouteHandler.cs b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByUdiRouteHandler.cs new file mode 100644 index 0000000000..a1ee6c732a --- /dev/null +++ b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByUdiRouteHandler.cs @@ -0,0 +1,27 @@ +using System.Web.Routing; +using Umbraco.Core; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.Mvc +{ + public class UmbracoVirtualNodeByUdiRouteHandler : UmbracoVirtualNodeRouteHandler + { + private readonly Udi _realNodeUdi; + + public UmbracoVirtualNodeByUdiRouteHandler(GuidUdi realNodeUdi) + { + _realNodeUdi = realNodeUdi; + } + + protected sealed override IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext) + { + var byId = umbracoContext.Content.GetById(_realNodeUdi); + return byId == null ? null : FindContent(requestContext, umbracoContext, byId); + } + + protected virtual IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext, IPublishedContent baseContent) + { + return baseContent; + } + } +} diff --git a/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs index 756ca7f05c..0803941a70 100644 --- a/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs +++ b/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.Mvc var userIdentity = filterContext.HttpContext.User.Identity as ClaimsIdentity; if (userIdentity != null) { - //if there is not CookiePath claim, then exist + //if there is not CookiePath claim, then exit if (userIdentity.HasClaim(x => x.Type == ClaimTypes.CookiePath) == false) { base.OnActionExecuting(filterContext); diff --git a/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs new file mode 100644 index 0000000000..8d929197e1 --- /dev/null +++ b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs @@ -0,0 +1,62 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Web.Mvc; +using Umbraco.Core; + +namespace Umbraco.Web.Mvc +{ + /// + /// Attribute used to check that the request contains a valid Umbraco form request string. + /// + /// + /// + /// + /// Applying this attribute/filter to a or SurfaceController Action will ensure that the Action can only be executed + /// when it is routed to from within Umbraco, typically when rendering a form with BegingUmbracoForm. It will mean that the natural MVC route for this Action + /// will fail with a . + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class ValidateUmbracoFormRouteStringAttribute : FilterAttribute, IAuthorizationFilter + { + /// + /// Called when authorization is required. + /// + /// The filter context. + /// filterContext + /// The required request field \"ufprt\" is not present. + /// or + /// The Umbraco form request route string could not be decrypted. + /// or + /// The provided Umbraco form request route string was meant for a different controller and action. + public void OnAuthorization(AuthorizationContext filterContext) + { + if (filterContext == null) + throw new ArgumentNullException(nameof(filterContext)); + + var ufprt = filterContext.HttpContext.Request["ufprt"]; + ValidateRouteString(ufprt, filterContext.ActionDescriptor?.ControllerDescriptor.ControllerName, filterContext.ActionDescriptor?.ActionName, filterContext.RouteData?.DataTokens["area"]?.ToString()); + } + + public void ValidateRouteString(string ufprt, string currentController, string currentAction, string currentArea) + { + if (ufprt.IsNullOrWhiteSpace()) + { + throw new HttpUmbracoFormRouteStringException("The required request field \"ufprt\" is not present."); + } + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprt, out var additionalDataParts)) + { + throw new HttpUmbracoFormRouteStringException("The Umbraco form request route string could not be decrypted."); + } + + if (!additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller].InvariantEquals(currentController) || + !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action].InvariantEquals(currentAction) || + (!additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].IsNullOrWhiteSpace() && !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].InvariantEquals(currentArea))) + { + throw new HttpUmbracoFormRouteStringException("The provided Umbraco form request route string was meant for a different controller and action."); + } + + } + } +} diff --git a/src/Umbraco.Web/Profiling/WebProfilingController.cs b/src/Umbraco.Web/Profiling/WebProfilingController.cs new file mode 100644 index 0000000000..b3d580bc38 --- /dev/null +++ b/src/Umbraco.Web/Profiling/WebProfilingController.cs @@ -0,0 +1,19 @@ +using Umbraco.Web.Editors; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.Web.Profiling +{ + /// + /// The API controller used to display the state of the web profiler + /// + [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] + public class WebProfilingController : UmbracoAuthorizedJsonController + { + public object GetStatus() + { + return new + { + Enabled = Core.Configuration.GlobalSettings.DebugMode + }; + } + }} diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs index 359e8ac3b6..ce2cbf0282 100644 --- a/src/Umbraco.Web/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs @@ -12,7 +12,7 @@ using System.Runtime.InteropServices; // Umbraco Cms [assembly: InternalsVisibleTo("Umbraco.Web.UI")] - +[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.Embedded")] [assembly: InternalsVisibleTo("Umbraco.Tests")] [assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] @@ -30,10 +30,7 @@ using System.Runtime.InteropServices; [assembly: InternalsVisibleTo("Umbraco.Forms.Web")] // Umbraco Headless -[assembly: InternalsVisibleTo("Umbraco.Headless")] - -// Umbraco ModelsBuilder -[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder")] +[assembly: InternalsVisibleTo("Umbraco.Cloud.Headless")] // code analysis // IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it diff --git a/src/Umbraco.Web/Properties/Settings.Designer.cs b/src/Umbraco.Web/Properties/Settings.Designer.cs index 5a5a863f4f..5f7ccbd7e1 100644 --- a/src/Umbraco.Web/Properties/Settings.Designer.cs +++ b/src/Umbraco.Web/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.2.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -50,15 +50,5 @@ namespace Umbraco.Web.Properties { return ((string)(this["test"])); } } - - [global::System.Configuration.ApplicationScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.WebServiceUrl)] - [global::System.Configuration.DefaultSettingValueAttribute("https://our.umbraco.com/umbraco/webservices/api/repository.asmx")] - public string umbraco_org_umbraco_our_Repository { - get { - return ((string)(this["umbraco_org_umbraco_our_Repository"])); - } - } } } diff --git a/src/Umbraco.Web/Properties/Settings.settings b/src/Umbraco.Web/Properties/Settings.settings index 757b363da2..3a6f68023e 100644 --- a/src/Umbraco.Web/Properties/Settings.settings +++ b/src/Umbraco.Web/Properties/Settings.settings @@ -11,8 +11,5 @@ Something - - https://our.umbraco.com/umbraco/webservices/api/repository.asmx - \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs index e0f7184dc7..d81b63f2ad 100644 --- a/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs @@ -8,7 +8,12 @@ namespace Umbraco.Web.PropertyEditors /// /// A property editor to allow multiple checkbox selection of pre-defined items. /// - [DataEditor(Constants.PropertyEditors.Aliases.CheckBoxList, "Checkbox list", "checkboxlist", Icon="icon-bulleted-list", Group="lists")] + [DataEditor( + Constants.PropertyEditors.Aliases.CheckBoxList, + "Checkbox list", + "checkboxlist", + Icon = "icon-bulleted-list", + Group = Constants.PropertyEditors.Groups.Lists)] public class CheckBoxListPropertyEditor : DataEditor { private readonly ILocalizedTextService _textService; diff --git a/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs index 5da9d2888d..be6bde248f 100644 --- a/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs @@ -4,7 +4,12 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.ColorPicker, "Color Picker", "colorpicker", Icon="icon-colorpicker", Group="Pickers")] + [DataEditor( + Constants.PropertyEditors.Aliases.ColorPicker, + "Color Picker", + "colorpicker", + Icon = "icon-colorpicker", + Group = Constants.PropertyEditors.Groups.Pickers)] public class ColorPickerPropertyEditor : DataEditor { public ColorPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs index 5653e3fe03..021d416781 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs @@ -3,15 +3,17 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - public class ContentPickerConfiguration + public class ContentPickerConfiguration : IIgnoreUserStartNodesConfig { - [ConfigurationField("showOpenButton", "Show open button (this feature is in beta!)", "boolean", Description = "Opens the node in a dialog")] + [ConfigurationField("showOpenButton", "Show open button", "boolean", Description = "Opens the node in a dialog")] public bool ShowOpenButton { get; set; } [ConfigurationField("startNodeId", "Start node", "treepicker")] // + config in configuration editor ctor public Udi StartNodeId { get; set; } - [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index 391257ca58..683f1a05c3 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors @@ -8,7 +9,13 @@ namespace Umbraco.Web.PropertyEditors /// /// Content property editor that stores UDI /// - [DataEditor(Constants.PropertyEditors.Aliases.ContentPicker, EditorType.PropertyValue | EditorType.MacroParameter, "Content Picker", "contentpicker", ValueType = ValueTypes.String, Group = "Pickers")] + [DataEditor( + Constants.PropertyEditors.Aliases.ContentPicker, + EditorType.PropertyValue | EditorType.MacroParameter, + "Content Picker", + "contentpicker", + ValueType = ValueTypes.String, + Group = Constants.PropertyEditors.Groups.Pickers)] public class ContentPickerPropertyEditor : DataEditor { public ContentPickerPropertyEditor(ILogger logger) @@ -19,5 +26,24 @@ namespace Umbraco.Web.PropertyEditors { return new ContentPickerConfigurationEditor(); } + + protected override IDataValueEditor CreateValueEditor() => new ContentPickerPropertyValueEditor(Attribute); + + internal class ContentPickerPropertyValueEditor : DataValueEditor, IDataValueReference + { + public ContentPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) + { + } + + public IEnumerable GetReferences(object value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } + } } } diff --git a/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs index fa3734e8aa..31fbc9e3f1 100644 --- a/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs @@ -7,7 +7,12 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a date and time property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.DateTime, "Date/Time", "datepicker", ValueType = ValueTypes.DateTime, Icon="icon-time")] + [DataEditor( + Constants.PropertyEditors.Aliases.DateTime, + "Date/Time", + "datepicker", + ValueType = ValueTypes.DateTime, + Icon = "icon-time")] public class DateTimePropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/DecimalPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DecimalPropertyEditor.cs index 15b33f28b7..880cf8f204 100644 --- a/src/Umbraco.Web/PropertyEditors/DecimalPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DecimalPropertyEditor.cs @@ -8,7 +8,12 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a decimal property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.Decimal, EditorType.PropertyValue | EditorType.MacroParameter, "Decimal", "decimal", ValueType = ValueTypes.Decimal)] + [DataEditor( + Constants.PropertyEditors.Aliases.Decimal, + EditorType.PropertyValue | EditorType.MacroParameter, + "Decimal", + "decimal", + ValueType = ValueTypes.Decimal)] public class DecimalPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs index eac692fcdd..98a19f39ad 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs @@ -5,7 +5,12 @@ using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.DropDownListFlexible, "Dropdown", "dropdownFlexible", Group = "lists", Icon = "icon-indent")] + [DataEditor( + Constants.PropertyEditors.Aliases.DropDownListFlexible, + "Dropdown", + "dropdownFlexible", + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-indent")] public class DropDownFlexiblePropertyEditor : DataEditor { private readonly ILocalizedTextService _textService; diff --git a/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs index d55d3327e1..cd3a830c72 100644 --- a/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs @@ -5,7 +5,12 @@ using Umbraco.Core.PropertyEditors.Validators; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.EmailAddress, EditorType.PropertyValue | EditorType.MacroParameter, "Email address", "email", Icon="icon-message")] + [DataEditor( + Constants.PropertyEditors.Aliases.EmailAddress, + EditorType.PropertyValue | EditorType.MacroParameter, + "Email address", + "email", + Icon = "icon-message")] public class EmailAddressPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 6d87bfe495..052af18aa1 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -12,8 +12,13 @@ using Umbraco.Web.Media; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.UploadField, "File upload", "fileupload", Icon = "icon-download-alt", Group = "media")] - public class FileUploadPropertyEditor : DataEditor + [DataEditor( + Constants.PropertyEditors.Aliases.UploadField, + "File upload", + "fileupload", + Group = Constants.PropertyEditors.Groups.Media, + Icon = "icon-download-alt")] + public class FileUploadPropertyEditor : DataEditor, IDataEditorWithMediaPath { private readonly IMediaFileSystem _mediaFileSystem; private readonly IContentSection _contentSection; @@ -38,6 +43,8 @@ namespace Umbraco.Web.PropertyEditors return editor; } + public string GetMediaPath(object value) => value?.ToString(); + /// /// Gets a value indicating whether a property is an upload field. /// @@ -47,7 +54,7 @@ namespace Umbraco.Web.PropertyEditors { return property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.UploadField; } - + /// /// Ensures any files associated are removed /// diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs index 81239caec0..942f53b561 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -99,7 +99,7 @@ namespace Umbraco.Web.PropertyEditors { // process the file // no file, invalid file, reject change - if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) + if (UploadFileTypeValidator.IsValidFileExtension(file.FileName) == false) return null; // get the filepath diff --git a/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs b/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs index 136ab88204..74ea517fa2 100644 --- a/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json.Linq; +using Umbraco.Core; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors @@ -6,17 +7,23 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents the configuration for the grid value editor. /// - public class GridConfiguration + public class GridConfiguration : IIgnoreUserStartNodesConfig { // TODO: Make these strongly typed, for now this works though [ConfigurationField("items", "Grid", "views/propertyeditors/grid/grid.prevalues.html", Description = "Grid configuration")] public JObject Items { get; set; } // TODO: Make these strongly typed, for now this works though - [ConfigurationField("rte", "Rich text editor", "views/propertyeditors/rte/rte.prevalues.html", Description = "Rich text editor configuration")] + [ConfigurationField("rte", "Rich text editor", "views/propertyeditors/rte/rte.prevalues.html", Description = "Rich text editor configuration", HideLabel = true)] public JObject Rte { get; set; } - [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.
Note: this applies to all editors in this grid editor except for the rich text editor, which has it's own option for that.")] + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] public bool IgnoreUserStartNodes { get; set; } + + [ConfigurationField("mediaParentId", "Image Upload Folder", "MediaFolderPicker", + Description = "Choose the upload location of pasted images")] + public GuidUdi MediaParentId { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/GridConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/GridConfigurationEditor.cs index 902cd5b41b..e6b1462df0 100644 --- a/src/Umbraco.Web/PropertyEditors/GridConfigurationEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridConfigurationEditor.cs @@ -8,7 +8,7 @@ using Umbraco.Core.PropertyEditors.Validators; namespace Umbraco.Web.PropertyEditors { /// - /// Represents the configuration for the grid value editor. + /// Represents the configuration editor for the grid value editor. /// public class GridConfigurationEditor : ConfigurationEditor { diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 99972a27cf..862837381a 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -1,23 +1,62 @@ -using System.Linq; -using Umbraco.Core.Logging; -using Examine; -using Lucene.Net.Documents; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web.Templates; namespace Umbraco.Web.PropertyEditors { - using Examine = global::Examine; /// /// Represents a grid property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.Grid, "Grid layout", "grid", HideLabel = true, ValueType = ValueTypes.Json, Group="rich content", Icon="icon-layout")] + [DataEditor( + Constants.PropertyEditors.Aliases.Grid, + "Grid layout", + "grid", + HideLabel = true, + ValueType = ValueTypes.Json, + Icon = "icon-layout", + Group = Constants.PropertyEditors.Groups.RichContent)] public class GridPropertyEditor : DataEditor { - public GridPropertyEditor(ILogger logger) + private IUmbracoContextAccessor _umbracoContextAccessor; + private readonly HtmlImageSourceParser _imageSourceParser; + private readonly RichTextEditorPastedImages _pastedImages; + private readonly HtmlLocalLinkParser _localLinkParser; + private readonly IImageUrlGenerator _imageUrlGenerator; + + [Obsolete("Use the constructor which takes an IImageUrlGenerator")] + public GridPropertyEditor(ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser) + : this(logger, umbracoContextAccessor, imageSourceParser, pastedImages, localLinkParser, Current.ImageUrlGenerator) + { + } + + public GridPropertyEditor(ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser, + IImageUrlGenerator imageUrlGenerator) : base(logger) - { } + { + _umbracoContextAccessor = umbracoContextAccessor; + _imageSourceParser = imageSourceParser; + _pastedImages = pastedImages; + _localLinkParser = localLinkParser; + _imageUrlGenerator = imageUrlGenerator; + } public override IPropertyIndexValueFactory PropertyIndexValueFactory => new GridPropertyIndexValueFactory(); @@ -25,15 +64,148 @@ namespace Umbraco.Web.PropertyEditors /// Overridden to ensure that the value is validated ///
/// - protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute); + protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _pastedImages, _localLinkParser, _imageUrlGenerator); protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); - internal class GridPropertyValueEditor : DataValueEditor + internal class GridPropertyValueEditor : DataValueEditor, IDataValueReference { - public GridPropertyValueEditor(DataEditorAttribute attribute) + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly HtmlImageSourceParser _imageSourceParser; + private readonly RichTextEditorPastedImages _pastedImages; + private readonly RichTextPropertyEditor.RichTextPropertyValueEditor _richTextPropertyValueEditor; + private readonly MediaPickerPropertyEditor.MediaPickerPropertyValueEditor _mediaPickerPropertyValueEditor; + private readonly IImageUrlGenerator _imageUrlGenerator; + + [Obsolete("Use the constructor which takes an IImageUrlGenerator")] + public GridPropertyValueEditor(DataEditorAttribute attribute, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser) + : this(attribute, umbracoContextAccessor, imageSourceParser, pastedImages, localLinkParser, Current.ImageUrlGenerator) + { + } + + public GridPropertyValueEditor(DataEditorAttribute attribute, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser, + IImageUrlGenerator imageUrlGenerator) : base(attribute) - { } + { + _umbracoContextAccessor = umbracoContextAccessor; + _imageSourceParser = imageSourceParser; + _pastedImages = pastedImages; + _richTextPropertyValueEditor = new RichTextPropertyEditor.RichTextPropertyValueEditor(attribute, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, _imageUrlGenerator); + _mediaPickerPropertyValueEditor = new MediaPickerPropertyEditor.MediaPickerPropertyValueEditor(attribute); + _imageUrlGenerator = imageUrlGenerator; + } + + /// + /// Format the data for persistence + /// This to ensure if a RTE is used in a Grid cell/control that we parse it for tmp stored images + /// to persist to the media library when we go to persist this to the DB + /// + /// + /// + /// + public override object FromEditor(ContentPropertyData editorValue, object currentValue) + { + if (editorValue.Value == null) + return null; + + // editorValue.Value is a JSON string of the grid + var rawJson = editorValue.Value.ToString(); + if (rawJson.IsNullOrWhiteSpace()) + return null; + + var config = editorValue.DataTypeConfiguration as GridConfiguration; + var mediaParent = config?.MediaParentId; + var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid; + + var grid = DeserializeGridValue(rawJson, out var rtes, out _); + + var userId = _umbracoContextAccessor.UmbracoContext?.Security?.CurrentUser?.Id ?? Constants.Security.SuperUserId; + + // Process the rte values + foreach (var rte in rtes) + { + // Parse the HTML + var html = rte.Value?.ToString(); + + var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(html, mediaParentId, userId, _imageUrlGenerator); + var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); + + rte.Value = editorValueWithMediaUrlsRemoved; + } + + // Convert back to raw JSON for persisting + return JsonConvert.SerializeObject(grid); + } + + /// + /// Ensures that the rich text editor values are processed within the grid + /// + /// + /// + /// + /// + /// + public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) + { + var val = property.GetValue(culture, segment)?.ToString(); + if (val.IsNullOrWhiteSpace()) return string.Empty; + + var grid = DeserializeGridValue(val, out var rtes, out _); + + //process the rte values + foreach (var rte in rtes.ToList()) + { + var html = rte.Value?.ToString(); + + var propertyValueWithMediaResolved = _imageSourceParser.EnsureImageSources(html); + rte.Value = propertyValueWithMediaResolved; + } + + return grid; + } + + private GridValue DeserializeGridValue(string rawJson, out IEnumerable richTextValues, out IEnumerable mediaValues) + { + var grid = JsonConvert.DeserializeObject(rawJson); + + // Find all controls that use the RTE editor + var controls = grid.Sections.SelectMany(x => x.Rows.SelectMany(r => r.Areas).SelectMany(a => a.Controls)).ToArray(); + richTextValues = controls.Where(x => x.Editor.Alias.ToLowerInvariant() == "rte"); + mediaValues = controls.Where(x => x.Editor.Alias.ToLowerInvariant() == "media"); + + return grid; + } + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (rawJson.IsNullOrWhiteSpace()) + yield break; + + DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); + + foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => + _richTextPropertyValueEditor.GetReferences(x.Value))) + yield return umbracoEntityReference; + + foreach (var umbracoEntityReference in mediaValues.SelectMany(x => + _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) + yield return umbracoEntityReference; + } } } } diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs index 085d66f3c6..0cf36122d7 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs @@ -41,9 +41,7 @@ namespace Umbraco.Web.PropertyEditors { var controlVal = control.Value; - // TODO: If it's not a string, then it's a json formatted value - - // we cannot really index this in a smart way since it could be 'anything' - if (controlVal.Type == JTokenType.String) + if (controlVal?.Type == JTokenType.String) { var str = controlVal.Value(); str = XmlHelper.CouldItBeXml(str) ? str.StripHtml() : str; @@ -53,14 +51,22 @@ namespace Umbraco.Web.PropertyEditors //add the row name as an individual field result.Add(new KeyValuePair>($"{property.Alias}.{rowName}", new[] { str })); } + else if (controlVal is JContainer jc) + { + foreach (var s in jc.Descendants().Where(t => t.Type == JTokenType.String)) + { + sb.Append(s.Value()); + sb.Append(" "); + } + } } } + //First save the raw value to a raw field + result.Add(new KeyValuePair>($"{UmbracoExamineIndex.RawFieldPrefix}{property.Alias}", new[] { rawVal })); + if (sb.Length > 0) { - //First save the raw value to a raw field - result.Add(new KeyValuePair>($"{UmbracoExamineIndex.RawFieldPrefix}{property.Alias}", new[] { rawVal })); - //index the property with the combined/cleaned value result.Add(new KeyValuePair>(property.Alias, new[] { sb.ToString() })); } diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs index 86f3412593..b0e5bf30bd 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs @@ -19,8 +19,15 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents an image cropper property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.ImageCropper, "Image Cropper", "imagecropper", ValueType = ValueTypes.Json, HideLabel = false, Group="media", Icon="icon-crop")] - public class ImageCropperPropertyEditor : DataEditor + [DataEditor( + Constants.PropertyEditors.Aliases.ImageCropper, + "Image Cropper", + "imagecropper", + ValueType = ValueTypes.Json, + HideLabel = false, + Group = Constants.PropertyEditors.Groups.Media, + Icon = "icon-crop")] + public class ImageCropperPropertyEditor : DataEditor, IDataEditorWithMediaPath { private readonly IMediaFileSystem _mediaFileSystem; private readonly IContentSection _contentSettings; @@ -41,6 +48,8 @@ namespace Umbraco.Web.PropertyEditors _autoFillProperties = new UploadAutoFillProperties(_mediaFileSystem, logger, _contentSettings); } + public string GetMediaPath(object value) => GetFileSrcFromPropertyValue(value, out _, false); + /// /// Creates the corresponding property value editor. /// @@ -56,7 +65,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Gets a value indicating whether a property is an image cropper field. /// - /// The property. + /// The property. /// A value indicating whether a property is an image cropper field, and (optionally) has a non-empty value. private static bool IsCropperField(Property property) { @@ -125,8 +134,9 @@ namespace Umbraco.Web.PropertyEditors ///
/// /// The deserialized value + /// Should the path returned be the application relative path /// - private string GetFileSrcFromPropertyValue(object propVal, out JObject deserializedValue) + private string GetFileSrcFromPropertyValue(object propVal, out JObject deserializedValue, bool relative = true) { deserializedValue = null; if (propVal == null || !(propVal is string str)) return null; @@ -134,7 +144,7 @@ namespace Umbraco.Web.PropertyEditors deserializedValue = GetJObject(str, true); if (deserializedValue?["src"] == null) return null; var src = deserializedValue["src"].Value(); - return _mediaFileSystem.GetRelativePath(src); + return relative ? _mediaFileSystem.GetRelativePath(src) : src; } /// diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index 7bea542521..4aac8f54aa 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -142,7 +142,7 @@ namespace Umbraco.Web.PropertyEditors { // process the file // no file, invalid file, reject change - if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) + if (UploadFileTypeValidator.IsValidFileExtension(file.FileName) == false) return null; // get the filepath @@ -174,7 +174,12 @@ namespace Umbraco.Web.PropertyEditors // more magic here ;-( var configuration = dataTypeService.GetDataType(propertyType.DataTypeId).ConfigurationAs(); var crops = configuration?.Crops ?? Array.Empty(); - return "{src: '" + val + "', crops: " + crops + "}"; + + return JsonConvert.SerializeObject(new + { + src = val, + crops = crops + }); } } } diff --git a/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs index 5d6adc018c..7717ced4a5 100644 --- a/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs @@ -8,7 +8,12 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents an integer property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.Integer, EditorType.PropertyValue | EditorType.MacroParameter, "Numeric", "integer", ValueType = ValueTypes.Integer)] + [DataEditor( + Constants.PropertyEditors.Aliases.Integer, + EditorType.PropertyValue | EditorType.MacroParameter, + "Numeric", + "integer", + ValueType = ValueTypes.Integer)] public class IntegerPropertyEditor : DataEditor { public IntegerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs index 4f854a7556..ce41628ec1 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs @@ -60,6 +60,9 @@ namespace Umbraco.Web.PropertyEditors Description = "The bulk actions that are allowed from the list view")] public BulkActionPermissionSettings BulkActionPermissions { get; set; } = new BulkActionPermissionSettings(); // TODO: managing defaults? + [ConfigurationField("icon", "Content app icon", "views/propertyeditors/listview/icon.prevalues.html", Description = "The icon of the listview content app")] + public string Icon { get; set; } + [ConfigurationField("tabName", "Content app name", "textstring", Description = "The name of the listview content app (default if empty: 'Child Items')")] public string TabName { get; set; } @@ -74,6 +77,9 @@ namespace Umbraco.Web.PropertyEditors [JsonProperty("header")] public string Header { get; set; } + [JsonProperty("nameTemplate")] + public string Template { get; set; } + [JsonProperty("isSystem")] public int IsSystem { get; set; } // TODO: bool } diff --git a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs index 8e2732cc0f..53f9cb94ef 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs @@ -8,7 +8,13 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a list-view editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.ListView, "List view", "listview", HideLabel = true, Group = "lists", Icon = "icon-item-arrangement")] + [DataEditor( + Constants.PropertyEditors.Aliases.ListView, + "List view", + "listview", + HideLabel = true, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = Constants.Icons.ListView)] public class ListViewPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/MacroContainerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MacroContainerConfiguration.cs deleted file mode 100644 index 1490c4d2a8..0000000000 --- a/src/Umbraco.Web/PropertyEditors/MacroContainerConfiguration.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors -{ - /// - /// Represents the configuration for the macro container value editor. - /// - public class MacroContainerConfiguration - { - [ConfigurationField("max", "Max items", "number", Description = "The maximum number of macros that are allowed in the container")] - public int MaxItems { get; set; } - - [ConfigurationField("allowed", "Allowed items", "views/propertyeditors/macrocontainer/macrolist.prevalues.html", Description = "The macro types allowed, if none are selected all macros will be allowed")] - public object AllowedItems { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/MacroContainerConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/MacroContainerConfigurationEditor.cs deleted file mode 100644 index 1ac4f5c874..0000000000 --- a/src/Umbraco.Web/PropertyEditors/MacroContainerConfigurationEditor.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors -{ - /// - /// Represents the configuration editor for the macro container value editor. - /// - public class MacroContainerConfigurationEditor : ConfigurationEditor - { } -} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs deleted file mode 100644 index d4d23da3a4..0000000000 --- a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors -{ - // TODO: MacroContainerPropertyEditor is deprecated, but what's the alternative? - [DataEditor(Constants.PropertyEditors.Aliases.MacroContainer, "(Obsolete) Macro Picker", "macrocontainer", ValueType = ValueTypes.Text, Group="rich content", Icon="icon-settings-alt", IsDeprecated = true)] - public class MacroContainerPropertyEditor : DataEditor - { - public MacroContainerPropertyEditor(ILogger logger) - : base(logger) - { } - - protected override IConfigurationEditor CreateConfigurationEditor() => new MacroContainerConfigurationEditor(); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs index 99b68a507b..2d66da5461 100644 --- a/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs @@ -7,7 +7,13 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a markdown editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.MarkdownEditor, "Markdown editor", "markdowneditor", ValueType = ValueTypes.Text, Icon="icon-code", Group="rich content")] + [DataEditor( + Constants.PropertyEditors.Aliases.MarkdownEditor, + "Markdown editor", + "markdowneditor", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.RichContent, + Icon = "icon-code")] public class MarkdownPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs index fa430e103b..b8b9476184 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents the configuration for the media picker value editor. /// - public class MediaPickerConfiguration + public class MediaPickerConfiguration : IIgnoreUserStartNodesConfig { [ConfigurationField("multiPicker", "Pick multiple items", "boolean")] public bool Multiple { get; set; } @@ -20,7 +20,9 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("startNodeId", "Start node", "mediapicker")] public Udi StartNodeId { get; set; } - [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 6c768f4932..fa8060bd15 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -1,5 +1,7 @@ -using Umbraco.Core; +using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors @@ -7,18 +9,50 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a media picker property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.MediaPicker, EditorType.PropertyValue | EditorType.MacroParameter, - "Media Picker", "mediapicker", ValueType = ValueTypes.Text, Group = "media", Icon = "icon-picture")] + [DataEditor( + Constants.PropertyEditors.Aliases.MediaPicker, + EditorType.PropertyValue | EditorType.MacroParameter, + "Media Picker", + "mediapicker", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.Media, + Icon = Constants.Icons.MediaImage)] public class MediaPickerPropertyEditor : DataEditor { + /// /// Initializes a new instance of the class. /// public MediaPickerPropertyEditor(ILogger logger) : base(logger) - { } + { + } /// protected override IConfigurationEditor CreateConfigurationEditor() => new MediaPickerConfigurationEditor(); + + protected override IDataValueEditor CreateValueEditor() => new MediaPickerPropertyValueEditor(Attribute); + + internal class MediaPickerPropertyValueEditor : DataValueEditor, IDataValueReference + { + public MediaPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) + { + } + + public IEnumerable GetReferences(object value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + foreach (var udiStr in asString.Split(',')) + { + if (Udi.TryParse(udiStr, out var udi)) + yield return new UmbracoEntityReference(udi); + } + } + } } + + } diff --git a/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs index b917145dbd..b8e67c863c 100644 --- a/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs @@ -4,7 +4,13 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.MemberGroupPicker, "Member Group Picker", "membergrouppicker", ValueType = ValueTypes.Text, Group = "People", Icon = "icon-users")] + [DataEditor( + Constants.PropertyEditors.Aliases.MemberGroupPicker, + "Member Group Picker", + "membergrouppicker", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.MemberGroup)] public class MemberGroupPickerPropertyEditor : DataEditor { public MemberGroupPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs index a0705fb373..69020ba350 100644 --- a/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs @@ -4,7 +4,13 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.MemberPicker, "Member Picker", "memberpicker", ValueType = ValueTypes.String, Group = "People", Icon = "icon-user")] + [DataEditor( + Constants.PropertyEditors.Aliases.MemberPicker, + "Member Picker", + "memberpicker", + ValueType = ValueTypes.String, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.Member)] public class MemberPickerPropertyEditor : DataEditor { public MemberPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs index a0a2467b1c..81a96b5ad7 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs @@ -6,12 +6,12 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents the configuration for the multinode picker value editor. /// - public class MultiNodePickerConfiguration + public class MultiNodePickerConfiguration : IIgnoreUserStartNodesConfig { [ConfigurationField("startNode", "Node type", "treesource")] public MultiNodePickerConfigurationTreeSource TreeSource { get; set; } - [ConfigurationField("filter", "Allow items of type", "textstring", Description = "Separate with comma")] + [ConfigurationField("filter", "Allow items of type", "treesourcetypepicker", Description = "Select the applicable types")] public string Filter { get; set; } [ConfigurationField("minNumber", "Minimum number of items", "number")] @@ -20,10 +20,12 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("maxNumber", "Maximum number of items", "number")] public int MaxNumber { get; set; } - [ConfigurationField("showOpenButton", "Show open button (this feature is in preview!)", "boolean", Description = "Opens the node in a dialog")] + [ConfigurationField("showOpenButton", "Show open button", "boolean", Description = "Opens the node in a dialog")] public bool ShowOpen { get; set; } - - [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index ad75e16717..fd7f735e68 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -1,10 +1,18 @@ -using Umbraco.Core; +using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.MultiNodeTreePicker, "Multinode Treepicker", "contentpicker", ValueType = ValueTypes.Text, Group = "pickers", Icon = "icon-page-add")] + [DataEditor( + Constants.PropertyEditors.Aliases.MultiNodeTreePicker, + "Multinode Treepicker", + "contentpicker", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.Pickers, + Icon = "icon-page-add")] public class MultiNodeTreePickerPropertyEditor : DataEditor { public MultiNodeTreePickerPropertyEditor(ILogger logger) @@ -12,5 +20,27 @@ namespace Umbraco.Web.PropertyEditors { } protected override IConfigurationEditor CreateConfigurationEditor() => new MultiNodePickerConfigurationEditor(); + + protected override IDataValueEditor CreateValueEditor() => new MultiNodeTreePickerPropertyValueEditor(Attribute); + + public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor, IDataValueReference + { + public MultiNodeTreePickerPropertyValueEditor(DataEditorAttribute attribute): base(attribute) + { + + } + + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var udiPaths = asString.Split(','); + foreach (var udiPath in udiPaths) + if (Udi.TryParse(udiPath, out var udi)) + yield return new UmbracoEntityReference(udi); + } + } } + + } diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs index ec9439ceea..239569478f 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs @@ -2,7 +2,8 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - public class MultiUrlPickerConfiguration + + public class MultiUrlPickerConfiguration : IIgnoreUserStartNodesConfig { [ConfigurationField("minNumber", "Minimum number of items", "number")] public int MinNumber { get; set; } @@ -10,7 +11,14 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("maxNumber", "Maximum number of items", "number")] public int MaxNumber { get; set; } - [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] public bool IgnoreUserStartNodes { get; set; } + + [ConfigurationField("hideAnchor", + "Hide anchor/query string input", "boolean", + Description = "Selecting this hides the anchor/query string input field in the linkpicker overlay.")] + public bool HideAnchor { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs index ea5d7aac8e..8af2d98018 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -4,10 +4,20 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Logging; using Umbraco.Core.Services; using Umbraco.Web.PublishedCache; +using System.Collections.Generic; +using Umbraco.Core.Models.Editors; +using Newtonsoft.Json; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.MultiUrlPicker, EditorType.PropertyValue, "Multi Url Picker", "multiurlpicker", ValueType = ValueTypes.Json, Group = "pickers", Icon = "icon-link")] + [DataEditor( + Constants.PropertyEditors.Aliases.MultiUrlPicker, + EditorType.PropertyValue, + "Multi Url Picker", + "multiurlpicker", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Pickers, + Icon = "icon-link")] public class MultiUrlPickerPropertyEditor : DataEditor { private readonly IEntityService _entityService; @@ -18,7 +28,7 @@ namespace Umbraco.Web.PropertyEditors _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); } - + protected override IConfigurationEditor CreateConfigurationEditor() => new MultiUrlPickerConfigurationEditor(); protected override IDataValueEditor CreateValueEditor() => new MultiUrlPickerValueEditor(_entityService, _publishedSnapshotAccessor, Logger, Attribute); diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index 129df2bacf..853e995ed8 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -15,7 +15,7 @@ using Umbraco.Web.PublishedCache; namespace Umbraco.Web.PropertyEditors { - public class MultiUrlPickerValueEditor : DataValueEditor + public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference { private readonly IEntityService _entityService; private readonly ILogger _logger; @@ -81,7 +81,7 @@ namespace Umbraco.Web.PropertyEditors icon = documentEntity.ContentTypeIcon; published = culture == null ? documentEntity.Published : documentEntity.PublishedCultures.Contains(culture); udi = new GuidUdi(Constants.UdiEntityType.Document, documentEntity.Key); - url = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(entity.Key)?.Url ?? "#"; + url = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(entity.Key)?.Url() ?? "#"; trashed = documentEntity.Trashed; } else if(entity is IContentEntitySlim contentEntity) @@ -89,7 +89,7 @@ namespace Umbraco.Web.PropertyEditors icon = contentEntity.ContentTypeIcon; published = !contentEntity.Trashed; udi = new GuidUdi(Constants.UdiEntityType.Media, contentEntity.Key); - url = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(entity.Key)?.Url ?? "#"; + url = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(entity.Key)?.Url() ?? "#"; trashed = contentEntity.Trashed; } else @@ -174,5 +174,22 @@ namespace Umbraco.Web.PropertyEditors [DataMember(Name = "queryString")] public string QueryString { get; set; } } + + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + var links = JsonConvert.DeserializeObject>(asString); + foreach (var link in links) + { + if (link.Udi != null) // Links can be absolute links without a Udi + { + yield return new UmbracoEntityReference(link.Udi); + } + + } + } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs index 141bdea7b6..fa82bc555c 100644 --- a/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs @@ -1,11 +1,15 @@ using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using Newtonsoft.Json.Linq; using Umbraco.Core; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.Validators; using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors @@ -13,7 +17,13 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a multiple text string property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.MultipleTextstring, "Repeatable textstrings", "multipletextbox", ValueType = ValueTypes.Text, Icon="icon-ordered-list", Group="lists")] + [DataEditor( + Constants.PropertyEditors.Aliases.MultipleTextstring, + "Repeatable textstrings", + "multipletextbox", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-ordered-list")] public class MultipleTextStringPropertyEditor : DataEditor { /// @@ -56,7 +66,7 @@ namespace Umbraco.Web.PropertyEditors } if (!(editorValue.DataTypeConfiguration is MultipleTextStringConfiguration config)) - throw new Exception("panic"); + throw new PanicException($"editorValue.DataTypeConfiguration is {editorValue.DataTypeConfiguration.GetType()} but must be {typeof(MultipleTextStringConfiguration)}"); var max = config.Maximum; //The legacy property editor saved this data as new line delimited! strange but we have to maintain that. @@ -79,7 +89,7 @@ namespace Umbraco.Web.PropertyEditors /// /// /// - /// + /// /// /// /// @@ -90,10 +100,40 @@ namespace Umbraco.Web.PropertyEditors var val = property.GetValue(culture, segment); return val?.ToString().Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries) .Select(x => JObject.FromObject(new {value = x})) ?? new JObject[] { }; - - } + /// + /// A custom FormatValidator is used as for multiple text strings, each string should individually be checked + /// against the configured regular expression, rather than the JSON representing all the strings as a whole. + /// + public override IValueFormatValidator FormatValidator => new MultipleTextStringFormatValidator(); + } + + internal class MultipleTextStringFormatValidator : IValueFormatValidator + { + public IEnumerable ValidateFormat(object value, string valueType, string format) + { + var asArray = value as JArray; + if (asArray == null) + { + return Enumerable.Empty(); + } + + var textStrings = asArray.OfType() + .Where(x => x["value"] != null) + .Select(x => x["value"].Value()); + var textStringValidator = new RegexValidator(); + foreach (var textString in textStrings) + { + var validationResults = textStringValidator.ValidateFormat(textString, valueType, format).ToList(); + if (validationResults.Any()) + { + return validationResults; + } + } + + return Enumerable.Empty(); + } } } } diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs b/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs index b904a2250d..0f53207462 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs @@ -8,22 +8,22 @@ namespace Umbraco.Web.PropertyEditors /// public class NestedContentConfiguration { - [ConfigurationField("contentTypes", "Document types", "views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html", Description = "Select the document types to use as the item blueprints. Only \"element\" types can be used.")] + [ConfigurationField("contentTypes", "Element Types", "views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html", Description = "Select the Element Types to use as models for the items.")] public ContentType[] ContentTypes { get; set; } - [ConfigurationField("minItems", "Min Items", "number", Description = "Set the minimum number of items allowed.")] + [ConfigurationField("minItems", "Min Items", "number", Description = "Minimum number of items allowed.")] public int? MinItems { get; set; } - [ConfigurationField("maxItems", "Max Items", "number", Description = "Set the maximum number of items allowed.")] + [ConfigurationField("maxItems", "Max Items", "number", Description = "Maximum number of items allowed.")] public int? MaxItems { get; set; } - [ConfigurationField("confirmDeletes", "Confirm Deletes", "boolean", Description = "Set whether item deletions should require confirming.")] + [ConfigurationField("confirmDeletes", "Confirm Deletes", "boolean", Description = "Requires editor confirmation for delete actions.")] public bool ConfirmDeletes { get; set; } = true; - [ConfigurationField("showIcons", "Show Icons", "boolean", Description = "Set whether to show the items doc type icon in the list.")] + [ConfigurationField("showIcons", "Show Icons", "boolean", Description = "Show the Element Type icons.")] public bool ShowIcons { get; set; } = true; - [ConfigurationField("hideLabel", "Hide Label", "boolean", Description = "Set whether to hide the editor label and have the list take up the full width of the editor window.")] + [ConfigurationField("hideLabel", "Hide Label", "boolean", Description = "Hide the property label and let the item list span the full width of the editor window.")] public bool HideLabel { get; set; } public class ContentType diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentController.cs b/src/Umbraco.Web/PropertyEditors/NestedContentController.cs index 8d144c5904..590a286c5d 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentController.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Services; using Umbraco.Web.Editors; using Umbraco.Web.Mvc; @@ -11,8 +12,8 @@ namespace Umbraco.Web.PropertyEditors [System.Web.Http.HttpGet] public IEnumerable GetContentTypes() { - return Services.ContentTypeService.GetAll() - .Where(x => x.IsElement) + return Services.ContentTypeService + .GetAllElementTypes() .OrderBy(x => x.SortOrder) .Select(x => new { diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 6dee2f78b5..564630c574 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -18,30 +19,31 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a nested content property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.NestedContent, "Nested Content", "nestedcontent", ValueType = "JSON", Group = "lists", Icon = "icon-thumbnail-list")] + [DataEditor( + Constants.PropertyEditors.Aliases.NestedContent, + "Nested Content", + "nestedcontent", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-thumbnail-list")] public class NestedContentPropertyEditor : DataEditor { private readonly Lazy _propertyEditors; - + private readonly IDataTypeService _dataTypeService; + private readonly IContentTypeService _contentTypeService; internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; - public NestedContentPropertyEditor(ILogger logger, Lazy propertyEditors) + public NestedContentPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) : base (logger) { _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _contentTypeService = contentTypeService; } // has to be lazy else circular dep in ctor private PropertyEditorCollection PropertyEditors => _propertyEditors.Value; - private static IContentType GetElementType(JObject item) - { - var contentTypeAlias = item[ContentTypeAliasPropertyKey]?.ToObject(); - return string.IsNullOrEmpty(contentTypeAlias) - ? null - : Current.Services.ContentTypeService.Get(contentTypeAlias); - } - #region Pre Value Editor protected override IConfigurationEditor CreateConfigurationEditor() => new NestedContentConfigurationEditor(); @@ -50,21 +52,23 @@ namespace Umbraco.Web.PropertyEditors #region Value Editor - protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors); + protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService); - internal class NestedContentPropertyValueEditor : DataValueEditor + internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference { private readonly PropertyEditorCollection _propertyEditors; - - public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors) + private readonly IDataTypeService _dataTypeService; + private readonly NestedContentValues _nestedContentValues; + + public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) : base(attribute) { _propertyEditors = propertyEditors; - Validators.Add(new NestedContentValidator(propertyEditors)); + _dataTypeService = dataTypeService; + _nestedContentValues = new NestedContentValues(contentTypeService); + Validators.Add(new NestedContentValidator(propertyEditors, dataTypeService, _nestedContentValues)); } - internal ServiceContext Services => Current.Services; - /// public override object Configuration { @@ -85,56 +89,47 @@ namespace Umbraco.Web.PropertyEditors public override string ConvertDbToString(PropertyType propertyType, object propertyValue, IDataTypeService dataTypeService) { - if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) + var vals = _nestedContentValues.GetPropertyValues(propertyValue, out var deserialized).ToList(); + + if (vals.Count == 0) return string.Empty; - var value = JsonConvert.DeserializeObject>(propertyValue.ToString()); - if (value == null) - return string.Empty; - - foreach (var o in value) + foreach (var row in vals) { - var propValues = (JObject) o; - - var contentType = GetElementType(propValues); - if (contentType == null) - continue; - - var propAliases = propValues.Properties().Select(x => x.Name).ToArray(); - foreach (var propAlias in propAliases) + if (row.PropType == null) { - var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propAlias); - if (propType == null) + // type not found, and property is not system: just delete the value + if (IsSystemPropertyKey(row.PropKey) == false) + row.JsonRowValue[row.PropKey] = null; + } + else + { + try { - // type not found, and property is not system: just delete the value - if (IsSystemPropertyKey(propAlias) == false) - propValues[propAlias] = null; + // convert the value, and store the converted value + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + if (propEditor == null) continue; + + var tempConfig = dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; + var valEditor = propEditor.GetValueEditor(tempConfig); + var convValue = valEditor.ConvertDbToString(row.PropType, row.JsonRowValue[row.PropKey]?.ToString(), dataTypeService); + row.JsonRowValue[row.PropKey] = convValue; } - else + catch (InvalidOperationException) { - try - { - // convert the value, and store the converted value - var propEditor = _propertyEditors[propType.PropertyEditorAlias]; - var tempConfig = dataTypeService.GetDataType(propType.DataTypeId).Configuration; - var valEditor = propEditor.GetValueEditor(tempConfig); - var convValue = valEditor.ConvertDbToString(propType, propValues[propAlias]?.ToString(), dataTypeService); - propValues[propAlias] = convValue; - } - catch (InvalidOperationException) - { - // deal with weird situations by ignoring them (no comment) - propValues[propAlias] = null; - } + // deal with weird situations by ignoring them (no comment) + row.JsonRowValue[row.PropKey] = null; } } } - return JsonConvert.SerializeObject(value).ToXmlString(); + return JsonConvert.SerializeObject(deserialized).ToXmlString(); } #endregion + + #region Convert database // editor // note: there is NO variant support here @@ -142,60 +137,53 @@ namespace Umbraco.Web.PropertyEditors public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { var val = property.GetValue(culture, segment); - if (val == null || string.IsNullOrWhiteSpace(val.ToString())) + + var vals = _nestedContentValues.GetPropertyValues(val, out var deserialized).ToList(); + + if (vals.Count == 0) return string.Empty; - var value = JsonConvert.DeserializeObject>(val.ToString()); - if (value == null) - return string.Empty; - - foreach (var o in value) + foreach (var row in vals) { - var propValues = (JObject) o; - - var contentType = GetElementType(propValues); - if (contentType == null) - continue; - - var propAliases = propValues.Properties().Select(x => x.Name).ToArray(); - foreach (var propAlias in propAliases) + if (row.PropType == null) { - var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propAlias); - if (propType == null) + // type not found, and property is not system: just delete the value + if (IsSystemPropertyKey(row.PropKey) == false) + row.JsonRowValue[row.PropKey] = null; + } + else + { + try { - // type not found, and property is not system: just delete the value - if (IsSystemPropertyKey(propAlias) == false) - propValues[propAlias] = null; + // create a temp property with the value + // - force it to be culture invariant as NC can't handle culture variant element properties + row.PropType.Variations = ContentVariation.Nothing; + var tempProp = new Property(row.PropType); + tempProp.SetValue(row.JsonRowValue[row.PropKey] == null ? null : row.JsonRowValue[row.PropKey].ToString()); + + // convert that temp property, and store the converted value + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + if (propEditor == null) + { + row.JsonRowValue[row.PropKey] = tempProp.GetValue()?.ToString(); + continue; + } + + var tempConfig = dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; + var valEditor = propEditor.GetValueEditor(tempConfig); + var convValue = valEditor.ToEditor(tempProp, dataTypeService); + row.JsonRowValue[row.PropKey] = convValue == null ? null : JToken.FromObject(convValue); } - else + catch (InvalidOperationException) { - try - { - // create a temp property with the value - // - force it to be culture invariant as NC can't handle culture variant element properties - propType.Variations = ContentVariation.Nothing; - var tempProp = new Property(propType); - tempProp.SetValue(propValues[propAlias] == null ? null : propValues[propAlias].ToString()); - - // convert that temp property, and store the converted value - var propEditor = _propertyEditors[propType.PropertyEditorAlias]; - var tempConfig = dataTypeService.GetDataType(propType.DataTypeId).Configuration; - var valEditor = propEditor.GetValueEditor(tempConfig); - var convValue = valEditor.ToEditor(tempProp, dataTypeService); - propValues[propAlias] = convValue == null ? null : JToken.FromObject(convValue); - } - catch (InvalidOperationException) - { - // deal with weird situations by ignoring them (no comment) - propValues[propAlias] = null; - } + // deal with weird situations by ignoring them (no comment) + row.JsonRowValue[row.PropKey] = null; } - } } // return json - return value; + return deserialized; } public override object FromEditor(ContentPropertyData editorValue, object currentValue) @@ -203,134 +191,229 @@ namespace Umbraco.Web.PropertyEditors if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) return null; - var value = JsonConvert.DeserializeObject>(editorValue.Value.ToString()); - if (value == null) - return null; + var vals = _nestedContentValues.GetPropertyValues(editorValue.Value, out var deserialized).ToList(); - // Issue #38 - Keep recursive property lookups working - if (!value.Any()) - return null; + if (vals.Count == 0) + return string.Empty; - // Process value - for (var i = 0; i < value.Count; i++) + foreach (var row in vals) { - var o = value[i]; - var propValues = ((JObject)o); - - var contentType = GetElementType(propValues); - if (contentType == null) + if (row.PropType == null) { - continue; + // type not found, and property is not system: just delete the value + if (IsSystemPropertyKey(row.PropKey) == false) + row.JsonRowValue[row.PropKey] = null; } - - var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray(); - - foreach (var propKey in propValueKeys) + else { - var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey); - if (propType == null) - { - if (IsSystemPropertyKey(propKey) == false) - { - // Property missing so just delete the value - propValues[propKey] = null; - } - } - else - { - // Fetch the property types prevalue - var propConfiguration = Services.DataTypeService.GetDataType(propType.DataTypeId).Configuration; + // Fetch the property types prevalue + var propConfiguration = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; - // Lookup the property editor - var propEditor = _propertyEditors[propType.PropertyEditorAlias]; + // Lookup the property editor + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + if (propEditor == null) continue; - // Create a fake content property data object - var contentPropData = new ContentPropertyData(propValues[propKey], propConfiguration); + // Create a fake content property data object + var contentPropData = new ContentPropertyData(row.JsonRowValue[row.PropKey], propConfiguration); - // Get the property editor to do it's conversion - var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, propValues[propKey]); - - // Store the value back - propValues[propKey] = (newValue == null) ? null : JToken.FromObject(newValue); - } + // Get the property editor to do it's conversion + var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, row.JsonRowValue[row.PropKey]); + // Store the value back + row.JsonRowValue[row.PropKey] = (newValue == null) ? null : JToken.FromObject(newValue); } } - return JsonConvert.SerializeObject(value); + // return json + return JsonConvert.SerializeObject(deserialized); } - #endregion + + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var result = new List(); + + foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _)) + { + if (row.PropType == null) continue; + + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + + var valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) continue; + + var val = row.JsonRowValue[row.PropKey]?.ToString(); + + var refs = reference.GetReferences(val); + + result.AddRange(refs); + } + + return result; + } } internal class NestedContentValidator : IValueValidator { private readonly PropertyEditorCollection _propertyEditors; + private readonly IDataTypeService _dataTypeService; + private readonly NestedContentValues _nestedContentValues; - public NestedContentValidator(PropertyEditorCollection propertyEditors) + public NestedContentValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, NestedContentValues nestedContentValues) { _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _nestedContentValues = nestedContentValues; } public IEnumerable Validate(object rawValue, string valueType, object dataTypeConfiguration) { - if (rawValue == null) - yield break; + var validationResults = new List(); - var value = JsonConvert.DeserializeObject>(rawValue.ToString()); - if (value == null) - yield break; - - var dataTypeService = Current.Services.DataTypeService; - for (var i = 0; i < value.Count; i++) + foreach(var row in _nestedContentValues.GetPropertyValues(rawValue, out _)) { - var o = value[i]; - var propValues = (JObject) o; + if (row.PropType == null) continue; - var contentType = GetElementType(propValues); - if (contentType == null) continue; + var config = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; + var propertyEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + if (propertyEditor == null) continue; - var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray(); - - foreach (var propKey in propValueKeys) + foreach (var validator in propertyEditor.GetValueEditor().Validators) { - var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey); - if (propType != null) + foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config)) { - var config = dataTypeService.GetDataType(propType.DataTypeId).Configuration; - var propertyEditor = _propertyEditors[propType.PropertyEditorAlias]; + result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage; + validationResults.Add(result); + } + } - foreach (var validator in propertyEditor.GetValueEditor().Validators) - { - foreach (var result in validator.Validate(propValues[propKey], propertyEditor.GetValueEditor().ValueType, config)) - { - result.ErrorMessage = "Item " + (i + 1) + " '" + propType.Name + "' " + result.ErrorMessage; - yield return result; - } - } + // Check mandatory + if (row.PropType.Mandatory) + { + if (row.JsonRowValue[row.PropKey] == null) + { + var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) + ? $"'{row.PropType.Name}' cannot be null" + : row.PropType.MandatoryMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } + else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues)) + { + var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) + ? $"'{row.PropType.Name}' cannot be empty" + : row.PropType.MandatoryMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } + } - // Check mandatory - if (propType.Mandatory) - { - if (propValues[propKey] == null) - yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be null", new[] { propKey }); - else if (propValues[propKey].ToString().IsNullOrWhiteSpace() || (propValues[propKey].Type == JTokenType.Array && !propValues[propKey].HasValues)) - yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be empty", new[] { propKey }); - } - - // Check regex - if (!propType.ValidationRegExp.IsNullOrWhiteSpace() - && propValues[propKey] != null && !propValues[propKey].ToString().IsNullOrWhiteSpace()) - { - var regex = new Regex(propType.ValidationRegExp); - if (!regex.IsMatch(propValues[propKey].ToString())) - { - yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' is invalid, it does not match the correct pattern", new[] { propKey }); - } - } + // Check regex + if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace() + && row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace()) + { + var regex = new Regex(row.PropType.ValidationRegExp); + if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString())) + { + var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage) + ? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern" + : row.PropType.ValidationRegExpMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); } } } + + return validationResults; + } + } + + internal class NestedContentValues + { + private readonly Lazy> _contentTypes; + + public NestedContentValues(IContentTypeService contentTypeService) + { + _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Alias)); + } + + private IContentType GetElementType(JObject item) + { + var contentTypeAlias = item[ContentTypeAliasPropertyKey]?.ToObject() ?? string.Empty; + _contentTypes.Value.TryGetValue(contentTypeAlias, out var contentType); + return contentType; + } + + public IEnumerable GetPropertyValues(object propertyValue, out List deserialized) + { + var rowValues = new List(); + + deserialized = null; + + if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) + return Enumerable.Empty(); + + deserialized = JsonConvert.DeserializeObject>(propertyValue.ToString()); + + // There was a note here about checking if the result had zero items and if so it would return null, so we'll continue to do that + // The original note was: "Issue #38 - Keep recursive property lookups working" + // Which is from the original NC tracker: https://github.com/umco/umbraco-nested-content/issues/38 + // This check should be used everywhere when iterating NC prop values, instead of just the one previous place so that + // empty values don't get persisted when there is nothing, it should actually be null. + if (deserialized == null || deserialized.Count == 0) + return Enumerable.Empty(); + + var index = 0; + + foreach (var o in deserialized) + { + var propValues = o; + + var contentType = GetElementType(propValues); + if (contentType == null) + continue; + + var propertyTypes = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); + var propAliases = propValues.Properties().Select(x => x.Name); + foreach (var propAlias in propAliases) + { + propertyTypes.TryGetValue(propAlias, out var propType); + rowValues.Add(new RowValue(propAlias, propType, propValues, index)); + } + index++; + } + + return rowValues; + } + + internal class RowValue + { + public RowValue(string propKey, PropertyType propType, JObject propValues, int index) + { + PropKey = propKey ?? throw new ArgumentNullException(nameof(propKey)); + PropType = propType; + JsonRowValue = propValues ?? throw new ArgumentNullException(nameof(propValues)); + RowIndex = index; + } + + /// + /// The current property key being iterated for the row value + /// + public string PropKey { get; } + + /// + /// The of the value (if any), this may be null + /// + public PropertyType PropType { get; } + + /// + /// The json values for the current row + /// + public JObject JsonRowValue { get; } + + /// + /// The Nested Content row index + /// + public int RowIndex { get; } } } diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs index cdab78d119..149243f122 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs @@ -6,7 +6,11 @@ namespace Umbraco.Web.PropertyEditors.ParameterEditors /// /// Represents a content type parameter editor. /// - [DataEditor("contentType", EditorType.MacroParameter, "Content Type Picker", "entitypicker")] + [DataEditor( + "contentType", + EditorType.MacroParameter, + "Content Type Picker", + "entitypicker")] public class ContentTypeParameterEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs index 3d0d0eb923..0ca657ac39 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs @@ -7,7 +7,11 @@ namespace Umbraco.Web.PropertyEditors.ParameterEditors /// /// Represents a parameter editor of some sort. /// - [DataEditor(Constants.PropertyEditors.Aliases.MultiNodeTreePicker, EditorType.MacroParameter, "Multiple Content Picker", "contentpicker")] + [DataEditor( + Constants.PropertyEditors.Aliases.MultiNodeTreePicker, + EditorType.MacroParameter, + "Multiple Content Picker", + "contentpicker")] public class MultipleContentPickerParameterEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs index 74bcafe30f..c8f19f8acd 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs @@ -3,7 +3,11 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors.ParameterEditors { - [DataEditor("contentTypeMultiple", EditorType.MacroParameter, "Multiple Content Type Picker", "entitypicker")] + [DataEditor( + "contentTypeMultiple", + EditorType.MacroParameter, + "Multiple Content Type Picker", + "entitypicker")] public class MultipleContentTypeParameterEditor : DataEditor { public MultipleContentTypeParameterEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs new file mode 100644 index 0000000000..1208a5eecc --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs @@ -0,0 +1,27 @@ +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ParameterEditors +{ + /// + /// Represents a multiple media picker macro parameter editor. + /// + [DataEditor( + Constants.PropertyEditors.Aliases.MultipleMediaPicker, + EditorType.MacroParameter, + "Multiple Media Picker", + "mediapicker", + ValueType = ValueTypes.Text)] + public class MultipleMediaPickerParameterEditor : DataEditor + { + /// + /// Initializes a new instance of the class. + /// + public MultipleMediaPickerParameterEditor(ILogger logger) + : base(logger) + { + DefaultConfiguration.Add("multiPicker", "1"); + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs index 87ddfb0b54..2a7079d578 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs @@ -3,7 +3,11 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors.ParameterEditors { - [DataEditor("tabPickerMultiple", EditorType.MacroParameter, "Multiple Tab Picker", "entitypicker")] + [DataEditor( + "tabPickerMultiple", + EditorType.MacroParameter, + "Multiple Tab Picker", + "entitypicker")] public class MultiplePropertyGroupParameterEditor : DataEditor { public MultiplePropertyGroupParameterEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs index 124325dbd7..fe1432a655 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs @@ -3,7 +3,11 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors.ParameterEditors { - [DataEditor("propertyTypePickerMultiple", EditorType.MacroParameter, "Multiple Property Type Picker", "entitypicker")] + [DataEditor( + "propertyTypePickerMultiple", + EditorType.MacroParameter, + "Multiple Property Type Picker", + "entitypicker")] public class MultiplePropertyTypeParameterEditor : DataEditor { public MultiplePropertyTypeParameterEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs index 76fa34341d..65ce595852 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs @@ -3,7 +3,11 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors.ParameterEditors { - [DataEditor("tabPicker", EditorType.MacroParameter, "Tab Picker", "entitypicker")] + [DataEditor( + "tabPicker", + EditorType.MacroParameter, + "Tab Picker", + "entitypicker")] public class PropertyGroupParameterEditor : DataEditor { public PropertyGroupParameterEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs index b84749da27..4ea7c0ebdc 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs @@ -3,7 +3,11 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors.ParameterEditors { - [DataEditor("propertyTypePicker", EditorType.MacroParameter, "Property Type Picker", "entitypicker")] + [DataEditor( + "propertyTypePicker", + EditorType.MacroParameter, + "Property Type Picker", + "entitypicker")] public class PropertyTypeParameterEditor : DataEditor { public PropertyTypeParameterEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs index 601728189c..f729decd2c 100644 --- a/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs @@ -8,7 +8,13 @@ namespace Umbraco.Web.PropertyEditors /// /// A property editor to allow the individual selection of pre-defined items. /// - [DataEditor(Constants.PropertyEditors.Aliases.RadioButtonList, "Radio button list", "radiobuttons", ValueType = ValueTypes.String, Group="lists", Icon="icon-target")] + [DataEditor( + Constants.PropertyEditors.Aliases.RadioButtonList, + "Radio button list", + "radiobuttons", + ValueType = ValueTypes.String, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-target")] public class RadioButtonsPropertyEditor : DataEditor { private readonly ILocalizedTextService _textService; diff --git a/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs b/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs index d99c2b17e0..c6d4e3ce70 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json.Linq; +using Umbraco.Core; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors @@ -6,7 +7,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents the configuration for the rich text value editor. /// - public class RichTextConfiguration + public class RichTextConfiguration : IIgnoreUserStartNodesConfig { // TODO: Make these strongly typed, for now this works though [ConfigurationField("editor", "Editor", "views/propertyeditors/rte/rte.prevalues.html", HideLabel = true)] @@ -15,7 +16,13 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("hideLabel", "Hide Label", "boolean")] public bool HideLabel { get; set; } - [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] public bool IgnoreUserStartNodes { get; set; } + + [ConfigurationField("mediaParentId", "Image Upload Folder", "MediaFolderPicker", + Description = "Choose the upload location of pasted images")] + public GuidUdi MediaParentId { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs new file mode 100644 index 0000000000..67ff82a3e1 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs @@ -0,0 +1,143 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.IO; +using Umbraco.Core; +using Umbraco.Core.Exceptions; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.Templates; + +namespace Umbraco.Web.PropertyEditors +{ + public sealed class RichTextEditorPastedImages + { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILogger _logger; + private readonly IMediaService _mediaService; + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + + const string TemporaryImageDataAttribute = "data-tmpimg"; + + public RichTextEditorPastedImages(IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) + { + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ?? throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider)); + } + + /// + /// Used by the RTE (and grid RTE) for drag/drop/persisting images + /// + /// + /// + /// + /// + internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator) + { + // Find all img's that has data-tmpimg attribute + // Use HTML Agility Pack - https://html-agility-pack.net + var htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(html); + + var tmpImages = htmlDoc.DocumentNode.SelectNodes($"//img[@{TemporaryImageDataAttribute}]"); + if (tmpImages == null || tmpImages.Count == 0) + return html; + + // An array to contain a list of URLs that + // we have already processed to avoid dupes + var uploadedImages = new Dictionary(); + + foreach (var img in tmpImages) + { + // The data attribute contains the path to the tmp img to persist as a media item + var tmpImgPath = img.GetAttributeValue(TemporaryImageDataAttribute, string.Empty); + + if (string.IsNullOrEmpty(tmpImgPath)) + continue; + + var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath); + var fileName = Path.GetFileName(absoluteTempImagePath); + var safeFileName = fileName.ToSafeFileName(); + + var mediaItemName = safeFileName.ToFriendlyName(); + IMedia mediaFile; + GuidUdi udi; + + if (uploadedImages.ContainsKey(tmpImgPath) == false) + { + if (mediaParentFolder == Guid.Empty) + mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId); + else + mediaFile = _mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId); + + var fileInfo = new FileInfo(absoluteTempImagePath); + + var fileStream = fileInfo.OpenReadWithRetry(); + if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream"); + using (fileStream) + { + mediaFile.SetValue(_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream); + } + + _mediaService.Save(mediaFile, userId); + + udi = mediaFile.GetUdi(); + } + else + { + // Already been uploaded & we have it's UDI + udi = uploadedImages[tmpImgPath]; + } + + // Add the UDI to the img element as new data attribute + img.SetAttributeValue("data-udi", udi.ToString()); + + // Get the new persisted image url + var mediaTyped = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(udi.Guid); + if (mediaTyped == null) + throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available."); + + var location = mediaTyped.Url(); + + // Find the width & height attributes as we need to set the imageprocessor QueryString + var width = img.GetAttributeValue("width", int.MinValue); + var height = img.GetAttributeValue("height", int.MinValue); + + if (width != int.MinValue && height != int.MinValue) + { + location = imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(location) { ImageCropMode = "max", Width = width, Height = height }); + } + + img.SetAttributeValue("src", location); + + // Remove the data attribute (so we do not re-process this) + img.Attributes.Remove(TemporaryImageDataAttribute); + + // Add to the dictionary to avoid dupes + if (uploadedImages.ContainsKey(tmpImgPath) == false) + { + uploadedImages.Add(tmpImgPath, udi); + + // Delete folder & image now its saved in media + // The folder should contain one image - as a unique guid folder created + // for each image uploaded from TinyMceController + var folderName = Path.GetDirectoryName(absoluteTempImagePath); + try + { + Directory.Delete(folderName, true); + } + catch (Exception ex) + { + _logger.Error(typeof(HtmlImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); + } + } + } + + return htmlDoc.DocumentNode.OuterHtml; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index c05a983346..42777f11ad 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Logging; -using Umbraco.Core.Macros; using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Examine; @@ -15,21 +17,50 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a rich text property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.TinyMce, "Rich Text Editor", "rte", ValueType = ValueTypes.Text, HideLabel = false, Group="Rich Content", Icon="icon-browser-window")] + [DataEditor( + Constants.PropertyEditors.Aliases.TinyMce, + "Rich Text Editor", + "rte", + ValueType = ValueTypes.Text, + HideLabel = false, + Group = Constants.PropertyEditors.Groups.RichContent, + Icon = "icon-browser-window")] public class RichTextPropertyEditor : DataEditor { + private IUmbracoContextAccessor _umbracoContextAccessor; + private readonly HtmlImageSourceParser _imageSourceParser; + private readonly HtmlLocalLinkParser _localLinkParser; + private readonly RichTextEditorPastedImages _pastedImages; + private readonly IImageUrlGenerator _imageUrlGenerator; + + /// /// The constructor will setup the property editor based on the attribute if one is found /// - public RichTextPropertyEditor(ILogger logger) : base(logger) + [Obsolete("Use the constructor which takes an IImageUrlGenerator")] + public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages) + : this(logger, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, Current.ImageUrlGenerator) { } + /// + /// The constructor will setup the property editor based on the attribute if one is found + /// + public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages, IImageUrlGenerator imageUrlGenerator) + : base(logger) + { + _umbracoContextAccessor = umbracoContextAccessor; + _imageSourceParser = imageSourceParser; + _localLinkParser = localLinkParser; + _pastedImages = pastedImages; + _imageUrlGenerator = imageUrlGenerator; + } + /// /// Create a custom value editor /// /// - protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute); + protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _localLinkParser, _pastedImages, _imageUrlGenerator); protected override IConfigurationEditor CreateConfigurationEditor() => new RichTextConfigurationEditor(); @@ -38,11 +69,23 @@ namespace Umbraco.Web.PropertyEditors /// /// A custom value editor to ensure that macro syntax is parsed when being persisted and formatted correctly for display in the editor /// - internal class RichTextPropertyValueEditor : DataValueEditor + internal class RichTextPropertyValueEditor : DataValueEditor, IDataValueReference { - public RichTextPropertyValueEditor(DataEditorAttribute attribute) + private IUmbracoContextAccessor _umbracoContextAccessor; + private readonly HtmlImageSourceParser _imageSourceParser; + private readonly HtmlLocalLinkParser _localLinkParser; + private readonly RichTextEditorPastedImages _pastedImages; + private readonly IImageUrlGenerator _imageUrlGenerator; + + public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages, IImageUrlGenerator imageUrlGenerator) : base(attribute) - { } + { + _umbracoContextAccessor = umbracoContextAccessor; + _imageSourceParser = imageSourceParser; + _localLinkParser = localLinkParser; + _pastedImages = pastedImages; + _imageUrlGenerator = imageUrlGenerator; + } /// public override object Configuration @@ -67,14 +110,13 @@ namespace Umbraco.Web.PropertyEditors /// /// /// - /// public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { var val = property.GetValue(culture, segment); if (val == null) return null; - var propertyValueWithMediaResolved = TemplateUtilities.ResolveMediaFromTextString(val.ToString()); + var propertyValueWithMediaResolved = _imageSourceParser.EnsureImageSources(val.ToString()); var parsed = MacroTagParser.FormatRichTextPersistedDataForEditor(propertyValueWithMediaResolved, new Dictionary()); return parsed; } @@ -90,9 +132,36 @@ namespace Umbraco.Web.PropertyEditors if (editorValue.Value == null) return null; - var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValue.Value.ToString()); + var userId = _umbracoContextAccessor.UmbracoContext?.Security?.CurrentUser?.Id ?? Constants.Security.SuperUserId; + + var config = editorValue.DataTypeConfiguration as RichTextConfiguration; + var mediaParent = config?.MediaParentId; + var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid; + + var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId, _imageUrlGenerator); + var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); + var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); + return parsed; } + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) + yield return new UmbracoEntityReference(udi); + + foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) + yield return new UmbracoEntityReference(udi); + + //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs + } } internal class RichTextPropertyIndexValueFactory : IPropertyIndexValueFactory diff --git a/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs index c4a966cf49..8a98f15081 100644 --- a/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs @@ -7,7 +7,11 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a slider editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.Slider, "Slider", "slider", Icon = "icon-navigation-horizontal")] + [DataEditor( + Constants.PropertyEditors.Aliases.Slider, + "Slider", + "slider", + Icon = "icon-navigation-horizontal")] public class SliderPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index 90527a8b8d..fd7e8694a3 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Newtonsoft.Json.Linq; @@ -14,7 +15,11 @@ namespace Umbraco.Web.PropertyEditors /// Represents a tags property editor. /// [TagsPropertyEditor] - [DataEditor(Constants.PropertyEditors.Aliases.Tags, "Tags", "tags", Icon="icon-tags")] + [DataEditor( + Constants.PropertyEditors.Aliases.Tags, + "Tags", + "tags", + Icon = "icon-tags")] public class TagsPropertyEditor : DataEditor { private readonly ManifestValueValidatorCollection _validators; @@ -38,9 +43,24 @@ namespace Umbraco.Web.PropertyEditors /// public override object FromEditor(ContentPropertyData editorValue, object currentValue) { - return editorValue.Value is JArray json - ? json.Select(x => x.Value()) - : null; + var value = editorValue?.Value?.ToString(); + + if (string.IsNullOrEmpty(value)) + { + return null; + } + + if (editorValue.Value is JArray json) + { + return json.Select(x => x.Value()); + } + + if (string.IsNullOrWhiteSpace(value) == false) + { + return value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + } + + return null; } /// diff --git a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs index f9ba99fd1e..c7bc2efbda 100644 --- a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs @@ -7,7 +7,13 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a textarea property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.TextArea, EditorType.PropertyValue | EditorType.MacroParameter, "Textarea", "textarea", ValueType = ValueTypes.Text, Icon="icon-application-window-alt")] + [DataEditor( + Constants.PropertyEditors.Aliases.TextArea, + EditorType.PropertyValue | EditorType.MacroParameter, + "Textarea", + "textarea", + ValueType = ValueTypes.Text, + Icon = "icon-application-window-alt")] public class TextAreaPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs index 4862f6c9aa..18f8155198 100644 --- a/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs @@ -7,7 +7,12 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a textbox property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.TextBox, EditorType.PropertyValue | EditorType.MacroParameter, "Textbox", "textbox", Group = "Common")] + [DataEditor( + Constants.PropertyEditors.Aliases.TextBox, + EditorType.PropertyValue | EditorType.MacroParameter, + "Textbox", + "textbox", + Group = Constants.PropertyEditors.Groups.Common)] public class TextboxPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs index ccd3bee744..fb2d2b29fe 100644 --- a/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs @@ -7,7 +7,14 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a checkbox property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.Boolean, EditorType.PropertyValue | EditorType.MacroParameter, "Checkbox", "boolean", ValueType = ValueTypes.Integer, Group = "Common", Icon="icon-checkbox")] + [DataEditor( + Constants.PropertyEditors.Aliases.Boolean, + EditorType.PropertyValue | EditorType.MacroParameter, + "Checkbox", + "boolean", + ValueType = ValueTypes.Integer, + Group = Constants.PropertyEditors.Groups.Common, + Icon = "icon-checkbox")] public class TrueFalsePropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs index 5394aca5ba..8d7bd29889 100644 --- a/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs +++ b/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; +using System.Linq; using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Services; -using Umbraco.Core.Configuration; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Web.Composing; @@ -16,13 +16,27 @@ namespace Umbraco.Web.PropertyEditors { public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration) { - if (!(value is JObject jobject) || jobject["selectedFiles"] == null) yield break; - - var fileNames = jobject["selectedFiles"].ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - foreach (var fileName in fileNames) + string selectedFiles = null; + if (value is JObject jobject && jobject["selectedFiles"] is JToken jToken) { - if (ValidateFileExtension(fileName) == false) + selectedFiles = jToken.ToString(); + } + else if (valueType?.InvariantEquals(ValueTypes.String) == true) + { + selectedFiles = value as string; + + if (string.IsNullOrWhiteSpace(selectedFiles)) + yield break; + } + + var fileNames = selectedFiles?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + if (fileNames == null || !fileNames.Any()) + yield break; + + foreach (string filename in fileNames) + { + if (IsValidFileExtension(filename) == false) { //we only store a single value for this editor so the 'member' or 'field' // we'll associate this error with will simply be called 'value' @@ -30,11 +44,11 @@ namespace Umbraco.Web.PropertyEditors } } } - - internal static bool ValidateFileExtension(string fileName) + + internal static bool IsValidFileExtension(string fileName) { if (fileName.IndexOf('.') <= 0) return false; - var extension = Path.GetExtension(fileName).TrimStart("."); + var extension = fileName.GetFileExtension().TrimStart("."); return Current.Configs.Settings().Content.IsFileAllowedForUpload(extension); } } diff --git a/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs index 8cb8b64594..daf574719a 100644 --- a/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs @@ -6,7 +6,13 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.UserPicker, "User picker", "entitypicker", ValueType = ValueTypes.Integer, Group = "People", Icon = "icon-user")] + [DataEditor( + Constants.PropertyEditors.Aliases.UserPicker, + "User picker", + "entitypicker", + ValueType = ValueTypes.Integer, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.User)] public class UserPickerPropertyEditor : DataEditor { public UserPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs index d30762f13f..8b3655f0cc 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs @@ -23,16 +23,16 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _publishedSnapshotAccessor = publishedSnapshotAccessor; } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.ContentPicker); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IPublishedContent); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; @@ -45,7 +45,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return null; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { if (inter == null) return null; @@ -65,7 +65,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (udi == null) return null; content = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(udi.Guid); - if (content != null && content.ItemType == PublishedItemType.Content) + if (content != null && content.ContentType.ItemType == PublishedItemType.Content) return content; } } @@ -73,7 +73,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return inter; } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { if (inter == null) return null; return inter.ToString(); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs index 43add34327..0d6089f0f4 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs @@ -11,12 +11,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class FlexibleDropdownPropertyValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) { return propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.DropDownListFlexible); } - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if(source == null) return Array.Empty(); @@ -24,7 +24,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return JsonConvert.DeserializeObject(source.ToString()) ?? Array.Empty(); } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { if (inter == null) return null; @@ -43,7 +43,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters : string.Empty; } - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { return propertyType.DataType.ConfigurationAs().Multiple ? typeof(IEnumerable) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs deleted file mode 100644 index e6e66b79ff..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Web; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Macros; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using Umbraco.Web.Macros; - -namespace Umbraco.Web.PropertyEditors.ValueConverters -{ - /// - /// Ensures macro syntax is parsed for the macro container which will work when getting the field - /// values in any way (i.e. dynamically, using Field(), or IPublishedContent) - /// - [DefaultPropertyValueConverter] - public class MacroContainerValueConverter : PropertyValueConverterBase - { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IMacroRenderer _macroRenderer; - - public MacroContainerValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer) - { - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _macroRenderer = macroRenderer ?? throw new ArgumentNullException(nameof(macroRenderer)); - } - - public override bool IsConverter(PublishedPropertyType propertyType) - => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.MacroContainer; - - public override Type GetPropertyValueType(PublishedPropertyType propertyType) - => typeof (IHtmlString); - - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; - - // NOT thread-safe over a request because it modifies the - // global UmbracoContext.Current.InPreviewMode status. So it - // should never execute in // over the same UmbracoContext with - // different preview modes. - string RenderMacros(string source, bool preview) - { - var umbracoContext = _umbracoContextAccessor.UmbracoContext; - using (umbracoContext.ForcedPreview(preview)) // force for macro rendering - { - var sb = new StringBuilder(); - - MacroTagParser.ParseMacros( - source, - //callback for when text block is found - textBlock => sb.Append(textBlock), - //callback for when macro syntax is found - (macroAlias, macroAttributes) => sb.Append(_macroRenderer.Render( - macroAlias, - umbracoContext.PublishedRequest?.PublishedContent, - //needs to be explicitly casted to Dictionary - macroAttributes.ConvertTo(x => (string)x, x => x)).ToString())); - - return sb.ToString(); - } - } - - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) - { - if (source == null) return null; - var sourceString = source.ToString(); - - // ensure string is parsed for macros and macros are executed correctly - sourceString = RenderMacros(sourceString, preview); - - return new HtmlString(sourceString); - } - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index cb9709157f..5c027ed9b6 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -12,28 +12,43 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MarkdownEditorValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + private readonly HtmlLocalLinkParser _localLinkParser; + private readonly HtmlUrlParser _urlParser; + + [Obsolete("Use ctor defining all dependencies instead")] + public MarkdownEditorValueConverter() + : this(Current.Factory.GetInstance(), Current.Factory.GetInstance()) + { + } + + public MarkdownEditorValueConverter(HtmlLocalLinkParser localLinkParser, HtmlUrlParser urlParser) + { + _localLinkParser = localLinkParser; + _urlParser = urlParser; + } + + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MarkdownEditor == propertyType.EditorAlias; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) - => typeof (IHtmlString); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IHtmlString); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; var sourceString = source.ToString(); // ensures string is parsed for {localLink} and urls are resolved correctly - sourceString = TemplateUtilities.ParseInternalLinks(sourceString, preview, Current.UmbracoContext); - sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString); + sourceString = _localLinkParser.EnsureInternalLinks(sourceString, preview); + sourceString = _urlParser.EnsureUrls(sourceString); return sourceString; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // convert markup to HTML for frontend rendering. // source should come from ConvertSource and be a string (or null) already diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs index 0218867bb4..a56630d7c5 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs @@ -29,26 +29,20 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _publishedModelFactory = publishedModelFactory; } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) { return propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MediaPicker); } - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var isMultiple = IsMultipleDataType(propertyType.DataType); - var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType); - return isMultiple - ? isOnlyImages - ? typeof(IEnumerable<>).MakeGenericType(ModelType.For(ImageTypeAlias)) - : typeof(IEnumerable) - : isOnlyImages - ? ModelType.For(ImageTypeAlias) + ? typeof(IEnumerable) : typeof(IPublishedContent); } - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; private bool IsMultipleDataType(PublishedDataType dataType) @@ -57,13 +51,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return config.Multiple; } - private bool IsOnlyImagesDataType(PublishedDataType dataType) - { - var config = ConfigurationEditor.ConfigurationAs(dataType.Configuration); - return config.OnlyImages; - } - - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; @@ -75,16 +63,13 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return nodeIds; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { var isMultiple = IsMultipleDataType(propertyType.DataType); - var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType); var udis = (Udi[]) source; - var mediaItems = isOnlyImages - ? _publishedModelFactory.CreateModelList(ImageTypeAlias) - : new List(); + var mediaItems = new List(); if (source == null) return isMultiple ? mediaItems : null; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs index b4c7f99a75..cd69fd9de6 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs @@ -18,18 +18,18 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) { return propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberPicker); } - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IPublishedContent); - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { var attemptConvertInt = source.TryConvertTo(); if (attemptConvertInt.Success) @@ -40,7 +40,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return null; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { if (source == null) return null; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs index a210aa62c6..47f8797295 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs @@ -34,18 +34,20 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) { return propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker); } - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) - => typeof (IEnumerable); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => IsSingleNodePicker(propertyType) + ? typeof(IPublishedContent) + : typeof(IEnumerable); - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; @@ -60,7 +62,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return null; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { if (source == null) { @@ -73,6 +75,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) { var udis = (Udi[])source; + var isSingleNodePicker = IsSingleNodePicker(propertyType); if ((propertyType.Alias != null && PropertiesToExclude.InvariantContains(propertyType.Alias)) == false) { @@ -99,12 +102,20 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters break; } - if (multiNodeTreePickerItem != null && multiNodeTreePickerItem.ItemType != PublishedItemType.Element) + if (multiNodeTreePickerItem != null && multiNodeTreePickerItem.ContentType.ItemType != PublishedItemType.Element) { multiNodeTreePicker.Add(multiNodeTreePickerItem); + if (isSingleNodePicker) + { + break; + } } } + if (isSingleNodePicker) + { + return multiNodeTreePicker.FirstOrDefault(); + } return multiNodeTreePicker; } @@ -141,5 +152,10 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } return content; } + + private static bool IsSingleNodePicker(IPublishedPropertyType propertyType) + { + return propertyType.DataType.ConfigurationAs().MaxNumber == 1; + } } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs index 48cec2a3d2..2ceac6cfa8 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs @@ -23,20 +23,20 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _proflog = proflog ?? throw new ArgumentNullException(nameof(proflog)); } - public override bool IsConverter(PublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MultiUrlPicker.Equals(propertyType.EditorAlias); + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MultiUrlPicker.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) => + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => propertyType.DataType.ConfigurationAs().MaxNumber == 1 ? typeof(Link) : typeof(IEnumerable); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; public override bool? IsValue(object value, PropertyValueLevel level) => value?.ToString() != "[]"; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) => source?.ToString(); + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source?.ToString(); - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { using (_proflog.DebugDuration($"ConvertPropertyToLinks ({propertyType.DataType.Id})")) { @@ -65,11 +65,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(preview, dto.Udi.Guid) : _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(preview, dto.Udi.Guid); - if (content == null || content.ItemType == PublishedItemType.Element) + if (content == null || content.ContentType.ItemType == PublishedItemType.Element) { continue; } - url = content.Url; + url = content.Url(); } links.Add( diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs index 2181c4fd49..4a25049695 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json.Linq; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Web.PublishedCache; namespace Umbraco.Web.PropertyEditors.ValueConverters @@ -14,6 +15,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// /// Provides an implementation for for nested content. /// + [DefaultPropertyValueConverter(typeof(JsonValueConverter))] public class NestedContentManyValueConverter : NestedContentValueConverterBase { private readonly IProfilingLogger _proflog; @@ -28,11 +30,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } /// - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => IsNestedMany(propertyType); /// - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var contentTypes = propertyType.DataType.ConfigurationAs().ContentTypes; return contentTypes.Length == 1 @@ -41,19 +43,19 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } /// - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; /// - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { return source?.ToString(); } /// - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { - using (_proflog.DebugDuration($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})")) + using (_proflog.DebugDuration($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})")) { var configuration = propertyType.DataType.ConfigurationAs(); var contentTypes = configuration.ContentTypes; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs index e084b3a343..c9c99615f6 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json.Linq; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Web.PublishedCache; namespace Umbraco.Web.PropertyEditors.ValueConverters @@ -13,6 +14,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// /// Provides an implementation for for nested content. /// + [DefaultPropertyValueConverter(typeof(JsonValueConverter))] public class NestedContentSingleValueConverter : NestedContentValueConverterBase { private readonly IProfilingLogger _proflog; @@ -27,11 +29,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } /// - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => IsNestedSingle(propertyType); /// - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var contentTypes = propertyType.DataType.ConfigurationAs().ContentTypes; return contentTypes.Length > 1 @@ -40,19 +42,19 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } /// - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; /// - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { return source?.ToString(); } /// - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { - using (_proflog.DebugDuration($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})")) + using (_proflog.DebugDuration($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})")) { var value = (string) inter; if (string.IsNullOrWhiteSpace(value)) return null; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs index e3723e2221..7c18d8ebca 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs @@ -20,12 +20,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters protected IPublishedModelFactory PublishedModelFactory { get; } - public static bool IsNested(PublishedPropertyType publishedProperty) + public static bool IsNested(IPublishedPropertyType publishedProperty) { return publishedProperty.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent); } - public static bool IsNestedSingle(PublishedPropertyType publishedProperty) + public static bool IsNestedSingle(IPublishedPropertyType publishedProperty) { if (!IsNested(publishedProperty)) return false; @@ -34,7 +34,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return config.MinItems == 1 && config.MaxItems == 1; } - public static bool IsNestedMany(PublishedPropertyType publishedProperty) + public static bool IsNestedMany(IPublishedPropertyType publishedProperty) { return IsNested(publishedProperty) && !IsNestedSingle(publishedProperty); } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index fbbf058c49..3ab502742c 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web.Macros; +using System.Web; namespace Umbraco.Web.PropertyEditors.ValueConverters { @@ -23,18 +24,25 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IMacroRenderer _macroRenderer; + private readonly HtmlLocalLinkParser _linkParser; + private readonly HtmlUrlParser _urlParser; + private readonly HtmlImageSourceParser _imageSourceParser; - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) { // because that version of RTE converter parses {locallink} and executes macros, its value has // to be cached at the published snapshot level, because we have no idea what the macros may depend on actually. return PropertyCacheLevel.Snapshot; } - public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer) + public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, + HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; + _linkParser = linkParser; + _urlParser = urlParser; + _imageSourceParser = imageSourceParser; } // NOT thread-safe over a request because it modifies the @@ -63,7 +71,14 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } } - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + { + var converted = Convert(inter, preview); + + return new HtmlString(converted == null ? string.Empty : converted); + } + + private string Convert(object source, bool preview) { if (source == null) { @@ -73,9 +88,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var sourceString = source.ToString(); // ensures string is parsed for {localLink} and urls and media are resolved correctly - sourceString = TemplateUtilities.ParseInternalLinks(sourceString, preview, Current.UmbracoContext); - sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString); - sourceString = TemplateUtilities.ResolveMediaFromTextString(sourceString); + sourceString = _linkParser.EnsureInternalLinks(sourceString, preview); + sourceString = _urlParser.EnsureUrls(sourceString); + sourceString = _imageSourceParser.EnsureImageSources(sourceString); // ensure string is parsed for macros and macros are executed correctly sourceString = RenderRteMacros(sourceString, preview); @@ -89,32 +104,35 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters // Find all images with rel attribute var imgNodes = doc.DocumentNode.SelectNodes("//img[@rel]"); + var modified = false; if (imgNodes != null) { - var modified = false; - foreach (var img in imgNodes) { - var firstOrDefault = img.Attributes.FirstOrDefault(x => x.Name == "rel"); - if (firstOrDefault != null) + var nodeId = img.GetAttributeValue("rel", string.Empty); + if (int.TryParse(nodeId, out _)) { - var rel = firstOrDefault.Value; - - // Check that the rel attribute is a integer before removing - int nodeId; - if (int.TryParse(rel, out nodeId)) - { - img.Attributes.Remove("rel"); - modified = true; - } + img.Attributes.Remove("rel"); + modified = true; } } + } - if (modified) + // Find all a and img tags with a data-udi attribute + var dataUdiNodes = doc.DocumentNode.SelectNodes("(//a|//img)[@data-udi]"); + if (dataUdiNodes != null) + { + foreach (var node in dataUdiNodes) { - return doc.DocumentNode.OuterHtml; + node.Attributes.Remove("data-udi"); + modified = true; } } + + if (modified) + { + return doc.DocumentNode.OuterHtml; + } } return sourceString; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs index 2d5c322f58..939a658407 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -11,40 +11,48 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class TextStringValueConverter : PropertyValueConverterBase { + public TextStringValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser) + { + _linkParser = linkParser; + _urlParser = urlParser; + } + private static readonly string[] PropertyTypeAliases = { Constants.PropertyEditors.Aliases.TextBox, Constants.PropertyEditors.Aliases.TextArea }; + private readonly HtmlLocalLinkParser _linkParser; + private readonly HtmlUrlParser _urlParser; - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => PropertyTypeAliases.Contains(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; var sourceString = source.ToString(); // ensures string is parsed for {localLink} and urls are resolved correctly - sourceString = TemplateUtilities.ParseInternalLinks(sourceString, preview, Current.UmbracoContext); - sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString); + sourceString = _linkParser.EnsureInternalLinks(sourceString, preview); + sourceString = _urlParser.EnsureUrls(sourceString); return sourceString; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter ?? string.Empty; } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter; diff --git a/src/Umbraco.Web/PublishedCache/IDomainCache.cs b/src/Umbraco.Web/PublishedCache/IDomainCache.cs index dbee8908a0..3ec84c9d48 100644 --- a/src/Umbraco.Web/PublishedCache/IDomainCache.cs +++ b/src/Umbraco.Web/PublishedCache/IDomainCache.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Routing; namespace Umbraco.Web.PublishedCache @@ -6,20 +8,29 @@ namespace Umbraco.Web.PublishedCache public interface IDomainCache { /// - /// Returns all in the current domain cache including any domains that may be referenced by content items that are no longer published + /// Gets all in the current domain cache, including any domains that may be referenced by documents that are no longer published. /// /// /// IEnumerable GetAll(bool includeWildcards); /// - /// Returns all assigned for the content id specified even if the content item is not published + /// Gets all assigned for specified document, even if it is not published. /// - /// - /// - /// - IEnumerable GetAssigned(int contentId, bool includeWildcards); + /// The document identifier. + /// A value indicating whether to consider wildcard domains. + IEnumerable GetAssigned(int documentId, bool includeWildcards = false); + /// + /// Determines whether a document has domains. + /// + /// The document identifier. + /// A value indicating whether to consider wildcard domains. + bool HasAssigned(int documentId, bool includeWildcards = false); + + /// + /// Gets the system default culture. + /// string DefaultCulture { get; } } } diff --git a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs index 3cd7b924fb..0370088f77 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs @@ -84,16 +84,18 @@ namespace Umbraco.Web.PublishedCache /// Gets contents at root. /// /// A value indicating whether to consider unpublished content. + /// A culture. /// The contents. /// The value of overrides defaults. - IEnumerable GetAtRoot(bool preview); + IEnumerable GetAtRoot(bool preview, string culture = null); /// /// Gets contents at root. /// + /// A culture. /// The contents. /// Considers published or unpublished content depending on defaults. - IEnumerable GetAtRoot(); + IEnumerable GetAtRoot(string culture = null); /// /// Gets a content resulting from an XPath query. @@ -217,7 +219,7 @@ namespace Umbraco.Web.PublishedCache /// /// The content type unique identifier. /// The content type, or null. - PublishedContentType GetContentType(int id); + IPublishedContentType GetContentType(int id); /// /// Gets a content type identified by its alias. @@ -225,13 +227,13 @@ namespace Umbraco.Web.PublishedCache /// The content type alias. /// The content type, or null. /// The alias is case-insensitive. - PublishedContentType GetContentType(string alias); + IPublishedContentType GetContentType(string alias); /// /// Gets contents of a given content type. /// /// The content type. /// The contents. - IEnumerable GetByContentType(PublishedContentType contentType); + IEnumerable GetByContentType(IPublishedContentType contentType); } } diff --git a/src/Umbraco.Web/PublishedCache/IPublishedMemberCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedMemberCache.cs index 53d37a8d31..0ea812db83 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedMemberCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedMemberCache.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.PublishedCache /// /// The content type unique identifier. /// The content type, or null. - PublishedContentType GetContentType(int id); + IPublishedContentType GetContentType(int id); /// /// Gets a content type identified by its alias. @@ -30,6 +30,6 @@ namespace Umbraco.Web.PublishedCache /// The content type alias. /// The content type, or null. /// The alias is case-insensitive. - PublishedContentType GetContentType(string alias); + IPublishedContentType GetContentType(string alias); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs index 4bd3fcf247..24c6a7018b 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -1,20 +1,15 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.XPath; using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; using Umbraco.Core.Xml; using Umbraco.Core.Xml.XPath; using Umbraco.Web.PublishedCache.NuCache.Navigable; -using Umbraco.Web.Routing; namespace Umbraco.Web.PublishedCache.NuCache { @@ -23,9 +18,9 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly ContentStore.Snapshot _snapshot; private readonly IAppCache _snapshotCache; private readonly IAppCache _elementsCache; - private readonly DomainHelper _domainHelper; + private readonly IDomainCache _domainCache; private readonly IGlobalSettings _globalSettings; - private readonly ILocalizationService _localizationService; + private readonly IVariationContextAccessor _variationContextAccessor; #region Constructor @@ -34,15 +29,15 @@ namespace Umbraco.Web.PublishedCache.NuCache // it's too late for UmbracoContext which has captured previewDefault and stuff into these ctor vars // but, no, UmbracoContext returns snapshot.Content which comes from elements SO a resync should create a new cache - public ContentCache(bool previewDefault, ContentStore.Snapshot snapshot, IAppCache snapshotCache, IAppCache elementsCache, DomainHelper domainHelper, IGlobalSettings globalSettings, ILocalizationService localizationService) + public ContentCache(bool previewDefault, ContentStore.Snapshot snapshot, IAppCache snapshotCache, IAppCache elementsCache, IDomainCache domainCache, IGlobalSettings globalSettings, IVariationContextAccessor variationContextAccessor) : base(previewDefault) { _snapshot = snapshot; _snapshotCache = snapshotCache; _elementsCache = elementsCache; - _domainHelper = domainHelper; + _domainCache = domainCache; _globalSettings = globalSettings; - _localizationService = localizationService; + _variationContextAccessor = variationContextAccessor; } private bool HideTopLevelNodeFromPath => _globalSettings.HideTopLevelNodeFromPath; @@ -109,8 +104,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // hideTopLevelNode = support legacy stuff, look for /*/path/to/node // else normal, look for /path/to/node content = hideTopLevelNode.Value - ? GetAtRoot(preview).SelectMany(x => x.Children).FirstOrDefault(x => x.GetUrlSegment(culture) == parts[0]) - : GetAtRoot(preview).FirstOrDefault(x => x.GetUrlSegment(culture) == parts[0]); + ? GetAtRoot(preview).SelectMany(x => x.Children(culture)).FirstOrDefault(x => x.UrlSegment(culture) == parts[0]) + : GetAtRoot(preview).FirstOrDefault(x => x.UrlSegment(culture) == parts[0]); content = FollowRoute(content, parts, 1, culture); } @@ -119,7 +114,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // have to look for /foo (see note in ApplyHideTopLevelNodeFromPath). if (content == null && hideTopLevelNode.Value && parts.Length == 1) { - content = GetAtRoot(preview).FirstOrDefault(x => x.GetUrlSegment(culture) == parts[0]); + content = GetAtRoot(preview).FirstOrDefault(x => x.UrlSegment(culture) == parts[0]); } return content; @@ -149,8 +144,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // or we reach the content root, collecting urls in the way var pathParts = new List(); var n = node; - var urlSegment = n.GetUrlSegment(culture); - var hasDomains = _domainHelper.NodeHasDomains(n.Id); + var urlSegment = n.UrlSegment(culture); + var hasDomains = _domainCache.HasAssigned(n.Id); while (hasDomains == false && n != null) // n is null at root { // no segment indicates this is not published when this is a variant @@ -161,9 +156,9 @@ namespace Umbraco.Web.PublishedCache.NuCache // move to parent node n = n.Parent; if (n != null) - urlSegment = n.GetUrlSegment(culture); + urlSegment = n.UrlSegment(culture); - hasDomains = n != null && _domainHelper.NodeHasDomains(n.Id); + hasDomains = n != null && _domainCache.HasAssigned(n.Id); } // at this point this will be the urlSegment of the root, no segment indicates this is not published when this is a variant @@ -189,9 +184,9 @@ namespace Umbraco.Web.PublishedCache.NuCache while (content != null && i < parts.Count) { var part = parts[i++]; - content = content.Children.FirstOrDefault(x => + content = content.Children(culture).FirstOrDefault(x => { - var urlSegment = x.GetUrlSegment(culture); + var urlSegment = x.UrlSegment(culture); return urlSegment == part; }); } @@ -243,7 +238,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var guidUdi = contentId as GuidUdi; if (guidUdi == null) throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); - + if (guidUdi.EntityType != Constants.UdiEntityType.Document) throw new ArgumentException($"Udi entity type must be \"{Constants.UdiEntityType.Document}\".", nameof(contentId)); @@ -258,30 +253,30 @@ namespace Umbraco.Web.PublishedCache.NuCache return preview || n.PublishedModel != null; } - public override IEnumerable GetAtRoot(bool preview) + IEnumerable INavigableData.GetAtRoot(bool preview) => GetAtRoot(preview); + + public override IEnumerable GetAtRoot(bool preview, string culture = null) { - if (PublishedSnapshotService.CacheContentCacheRoots == false) - return GetAtRootNoCache(preview); - - var cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing - ? _elementsCache - : _snapshotCache; - - if (cache == null) - return GetAtRootNoCache(preview); - - // note: ToArray is important here, we want to cache the result, not the function! - return (IEnumerable)cache.Get( - CacheKeys.ContentCacheRoots(preview), - () => GetAtRootNoCache(preview).ToArray()); - } - - private IEnumerable GetAtRootNoCache(bool preview) - { - var c = _snapshot.GetAtRoot(); + // handle context culture for variant + if (culture == null) + culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; + // _snapshot.GetAtRoot() returns all ContentNode at root // both .Draft and .Published cannot be null at the same time - return c.Select(n => GetNodePublishedContent(n, preview)).WhereNotNull().OrderBy(x => x.SortOrder); + // root is already sorted by sortOrder, and does not contain nulls + // + // GetNodePublishedContent may return null if !preview and there is no + // published model, so we need to filter these nulls out + + var atRoot = _snapshot.GetAtRoot() + .Select(n => GetNodePublishedContent(n, preview)) + .WhereNotNull(); + + // if a culture is specified, we must ensure that it is avail/published + if (culture != "*") + atRoot = atRoot.Where(x => x.IsInvariantOrHasCulture(culture)); + + return atRoot; } private static IPublishedContent GetNodePublishedContent(ContentNode node, bool preview) @@ -360,6 +355,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private static IEnumerable GetByXPath(XPathNodeIterator iterator) { + iterator = iterator.Clone(); while (iterator.MoveNext()) { var xnav = iterator.Current as NavigableNavigator; @@ -388,12 +384,12 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public override PublishedContentType GetContentType(int id) + public override IPublishedContentType GetContentType(int id) { return _snapshot.GetContentType(id); } - public override PublishedContentType GetContentType(string alias) + public override IPublishedContentType GetContentType(string alias) { return _snapshot.GetContentType(alias); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs index db7aa0d5d1..a724e78b72 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Diagnostics; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -7,13 +7,24 @@ namespace Umbraco.Web.PublishedCache.NuCache { // represents a content "node" ie a pair of draft + published versions // internal, never exposed, to be accessed from ContentStore (only!) + [DebuggerDisplay("Id: {Id}, Path: {Path}")] internal class ContentNode { + // special ctor for root pseudo node + public ContentNode() + { + FirstChildContentId = -1; + LastChildContentId = -1; + NextSiblingContentId = -1; + PreviousSiblingContentId = -1; + } + // special ctor with no content data - for members - public ContentNode(int id, Guid uid, PublishedContentType contentType, + public ContentNode(int id, Guid uid, IPublishedContentType contentType, int level, string path, int sortOrder, int parentContentId, DateTime createDate, int creatorId) + : this() { Id = id; Uid = uid; @@ -24,21 +35,18 @@ namespace Umbraco.Web.PublishedCache.NuCache ParentContentId = parentContentId; CreateDate = createDate; CreatorId = creatorId; - - ChildContentIds = new List(); } - public ContentNode(int id, Guid uid, PublishedContentType contentType, + public ContentNode(int id, Guid uid, IPublishedContentType contentType, int level, string path, int sortOrder, int parentContentId, DateTime createDate, int creatorId, ContentData draftData, ContentData publishedData, IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + IVariationContextAccessor variationContextAccessor) : this(id, uid, level, path, sortOrder, parentContentId, createDate, creatorId) { - SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor); + SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor, variationContextAccessor); } // 2-phases ctor, phase 1 @@ -53,125 +61,118 @@ namespace Umbraco.Web.PublishedCache.NuCache Path = path; SortOrder = sortOrder; ParentContentId = parentContentId; + FirstChildContentId = -1; + LastChildContentId = -1; + NextSiblingContentId = -1; + PreviousSiblingContentId = -1; CreateDate = createDate; CreatorId = creatorId; - - ChildContentIds = new List(); } // two-phase ctor, phase 2 - public void SetContentTypeAndData(PublishedContentType contentType, ContentData draftData, ContentData publishedData, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IUmbracoContextAccessor umbracoContextAccessor) + public void SetContentTypeAndData(IPublishedContentType contentType, ContentData draftData, ContentData publishedData, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { ContentType = contentType; if (draftData == null && publishedData == null) throw new ArgumentException("Both draftData and publishedData cannot be null at the same time."); - if (draftData != null) - { - DraftContent = new PublishedContent(this, draftData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor); - DraftModel = DraftContent.CreateModel(); - } + _publishedSnapshotAccessor = publishedSnapshotAccessor; + _variationContextAccessor = variationContextAccessor; - if (publishedData != null) - { - PublishedContent = new PublishedContent(this, publishedData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor); - PublishedModel = PublishedContent.CreateModel(); - } + _draftData = draftData; + _publishedData = publishedData; } - // clone parent - private ContentNode(ContentNode origin, IUmbracoContextAccessor umbracoContextAccessor) + // clone + public ContentNode(ContentNode origin, IPublishedContentType contentType = null) { - // everything is the same, except for the child items - // list which is a clone of the original list - Id = origin.Id; Uid = origin.Uid; - ContentType = origin.ContentType; + ContentType = contentType ?? origin.ContentType; Level = origin.Level; Path = origin.Path; SortOrder = origin.SortOrder; ParentContentId = origin.ParentContentId; + FirstChildContentId = origin.FirstChildContentId; + LastChildContentId = origin.LastChildContentId; + NextSiblingContentId = origin.NextSiblingContentId; + PreviousSiblingContentId = origin.PreviousSiblingContentId; CreateDate = origin.CreateDate; CreatorId = origin.CreatorId; - var originDraft = origin.DraftContent; - var originPublished = origin.PublishedContent; - - - DraftContent = originDraft == null ? null : new PublishedContent(this, originDraft, umbracoContextAccessor); - PublishedContent = originPublished == null ? null : new PublishedContent(this, originPublished, umbracoContextAccessor); - DraftModel = DraftContent?.CreateModel(); - PublishedModel = PublishedContent?.CreateModel(); - - ChildContentIds = new List(origin.ChildContentIds); // needs to be *another* list - } - - // clone with new content type - public ContentNode(ContentNode origin, PublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IUmbracoContextAccessor umbracoContextAccessor) - { - Id = origin.Id; - Uid = origin.Uid; - ContentType = contentType; // change! - Level = origin.Level; - Path = origin.Path; - SortOrder = origin.SortOrder; - ParentContentId = origin.ParentContentId; - CreateDate = origin.CreateDate; - CreatorId = origin.CreatorId; - - var originDraft = origin.DraftContent; - var originPublished = origin.PublishedContent; - - DraftContent = originDraft == null ? null : new PublishedContent(this, originDraft.ContentData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor); - DraftModel = DraftContent?.CreateModel(); - PublishedContent = originPublished == null ? null : new PublishedContent(this, originPublished.ContentData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor); - PublishedModel = PublishedContent?.CreateModel(); - - ChildContentIds = origin.ChildContentIds; // can be the *same* list + _draftData = origin._draftData; + _publishedData = origin._publishedData; + _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; + _variationContextAccessor = origin._variationContextAccessor; } // everything that is common to both draft and published versions // keep this as small as possible + + public readonly int Id; public readonly Guid Uid; - public PublishedContentType ContentType; + public IPublishedContentType ContentType; public readonly int Level; public readonly string Path; public readonly int SortOrder; public readonly int ParentContentId; - public List ChildContentIds; + + // TODO: Can we make everything readonly?? This would make it easier to debug and be less error prone especially for new developers. + // Once a Node is created and exists in the cache it is readonly so we should be able to make that happen at the API level too. + public int FirstChildContentId; + public int LastChildContentId; + public int NextSiblingContentId; + public int PreviousSiblingContentId; + public readonly DateTime CreateDate; public readonly int CreatorId; - // draft and published version (either can be null, but not both) - // are the direct PublishedContent instances - public PublishedContent DraftContent; - public PublishedContent PublishedContent; + private ContentData _draftData; + private ContentData _publishedData; + private IVariationContextAccessor _variationContextAccessor; + private IPublishedSnapshotAccessor _publishedSnapshotAccessor; + + public bool HasPublished => _publishedData != null; + public bool HasPublishedCulture(string culture) => _publishedData != null && _publishedData.CultureInfos.ContainsKey(culture); // draft and published version (either can be null, but not both) // are models not direct PublishedContent instances - public IPublishedContent DraftModel; - public IPublishedContent PublishedModel; + private IPublishedContent _draftModel; + private IPublishedContent _publishedModel; - public ContentNode CloneParent( - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + private IPublishedContent GetModel(ref IPublishedContent model, ContentData contentData) { - return new ContentNode(this, umbracoContextAccessor); + if (model != null) return model; + if (contentData == null) return null; + + // create the model - we want to be fast, so no lock here: we may create + // more than 1 instance, but the lock below ensures we only ever return + // 1 unique instance - and locking is a nice explicit way to ensure this + + var m = new PublishedContent(this, contentData, _publishedSnapshotAccessor, _variationContextAccessor).CreateModel(); + + // locking 'this' is not a best-practice but ContentNode is internal and + // we know what we do, so it is fine here and avoids allocating an object + lock (this) + { + return model = model ?? m; + } } + public IPublishedContent DraftModel => GetModel(ref _draftModel, _draftData); + + public IPublishedContent PublishedModel => GetModel(ref _publishedModel, _publishedData); + public ContentNodeKit ToKit() - { - return new ContentNodeKit - { - Node = this, - ContentTypeId = ContentType.Id, + => new ContentNodeKit + { + Node = this, + ContentTypeId = ContentType.Id, - DraftData = DraftContent?.ContentData, - PublishedData = PublishedContent?.ContentData - }; - } + DraftData = _draftData, + PublishedData = _publishedData + }; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs index 753ba5cc94..61fb5c12a3 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs @@ -18,11 +18,10 @@ namespace Umbraco.Web.PublishedCache.NuCache public static ContentNodeKit Null { get; } = new ContentNodeKit { ContentTypeId = -1 }; public void Build( - PublishedContentType contentType, + IPublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, - bool canBePublished, - IUmbracoContextAccessor umbracoContextAccessor) + bool canBePublished) { var draftData = DraftData; @@ -35,7 +34,16 @@ namespace Umbraco.Web.PublishedCache.NuCache if (draftData == null && !canBePublished) draftData = PublishedData; - Node.SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor, variationContextAccessor,umbracoContextAccessor); + Node.SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor, variationContextAccessor); } + + public ContentNodeKit Clone() + => new ContentNodeKit + { + ContentTypeId = ContentTypeId, + DraftData = DraftData, + PublishedData = PublishedData, + Node = new ContentNode(Node) + }; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 48c68ab9bf..a3f918c92c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CSharpTest.Net.Collections; +using Umbraco.Core; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Scoping; @@ -12,20 +14,31 @@ using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Web.PublishedCache.NuCache { - // stores content + /// + /// Stores content in memory and persists it back to disk + /// + /// + /// + /// Methods in this class suffixed with the term "Locked" means that those methods can only be called within a WriteLock. A WriteLock + /// is acquired by the GetScopedWriteLock method. Locks are not allowed to be recursive. + /// + /// + /// This class's logic is based on the class but has been slightly modified to suit these purposes. + /// + /// internal class ContentStore { // this class is an extended version of SnapDictionary // most of the snapshots management code, etc is an exact copy // SnapDictionary has unit tests to ensure it all works correctly + // For locking information, see SnapDictionary private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IVariationContextAccessor _variationContextAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly ConcurrentDictionary> _contentNodes; - private readonly ConcurrentDictionary> _contentRootNodes; - private readonly ConcurrentDictionary> _contentTypesById; - private readonly ConcurrentDictionary> _contentTypesByAlias; + private LinkedNode _root; + private readonly ConcurrentDictionary> _contentTypesById; + private readonly ConcurrentDictionary> _contentTypesByAlias; private readonly ConcurrentDictionary _xmap; private readonly ILogger _logger; @@ -37,7 +50,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private long _liveGen, _floorGen; private bool _nextGen, _collectAuto; private Task _collectTask; - private volatile int _wlocked; private List> _wchanges; // TODO: collection trigger (ok for now) @@ -49,20 +61,18 @@ namespace Umbraco.Web.PublishedCache.NuCache public ContentStore( IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, BPlusTree localDb = null) { _publishedSnapshotAccessor = publishedSnapshotAccessor; _variationContextAccessor = variationContextAccessor; - _umbracoContextAccessor = umbracoContextAccessor; _logger = logger; _localDb = localDb; _contentNodes = new ConcurrentDictionary>(); - _contentRootNodes = new ConcurrentDictionary>(); - _contentTypesById = new ConcurrentDictionary>(); - _contentTypesByAlias = new ConcurrentDictionary>(StringComparer.InvariantCultureIgnoreCase); + _root = new LinkedNode(new ContentNode(), 0); + _contentTypesById = new ConcurrentDictionary>(); + _contentTypesByAlias = new ConcurrentDictionary>(StringComparer.InvariantCultureIgnoreCase); _xmap = new ConcurrentDictionary(); _genObjs = new ConcurrentQueue(); @@ -80,15 +90,9 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly string _instanceId = Guid.NewGuid().ToString("N"); - private class ReadLockInfo - { - public bool Taken; - } - private class WriteLockInfo { public bool Taken; - public bool Count; } // a scope contextual that represents a locked writer to the dictionary @@ -119,21 +123,26 @@ namespace Umbraco.Web.PublishedCache.NuCache return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } + private void EnsureLocked() + { + if (!Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Write lock must be acquried."); + } + private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { + if (Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Recursive locks not allowed"); + Monitor.Enter(_wlocko, ref lockInfo.Taken); - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - // see SnapDictionary - try { } finally + try { } + finally { - _wlocked++; - lockInfo.Count = true; - if (_nextGen == false || (forceGen && _wlocked == 1)) + if (_nextGen == false || (forceGen)) { // because we are changing things, a new generation // is created, which will trigger a new snapshot @@ -143,63 +152,57 @@ namespace Umbraco.Web.PublishedCache.NuCache _nextGen = true; } } - } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - } - - private void Lock(ReadLockInfo lockInfo) - { - Monitor.Enter(_rlocko, ref lockInfo.Taken); + } } private void Release(WriteLockInfo lockInfo, bool commit = true) { - if (commit == false) + try { - var rtaken = false; - try + if (commit == false) { - Monitor.Enter(_rlocko, ref rtaken); - try { } - finally + lock (_rlocko) { - _nextGen = false; - _liveGen -= 1; + // see SnapDictionary + try { } + finally + { + _nextGen = false; + _liveGen -= 1; + } } - } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - Rollback(_contentNodes); - Rollback(_contentRootNodes); - Rollback(_contentTypesById); - Rollback(_contentTypesByAlias); + Rollback(_contentNodes); + RollbackRoot(); + Rollback(_contentTypesById); + Rollback(_contentTypesByAlias); + } + else if (_localDb != null && _wchanges != null) + { + foreach (var change in _wchanges) + { + if (change.Value.IsNull) + _localDb.TryRemove(change.Key, out ContentNodeKit unused); + else + _localDb[change.Key] = change.Value; + } + _wchanges = null; + _localDb.Commit(); + } } - else if (_localDb != null && _wchanges != null) + finally { - foreach (var change in _wchanges) - { - if (change.Value.IsNull) - _localDb.TryRemove(change.Key, out ContentNodeKit unused); - else - _localDb[change.Key] = change.Value; - } - _wchanges = null; - _localDb.Commit(); + if (lockInfo.Taken) + Monitor.Exit(_wlocko); } - - if (lockInfo.Count) _wlocked--; - if (lockInfo.Taken) Monitor.Exit(_wlocko); } - private void Release(ReadLockInfo lockInfo) + private void RollbackRoot() { - if (lockInfo.Taken) Monitor.Exit(_rlocko); + if (_root.Gen <= _liveGen) return; + + if (_root.Next != null) + _root = _root.Next; } private void Rollback(ConcurrentDictionary> dictionary) @@ -227,11 +230,33 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { - Lock(lockInfo); + try + { + // Trying to lock could throw exceptions so always make sure to clean up. + Lock(lockInfo); + } + finally + { + try + { + _localDb?.Dispose(); + } + catch (Exception ex) + { + /* TBD: May already be throwing so don't throw again */ + _logger.Error(ex, "Error trying to release DB"); + } + finally + { + _localDb = null; + } + } - if (_localDb == null) return; - _localDb.Dispose(); - _localDb = null; + } + catch (Exception ex) + { + _logger.Error(ex, "Error trying to lock"); + throw; } finally { @@ -249,142 +274,197 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public void NewContentTypes(IEnumerable types) + /// + /// Sets data for new content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void NewContentTypesLocked(IEnumerable types) { - var lockInfo = new WriteLockInfo(); - try - { - Lock(lockInfo); + EnsureLocked(); - foreach (var type in types) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - } - finally + foreach (var type in types) { - Release(lockInfo); + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); } } - public void UpdateContentTypes(IEnumerable types) + /// + /// Sets data for updated content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateContentTypesLocked(IEnumerable types) { - var lockInfo = new WriteLockInfo(); - try + //nothing to do if this is empty, no need to lock/allocate/iterate/etc... + if (!types.Any()) return; + + EnsureLocked(); + + var index = types.ToDictionary(x => x.Id, x => x); + + foreach (var type in index.Values) { - Lock(lockInfo); - - var index = types.ToDictionary(x => x.Id, x => x); - - foreach (var type in index.Values) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - - foreach (var link in _contentNodes.Values) - { - var node = link.Value; - if (node == null) continue; - var contentTypeId = node.ContentType.Id; - if (index.TryGetValue(contentTypeId, out PublishedContentType contentType) == false) continue; - SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType, _publishedSnapshotAccessor, _variationContextAccessor, _umbracoContextAccessor)); - } + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); } - finally + + foreach (var link in _contentNodes.Values) { - Release(lockInfo); + var node = link.Value; + if (node == null) continue; + var contentTypeId = node.ContentType.Id; + if (index.TryGetValue(contentTypeId, out var contentType) == false) continue; + SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType)); } } - public void UpdateContentTypes(IEnumerable removedIds, IEnumerable refreshedTypes, IEnumerable kits) + /// + /// Updates/sets data for all content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void SetAllContentTypesLocked(IEnumerable types) { - var removedIdsA = removedIds?.ToArray() ?? Array.Empty(); - var refreshedTypesA = refreshedTypes?.ToArray() ?? Array.Empty(); - var refreshedIdsA = refreshedTypesA.Select(x => x.Id).ToArray(); + EnsureLocked(); + + // clear all existing content types + ClearLocked(_contentTypesById); + ClearLocked(_contentTypesByAlias); + + // set all new content types + foreach (var type in types) + { + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); + } + + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! + } + + /// + /// Updates/sets/removes data for content types + /// + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateContentTypesLocked(IReadOnlyCollection removedIds, IReadOnlyCollection refreshedTypes, IReadOnlyCollection kits) + { + EnsureLocked(); + + var removedIdsA = removedIds ?? Array.Empty(); + var refreshedTypesA = refreshedTypes ?? Array.Empty(); + var refreshedIdsA = refreshedTypesA.Select(x => x.Id).ToList(); kits = kits ?? Array.Empty(); - var lockInfo = new WriteLockInfo(); - try + if (kits.Count == 0 && refreshedIdsA.Count == 0 && removedIdsA.Count == 0) + return; //exit - there is nothing to do here + + var removedContentTypeNodes = new List(); + var refreshedContentTypeNodes = new List(); + + // find all the nodes that are either refreshed or removed, + // because of their content type being either refreshed or removed + foreach (var link in _contentNodes.Values) { - Lock(lockInfo); - - var removedContentTypeNodes = new List(); - var refreshedContentTypeNodes = new List(); - - // find all the nodes that are either refreshed or removed, - // because of their content type being either refreshed or removed - foreach (var link in _contentNodes.Values) - { - var node = link.Value; - if (node == null) continue; - var contentTypeId = node.ContentType.Id; - if (removedIdsA.Contains(contentTypeId)) removedContentTypeNodes.Add(node.Id); - if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id); - } - - // perform deletion of content with removed content type - // removing content types should have removed their content already - // but just to be 100% sure, clear again here - foreach (var node in removedContentTypeNodes) - ClearBranchLocked(node); - - // perform deletion of removed content types - foreach (var id in removedIdsA) - { - if (_contentTypesById.TryGetValue(id, out var link) == false || link.Value == null) - continue; - SetValueLocked(_contentTypesById, id, null); - SetValueLocked(_contentTypesByAlias, link.Value.Alias, null); - } - - // perform update of refreshed content types - foreach (var type in refreshedTypesA) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - - // perform update of content with refreshed content type - from the kits - // skip missing type, skip missing parents & unbuildable kits - what else could we do? - // kits are ordered by level, so ParentExits is ok here - var visited = new List(); - foreach (var kit in kits.Where(x => - refreshedIdsA.Contains(x.ContentTypeId) && - ParentExistsLocked(x) && - BuildKit(x))) - { - // replacing the node: must preserve the parents - var node = GetHead(_contentNodes, kit.Node.Id)?.Value; - if (node != null) - kit.Node.ChildContentIds = node.ChildContentIds; - - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - - visited.Add(kit.Node.Id); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - } - - // all content should have been refreshed - but... - var orphans = refreshedContentTypeNodes.Except(visited); - foreach (var id in orphans) - ClearBranchLocked(id); + var node = link.Value; + if (node == null) continue; + var contentTypeId = node.ContentType.Id; + if (removedIdsA.Contains(contentTypeId)) removedContentTypeNodes.Add(node.Id); + if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id); } - finally + + // perform deletion of content with removed content type + // removing content types should have removed their content already + // but just to be 100% sure, clear again here + foreach (var node in removedContentTypeNodes) + ClearBranchLocked(node); + + // perform deletion of removed content types + foreach (var id in removedIdsA) { - Release(lockInfo); + if (_contentTypesById.TryGetValue(id, out var link) == false || link.Value == null) + continue; + SetValueLocked(_contentTypesById, id, null); + SetValueLocked(_contentTypesByAlias, link.Value.Alias, null); } + + // perform update of refreshed content types + foreach (var type in refreshedTypesA) + { + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); + } + + // perform update of content with refreshed content type - from the kits + // skip missing type, skip missing parents & un-buildable kits - what else could we do? + // kits are ordered by level, so ParentExists is ok here + var visited = new List(); + foreach (var kit in kits.Where(x => + refreshedIdsA.Contains(x.ContentTypeId) && + BuildKit(x, out _))) + { + // replacing the node: must preserve the parents + var node = GetHead(_contentNodes, kit.Node.Id)?.Value; + if (node != null) + kit.Node.FirstChildContentId = node.FirstChildContentId; + + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + + visited.Add(kit.Node.Id); + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + } + + // all content should have been refreshed - but... + var orphans = refreshedContentTypeNodes.Except(visited); + foreach (var id in orphans) + ClearBranchLocked(id); } - public void UpdateDataTypes(IEnumerable dataTypeIds, Func getContentType) + /// + /// Updates data types + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateDataTypesLocked(IEnumerable dataTypeIds, Func getContentType) { - var lockInfo = new WriteLockInfo(); - try - { - Lock(lockInfo); + EnsureLocked(); - var contentTypes = _contentTypesById + var contentTypes = _contentTypesById .Where(kvp => kvp.Value.Value != null && kvp.Value.Value.PropertyTypes.Any(p => dataTypeIds.Contains(p.DataType.Id))) @@ -393,55 +473,81 @@ namespace Umbraco.Web.PublishedCache.NuCache .Where(x => x != null) // poof, gone, very unlikely and probably an anomaly .ToArray(); - var contentTypeIdsA = contentTypes.Select(x => x.Id).ToArray(); - var contentTypeNodes = new Dictionary>(); - foreach (var id in contentTypeIdsA) - contentTypeNodes[id] = new List(); - foreach (var link in _contentNodes.Values) - { - var node = link.Value; - if (node != null && contentTypeIdsA.Contains(node.ContentType.Id)) - contentTypeNodes[node.ContentType.Id].Add(node.Id); - } - - foreach (var contentType in contentTypes) - { - // again, weird situation - if (contentTypeNodes.ContainsKey(contentType.Id) == false) - continue; - - foreach (var id in contentTypeNodes[contentType.Id]) - { - _contentNodes.TryGetValue(id, out LinkedNode link); - if (link?.Value == null) - continue; - var node = new ContentNode(link.Value, contentType, _publishedSnapshotAccessor, _variationContextAccessor, _umbracoContextAccessor); - SetValueLocked(_contentNodes, id, node); - if (_localDb != null) RegisterChange(id, node.ToKit()); - } - } - } - finally + var contentTypeIdsA = contentTypes.Select(x => x.Id).ToArray(); + var contentTypeNodes = new Dictionary>(); + foreach (var id in contentTypeIdsA) + contentTypeNodes[id] = new List(); + foreach (var link in _contentNodes.Values) { - Release(lockInfo); + var node = link.Value; + if (node != null && contentTypeIdsA.Contains(node.ContentType.Id)) + contentTypeNodes[node.ContentType.Id].Add(node.Id); + } + + foreach (var contentType in contentTypes) + { + // again, weird situation + if (contentTypeNodes.ContainsKey(contentType.Id) == false) + continue; + + foreach (var id in contentTypeNodes[contentType.Id]) + { + _contentNodes.TryGetValue(id, out var link); + if (link?.Value == null) + continue; + var node = new ContentNode(link.Value, contentType); + SetValueLocked(_contentNodes, id, node); + if (_localDb != null) RegisterChange(id, node.ToKit()); + } } } - private bool BuildKit(ContentNodeKit kit) + /// + /// Validate the and try to create a parent + /// + /// + /// + /// + /// Returns false if the parent was not found or if the kit validation failed + /// + private bool BuildKit(ContentNodeKit kit, out LinkedNode parent) { + // make sure parent exists + parent = GetParentLink(kit.Node, null); + if (parent == null) + { + _logger.Warn($"Skip item id={kit.Node.Id}, could not find parent id={kit.Node.ParentContentId}."); + return false; + } + + // We cannot continue if there's no value. This shouldn't happen but it can happen if the database umbracoNode.path + // data is invalid/corrupt. If that is the case, the parentId might be ok but not the Path which can result in null + // because the data sort operation is by path. + if (parent.Value == null) + { + _logger.Warn($"Skip item id={kit.Node.Id}, no Data assigned for linked node with path {kit.Node.Path} and parent id {kit.Node.ParentContentId}. This can indicate data corruption for the Path value for node {kit.Node.Id}. See the Health Check dashboard in Settings to resolve data integrity issues."); + return false; + } + // make sure the kit is valid if (kit.DraftData == null && kit.PublishedData == null) + { + _logger.Warn($"Skip item id={kit.Node.Id}, both draft and published data are null."); return false; + } // unknown = bad - if (_contentTypesById.TryGetValue(kit.ContentTypeId, out LinkedNode link) == false || link.Value == null) + if (_contentTypesById.TryGetValue(kit.ContentTypeId, out var link) == false || link.Value == null) + { + _logger.Warn($"Skip item id={kit.Node.Id}, could not find content type id={kit.ContentTypeId}."); return false; + } // check whether parent is published var canBePublished = ParentPublishedLocked(kit); // and use - kit.Build(link.Value, _publishedSnapshotAccessor, _variationContextAccessor, canBePublished, _umbracoContextAccessor); + kit.Build(link.Value, _publishedSnapshotAccessor, _variationContextAccessor, canBePublished); return true; } @@ -452,163 +558,318 @@ namespace Umbraco.Web.PublishedCache.NuCache public int Count => _contentNodes.Count; + /// + /// Get the most recent version of the LinkedNode stored in the dictionary for the supplied key + /// + /// + /// + /// + /// + /// private static LinkedNode GetHead(ConcurrentDictionary> dict, TKey key) where TValue : class { - dict.TryGetValue(key, out LinkedNode link); // else null + dict.TryGetValue(key, out var link); // else null return link; } - public void Set(ContentNodeKit kit) + /// + /// Sets the data for a + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetLocked(ContentNodeKit kit) { + EnsureLocked(); + // ReSharper disable LocalizableElement if (kit.IsEmpty) throw new ArgumentException("Kit is empty.", nameof(kit)); - if (kit.Node.ChildContentIds.Count > 0) + if (kit.Node.FirstChildContentId > 0) throw new ArgumentException("Kit content cannot have children.", nameof(kit)); // ReSharper restore LocalizableElement _logger.Debug("Set content ID: {KitNodeId}", kit.Node.Id); - var lockInfo = new WriteLockInfo(); - try + // get existing + _contentNodes.TryGetValue(kit.Node.Id, out var link); + var existing = link?.Value; + + if (!BuildKit(kit, out var parent)) + return false; + + // moving? + var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; + + // manage children + if (existing != null) { - Lock(lockInfo); + kit.Node.FirstChildContentId = existing.FirstChildContentId; + kit.Node.LastChildContentId = existing.LastChildContentId; + } - // get existing - _contentNodes.TryGetValue(kit.Node.Id, out LinkedNode link); - var existing = link?.Value; + // set + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + if (_localDb != null) RegisterChange(kit.Node.Id, kit); - // else ignore, what else could we do? - if (ParentExistsLocked(kit) == false || BuildKit(kit) == false) - return; + // manage the tree + if (existing == null) + { + // new, add to parent + AddTreeNodeLocked(kit.Node, parent); + } + else if (moving || existing.SortOrder != kit.Node.SortOrder) + { + // moved, remove existing from its parent, add content to its parent + RemoveTreeNodeLocked(existing); + AddTreeNodeLocked(kit.Node); + } + else + { + // replacing existing, handle siblings + kit.Node.NextSiblingContentId = existing.NextSiblingContentId; + kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId; + } - // moving? - var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; + _xmap[kit.Node.Uid] = kit.Node.Id; - // manage children - if (existing != null) - kit.Node.ChildContentIds = existing.ChildContentIds; + return true; + } - // set - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); + private void ClearRootLocked() + { + if (_root.Gen != _liveGen) + _root = new LinkedNode(new ContentNode(), _liveGen, _root); + else + _root.Value.FirstChildContentId = -1; + } - // manage the tree - if (existing == null) + /// + /// Builds all kits on startup using a fast forward only cursor + /// + /// + /// All kits sorted by Level + Parent Id + Sort order + /// + /// True if the data is coming from the database (not the local cache db) + /// + /// + /// + /// This requires that the collection is sorted by Level + ParentId + Sort Order. + /// This should be used only on a site startup as the first generations. + /// This CANNOT be used after startup since it bypasses all checks for Generations. + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllFastSortedLocked(IEnumerable kits, bool fromDb) + { + EnsureLocked(); + + var ok = true; + + ClearLocked(_contentNodes); + ClearRootLocked(); + + // The name of the game here is to populate each kit's + // FirstChildContentId + // LastChildContentId + // NextSiblingContentId + // PreviousSiblingContentId + + ContentNode previousNode = null; + ContentNode parent = null; + + foreach (var kit in kits) + { + if (!BuildKit(kit, out var parentLink)) { - // new, add to parent - AddToParentLocked(kit.Node); + ok = false; + continue; // skip that one } - else if (moving) + + var thisNode = kit.Node; + + if (parent == null) { - // moved, remove existing from its parent, add content to its parent - RemoveFromParentLocked(existing); - AddToParentLocked(kit.Node); + // first parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node } + else if (parent.Id != parentLink.Value.Id) + { + // new parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node + previousNode = null; // there is no previous sibling + } + + _logger.Debug($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); + SetValueLocked(_contentNodes, thisNode.Id, thisNode); + + // if we are initializing from the database source ensure the local db is updated + if (fromDb && _localDb != null) RegisterChange(thisNode.Id, kit); + + // this node is always the last child + parent.LastChildContentId = thisNode.Id; + + // wire previous node as previous sibling + if (previousNode != null) + { + previousNode.NextSiblingContentId = thisNode.Id; + thisNode.PreviousSiblingContentId = previousNode.Id; + } + + // this node becomes the previous node + previousNode = thisNode; _xmap[kit.Node.Uid] = kit.Node.Id; } - finally - { - Release(lockInfo); - } + + return ok; } - // IMPORTANT kits must be sorted out by LEVEL - public void SetAll(IEnumerable kits) + /// + /// Set all data for a collection of + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllLocked(IEnumerable kits) { - var lockInfo = new WriteLockInfo(); - try + EnsureLocked(); + + var ok = true; + + ClearLocked(_contentNodes); + ClearRootLocked(); + + // do NOT clear types else they are gone! + //ClearLocked(_contentTypesById); + //ClearLocked(_contentTypesByAlias); + + foreach (var kit in kits) { - Lock(lockInfo); - - ClearLocked(_contentNodes); - ClearLocked(_contentRootNodes); - - // do NOT clear types else they are gone! - //ClearLocked(_contentTypesById); - //ClearLocked(_contentTypesByAlias); - - // skip missing parents & unbuildable kits - what else could we do? - foreach (var kit in kits.Where(x => ParentExistsLocked(x) && BuildKit(x))) + if (!BuildKit(kit, out var parent)) { - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddToParentLocked(kit.Node); - - _xmap[kit.Node.Uid] = kit.Node.Id; + ok = false; + continue; // skip that one } + _logger.Debug($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + AddTreeNodeLocked(kit.Node, parent); + + _xmap[kit.Node.Uid] = kit.Node.Id; } - finally - { - Release(lockInfo); - } + + return ok; } - // IMPORTANT kits must be sorted out by LEVEL - public void SetBranch(int rootContentId, IEnumerable kits) + /// + /// Sets data for a branch of + /// + /// + /// + /// + /// + /// + /// IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetBranchLocked(int rootContentId, IEnumerable kits) { - var lockInfo = new WriteLockInfo(); - try + EnsureLocked(); + + var ok = true; + + // get existing + _contentNodes.TryGetValue(rootContentId, out var link); + var existing = link?.Value; + + // clear + if (existing != null) { - Lock(lockInfo); - - // get existing - _contentNodes.TryGetValue(rootContentId, out LinkedNode link); - var existing = link?.Value; - - // clear - if (existing != null) - { - ClearBranchLocked(existing); - RemoveFromParentLocked(existing); - } - - // now add them all back - // skip missing parents & unbuildable kits - what else could we do? - foreach (var kit in kits.Where(x => ParentExistsLocked(x) && BuildKit(x))) - { - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddToParentLocked(kit.Node); - - _xmap[kit.Node.Uid] = kit.Node.Id; - } + //this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen + ClearBranchLocked(existing); + //TODO: This removes the current GEN from the tree - do we really want to do that? (not sure if this is still an issue....) + RemoveTreeNodeLocked(existing); } - finally + + // now add them all back + foreach (var kit in kits) { - Release(lockInfo); + if (!BuildKit(kit, out var parent)) + { + ok = false; + continue; // skip that one + } + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + AddTreeNodeLocked(kit.Node, parent); + + _xmap[kit.Node.Uid] = kit.Node.Id; } + + return ok; } - public bool Clear(int id) + /// + /// Clears data for a given node id + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool ClearLocked(int id) { - var lockInfo = new WriteLockInfo(); - try - { - Lock(lockInfo); + EnsureLocked(); - // try to find the content - // if it is not there, nothing to do - _contentNodes.TryGetValue(id, out LinkedNode link); // else null - if (link?.Value == null) return false; + // try to find the content + // if it is not there, nothing to do + _contentNodes.TryGetValue(id, out var link); // else null + if (link?.Value == null) return false; - var content = link.Value; - _logger.Debug("Clear content ID: {ContentId}", content.Id); + var content = link.Value; + _logger.Debug("Clear content ID: {ContentId}", content.Id); - // clear the entire branch - ClearBranchLocked(content); + // clear the entire branch + ClearBranchLocked(content); - // manage the tree - RemoveFromParentLocked(content); + // manage the tree + RemoveTreeNodeLocked(content); - return true; - } - finally - { - Release(lockInfo); - } + return true; } private void ClearBranchLocked(int id) @@ -621,86 +882,279 @@ namespace Umbraco.Web.PublishedCache.NuCache private void ClearBranchLocked(ContentNode content) { + // This should never be null, all code that calls this method is null checking but we've seen + // issues of null ref exceptions in issue reports so we'll double check here + if (content == null) throw new ArgumentNullException(nameof(content)); + SetValueLocked(_contentNodes, content.Id, null); if (_localDb != null) RegisterChange(content.Id, ContentNodeKit.Null); _xmap.TryRemove(content.Uid, out _); - foreach (var childId in content.ChildContentIds) + var id = content.FirstChildContentId; + while (id > 0) { - if (_contentNodes.TryGetValue(childId, out LinkedNode link) == false || link.Value == null) continue; - ClearBranchLocked(link.Value); + // get the required link node, this ensures that both `link` and `link.Value` are not null + var link = GetRequiredLinkedNode(id, "child", null); + var linkValue = link.Value; // capture local since clearing in recurse can clear it + ClearBranchLocked(linkValue); // recurse + id = linkValue.NextSiblingContentId; } } - private LinkedNode GetParentLink(ContentNode content) + /// + /// Gets the link node and if it doesn't exist throw a + /// + /// + /// + /// the generation requested, null for the latest stored + /// + private LinkedNode GetRequiredLinkedNode(int id, string description, long? gen) { - _contentNodes.TryGetValue(content.ParentContentId, out var link); // else null - //if (link == null || link.Value == null) - // throw new Exception("Panic: parent not found."); + if (_contentNodes.TryGetValue(id, out var link)) + { + link = GetLinkedNodeGen(link, gen); + if (link != null && link.Value != null) + return link; + } + + throw new PanicException($"failed to get {description} with id={id}"); + } + + /// + /// Gets the parent link node, may be null or root if ParentContentId is less than 0 + /// + /// the generation requested, null for the latest stored + private LinkedNode GetParentLink(ContentNode content, long? gen) + { + if (content.ParentContentId < 0) + { + var root = GetLinkedNodeGen(_root, gen); + return root; + } + + if (_contentNodes.TryGetValue(content.ParentContentId, out var link)) + link = GetLinkedNodeGen(link, gen); + return link; } - private void RemoveFromParentLocked(ContentNode content) + /// + /// Gets the linked parent node and if it doesn't exist throw a + /// + /// + /// the generation requested, null for the latest stored + /// + private LinkedNode GetRequiredParentLink(ContentNode content, long? gen) { - // remove from root content index, - // or parent's children index - if (content.ParentContentId < 0) - { - SetValueLocked(_contentRootNodes, content.Id, null); - } - else - { - // obviously parent has to exist - var link = GetParentLink(content); - var parent = link.Value; - if (link.Gen < _liveGen) - parent = parent.CloneParent(_publishedSnapshotAccessor, _umbracoContextAccessor); - parent.ChildContentIds.Remove(content.Id); - if (link.Gen < _liveGen) - SetValueLocked(_contentNodes, parent.Id, parent); - } + return content.ParentContentId < 0 ? _root : GetRequiredLinkedNode(content.ParentContentId, "parent", gen); } - private bool ParentExistsLocked(ContentNodeKit kit) + /// + /// Iterates over the LinkedNode's generations to find the correct one + /// + /// + /// The generation requested, use null to avoid the lookup + /// + private LinkedNode GetLinkedNodeGen(LinkedNode link, long? gen) + where TValue : class { - if (kit.Node.ParentContentId < 0) - return true; - var link = GetParentLink(kit.Node); - return link?.Value != null; + if (!gen.HasValue) return link; + + //find the correct snapshot, find the first that is <= the requested gen + while (link != null && link.Gen > gen) + { + link = link.Next; + } + return link; + } + + /// + /// This removes this current node from the tree hiearchy by removing it from it's parent's linked list + /// + /// + /// + /// This is called within a lock which means a new Gen is being created therefore this will not modify any existing content in a Gen. + /// + private void RemoveTreeNodeLocked(ContentNode content) + { + // NOTE: DO NOT modify `content` here, this would modify data for an existing Gen, all modifications are done to clones + // which would be targeting the new Gen. + + var parentLink = content.ParentContentId < 0 + ? _root + : GetRequiredLinkedNode(content.ParentContentId, "parent", null); + + var parent = parentLink.Value; + + // must have children + if (parent.FirstChildContentId < 0) + throw new PanicException("no children"); + + // if first/last, clone parent, then remove + + if (parent.FirstChildContentId == content.Id || parent.LastChildContentId == content.Id) + parent = GenCloneLocked(parentLink); + + if (parent.FirstChildContentId == content.Id) + parent.FirstChildContentId = content.NextSiblingContentId; + + if (parent.LastChildContentId == content.Id) + parent.LastChildContentId = content.PreviousSiblingContentId; + + // maintain linked list + + if (content.NextSiblingContentId > 0) + { + var nextLink = GetRequiredLinkedNode(content.NextSiblingContentId, "next sibling", null); + var next = GenCloneLocked(nextLink); + next.PreviousSiblingContentId = content.PreviousSiblingContentId; + } + + if (content.PreviousSiblingContentId > 0) + { + var prevLink = GetRequiredLinkedNode(content.PreviousSiblingContentId, "previous sibling", null); + var prev = GenCloneLocked(prevLink); + prev.NextSiblingContentId = content.NextSiblingContentId; + } } private bool ParentPublishedLocked(ContentNodeKit kit) { if (kit.Node.ParentContentId < 0) return true; - var link = GetParentLink(kit.Node); + var link = GetParentLink(kit.Node, null); var node = link?.Value; - return node?.PublishedModel != null; + return node != null && node.HasPublished; } - private void AddToParentLocked(ContentNode content) + private ContentNode GenCloneLocked(LinkedNode link) { - // add to root content index, - // or parent's children index - if (content.ParentContentId < 0) + var node = link.Value; + + if (node != null && link.Gen != _liveGen) { - // need an object reference... just use this... - SetValueLocked(_contentRootNodes, content.Id, this); + node = new ContentNode(link.Value); + if (link == _root) + SetRootLocked(node); + else + SetValueLocked(_contentNodes, node.Id, node); + } + + return node; + } + + /// + /// Adds a node to the tree structure. + /// + private void AddTreeNodeLocked(ContentNode content, LinkedNode parentLink = null) + { + parentLink = parentLink ?? GetRequiredParentLink(content, null); + + var parent = parentLink.Value; + + // We are doing a null check here but this should no longer be possible because we have a null check in BuildKit + // for the parent.Value property and we'll output a warning. However I'll leave this additional null check in place. + // see https://github.com/umbraco/Umbraco-CMS/issues/7868 + if (parent == null) + throw new PanicException($"A null Value was returned on the {nameof(parentLink)} LinkedNode with id={content.ParentContentId}, potentially your database paths are corrupted."); + + // if parent has no children, clone parent + add as first child + if (parent.FirstChildContentId < 0) + { + parent = GenCloneLocked(parentLink); + parent.FirstChildContentId = content.Id; + parent.LastChildContentId = content.Id; + return; + } + + // get parent's first child + var childLink = GetRequiredLinkedNode(parent.FirstChildContentId, "first child", null); + var child = childLink.Value; + + // if first, clone parent + insert as first child + // NOTE: Don't perform this check if loading from local DB since we know it's already sorted + if (child.SortOrder > content.SortOrder) + { + content.NextSiblingContentId = parent.FirstChildContentId; + content.PreviousSiblingContentId = -1; + + parent = GenCloneLocked(parentLink); + parent.FirstChildContentId = content.Id; + + child = GenCloneLocked(childLink); + child.PreviousSiblingContentId = content.Id; + + return; + } + + // get parent's last child + var lastChildLink = GetRequiredLinkedNode(parent.LastChildContentId, "last child", null); + var lastChild = lastChildLink.Value; + + // if last, clone parent + append as last child + if (lastChild.SortOrder <= content.SortOrder) + { + content.PreviousSiblingContentId = parent.LastChildContentId; + content.NextSiblingContentId = -1; + + parent = GenCloneLocked(parentLink); + parent.LastChildContentId = content.Id; + + lastChild = GenCloneLocked(lastChildLink); + lastChild.NextSiblingContentId = content.Id; + + return; + } + + // else it's going somewhere in the middle, + // TODO: There was a note about performance when this occurs and that this only happens when moving and not very often, but that is not true, + // this also happens anytime a middle node is unpublished or republished (which causes a branch update), i'm unsure if this has perf impacts, + // i think this used to but it doesn't seem bad anymore that I can see... + while (child.NextSiblingContentId > 0) + { + // get next child + var nextChildLink = GetRequiredLinkedNode(child.NextSiblingContentId, "next child", null); + var nextChild = nextChildLink.Value; + + // if here, clone previous + append/insert + // NOTE: Don't perform this check if loading from local DB since we know it's already sorted + if (nextChild.SortOrder > content.SortOrder) + { + content.NextSiblingContentId = nextChild.Id; + content.PreviousSiblingContentId = nextChild.PreviousSiblingContentId; + + child = GenCloneLocked(childLink); + child.NextSiblingContentId = content.Id; + + var nnext = GenCloneLocked(nextChildLink); + nnext.PreviousSiblingContentId = content.Id; + + return; + } + + childLink = nextChildLink; + child = nextChild; + } + + // should never get here + throw new PanicException("No more children."); + } + + // replaces the root node + private void SetRootLocked(ContentNode node) + { + if (_root.Gen != _liveGen) + { + _root = new LinkedNode(node, _liveGen, _root); } else { - // assume parent has been validated and exists - var link = GetParentLink(content); - var parent = link.Value; - if (link.Gen < _liveGen) - parent = parent.CloneParent(_publishedSnapshotAccessor, _umbracoContextAccessor); - parent.ChildContentIds.Add(content.Id); - if (link.Gen < _liveGen) - SetValueLocked(_contentNodes, parent.Id, parent); + _root.Value = node; } } + // set a node (just the node, not the tree) private void SetValueLocked(ConcurrentDictionary> dict, TKey key, TValue value) where TValue : class { @@ -738,7 +1192,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // this is safe only because we're write-locked foreach (var kvp in dict.Where(x => x.Value != null)) { - if (kvp.Value.Gen < _liveGen) + if (kvp.Value.Gen != _liveGen) { var link = new LinkedNode(null, _liveGen, kvp.Value); dict.TryUpdate(kvp.Key, link, kvp.Value); @@ -764,18 +1218,17 @@ namespace Umbraco.Web.PublishedCache.NuCache public IEnumerable GetAtRoot(long gen) { - // look ma, no lock! - foreach (var kvp in _contentRootNodes) + var root = GetLinkedNodeGen(_root, gen); + if (root == null) + yield break; + + var id = root.Value.FirstChildContentId; + + while (id > 0) { - var link = kvp.Value; - while (link != null) - { - if (link.Gen <= gen) - break; - link = link.Next; - } - if (link?.Value != null) - yield return Get(kvp.Key, gen); + var link = GetRequiredLinkedNode(id, "root", gen); + yield return link.Value; + id = link.Value.NextSiblingContentId; } } @@ -784,13 +1237,8 @@ namespace Umbraco.Web.PublishedCache.NuCache { // look ma, no lock! var link = GetHead(dict, key); - while (link != null) - { - if (link.Gen <= gen) - return link.Value; // may be null - link = link.Next; - } - return null; + link = GetLinkedNodeGen(link, gen); + return link?.Value; // may be null } public IEnumerable GetAll(long gen) @@ -800,17 +1248,9 @@ namespace Umbraco.Web.PublishedCache.NuCache var links = _contentNodes.Values.ToArray(); foreach (var l in links) { - var link = l; - while (link != null) - { - if (link.Gen <= gen) - { - if (link.Value != null) - yield return link.Value; - break; - } - link = link.Next; - } + var link = GetLinkedNodeGen(l, gen); + if (link?.Value != null) + yield return link.Value; } } @@ -818,24 +1258,18 @@ namespace Umbraco.Web.PublishedCache.NuCache { var has = _contentNodes.Any(x => { - var link = x.Value; - while (link != null) - { - if (link.Gen <= gen && link.Value != null) - return true; - link = link.Next; - } - return false; + var link = GetLinkedNodeGen(x.Value, gen); + return link?.Value != null; }); return has == false; } - public PublishedContentType GetContentType(int id, long gen) + public IPublishedContentType GetContentType(int id, long gen) { return GetValue(_contentTypesById, id, gen); } - public PublishedContentType GetContentType(string alias, long gen) + public IPublishedContentType GetContentType(string alias, long gen) { return GetValue(_contentTypesByAlias, alias, gen); } @@ -846,11 +1280,8 @@ namespace Umbraco.Web.PublishedCache.NuCache public Snapshot CreateSnapshot() { - var lockInfo = new ReadLockInfo(); - try + lock(_rlocko) { - Lock(lockInfo); - // if no next generation is required, and we already have one, // use it and create a new snapshot if (_nextGen == false && _genObj != null) @@ -863,7 +1294,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // else we need to try to create a new gen ref // whether we are wlocked or not, noone can rlock while we do, // so _liveGen and _nextGen are safe - if (_wlocked > 0) // volatile, cannot ++ but could -- + if (Monitor.IsEntered(_wlocko)) { // write-locked, cannot use latest gen (at least 1) so use previous var snapGen = _nextGen ? _liveGen - 1 : _liveGen; @@ -872,7 +1303,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (_genObj == null) _genObjs.Enqueue(_genObj = new GenObj(snapGen)); else if (_genObj.Gen != snapGen) - throw new Exception("panic"); + throw new PanicException($"The generation {_genObj.Gen} does not equal the snapshot generation {snapGen}"); } else { @@ -902,10 +1333,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return snapshot; } - finally - { - Release(lockInfo); - } } public Snapshot LiveSnapshot => new Snapshot(this, _liveGen @@ -928,7 +1355,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return _collectTask; // ReSharper disable InconsistentlySynchronizedField - var task = _collectTask = Task.Run(() => Collect()); + var task = _collectTask = Task.Run(Collect); _collectTask.ContinueWith(_ => { lock (_rlocko) @@ -957,11 +1384,19 @@ namespace Umbraco.Web.PublishedCache.NuCache } Collect(_contentNodes); - Collect(_contentRootNodes); + CollectRoot(); Collect(_contentTypesById); Collect(_contentTypesByAlias); } + private void CollectRoot() + { + var link = _root; + while (link.Next != null && link.Next.Gen > _floorGen) + link = link.Next; + link.Next = null; + } + private void Collect(ConcurrentDictionary> dict) where TValue : class { @@ -1015,16 +1450,17 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public async Task WaitForPendingCollect() - { - Task task; - lock (_rlocko) - { - task = _collectTask; - } - if (task != null) - await task; - } + // TODO: This is never used? Should it be? Maybe move to TestHelper below? + //public async Task WaitForPendingCollect() + //{ + // Task task; + // lock (_rlocko) + // { + // task = _collectTask; + // } + // if (task != null) + // await task; + //} public long GenCount => _genObjs.Count; @@ -1032,7 +1468,7 @@ namespace Umbraco.Web.PublishedCache.NuCache #endregion - #region Unit testing + #region Internals/Unit testing private TestHelper _unitTesting; @@ -1055,17 +1491,22 @@ namespace Umbraco.Web.PublishedCache.NuCache set => _store._collectAuto = value; } - public Tuple[] GetValues(int id) + /// + /// Return a list of Gen/ContentNode values + /// + /// + /// + public (long gen, ContentNode contentNode)[] GetValues(int id) { _store._contentNodes.TryGetValue(id, out LinkedNode link); // else null if (link == null) - return new Tuple[0]; + return Array.Empty<(long, ContentNode)>(); - var tuples = new List>(); + var tuples = new List<(long, ContentNode)>(); do { - tuples.Add(Tuple.Create(link.Gen, link.Value)); + tuples.Add((link.Gen, link.Value)); link = link.Next; } while (link != null); return tuples.ToArray(); @@ -1151,14 +1592,14 @@ namespace Umbraco.Web.PublishedCache.NuCache return _store.GetAll(_gen); } - public PublishedContentType GetContentType(int id) + public IPublishedContentType GetContentType(int id) { if (_gen < 0) throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); return _store.GetContentType(id, _gen); } - public PublishedContentType GetContentType(string alias) + public IPublishedContentType GetContentType(string alias) { if (_gen < 0) throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentNodeKitSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentNodeKitSerializer.cs index 4303f97d3c..f799869850 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentNodeKitSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentNodeKitSerializer.cs @@ -54,4 +54,4 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource DataSerializer.WriteTo(value.PublishedData, stream); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs index 958f6302fa..4521311302 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using CSharpTest.Net.Serialization; using Umbraco.Core; @@ -14,7 +15,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (pcount == 0) return Empty; // read each variation - var dict = new Dictionary(); + var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); for (var i = 0; i < pcount; i++) { var languageId = PrimitiveSerializer.String.ReadFrom(stream); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index 19c8beedb5..aa5dc9eb30 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using CSharpTest.Net.Serialization; using Umbraco.Core; @@ -9,7 +10,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { public IDictionary ReadFrom(Stream stream) { - var dict = new Dictionary(); + var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); // read properties count var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index dcb3f44487..694dac04df 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -20,6 +20,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // provides efficient database access for NuCache internal class DatabaseDataSource : IDataSource { + private const int PageSize = 500; + // we want arrays, we want them all loaded, not an enumerable private Sql ContentSourcesSelect(IScope scope, Func, Sql> joins = null) @@ -67,7 +69,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { var sql = ContentSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && x.NodeId == id && !x.Trashed) - .OrderBy(x => x.Level, x => x.SortOrder); + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); var dto = scope.Database.Fetch(sql).FirstOrDefault(); return dto == null ? new ContentNodeKit() : CreateContentNodeKit(dto); @@ -77,33 +79,45 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { var sql = ContentSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .OrderBy(x => x.Level, x => x.SortOrder); + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateContentNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateContentNodeKit(row); } public IEnumerable GetBranchContentSources(IScope scope, int id) { var syntax = scope.SqlContext.SqlSyntax; - var sql = ContentSourcesSelect(scope, s => s + var sql = ContentSourcesSelect(scope, + s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) + .Where(x => x.NodeId == id, "x") + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - .InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .Where(x => x.NodeId == id, "x") - .OrderBy(x => x.Level, x => x.SortOrder); - - return scope.Database.Query(sql).Select(CreateContentNodeKit); + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateContentNodeKit(row); } public IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids) { + if (!ids.Any()) yield break; + var sql = ContentSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) .WhereIn(x => x.ContentTypeId, ids) - .OrderBy(x => x.Level, x => x.SortOrder); + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateContentNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateContentNodeKit(row); } private Sql MediaSourcesSelect(IScope scope, Func, Sql> joins = null) @@ -114,11 +128,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) - .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) - .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) - .From(); if (joins != null) @@ -126,9 +137,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource sql = sql .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit"); return sql; @@ -138,7 +147,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { var sql = MediaSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && x.NodeId == id && !x.Trashed) - .OrderBy(x => x.Level, x => x.SortOrder); + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); var dto = scope.Database.Fetch(sql).FirstOrDefault(); return dto == null ? new ContentNodeKit() : CreateMediaNodeKit(dto); @@ -148,33 +157,45 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { var sql = MediaSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) - .OrderBy(x => x.Level, x => x.SortOrder); + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateMediaNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateMediaNodeKit(row); } public IEnumerable GetBranchMediaSources(IScope scope, int id) { var syntax = scope.SqlContext.SqlSyntax; - var sql = MediaSourcesSelect(scope, s => s - - .InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) - + var sql = MediaSourcesSelect(scope, + s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) .Where(x => x.NodeId == id, "x") - .OrderBy(x => x.Level, x => x.SortOrder); + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateMediaNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateMediaNodeKit(row); } public IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids) { + if (!ids.Any()) yield break; + var sql = MediaSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) .WhereIn(x => x.ContentTypeId, ids) - .OrderBy(x => x.Level, x => x.SortOrder); + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateMediaNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateMediaNodeKit(row); } private static ContentNodeKit CreateContentNodeKit(ContentSourceDto dto) @@ -203,7 +224,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource VersionDate = dto.EditVersionDate, WriterId = dto.EditWriterId, Properties = nested.PropertyData, - CultureInfos = nested.CultureData + CultureInfos = nested.CultureData, + UrlSegment = nested.UrlSegment }; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDataSource.cs index 323d954980..ec3ab38e84 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDataSource.cs @@ -8,14 +8,70 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// internal interface IDataSource { + //TODO: For these required sort orders, would sorting on Path 'just work'? + ContentNodeKit GetContentSource(IScope scope, int id); + + /// + /// Returns all content ordered by level + sortOrder + /// + /// + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// IEnumerable GetAllContentSources(IScope scope); + + /// + /// Returns branch for content ordered by level + sortOrder + /// + /// + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// IEnumerable GetBranchContentSources(IScope scope, int id); + + /// + /// Returns content by Ids ordered by level + sortOrder + /// + /// + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids); ContentNodeKit GetMediaSource(IScope scope, int id); + + /// + /// Returns all media ordered by level + sortOrder + /// + /// + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// IEnumerable GetAllMediaSources(IScope scope); - IEnumerable GetBranchMediaSources(IScope scope, int id); + + /// + /// Returns branch for media ordered by level + sortOrder + /// + /// + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// + IEnumerable GetBranchMediaSources(IScope scope, int id); // must order by level, sortOrder + + /// + /// Returns media by Ids ordered by level + sortOrder + /// + /// + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DomainCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/DomainCache.cs index 896a04a0b3..6bc0de7268 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DomainCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DomainCache.cs @@ -4,16 +4,23 @@ using Umbraco.Web.Routing; namespace Umbraco.Web.PublishedCache.NuCache { + /// + /// Implements for NuCache. + /// internal class DomainCache : IDomainCache { private readonly SnapDictionary.Snapshot _snapshot; + /// + /// Initializes a new instance of the class. + /// public DomainCache(SnapDictionary.Snapshot snapshot, string defaultCulture) { _snapshot = snapshot; - DefaultCulture = defaultCulture; // capture - fast + DefaultCulture = defaultCulture; } + /// public IEnumerable GetAll(bool includeWildcards) { var list = _snapshot.GetAll(); @@ -21,17 +28,23 @@ namespace Umbraco.Web.PublishedCache.NuCache return list; } - public IEnumerable GetAssigned(int contentId, bool includeWildcards) + /// + public IEnumerable GetAssigned(int documentId, bool includeWildcards = false) { // probably this could be optimized with an index // but then we'd need a custom DomainStore of some sort var list = _snapshot.GetAll(); - list = list.Where(x => x.ContentId == contentId); + list = list.Where(x => x.ContentId == documentId); if (includeWildcards == false) list = list.Where(x => x.IsWildcard == false); return list; } + /// + public bool HasAssigned(int documentId, bool includeWildcards = false) + => documentId > 0 && GetAssigned(documentId, includeWildcards).Any(); + + /// public string DefaultCulture { get; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs index 112ccd9931..182086ed7f 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs @@ -14,17 +14,15 @@ namespace Umbraco.Web.PublishedCache.NuCache internal class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableData, IDisposable { private readonly ContentStore.Snapshot _snapshot; - private readonly IAppCache _snapshotCache; - private readonly IAppCache _elementsCache; + private readonly IVariationContextAccessor _variationContextAccessor; #region Constructors - public MediaCache(bool previewDefault, ContentStore.Snapshot snapshot, IAppCache snapshotCache, IAppCache elementsCache) + public MediaCache(bool previewDefault, ContentStore.Snapshot snapshot, IVariationContextAccessor variationContextAccessor) : base(previewDefault) { _snapshot = snapshot; - _snapshotCache = snapshotCache; - _elementsCache = elementsCache; + _variationContextAccessor = variationContextAccessor; } #endregion @@ -65,30 +63,16 @@ namespace Umbraco.Web.PublishedCache.NuCache return n != null; } - public override IEnumerable GetAtRoot(bool preview) + IEnumerable INavigableData.GetAtRoot(bool preview) => GetAtRoot(preview); + + public override IEnumerable GetAtRoot(bool preview, string culture = null) { - if (PublishedSnapshotService.CacheContentCacheRoots == false) - return GetAtRootNoCache(); + // handle context culture for variant + if (culture == null) + culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; - var cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing - ? _elementsCache - : _snapshotCache; - - if (cache == null) - return GetAtRootNoCache(); - - // note: ToArray is important here, we want to cache the result, not the function! - return (IEnumerable)cache.Get( - CacheKeys.MediaCacheRoots(false), // ignore preview, only 1 key! - () => GetAtRootNoCache().ToArray()); - } - - private IEnumerable GetAtRootNoCache() - { - var c = _snapshot.GetAtRoot(); - - // ignore preview, there's only draft for media - return c.Select(n => n.PublishedModel); + var atRoot = _snapshot.GetAtRoot().Select(x => x.PublishedModel); + return culture == "*" ? atRoot : atRoot.Where(x => x.IsInvariantOrHasCulture(culture)); } public override bool HasContent(bool preview) @@ -171,12 +155,12 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public override PublishedContentType GetContentType(int id) + public override IPublishedContentType GetContentType(int id) { return _snapshot.GetContentType(id); } - public override PublishedContentType GetContentType(string alias) + public override IPublishedContentType GetContentType(string alias) { return _snapshot.GetContentType(alias); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs index f7ffe73109..2e196f629e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs @@ -8,7 +8,6 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Security; using Umbraco.Core.Services; -using Umbraco.Core.Services.Implement; using Umbraco.Core.Xml.XPath; using Umbraco.Web.PublishedCache.NuCache.Navigable; @@ -19,7 +18,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; public readonly IVariationContextAccessor VariationContextAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IEntityXmlSerializer _entitySerializer; private readonly IAppCache _snapshotCache; private readonly IMemberService _memberService; @@ -27,12 +25,11 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly bool _previewDefault; public MemberCache(bool previewDefault, IAppCache snapshotCache, IMemberService memberService, PublishedContentTypeCache contentTypeCache, - IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IUmbracoContextAccessor umbracoContextAccessor, IEntityXmlSerializer entitySerializer) + IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IEntityXmlSerializer entitySerializer) { _snapshotCache = snapshotCache; _publishedSnapshotAccessor = publishedSnapshotAccessor; VariationContextAccessor = variationContextAccessor; - _umbracoContextAccessor = umbracoContextAccessor; _entitySerializer = entitySerializer; _memberService = memberService; _previewDefault = previewDefault; @@ -68,14 +65,14 @@ namespace Umbraco.Web.PublishedCache.NuCache var member = _memberService.GetById(memberId); return member == null ? null - : PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor, VariationContextAccessor, _umbracoContextAccessor); + : PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor, VariationContextAccessor); }); } private IPublishedContent /*IPublishedMember*/ GetById(IMember member, bool previewing) { return GetCacheItem(CacheKeys.MemberCacheMember("ById", _previewDefault, member.Id), () => - PublishedMember.Create(member, GetContentType(member.ContentTypeId), previewing, _publishedSnapshotAccessor, VariationContextAccessor, _umbracoContextAccessor)); + PublishedMember.Create(member, GetContentType(member.ContentTypeId), previewing, _publishedSnapshotAccessor, VariationContextAccessor)); } public IPublishedContent /*IPublishedMember*/ GetByProviderKey(object key) @@ -110,7 +107,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public IPublishedContent /*IPublishedMember*/ GetByMember(IMember member) { - return PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor, VariationContextAccessor, _umbracoContextAccessor); + return PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor, VariationContextAccessor); } public IEnumerable GetAtRoot(bool preview) @@ -118,7 +115,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // because members are flat (not a tree) everything is at root // because we're loading everything... let's just not cache? var members = _memberService.GetAllMembers(); - return members.Select(m => PublishedMember.Create(m, GetContentType(m.ContentTypeId), preview, _publishedSnapshotAccessor, VariationContextAccessor, _umbracoContextAccessor)); + return members.Select(m => PublishedMember.Create(m, GetContentType(m.ContentTypeId), preview, _publishedSnapshotAccessor, VariationContextAccessor)); } public XPathNavigator CreateNavigator() @@ -151,12 +148,12 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public PublishedContentType GetContentType(int id) + public IPublishedContentType GetContentType(int id) { return _contentTypeCache.Get(PublishedItemType.Member, id); } - public PublishedContentType GetContentType(string alias) + public IPublishedContentType GetContentType(string alias) { return _contentTypeCache.Get(PublishedItemType.Member, alias); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContentType.cs b/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContentType.cs index 18bf3ead13..310dae9dd2 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContentType.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContentType.cs @@ -22,10 +22,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.Navigable // changes, but they are replaced by a new instance, so our map here will clean itself automatically and // we don't have to manage cache - ConditionalWeakTable does not prevent keys from being GCed - private static readonly ConditionalWeakTable TypesMap - = new ConditionalWeakTable(); + private static readonly ConditionalWeakTable TypesMap + = new ConditionalWeakTable(); - public static NavigableContentType GetContentType(PublishedContentType contentType) + public static NavigableContentType GetContentType(IPublishedContentType contentType) { return TypesMap.GetOrCreateValue(contentType).EnsureInitialized(contentType); } @@ -49,7 +49,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.Navigable }; } - private NavigableContentType EnsureInitialized(PublishedContentType contentType) + private NavigableContentType EnsureInitialized(IPublishedContentType contentType) { lock (_locko) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index 72fa39e7e4..f748fd555c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -15,7 +15,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // register the NuCache published snapshot service // must register default options, required in the service ctor - composition.Register(factory => new PublishedSnapshotService.Options()); + composition.Register(factory => new PublishedSnapshotServiceOptions()); composition.SetPublishedSnapshotService(); // add the NuCache health check (hidden from type finder) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs index 2c8bc94d90..86023bb302 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs @@ -38,12 +38,12 @@ namespace Umbraco.Web.PublishedCache.NuCache private string _valuesCacheKey; // initializes a published content property with no value - public Property(PublishedPropertyType propertyType, PublishedContent content, IPublishedSnapshotAccessor publishedSnapshotAccessor, PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) + public Property(IPublishedPropertyType propertyType, PublishedContent content, IPublishedSnapshotAccessor publishedSnapshotAccessor, PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) : this(propertyType, content, null, publishedSnapshotAccessor, referenceCacheLevel) { } // initializes a published content property with a value - public Property(PublishedPropertyType propertyType, PublishedContent content, PropertyData[] sourceValues, IPublishedSnapshotAccessor publishedSnapshotAccessor, PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) + public Property(IPublishedPropertyType propertyType, PublishedContent content, PropertyData[] sourceValues, IPublishedSnapshotAccessor publishedSnapshotAccessor, PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) : base(propertyType, referenceCacheLevel) { if (sourceValues != null) @@ -90,7 +90,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // determines whether a property has value public override bool HasValue(string culture = null, string segment = null) { - _content.VariationContextAccessor.ContextualizeVariation(_variations, ref culture, ref segment); + _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); var value = GetSourceValue(culture, segment); var hasValue = PropertyType.IsValue(value, PropertyValueLevel.Source); @@ -194,7 +194,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override object GetSourceValue(string culture = null, string segment = null) { - _content.VariationContextAccessor.ContextualizeVariation(_variations, ref culture, ref segment); + _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); if (culture == "" && segment == "") return _sourceValue; @@ -208,7 +208,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override object GetValue(string culture = null, string segment = null) { - _content.VariationContextAccessor.ContextualizeVariation(_variations, ref culture, ref segment); + _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); object value; lock (_locko) @@ -229,7 +229,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override object GetXPathValue(string culture = null, string segment = null) { - _content.VariationContextAccessor.ContextualizeVariation(_variations, ref culture, ref segment); + _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); lock (_locko) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index 057823f29c..d80affbfa9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Composing; using Umbraco.Web.Models; @@ -22,9 +23,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentNode contentNode, ContentData contentData, IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor) - : base(umbracoContextAccessor) + IVariationContextAccessor variationContextAccessor) { _contentNode = contentNode ?? throw new ArgumentNullException(nameof(contentNode)); ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData)); @@ -67,12 +66,8 @@ namespace Umbraco.Web.PublishedCache.NuCache return user?.Name; } - // (see ContentNode.CloneParent) - public PublishedContent( - ContentNode contentNode, - PublishedContent origin, - IUmbracoContextAccessor umbracoContextAccessor) - : base(umbracoContextAccessor) + // used when cloning in ContentNode + public PublishedContent(ContentNode contentNode, PublishedContent origin) { _contentNode = contentNode; _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; @@ -89,10 +84,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } // clone for previewing as draft a published content that is published and has no draft - private PublishedContent( - PublishedContent origin, - IUmbracoContextAccessor umbracoContextAccessor) - : base(umbracoContextAccessor) + private PublishedContent(PublishedContent origin) { _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; VariationContextAccessor = origin.VariationContextAccessor; @@ -119,45 +111,16 @@ namespace Umbraco.Web.PublishedCache.NuCache internal static Func GetMediaByIdFunc { get; set; } = (publishedShapshot, previewing, id) => publishedShapshot.Media.GetById(previewing, id); - private IPublishedContent GetContentById(bool previewing, int id) + private Func GetGetterById() { - return GetContentByIdFunc(_publishedSnapshotAccessor.PublishedSnapshot, previewing, id); - } - - private IEnumerable GetContentByIds(bool previewing, IEnumerable ids) - { - var publishedSnapshot = _publishedSnapshotAccessor.PublishedSnapshot; - - // beware! the loop below CANNOT be converted to query such as: - //return ids.Select(x => _getContentByIdFunc(publishedSnapshot, previewing, x)).Where(x => x != null); - // because it would capture the published snapshot and cause all sorts of issues - // - // we WANT to get the actual current published snapshot each time we run - - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var id in ids) + switch (ContentType.ItemType) { - var content = GetContentByIdFunc(publishedSnapshot, previewing, id); - if (content != null) yield return content; - } - } - - private IPublishedContent GetMediaById(bool previewing, int id) - { - return GetMediaByIdFunc(_publishedSnapshotAccessor.PublishedSnapshot, previewing, id); - } - - private IEnumerable GetMediaByIds(bool previewing, IEnumerable ids) - { - var publishedShapshot = _publishedSnapshotAccessor.PublishedSnapshot; - - // see note above for content - - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var id in ids) - { - var content = GetMediaByIdFunc(publishedShapshot, previewing, id); - if (content != null) yield return content; + case PublishedItemType.Content: + return GetContentByIdFunc; + case PublishedItemType.Media: + return GetMediaByIdFunc; + default: + throw new PanicException("invalid item type"); } } @@ -166,7 +129,7 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content Type /// - public override PublishedContentType ContentType => _contentNode.ContentType; + public override IPublishedContentType ContentType => _contentNode.ContentType; #endregion @@ -184,38 +147,6 @@ namespace Umbraco.Web.PublishedCache.NuCache /// public override int Id => _contentNode.Id; - /// - public override string Name - { - get - { - if (!ContentType.VariesByCulture()) - return ContentData.Name; - - var culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; - if (culture == "") - return ContentData.Name; - - return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos.Name : null; - } - } - - /// - public override string UrlSegment - { - get - { - if (!ContentType.VariesByCulture()) - return _urlSegment; - - var culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; - if (culture == "") - return _urlSegment; - - return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos.UrlSegment : null; - } - } - /// public override int SortOrder => _contentNode.SortOrder; @@ -232,6 +163,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override int CreatorId => _contentNode.CreatorId; /// + [Obsolete("Use CreatorName(IUserService) extension instead")] public override string CreatorName => GetProfileNameById(_contentNode.CreatorId); /// @@ -241,43 +173,33 @@ namespace Umbraco.Web.PublishedCache.NuCache public override int WriterId => ContentData.WriterId; /// + [Obsolete("Use WriterName(IUserService) extension instead")] public override string WriterName => GetProfileNameById(ContentData.WriterId); /// public override DateTime UpdateDate => ContentData.VersionDate; - private IReadOnlyDictionary _cultureInfos; - - private static readonly IReadOnlyDictionary NoCultureInfos = new Dictionary(); - - /// - public override PublishedCultureInfo GetCulture(string culture = null) - { - // handle context culture - if (culture == null) - culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; - - // no invariant culture infos - if (culture == "") return null; - - // get - return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos : null; - } + // ReSharper disable once CollectionNeverUpdated.Local + private static readonly IReadOnlyDictionary EmptyCultures = new Dictionary(); + private IReadOnlyDictionary _cultures; /// public override IReadOnlyDictionary Cultures { get { - if (!ContentType.VariesByCulture()) - return NoCultureInfos; + if (_cultures != null) return _cultures; - if (_cultureInfos != null) return _cultureInfos; + if (!ContentType.VariesByCulture()) + return _cultures = new Dictionary + { + { "", new PublishedCultureInfo("", ContentData.Name, _urlSegment, CreateDate) } + }; if (ContentData.CultureInfos == null) - throw new Exception("oops: _contentDate.CultureInfos is null."); + throw new PanicException("_contentDate.CultureInfos is null."); - return _cultureInfos = ContentData.CultureInfos + return _cultures = ContentData.CultureInfos .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value.Name, x.Value.UrlSegment, x.Value.Date), StringComparer.OrdinalIgnoreCase); } } @@ -313,8 +235,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // invariant content items) // if there is no 'published' published content, no culture can be published - var hasPublished = _contentNode.PublishedContent != null; - if (!hasPublished) + if (!_contentNode.HasPublished) return false; // if there is a 'published' published content, and does not vary = published @@ -327,7 +248,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // there is a 'published' published content, and varies // = depends on the culture - return _contentNode.PublishedContent.ContentData.CultureInfos.ContainsKey(culture); + return _contentNode.HasPublishedCulture(culture); } #endregion @@ -339,61 +260,48 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - // have to use the "current" cache because a PublishedContent can be shared - // amongst many snapshots and other content depend on the snapshots - switch (_contentNode.ContentType.ItemType) - { - case PublishedItemType.Content: - return GetContentById(IsPreviewing, _contentNode.ParentContentId); - case PublishedItemType.Media: - return GetMediaById(IsPreviewing, _contentNode.ParentContentId); - default: - throw new Exception($"Panic: unsupported item type \"{_contentNode.ContentType.ItemType}\"."); - } + var getById = GetGetterById(); + var publishedSnapshot = _publishedSnapshotAccessor.PublishedSnapshot; + return getById(publishedSnapshot, IsPreviewing, ParentId); } } /// - public override IEnumerable Children + public override IEnumerable ChildrenForAllCultures { get { - var cache = GetAppropriateCache(); - if (cache == null || PublishedSnapshotService.CachePublishedContentChildren == false) - return GetChildren(); + var getById = GetGetterById(); + var publishedSnapshot = _publishedSnapshotAccessor.PublishedSnapshot; + var id = _contentNode.FirstChildContentId; - // note: ToArray is important here, we want to cache the result, not the function! - return (IEnumerable)cache.Get(ChildrenCacheKey, () => GetChildren().ToArray()); + while (id > 0) + { + // is IsPreviewing is false, then this can return null + var content = getById(publishedSnapshot, IsPreviewing, id); + + if (content != null) + { + yield return content; + } + else + { + // but if IsPreviewing is true, we should have a child + if (IsPreviewing) + throw new PanicException($"failed to get content with id={id}"); + + // if IsPreviewing is false, get the unpublished child nevertheless + // we need it to keep enumerating children! but we don't return it + content = getById(publishedSnapshot, true, id); + if (content == null) + throw new PanicException($"failed to get content with id={id}"); + } + + id = UnwrapIPublishedContent(content)._contentNode.NextSiblingContentId; + } } } - private string _childrenCacheKey; - - private string ChildrenCacheKey => _childrenCacheKey ?? (_childrenCacheKey = CacheKeys.PublishedContentChildren(Key, IsPreviewing)); - - private IEnumerable GetChildren() - { - IEnumerable c; - switch (_contentNode.ContentType.ItemType) - { - case PublishedItemType.Content: - c = GetContentByIds(IsPreviewing, _contentNode.ChildContentIds); - break; - case PublishedItemType.Media: - c = GetMediaByIds(IsPreviewing, _contentNode.ChildContentIds); - break; - default: - throw new Exception("oops"); - } - - return c.OrderBy(x => x.SortOrder); - - // notes: - // _contentNode.ChildContentIds is an unordered int[] - // needs to fetch & sort - do it only once, lazily, though - // Q: perfs-wise, is it better than having the store managed an ordered list - } - #endregion #region Properties @@ -420,10 +328,10 @@ namespace Umbraco.Web.PublishedCache.NuCache // beware what you use that one for - you don't want to cache its result private IAppCache GetAppropriateCache() { - var publishedSnapshot = (PublishedSnapshot)_publishedSnapshotAccessor.PublishedSnapshot; + var publishedSnapshot = _publishedSnapshotAccessor.PublishedSnapshot; var cache = publishedSnapshot == null ? null - : ((IsPreviewing == false || PublishedSnapshotService.FullCacheWhenPreviewing) && (ItemType != PublishedItemType.Member) + : ((IsPreviewing == false || PublishedSnapshotService.FullCacheWhenPreviewing) && (ContentType.ItemType != PublishedItemType.Member) ? publishedSnapshot.ElementsCache : publishedSnapshot.SnapshotCache); return cache; @@ -431,7 +339,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private IAppCache GetCurrentSnapshotCache() { - var publishedSnapshot = (PublishedSnapshot)_publishedSnapshotAccessor.PublishedSnapshot; + var publishedSnapshot = _publishedSnapshotAccessor.PublishedSnapshot; return publishedSnapshot?.SnapshotCache; } @@ -451,7 +359,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // used by navigable content // includes all children, published or unpublished // NavigableNavigator takes care of selecting those it wants - internal IList ChildIds => _contentNode.ChildContentIds; + // note: this is not efficient - we do not try to be (would require a double-linked list) + internal IList ChildIds => Children.Select(x => x.Id).ToList(); // used by Property // gets a value indicating whether the content or media exists in @@ -470,18 +379,16 @@ namespace Umbraco.Web.PublishedCache.NuCache return this; var cache = GetAppropriateCache(); - if (cache == null) return new PublishedContent(this, UmbracoContextAccessor).CreateModel(); - return (IPublishedContent)cache.Get(AsPreviewingCacheKey, () => new PublishedContent(this, UmbracoContextAccessor).CreateModel()); + if (cache == null) return new PublishedContent(this).CreateModel(); + return (IPublishedContent)cache.Get(AsPreviewingCacheKey, () => new PublishedContent(this).CreateModel()); } // used by Navigable.Source,... internal static PublishedContent UnwrapIPublishedContent(IPublishedContent content) { - PublishedContentWrapped wrapped; - while ((wrapped = content as PublishedContentWrapped) != null) + while (content is PublishedContentWrapped wrapped) content = wrapped.Unwrap(); - var inner = content as PublishedContent; - if (inner == null) + if (!(content is PublishedContent inner)) throw new InvalidOperationException("Innermost content is not PublishedContent."); return inner; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs index 11ca169300..dc608fe391 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs @@ -20,21 +20,18 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentNode contentNode, ContentData contentData, IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor - ) - : base(contentNode, contentData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor) + IVariationContextAccessor variationContextAccessor) + : base(contentNode, contentData, publishedSnapshotAccessor, variationContextAccessor) { _member = member; } public static IPublishedContent Create( IMember member, - PublishedContentType contentType, + IPublishedContentType contentType, bool previewing, IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + IVariationContextAccessor variationContextAccessor) { var d = new ContentData { @@ -50,10 +47,10 @@ namespace Umbraco.Web.PublishedCache.NuCache member.Level, member.Path, member.SortOrder, member.ParentId, member.CreateDate, member.CreatorId); - return new PublishedMember(member, n, d, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor).CreateModel(); + return new PublishedMember(member, n, d, publishedSnapshotAccessor, variationContextAccessor).CreateModel(); } - private static Dictionary GetPropertyValues(PublishedContentType contentType, IMember member) + private static Dictionary GetPropertyValues(IPublishedContentType contentType, IMember member) { // see node in PublishedSnapshotService // we do not (want to) support ConvertDbToXml/String @@ -91,7 +88,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return properties; } - private static void AddIf(PublishedContentType contentType, IDictionary properties, string alias, object value) + private static void AddIf(IPublishedContentType contentType, IDictionary properties, string alias, object value) { var propertyType = contentType.GetPropertyType(alias); if (propertyType == null || propertyType.IsUserProperty) return; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 073428336d..7e78b2e96f 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -4,13 +4,13 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Web; +using System.Threading; +using System.Threading.Tasks; using CSharpTest.Net.Collections; using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -32,20 +32,20 @@ using File = System.IO.File; namespace Umbraco.Web.PublishedCache.NuCache { - class PublishedSnapshotService : PublishedSnapshotServiceBase + + internal class PublishedSnapshotService : PublishedSnapshotServiceBase { private readonly ServiceContext _serviceContext; private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IScopeProvider _scopeProvider; private readonly IDataSource _dataSource; - private readonly ILogger _logger; + private readonly IProfilingLogger _logger; private readonly IDocumentRepository _documentRepository; private readonly IMediaRepository _mediaRepository; private readonly IMemberRepository _memberRepository; private readonly IGlobalSettings _globalSettings; - private readonly ISiteDomainHelper _siteDomainHelper; private readonly IEntityXmlSerializer _entitySerializer; + private readonly IPublishedModelFactory _publishedModelFactory; private readonly IDefaultCultureAccessor _defaultCultureAccessor; private readonly UrlSegmentProviderCollection _urlSegmentProviders; @@ -56,10 +56,12 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly ContentStore _mediaStore; private readonly SnapDictionary _domainStore; private readonly object _storesLock = new object(); + private readonly object _elementsLock = new object(); private BPlusTree _localContentDb; private BPlusTree _localMediaDb; - private readonly bool _localDbExists; + private bool _localContentDbExists; + private bool _localMediaDbExists; // define constant - determines whether to use cache when previewing // to store eg routes, property converted values, anything - caching @@ -67,30 +69,18 @@ namespace Umbraco.Web.PublishedCache.NuCache // so making it configurable. public static readonly bool FullCacheWhenPreviewing = true; - // define constant - determines whether to cache the published content - // objects (in the elements cache, or snapshot cache, depending on preview) - // or to re-fetch them all the time. caching is faster but uses more - // memory. not sure what we want. - public static readonly bool CachePublishedContentChildren = true; - - // define constant - determines whether to cache the content cache root - // objects (in the elements cache, or snapshot cache, depending on preview) - // or to re-fetch them all the time. caching is faster but uses more - // memory - not sure what we want. - public static readonly bool CacheContentCacheRoots = true; - #region Constructors //private static int _singletonCheck; - public PublishedSnapshotService(Options options, IMainDom mainDom, IRuntimeState runtime, + public PublishedSnapshotService(PublishedSnapshotServiceOptions options, IMainDom mainDom, IRuntimeState runtime, ServiceContext serviceContext, IPublishedContentTypeFactory publishedContentTypeFactory, IdkMap idkMap, - IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IScopeProvider scopeProvider, + IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IProfilingLogger logger, IScopeProvider scopeProvider, IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository, IDefaultCultureAccessor defaultCultureAccessor, - IDataSource dataSource, IGlobalSettings globalSettings, ISiteDomainHelper siteDomainHelper, - IEntityXmlSerializer entitySerializer, IPublishedModelFactory publishedModelFactory, + IDataSource dataSource, IGlobalSettings globalSettings, + IEntityXmlSerializer entitySerializer, + IPublishedModelFactory publishedModelFactory, UrlSegmentProviderCollection urlSegmentProviders) : base(publishedSnapshotAccessor, variationContextAccessor) { @@ -99,7 +89,6 @@ namespace Umbraco.Web.PublishedCache.NuCache _serviceContext = serviceContext; _publishedContentTypeFactory = publishedContentTypeFactory; - _umbracoContextAccessor = umbracoContextAccessor; _dataSource = dataSource; _logger = logger; _scopeProvider = scopeProvider; @@ -108,12 +97,12 @@ namespace Umbraco.Web.PublishedCache.NuCache _memberRepository = memberRepository; _defaultCultureAccessor = defaultCultureAccessor; _globalSettings = globalSettings; - _siteDomainHelper = siteDomainHelper; _urlSegmentProviders = urlSegmentProviders; // we need an Xml serializer here so that the member cache can support XPath, // for members this is done by navigating the serialized-to-xml member _entitySerializer = entitySerializer; + _publishedModelFactory = publishedModelFactory; // we always want to handle repository events, configured or not // assuming no repository event will trigger before the whole db is ready @@ -128,48 +117,37 @@ namespace Umbraco.Web.PublishedCache.NuCache if (runtime.Level != RuntimeLevel.Run) return; - if (options.IgnoreLocalDb == false) + // lock this entire call, we only want a single thread to be accessing the stores at once and within + // the call below to mainDom.Register, a callback may occur on a threadpool thread to MainDomRelease + // at the same time as we are trying to write to the stores. MainDomRelease also locks on _storesLock so + // it will not be able to close the stores until we are done populating (if the store is empty) + lock (_storesLock) { - var registered = mainDom.Register( - null, - () => - { - lock (_storesLock) - { - _contentStore.ReleaseLocalDb(); - _localContentDb = null; - _mediaStore.ReleaseLocalDb(); - _localMediaDb = null; - } - }); - - if (registered) + if (options.IgnoreLocalDb == false) { - var path = GetLocalFilesPath(); - var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); - var localMediaDbPath = Path.Combine(path, "NuCache.Media.db"); - _localDbExists = System.IO.File.Exists(localContentDbPath) && System.IO.File.Exists(localMediaDbPath); + var registered = mainDom.Register(MainDomRegister, MainDomRelease); - // if both local databases exist then GetTree will open them, else new databases will be created - _localContentDb = BTree.GetTree(localContentDbPath, _localDbExists); - _localMediaDb = BTree.GetTree(localMediaDbPath, _localDbExists); + // stores are created with a db so they can write to it, but they do not read from it, + // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to + // figure out whether it can read the databases or it should populate them from sql + + _logger.Info("Creating the content store, localContentDbExists? {LocalContentDbExists}", _localContentDbExists); + _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localContentDb); + _logger.Info("Creating the media store, localMediaDbExists? {LocalMediaDbExists}", _localMediaDbExists); + _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localMediaDb); + } + else + { + _logger.Info("Creating the content store (local db ignored)"); + _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger); + _logger.Info("Creating the media store (local db ignored)"); + _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger); } - // stores are created with a db so they can write to it, but they do not read from it, - // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to - // figure out whether it can read the databases or it should populate them from sql - _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger, _localContentDb); - _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger, _localMediaDb); - } - else - { - _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger); - _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger); - } + _domainStore = new SnapDictionary(); - _domainStore = new SnapDictionary(); - - publishedModelFactory.WithSafeLiveFactory(LoadCaches); + LoadCachesOnStartup(); + } Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? default; int GetId(ContentStore store, Guid uid) => store.LiveSnapshot.Get(uid)?.Id ?? default; @@ -181,37 +159,99 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - private void LoadCaches() + /// + /// Install phase of + /// + /// + /// This is inside of a lock in MainDom so this is guaranteed to run if MainDom was acquired and guaranteed + /// to not run if MainDom wasn't acquired. + /// If MainDom was not acquired, then _localContentDb and _localMediaDb will remain null which means this appdomain + /// will load in published content via the DB and in that case this appdomain will probably not exist long enough to + /// serve more than a page of content. + /// + private void MainDomRegister() { + var path = GetLocalFilesPath(); + var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); + var localMediaDbPath = Path.Combine(path, "NuCache.Media.db"); + + _localContentDbExists = File.Exists(localContentDbPath); + _localMediaDbExists = File.Exists(localMediaDbPath); + + // if both local databases exist then GetTree will open them, else new databases will be created + _localContentDb = BTree.GetTree(localContentDbPath, _localContentDbExists); + _localMediaDb = BTree.GetTree(localMediaDbPath, _localMediaDbExists); + + _logger.Info("Registered with MainDom, localContentDbExists? {LocalContentDbExists}, localMediaDbExists? {LocalMediaDbExists}", _localContentDbExists, _localMediaDbExists); + } + + /// + /// Release phase of MainDom + /// + /// + /// This will execute on a threadpool thread + /// + private void MainDomRelease() + { + _logger.Debug("Releasing from MainDom..."); + lock (_storesLock) { - // populate the stores + _logger.Debug("Releasing content store..."); + _contentStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned + _localContentDb = null; - try - { - if (_localDbExists) - { - LockAndLoadContent(LoadContentFromLocalDbLocked); - LockAndLoadMedia(LoadMediaFromLocalDbLocked); - } - else - { - LockAndLoadContent(LoadContentFromDatabaseLocked); - LockAndLoadMedia(LoadMediaFromDatabaseLocked); - } + _logger.Debug("Releasing media store..."); + _mediaStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned + _localMediaDb = null; - LockAndLoadDomains(); - } - catch (Exception ex) - { - _logger.Fatal(ex, "Panic, exception while loading cache data."); - } - - // finally, cache is ready! - _isReady = true; + _logger.Info("Released from MainDom"); } } + /// + /// Populates the stores + /// + /// This is called inside of a lock for _storesLock + private void LoadCachesOnStartup() + { + var okContent = false; + var okMedia = false; + + try + { + if (_localContentDbExists) + { + okContent = LockAndLoadContent(scope => LoadContentFromLocalDbLocked(true)); + if (!okContent) + _logger.Warn("Loading content from local db raised warnings, will reload from database."); + } + + if (_localMediaDbExists) + { + okMedia = LockAndLoadMedia(scope => LoadMediaFromLocalDbLocked(true)); + if (!okMedia) + _logger.Warn("Loading media from local db raised warnings, will reload from database."); + } + + if (!okContent) + LockAndLoadContent(scope => LoadContentFromDatabaseLocked(scope, true)); + + if (!okMedia) + LockAndLoadMedia(scope => LoadMediaFromDatabaseLocked(scope, true)); + + LockAndLoadDomains(); + } + catch (Exception ex) + { + _logger.Fatal(ex, "Panic, exception while loading cache data."); + throw; + } + + // finally, cache is ready! + _isReady = true; + } + private void InitializeRepositoryEvents() { // TODO: The reason these events are in the repository is for legacy, the events should exist at the service @@ -234,6 +274,8 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentTypeService.ScopedRefreshedEntity += OnContentTypeRefreshedEntity; MediaTypeService.ScopedRefreshedEntity += OnMediaTypeRefreshedEntity; MemberTypeService.ScopedRefreshedEntity += OnMemberTypeRefreshedEntity; + + LocalizationService.SavedLanguage += OnLanguageSaved; } private void TearDownRepositoryEvents() @@ -251,6 +293,8 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentTypeService.ScopedRefreshedEntity -= OnContentTypeRefreshedEntity; MediaTypeService.ScopedRefreshedEntity -= OnMediaTypeRefreshedEntity; MemberTypeService.ScopedRefreshedEntity -= OnMemberTypeRefreshedEntity; + + LocalizationService.SavedLanguage -= OnLanguageSaved; } public override void Dispose() @@ -259,19 +303,6 @@ namespace Umbraco.Web.PublishedCache.NuCache base.Dispose(); } - public class Options - { - // disabled: prevents the published snapshot from updating and exposing changes - // or even creating a new published snapshot to see changes, uses old cache = bad - // - //// indicates that the snapshot cache should reuse the application request cache - //// otherwise a new cache object would be created for the snapshot specifically, - //// which is the default - web boot manager uses this to optimize facades - //public bool PublishedSnapshotCacheIsApplicationRequestCache; - - public bool IgnoreLocalDb; - } - #endregion #region Local files @@ -328,8 +359,10 @@ namespace Umbraco.Web.PublishedCache.NuCache // before I read it? NO! because the WHOLE content tree is read-locked using WithReadLocked. // don't panic. - private void LockAndLoadContent(Action action) + private bool LockAndLoadContent(Func action) { + + // first get a writer, then a scope // if there already is a scope, the writer will attach to it // otherwise, it will only exist here - cheap @@ -337,12 +370,13 @@ namespace Umbraco.Web.PublishedCache.NuCache using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTree); - action(scope); + var ok = action(scope); scope.Complete(); + return ok; } } - private void LoadContentFromDatabaseLocked(IScope scope) + private bool LoadContentFromDatabaseLocked(IScope scope, bool onStartup) { // locks: // contentStore is wlocked (1 thread) @@ -350,32 +384,35 @@ namespace Umbraco.Web.PublishedCache.NuCache var contentTypes = _serviceContext.ContentTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _contentStore.UpdateContentTypes(null, contentTypes, null); - _localContentDb?.Clear(); + _contentStore.SetAllContentTypesLocked(contentTypes); - _logger.Debug("Loading content from database..."); - var sw = Stopwatch.StartNew(); - // IMPORTANT GetAllContentSources sorts kits by level - var kits = _dataSource.GetAllContentSources(scope); - _contentStore.SetAll(kits); - sw.Stop(); - _logger.Debug("Loaded content from database ({Duration}ms)", sw.ElapsedMilliseconds); + using (_logger.TraceDuration("Loading content from database")) + { + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! + + _localContentDb?.Clear(); + + // IMPORTANT GetAllContentSources sorts kits by level + parentId + sortOrder + var kits = _dataSource.GetAllContentSources(scope); + return onStartup ? _contentStore.SetAllFastSortedLocked(kits, true) : _contentStore.SetAllLocked(kits); + } } - private void LoadContentFromLocalDbLocked(IScope scope) + private bool LoadContentFromLocalDbLocked(bool onStartup) { var contentTypes = _serviceContext.ContentTypeService.GetAll() - .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _contentStore.UpdateContentTypes(null, contentTypes, null); + .Select(x => _publishedContentTypeFactory.CreateContentType(x)); + _contentStore.SetAllContentTypesLocked(contentTypes); - _logger.Debug("Loading content from local db..."); - var sw = Stopwatch.StartNew(); - var kits = _localContentDb.Select(x => x.Value) - .OrderBy(x => x.Node.Level); // IMPORTANT sort by level - _contentStore.SetAll(kits); - sw.Stop(); - _logger.Debug("Loaded content from local db ({Duration}ms)", sw.ElapsedMilliseconds); + using (_logger.TraceDuration("Loading content from local cache file")) + { + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! + + return LoadEntitiesFromLocalDbLocked(onStartup, _localContentDb, _contentStore, "content"); + } } // keep these around - might be useful @@ -400,50 +437,87 @@ namespace Umbraco.Web.PublishedCache.NuCache // _contentStore.Set(contentNode); //} - private void LockAndLoadMedia(Action action) + private bool LockAndLoadMedia(Func action) { // see note in LockAndLoadContent using (_mediaStore.GetScopedWriteLock(_scopeProvider)) using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTree); - action(scope); + var ok = action(scope); scope.Complete(); + return ok; } } - private void LoadMediaFromDatabaseLocked(IScope scope) + private bool LoadMediaFromDatabaseLocked(IScope scope, bool onStartup) { // locks & notes: see content var mediaTypes = _serviceContext.MediaTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.UpdateContentTypes(null, mediaTypes, null); + _mediaStore.SetAllContentTypesLocked(mediaTypes); - _localMediaDb?.Clear(); + using (_logger.TraceDuration("Loading media from database")) + { + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! - _logger.Debug("Loading media from database..."); - var sw = Stopwatch.StartNew(); - // IMPORTANT GetAllMediaSources sorts kits by level - var kits = _dataSource.GetAllMediaSources(scope); - _mediaStore.SetAll(kits); - sw.Stop(); - _logger.Debug("Loaded media from database ({Duration}ms)", sw.ElapsedMilliseconds); + _localMediaDb?.Clear(); + + _logger.Debug("Loading media from database..."); + // IMPORTANT GetAllMediaSources sorts kits by level + parentId + sortOrder + var kits = _dataSource.GetAllMediaSources(scope); + return onStartup ? _mediaStore.SetAllFastSortedLocked(kits, true) : _mediaStore.SetAllLocked(kits); + } } - private void LoadMediaFromLocalDbLocked(IScope scope) + private bool LoadMediaFromLocalDbLocked(bool onStartup) { var mediaTypes = _serviceContext.MediaTypeService.GetAll() - .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.UpdateContentTypes(null, mediaTypes, null); + .Select(x => _publishedContentTypeFactory.CreateContentType(x)); + _mediaStore.SetAllContentTypesLocked(mediaTypes); - _logger.Debug("Loading media from local db..."); - var sw = Stopwatch.StartNew(); - var kits = _localMediaDb.Select(x => x.Value) - .OrderBy(x => x.Node.Level); // IMPORTANT sort by level - _mediaStore.SetAll(kits); - sw.Stop(); - _logger.Debug("Loaded media from local db ({Duration}ms)", sw.ElapsedMilliseconds); + using (_logger.TraceDuration("Loading media from local cache file")) + { + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! + + return LoadEntitiesFromLocalDbLocked(onStartup, _localMediaDb, _mediaStore, "media"); + } + + } + + private bool LoadEntitiesFromLocalDbLocked(bool onStartup, BPlusTree localDb, ContentStore store, string entityType) + { + var kits = localDb.Select(x => x.Value) + .OrderBy(x => x.Node.Level) + .ThenBy(x => x.Node.ParentContentId) + .ThenBy(x => x.Node.SortOrder) // IMPORTANT sort by level + parentId + sortOrder + .ToList(); + + if (kits.Count == 0) + { + // If there's nothing in the local cache file, we should return false? YES even though the site legitately might be empty. + // Is it possible that the cache file is empty but the database is not? YES... (well, it used to be possible) + // * A new file is created when one doesn't exist, this will only be done when MainDom is acquired + // * The new file will be populated as soon as LoadCachesOnStartup is called + // * If the appdomain is going down the moment after MainDom was acquired and we've created an empty cache file, + // then the MainDom release callback is triggered from on a different thread, which will close the file and + // set the cache file reference to null. At this moment, it is possible that the file is closed and the + // reference is set to null BEFORE LoadCachesOnStartup which would mean that the current appdomain would load + // in the in-mem cache via DB calls, BUT this now means that there is an empty cache file which will be + // loaded by the next appdomain and it won't check if it's empty, it just assumes that since the cache + // file is there, that is correct. + + // Update: We will still return false here even though the above mentioned race condition has been fixed since we now + // lock the entire operation of creating/populating the cache file with the same lock as releasing/closing the cache file + + _logger.Info($"Tried to load {entityType} from the local cache file but it was empty."); + return false; + } + + return onStartup ? store.SetAllFastSortedLocked(kits, false) : store.SetAllLocked(kits); } // keep these around - might be useful @@ -549,7 +623,7 @@ namespace Umbraco.Web.PublishedCache.NuCache .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard))) { - _domainStore.Set(domain.Id, domain); + _domainStore.SetLocked(domain.Id, domain); } } @@ -597,10 +671,12 @@ namespace Umbraco.Web.PublishedCache.NuCache publishedChanged = publishedChanged2; } + if (draftChanged || publishedChanged) ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } + // Calling this method means we have a lock on the contentStore (i.e. GetScopedWriteLock) private void NotifyLocked(IEnumerable payloads, out bool draftChanged, out bool publishedChanged) { publishedChanged = false; @@ -610,7 +686,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // content (and content types) are read-locked while reading content // contentStore is wlocked (so readable, only no new views) // and it can be wlocked by 1 thread only at a time - // contentStore is write-locked during changes + // contentStore is write-locked during changes - see note above, calls to this method are wrapped in contentStore.GetScopedWriteLock foreach (var payload in payloads) { @@ -621,7 +697,7 @@ namespace Umbraco.Web.PublishedCache.NuCache using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTree); - LoadContentFromDatabaseLocked(scope); + LoadContentFromDatabaseLocked(scope, false); scope.Complete(); } draftChanged = publishedChanged = true; @@ -630,7 +706,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { - if (_contentStore.Clear(payload.Id)) + if (_contentStore.ClearLocked(payload.Id)) draftChanged = publishedChanged = true; continue; } @@ -651,9 +727,9 @@ namespace Umbraco.Web.PublishedCache.NuCache if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { // ?? should we do some RV check here? - // IMPORTANT GetbranchContentSources sorts kits by level + // IMPORTANT GetbranchContentSources sorts kits by level and by sort order var kits = _dataSource.GetBranchContentSources(scope, capture.Id); - _contentStore.SetBranch(capture.Id, kits); + _contentStore.SetBranchLocked(capture.Id, kits); } else { @@ -661,11 +737,11 @@ namespace Umbraco.Web.PublishedCache.NuCache var kit = _dataSource.GetContentSource(scope, capture.Id); if (kit.IsEmpty) { - _contentStore.Clear(capture.Id); + _contentStore.ClearLocked(capture.Id); } else { - _contentStore.Set(kit); + _contentStore.SetLocked(kit); } } @@ -677,6 +753,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + /// public override void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged) { // no cache, trash everything @@ -713,7 +790,7 @@ namespace Umbraco.Web.PublishedCache.NuCache using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTree); - LoadMediaFromDatabaseLocked(scope); + LoadMediaFromDatabaseLocked(scope, false); scope.Complete(); } anythingChanged = true; @@ -722,7 +799,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { - if (_mediaStore.Clear(payload.Id)) + if (_mediaStore.ClearLocked(payload.Id)) anythingChanged = true; continue; } @@ -743,9 +820,9 @@ namespace Umbraco.Web.PublishedCache.NuCache if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { // ?? should we do some RV check here? - // IMPORTANT GetbranchContentSources sorts kits by level + // IMPORTANT GetbranchContentSources sorts kits by level and by sort order var kits = _dataSource.GetBranchMediaSources(scope, capture.Id); - _mediaStore.SetBranch(capture.Id, kits); + _mediaStore.SetBranchLocked(capture.Id, kits); } else { @@ -753,11 +830,11 @@ namespace Umbraco.Web.PublishedCache.NuCache var kit = _dataSource.GetMediaSource(scope, capture.Id); if (kit.IsEmpty) { - _mediaStore.Clear(capture.Id); + _mediaStore.ClearLocked(capture.Id); } else { - _mediaStore.Set(kit); + _mediaStore.SetLocked(kit); } } @@ -769,6 +846,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + /// public override void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads) { // no cache, nothing we can do @@ -781,33 +859,74 @@ namespace Umbraco.Web.PublishedCache.NuCache Notify(_contentStore, payloads, RefreshContentTypesLocked); Notify(_mediaStore, payloads, RefreshMediaTypesLocked); + if (_publishedModelFactory.IsLiveFactoryEnabled()) + { + //In the case of Pure Live - we actually need to refresh all of the content and the media + //see https://github.com/umbraco/Umbraco-CMS/issues/5671 + //The underlying issue is that in Pure Live the ILivePublishedModelFactory will re-compile all of the classes/models + //into a new DLL for the application which includes both content types and media types. + //Since the models in the cache are based on these actual classes, all of the objects in the cache need to be updated + //to use the newest version of the class. + + // NOTE: Ideally this can be run on background threads here which would prevent blocking the UI + // as is the case when saving a content type. Intially one would think that it won't be any different + // between running this here or in another background thread immediately after with regards to how the + // UI will respond because we already know between calling `WithSafeLiveFactoryReset` to reset the PureLive models + // and this code here, that many front-end requests could be attempted to be processed. If that is the case, those pages are going to get a + // model binding error and our ModelBindingExceptionFilter is going to to its magic to reload those pages so the end user is none the wiser. + // So whether or not this executes 'here' or on a background thread immediately wouldn't seem to make any difference except that we can return + // execution to the UI sooner. + // BUT!... there is a difference IIRC. There is still execution logic that continues after this call on this thread with the cache refreshers + // and those cache refreshers need to have the up-to-date data since other user cache refreshers will be expecting the data to be 'live'. If + // we ran this on a background thread then those cache refreshers are going to not get 'live' data when they query the content cache which + // they require. + + // These can be run side by side in parallel. + + Parallel.Invoke( + () => + { + using (_contentStore.GetScopedWriteLock(_scopeProvider)) + { + NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _); + } + }, + () => + { + using (_mediaStore.GetScopedWriteLock(_scopeProvider)) + { + NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _); + } + }); + } + ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } - private void Notify(ContentStore store, ContentTypeCacheRefresher.JsonPayload[] payloads, Action, IEnumerable, IEnumerable, IEnumerable> action) + private void Notify(ContentStore store, ContentTypeCacheRefresher.JsonPayload[] payloads, Action, List, List, List> action) + where T : IContentTypeComposition { + if (payloads.Length == 0) return; //nothing to do + var nameOfT = typeof(T).Name; - var removedIds = new List(); - var refreshedIds = new List(); - var otherIds = new List(); - var newIds = new List(); + List removedIds = null, refreshedIds = null, otherIds = null, newIds = null; foreach (var payload in payloads) { if (payload.ItemType != nameOfT) continue; if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Remove)) - removedIds.Add(payload.Id); + AddToList(ref removedIds, payload.Id); else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshMain)) - refreshedIds.Add(payload.Id); + AddToList(ref refreshedIds, payload.Id); else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshOther)) - otherIds.Add(payload.Id); + AddToList(ref otherIds, payload.Id); else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Create)) - newIds.Add(payload.Id); + AddToList(ref newIds, payload.Id); } - if (removedIds.Count == 0 && refreshedIds.Count == 0 && otherIds.Count == 0 && newIds.Count == 0) return; + if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && newIds.IsCollectionEmpty()) return; using (store.GetScopedWriteLock(_scopeProvider)) { @@ -842,14 +961,14 @@ namespace Umbraco.Web.PublishedCache.NuCache using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTree); - _contentStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Content, id)); + _contentStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Content, id)); scope.Complete(); } using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTree); - _mediaStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Media, id)); + _mediaStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Media, id)); scope.Complete(); } } @@ -879,7 +998,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } break; case DomainChangeTypes.Remove: - _domainStore.Clear(payload.Id); + _domainStore.ClearLocked(payload.Id); break; case DomainChangeTypes.Refresh: var domain = _serviceContext.DomainService.GetById(payload.Id); @@ -887,22 +1006,26 @@ namespace Umbraco.Web.PublishedCache.NuCache if (domain.RootContentId.HasValue == false) continue; // anomaly if (domain.LanguageIsoCode.IsNullOrWhiteSpace()) continue; // anomaly var culture = CultureInfo.GetCultureInfo(domain.LanguageIsoCode); - _domainStore.Set(domain.Id, new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard)); + _domainStore.SetLocked(domain.Id, new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard)); break; } } } } + //Methods used to prevent allocations of lists + private void AddToList(ref List list, int val) => GetOrCreateList(ref list).Add(val); + private List GetOrCreateList(ref List list) => list ?? (list = new List()); + #endregion #region Content Types - private IEnumerable CreateContentTypes(PublishedItemType itemType, int[] ids) + private IReadOnlyCollection CreateContentTypes(PublishedItemType itemType, int[] ids) { // XxxTypeService.GetAll(empty) returns everything! if (ids.Length == 0) - return Enumerable.Empty(); + return Array.Empty(); IEnumerable contentTypes; switch (itemType) @@ -922,10 +1045,10 @@ namespace Umbraco.Web.PublishedCache.NuCache // some may be missing - not checking here - return contentTypes.Select(x => _publishedContentTypeFactory.CreateContentType(x)); + return contentTypes.Select(x => _publishedContentTypeFactory.CreateContentType(x)).ToList(); } - private PublishedContentType CreateContentType(PublishedItemType itemType, int id) + private IPublishedContentType CreateContentType(PublishedItemType itemType, int id) { IContentTypeComposition contentType; switch (itemType) @@ -946,48 +1069,64 @@ namespace Umbraco.Web.PublishedCache.NuCache return contentType == null ? null : _publishedContentTypeFactory.CreateContentType(contentType); } - private void RefreshContentTypesLocked(IEnumerable removedIds, IEnumerable refreshedIds, IEnumerable otherIds, IEnumerable newIds) + private void RefreshContentTypesLocked(List removedIds, List refreshedIds, List otherIds, List newIds) { + if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && newIds.IsCollectionEmpty()) + return; + // locks: // content (and content types) are read-locked while reading content // contentStore is wlocked (so readable, only no new views) // and it can be wlocked by 1 thread only at a time - var refreshedIdsA = refreshedIds.ToArray(); - using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTypes); - var typesA = CreateContentTypes(PublishedItemType.Content, refreshedIdsA).ToArray(); - var kits = _dataSource.GetTypeContentSources(scope, refreshedIdsA); + var typesA = refreshedIds.IsCollectionEmpty() + ? Array.Empty() + : CreateContentTypes(PublishedItemType.Content, refreshedIds.ToArray()).ToArray(); - _contentStore.UpdateContentTypes(removedIds, typesA, kits); - _contentStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray()).ToArray()); - _contentStore.NewContentTypes(CreateContentTypes(PublishedItemType.Content, newIds.ToArray()).ToArray()); + var kits = refreshedIds.IsCollectionEmpty() + ? Array.Empty() + : _dataSource.GetTypeContentSources(scope, refreshedIds).ToArray(); + + _contentStore.UpdateContentTypesLocked(removedIds, typesA, kits); + if (!otherIds.IsCollectionEmpty()) + _contentStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); + if (!newIds.IsCollectionEmpty()) + _contentStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Content, newIds.ToArray())); scope.Complete(); } } - private void RefreshMediaTypesLocked(IEnumerable removedIds, IEnumerable refreshedIds, IEnumerable otherIds, IEnumerable newIds) + private void RefreshMediaTypesLocked(List removedIds, List refreshedIds, List otherIds, List newIds) { + if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && newIds.IsCollectionEmpty()) + return; + // locks: // media (and content types) are read-locked while reading media // mediaStore is wlocked (so readable, only no new views) // and it can be wlocked by 1 thread only at a time - var refreshedIdsA = refreshedIds.ToArray(); - using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTypes); - var typesA = CreateContentTypes(PublishedItemType.Media, refreshedIdsA).ToArray(); - var kits = _dataSource.GetTypeMediaSources(scope, refreshedIdsA); + var typesA = refreshedIds == null + ? Array.Empty() + : CreateContentTypes(PublishedItemType.Media, refreshedIds.ToArray()).ToArray(); - _mediaStore.UpdateContentTypes(removedIds, typesA, kits); - _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); - _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); + var kits = refreshedIds == null + ? Array.Empty() + : _dataSource.GetTypeMediaSources(scope, refreshedIds).ToArray(); + + _mediaStore.UpdateContentTypesLocked(removedIds, typesA, kits); + if (!otherIds.IsCollectionEmpty()) + _mediaStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); + if (!newIds.IsCollectionEmpty()) + _mediaStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); scope.Complete(); } } @@ -1027,7 +1166,13 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentStore.Snapshot contentSnap, mediaSnap; SnapDictionary.Snapshot domainSnap; IAppCache elementsCache; - lock (_storesLock) + + // Here we are reading/writing to shared objects so we need to lock (can't be _storesLock which manages the actual nucache files + // and would result in a deadlock). Even though we are locking around underlying readlocks (within CreateSnapshot) it's because + // we need to ensure that the result of contentSnap.Gen (etc) and the re-assignment of these values and _elements cache + // are done atomically. + + lock (_elementsLock) { var scopeContext = _scopeProvider.Context; @@ -1052,12 +1197,14 @@ namespace Umbraco.Web.PublishedCache.NuCache // elements // just need to make sure nothing gets elements in another enlisted action... so using // a MaxValue to make sure this one runs last, and it should be ok + scopeContext.Enlist("Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", () => this, (completed, svc) => { ((PublishedSnapshot)svc.CurrentPublishedSnapshot)?.Resync(); }, int.MaxValue); } + // create a new snapshot cache if snapshots are different gens if (contentSnap.Gen != _contentGen || mediaSnap.Gen != _mediaGen || domainSnap.Gen != _domainGen || _elementsCache == null) { @@ -1074,13 +1221,12 @@ namespace Umbraco.Web.PublishedCache.NuCache var defaultCulture = _defaultCultureAccessor.DefaultCulture; var domainCache = new DomainCache(domainSnap, defaultCulture); - var domainHelper = new DomainHelper(domainCache, _siteDomainHelper); return new PublishedSnapshot.PublishedSnapshotElements { - ContentCache = new ContentCache(previewDefault, contentSnap, snapshotCache, elementsCache, domainHelper, _globalSettings, _serviceContext.LocalizationService), - MediaCache = new MediaCache(previewDefault, mediaSnap, snapshotCache, elementsCache), - MemberCache = new MemberCache(previewDefault, snapshotCache, _serviceContext.MemberService, memberTypeCache, PublishedSnapshotAccessor, VariationContextAccessor, _umbracoContextAccessor, _entitySerializer), + ContentCache = new ContentCache(previewDefault, contentSnap, snapshotCache, elementsCache, domainCache, _globalSettings, VariationContextAccessor), + MediaCache = new MediaCache(previewDefault, mediaSnap, VariationContextAccessor), + MemberCache = new MemberCache(previewDefault, snapshotCache, _serviceContext.MemberService, memberTypeCache, PublishedSnapshotAccessor, VariationContextAccessor, _entitySerializer), DomainCache = domainCache, SnapshotCache = snapshotCache, ElementsCache = elementsCache @@ -1170,7 +1316,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var member = args.Entity; // refresh the edited data - OnRepositoryRefreshed(db, member, true); + OnRepositoryRefreshed(db, member, false); } private void OnRepositoryRefreshed(IUmbracoDatabase db, IContentBase content, bool published) @@ -1216,6 +1362,21 @@ namespace Umbraco.Web.PublishedCache.NuCache RebuildMemberDbCache(contentTypeIds: memberTypeIds); } + /// + /// If a is ever saved with a different culture, we need to rebuild all of the content nucache table + /// + /// + /// + private void OnLanguageSaved(ILocalizationService sender, Core.Events.SaveEventArgs e) + { + //culture changed on an existing language + var cultureChanged = e.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); + if (cultureChanged) + { + RebuildContentDbCache(); + } + } + private ContentNuDto GetDto(IContentBase content, bool published) { // should inject these in ctor @@ -1644,5 +1805,12 @@ AND cmsContentNu.nodeId IS NULL } #endregion + + #region Internals/Testing + + internal ContentStore GetContentStore() => _contentStore; + internal ContentStore GetMediaStore() => _mediaStore; + + #endregion } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotServiceOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotServiceOptions.cs new file mode 100644 index 0000000000..ef9d83a802 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotServiceOptions.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Web.PublishedCache.NuCache +{ + /// + /// Options class for configuring the + /// + public class PublishedSnapshotServiceOptions + { + // disabled: prevents the published snapshot from updating and exposing changes + // or even creating a new published snapshot to see changes, uses old cache = bad + // + //// indicates that the snapshot cache should reuse the application request cache + //// otherwise a new cache object would be created for the snapshot specifically, + //// which is the default - web boot manager uses this to optimize facades + //public bool PublishedSnapshotCacheIsApplicationRequestCache; + + + /// + /// If true this disables the persisted local cache files for content and media + /// + /// + /// By default this is false which means umbraco will use locally persisted cache files for reading in all published content and media on application startup. + /// The reason for this is to improve startup times because the alternative to populating the published content and media on application startup is to read + /// these values from the database. In scenarios where sites are relatively small (below a few thousand nodes) reading the content/media from the database to populate + /// the in memory cache isn't that slow and is only marginally slower than reading from the locally persisted cache files. + /// + public bool IgnoreLocalDb { get; set; } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs index 20d7e7ddcd..94f83ac4e5 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs @@ -1,11 +1,17 @@ namespace Umbraco.Web.PublishedCache.NuCache.Snap { + //NOTE: This cannot be struct because it references itself + + /// + /// Used to represent an item in a linked list + /// + /// internal class LinkedNode where TValue : class { public LinkedNode(TValue value, long gen, LinkedNode next = null) { - Value = value; + Value = value; // This is allowed to be null, we actually explicitly set this to null in ClearLocked Gen = gen; Next = next; } @@ -17,4 +23,4 @@ public volatile TValue Value; public volatile LinkedNode Next; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index c5b1df1206..c38940da25 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Umbraco.Core.Exceptions; using Umbraco.Core.Scoping; using Umbraco.Web.PublishedCache.NuCache.Snap; @@ -21,6 +22,11 @@ namespace Umbraco.Web.PublishedCache.NuCache // This class is optimized for many readers, few writers // Readers are lock-free + // NOTE - we used to lock _rlocko the long hand way with Monitor.Enter(_rlocko, ref lockTaken) but this has + // been replaced with a normal c# lock because that's exactly how the normal c# lock works, + // see https://blogs.msdn.microsoft.com/ericlippert/2009/03/06/locks-and-exceptions-do-not-mix/ + // for the readlock, there's no reason here to use the long hand way. + private readonly ConcurrentDictionary> _items; private readonly ConcurrentQueue _genObjs; private GenObj _genObj; @@ -29,7 +35,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private long _liveGen, _floorGen; private bool _nextGen, _collectAuto; private Task _collectTask; - private volatile int _wlocked; // minGenDelta to be adjusted // we may want to throttle collects even if delta is reached @@ -70,20 +75,14 @@ namespace Umbraco.Web.PublishedCache.NuCache // are all ignored - Release is private and meant to be invoked with 'commit' being false only // only on the outermost lock (by SnapDictionaryWriter) - // using (...) {} for locking is prone to nasty leaks in case of weird exceptions - // such as thread-abort or out-of-memory, but let's not worry about it now + // side note - using (...) {} for locking is prone to nasty leaks in case of weird exceptions + // such as thread-abort or out-of-memory, which is why we've moved away from the old using wrapper we had on locking. private readonly string _instanceId = Guid.NewGuid().ToString("N"); - private class ReadLockInfo - { - public bool Taken; - } - private class WriteLockInfo { public bool Taken; - public bool Count; } // a scope contextual that represents a locked writer to the dictionary @@ -91,8 +90,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { private readonly WriteLockInfo _lockinfo = new WriteLockInfo(); private readonly SnapDictionary _dictionary; - private int _released; - + public ScopedWriteLock(SnapDictionary dictionary, bool scoped) { _dictionary = dictionary; @@ -101,8 +99,6 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Release(bool completed) { - if (Interlocked.CompareExchange(ref _released, 1, 0) != 0) - return; _dictionary.Release(_lockinfo, completed); } } @@ -116,28 +112,31 @@ namespace Umbraco.Web.PublishedCache.NuCache return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } + private void EnsureLocked() + { + if (!Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Write lock must be acquried."); + } + private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { + if (Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Recursive locks not allowed"); + Monitor.Enter(_wlocko, ref lockInfo.Taken); - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - // assume everything in finally runs atomically // http://stackoverflow.com/questions/18501678/can-this-unexpected-behavior-of-prepareconstrainedregions-and-thread-abort-be-ex // http://joeduffyblog.com/2005/03/18/atomicity-and-asynchronous-exception-failures/ // http://joeduffyblog.com/2007/02/07/introducing-the-new-readerwriterlockslim-in-orcas/ // http://chabster.blogspot.fr/2013/12/readerwriterlockslim-fails-on-dual.html //RuntimeHelpers.PrepareConstrainedRegions(); - try { } finally + try { } + finally { - // increment the lock count, and register that this lock is counting - _wlocked++; - lockInfo.Count = true; - - if (_nextGen == false || (forceGen && _wlocked == 1)) + if (_nextGen == false || (forceGen)) { // because we are changing things, a new generation // is created, which will trigger a new snapshot @@ -148,15 +147,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } } } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - } - - private void Lock(ReadLockInfo lockInfo) - { - Monitor.Enter(_rlocko, ref lockInfo.Taken); } private void Release(WriteLockInfo lockInfo, bool commit = true) @@ -167,22 +157,17 @@ namespace Umbraco.Web.PublishedCache.NuCache if (commit == false) { - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - try { } finally + try { } + finally { // forget about the temp. liveGen _nextGen = false; _liveGen -= 1; } } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - + foreach (var item in _items) { var link = item.Value; @@ -196,16 +181,10 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - // decrement the lock count, if counting, then exit the lock - if (lockInfo.Count) _wlocked--; + // TODO: Shouldn't this be in a finally block? Monitor.Exit(_wlocko); } - private void Release(ReadLockInfo lockInfo) - { - if (lockInfo.Taken) Monitor.Exit(_rlocko); - } - #endregion #region Set, Clear, Get, Has @@ -218,75 +197,59 @@ namespace Umbraco.Web.PublishedCache.NuCache return link; } - public void Set(TKey key, TValue value) + public void SetLocked(TKey key, TValue value) { - var lockInfo = new WriteLockInfo(); - try - { - Lock(lockInfo); + EnsureLocked(); - // this is safe only because we're write-locked - var link = GetHead(key); - if (link != null) + // this is safe only because we're write-locked + var link = GetHead(key); + if (link != null) + { + // already in the dict + if (link.Gen != _liveGen) { - // already in the dict - if (link.Gen != _liveGen) - { - // for an older gen - if value is different then insert a new - // link for the new gen, with the new value - if (link.Value != value) - _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); - } - else - { - // for the live gen - we can fix the live gen - and remove it - // if value is null and there's no next gen - if (value == null && link.Next == null) - _items.TryRemove(key, out link); - else - link.Value = value; - } + // for an older gen - if value is different then insert a new + // link for the new gen, with the new value + if (link.Value != value) + _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); } else { - _items.TryAdd(key, new LinkedNode(value, _liveGen)); - } - } - finally - { - Release(lockInfo); - } - } - - public void Clear(TKey key) - { - Set(key, null); - } - - public void Clear() - { - var lockInfo = new WriteLockInfo(); - try - { - Lock(lockInfo); - - // this is safe only because we're write-locked - foreach (var kvp in _items.Where(x => x.Value != null)) - { - if (kvp.Value.Gen < _liveGen) - { - var link = new LinkedNode(null, _liveGen, kvp.Value); - _items.TryUpdate(kvp.Key, link, kvp.Value); - } + // for the live gen - we can fix the live gen - and remove it + // if value is null and there's no next gen + if (value == null && link.Next == null) + _items.TryRemove(key, out link); else - { - kvp.Value.Value = null; - } + link.Value = value; } } - finally + else { - Release(lockInfo); + _items.TryAdd(key, new LinkedNode(value, _liveGen)); + } + } + + public void ClearLocked(TKey key) + { + SetLocked(key, null); + } + + public void ClearLocked() + { + EnsureLocked(); + + // this is safe only because we're write-locked + foreach (var kvp in _items.Where(x => x.Value != null)) + { + if (kvp.Value.Gen < _liveGen) + { + var link = new LinkedNode(null, _liveGen, kvp.Value); + _items.TryUpdate(kvp.Key, link, kvp.Value); + } + else + { + kvp.Value.Value = null; + } } } @@ -346,11 +309,8 @@ namespace Umbraco.Web.PublishedCache.NuCache public Snapshot CreateSnapshot() { - var lockInfo = new ReadLockInfo(); - try + lock(_rlocko) { - Lock(lockInfo); - // if no next generation is required, and we already have a gen object, // use it to create a new snapshot if (_nextGen == false && _genObj != null) @@ -359,7 +319,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // else we need to try to create a new gen object // whether we are wlocked or not, noone can rlock while we do, // so _liveGen and _nextGen are safe - if (_wlocked > 0) // volatile, cannot ++ but could -- + if (Monitor.IsEntered(_wlocko)) { // write-locked, cannot use latest gen (at least 1) so use previous var snapGen = _nextGen ? _liveGen - 1 : _liveGen; @@ -371,7 +331,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // if we have one already, ensure it's consistent else if (_genObj.Gen != snapGen) - throw new Exception("panic"); + throw new PanicException($"The generation {_genObj.Gen} does not equal the snapshot generation {snapGen}"); } else { @@ -397,10 +357,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return snapshot; } - finally - { - Release(lockInfo); - } } public Task CollectAsync() @@ -495,17 +451,18 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public /*async*/ Task PendingCollect() - { - Task task; - lock (_rlocko) - { - task = _collectTask; - } - return task ?? Task.CompletedTask; - //if (task != null) - // await task; - } + // TODO: This is never used? Should it be? Maybe move to TestHelper below? + //public /*async*/ Task PendingCollect() + //{ + // Task task; + // lock (_rlocko) + // { + // task = _collectTask; + // } + // return task ?? Task.CompletedTask; + // //if (task != null) + // // await task; + //} public long GenCount => _genObjs.Count; @@ -530,7 +487,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public long LiveGen => _dict._liveGen; public long FloorGen => _dict._floorGen; public bool NextGen => _dict._nextGen; - public int WLocked => _dict._wlocked; + public bool IsLocked => Monitor.IsEntered(_dict._wlocko); public bool CollectAuto { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/notes.txt b/src/Umbraco.Web/PublishedCache/NuCache/readme.md similarity index 70% rename from src/Umbraco.Web/PublishedCache/NuCache/notes.txt rename to src/Umbraco.Web/PublishedCache/NuCache/readme.md index ff2d8dd48b..c8e8a363e9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/notes.txt +++ b/src/Umbraco.Web/PublishedCache/NuCache/readme.md @@ -1,10 +1,6 @@ NuCache Documentation ====================== -NOTE - RENAMING -Facade = PublishedSnapshot -and everything needs to be renamed accordingly - HOW IT WORKS ------------- @@ -22,12 +18,12 @@ When reading the cache, we read views up the chain until we find a value (which null) for the given id, and finally we read the store itself. -The FacadeService manages a ContentStore for content, and another for media. -When a Facade is created, the FacadeService gets ContentView objects from the stores. +The PublishedSnapshotService manages a ContentStore for content, and another for media. +When a PublishedSnapshot is created, the PublishedSnapshotService gets ContentView objects from the stores. Views provide an immutable snapshot of the content and media. -When the FacadeService is notified of changes, it notifies the stores. -Then it resync the current Facade, so that it requires new views, etc. +When the PublishedSnapshotService is notified of changes, it notifies the stores. +Then it resync the current PublishedSnapshot, so that it requires new views, etc. Whenever a content, media or member is modified or removed, the cmsContentNu table is updated with a json dictionary of alias => property value, so that a content, @@ -50,32 +46,32 @@ Each ContentStore has a _freezeLock object used to protect the 'Frozen' state of the store. It's a disposable object that releases the lock when disposed, so usage would be: using (store.Frozen) { ... }. -The FacadeService has a _storesLock object used to guarantee atomic access to the +The PublishedSnapshotService has a _storesLock object used to guarantee atomic access to the set of content, media stores. CACHE ------ -For each set of views, the FacadeService creates a SnapshotCache. So a SnapshotCache +For each set of views, the PublishedSnapshotService creates a SnapshotCache. So a SnapshotCache is valid until anything changes in the content or media trees. In other words, things that go in the SnapshotCache stay until a content or media is modified. -For each Facade, the FacadeService creates a FacadeCache. So a FacadeCache is valid -for the duration of the Facade (usually, the request). In other words, things that go -in the FacadeCache stay (and are visible to) for the duration of the request only. +For each PublishedSnapshot, the PublishedSnapshotService creates a PublishedSnapshotCache. So a PublishedSnapshotCache is valid +for the duration of the PublishedSnapshot (usually, the request). In other words, things that go +in the PublishedSnapshotCache stay (and are visible to) for the duration of the request only. -The FacadeService defines a static constant FullCacheWhenPreviewing, that defines +The PublishedSnapshotService defines a static constant FullCacheWhenPreviewing, that defines how caches operate when previewing: - when true, the caches in preview mode work normally. -- when false, everything that would go to the SnapshotCache goes to the FacadeCache. +- when false, everything that would go to the SnapshotCache goes to the PublishedSnapshotCache. At the moment it is true in the code, which means that eg converted values for previewed content will go in the SnapshotCache. Makes for faster preview, but uses more memory on the long term... would need some benchmarking to figure out what is best. -Members only live for the duration of the Facade. So, for members SnapshotCache is -never used, and everything goes to the FacadeCache. +Members only live for the duration of the PublishedSnapshot. So, for members SnapshotCache is +never used, and everything goes to the PublishedSnapshotCache. All cache keys are computed in the CacheKeys static class. @@ -85,15 +81,15 @@ TESTS For testing purposes the following mechanisms exist: -The Facade type has a static Current property that is used to obtain the 'current' -facade in many places, going through the PublishedCachesServiceResolver to get the -current service, and asking the current service for the current facade, which by +The PublishedSnapshot type has a static Current property that is used to obtain the 'current' +PublishedSnapshot in many places, going through the PublishedCachesServiceResolver to get the +current service, and asking the current service for the current PublishedSnapshot, which by default relies on UmbracoContext. For test purposes, it is possible to override the -entire mechanism by defining Facade.GetCurrentFacadeFunc which should return a facade. +entire mechanism by defining PublishedSnapshot.GetCurrentPublishedSnapshotFunc which should return a PublishedSnapshot. A PublishedContent keeps only id-references to its parent and children, and needs a way to retrieve the actual objects from the cache - which depends on the current -facade. It is possible to override the entire mechanism by defining PublishedContent. +PublishedSnapshot. It is possible to override the entire mechanism by defining PublishedContent. GetContentByIdFunc or .GetMediaByIdFunc. Setting these functions must be done before Resolution is frozen. @@ -110,7 +106,7 @@ possible to support detached contents & properties, even those that do not have int id, but again this should be refactored entirely anyway. Not doing any row-version checks (see XmlStore) when reloading from database, though it -is maintained in the database. Two FIXME in FacadeService. Should we do it? +is maintained in the database. Two FIXME in PublishedSnapshotService. Should we do it? There is no on-disk cache at all so everything is reloaded from the cmsContentNu table when the site restarts. This is pretty fast, but we should experiment with solutions to @@ -121,4 +117,4 @@ PublishedMember exposes properties that IPublishedContent does not, and that are to be lost soon as the member is wrapped (content set, model...) - so we probably need some sort of IPublishedMember. -/ \ No newline at end of file +/ diff --git a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs index b88ae26704..1f637663e5 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs @@ -37,11 +37,11 @@ namespace Umbraco.Web.PublishedCache public bool HasById(int contentId) => HasById(PreviewDefault, contentId); - public abstract IEnumerable GetAtRoot(bool preview); + public abstract IEnumerable GetAtRoot(bool preview, string culture = null); - public IEnumerable GetAtRoot() + public IEnumerable GetAtRoot(string culture = null) { - return GetAtRoot(PreviewDefault); + return GetAtRoot(PreviewDefault, culture); } public abstract IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars); @@ -88,11 +88,11 @@ namespace Umbraco.Web.PublishedCache return HasContent(PreviewDefault); } - public abstract PublishedContentType GetContentType(int id); + public abstract IPublishedContentType GetContentType(int id); - public abstract PublishedContentType GetContentType(string alias); + public abstract IPublishedContentType GetContentType(string alias); - public virtual IEnumerable GetByContentType(PublishedContentType contentType) + public virtual IEnumerable GetByContentType(IPublishedContentType contentType) { // this is probably not super-efficient, but works // some cache implementation may want to override it, though diff --git a/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs index ca30370598..e453471bb8 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs @@ -15,8 +15,8 @@ namespace Umbraco.Web.PublishedCache /// This cache is not snapshotted, so it refreshes any time things change. public class PublishedContentTypeCache { - private readonly Dictionary _typesByAlias = new Dictionary(); - private readonly Dictionary _typesById = new Dictionary(); + private readonly Dictionary _typesByAlias = new Dictionary(); + private readonly Dictionary _typesById = new Dictionary(); private readonly IContentTypeService _contentTypeService; private readonly IMediaTypeService _mediaTypeService; private readonly IMemberTypeService _memberTypeService; @@ -136,7 +136,7 @@ namespace Umbraco.Web.PublishedCache /// An item type. /// An alias. /// The published content type corresponding to the item type and alias. - public PublishedContentType Get(PublishedItemType itemType, string alias) + public IPublishedContentType Get(PublishedItemType itemType, string alias) { var aliasKey = GetAliasKey(itemType, alias); @@ -174,7 +174,7 @@ namespace Umbraco.Web.PublishedCache /// An item type. /// An identifier. /// The published content type corresponding to the item type and identifier. - public PublishedContentType Get(PublishedItemType itemType, int id) + public IPublishedContentType Get(PublishedItemType itemType, int id) { try { @@ -204,7 +204,7 @@ namespace Umbraco.Web.PublishedCache } } - private PublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias) + private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias) { if (GetPublishedContentTypeByAlias != null) return GetPublishedContentTypeByAlias(alias); @@ -231,7 +231,7 @@ namespace Umbraco.Web.PublishedCache return _publishedContentTypeFactory.CreateContentType(contentType); } - private PublishedContentType CreatePublishedContentType(PublishedItemType itemType, int id) + private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, int id) { if (GetPublishedContentTypeById != null) return GetPublishedContentTypeById(id); @@ -259,8 +259,8 @@ namespace Umbraco.Web.PublishedCache } // for unit tests - changing the callback must reset the cache obviously - private Func _getPublishedContentTypeByAlias; - internal Func GetPublishedContentTypeByAlias + private Func _getPublishedContentTypeByAlias; + internal Func GetPublishedContentTypeByAlias { get => _getPublishedContentTypeByAlias; set @@ -282,8 +282,8 @@ namespace Umbraco.Web.PublishedCache } // for unit tests - changing the callback must reset the cache obviously - private Func _getPublishedContentTypeById; - internal Func GetPublishedContentTypeById + private Func _getPublishedContentTypeById; + internal Func GetPublishedContentTypeById { get => _getPublishedContentTypeById; set @@ -326,7 +326,7 @@ namespace Umbraco.Web.PublishedCache return k + ":" + alias; } - private static string GetAliasKey(PublishedContentType contentType) + private static string GetAliasKey(IPublishedContentType contentType) { return GetAliasKey(contentType.ItemType, contentType.Alias); } diff --git a/src/Umbraco.Web/PublishedCache/PublishedElement.cs b/src/Umbraco.Web/PublishedCache/PublishedElement.cs index 41902e3e26..618c075b9b 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedElement.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedElement.cs @@ -18,7 +18,7 @@ namespace Umbraco.Web.PublishedCache { // initializes a new instance of the PublishedElement class // within the context of a published snapshot service (eg a published content property value) - public PublishedElement(PublishedContentType contentType, Guid key, Dictionary values, bool previewing, + public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary values, bool previewing, PropertyCacheLevel referenceCacheLevel, IPublishedSnapshotAccessor publishedSnapshotAccessor) { if (key == Guid.Empty) throw new ArgumentException("Empty guid."); @@ -46,7 +46,7 @@ namespace Umbraco.Web.PublishedCache // + using an initial reference cache level of .None ensures that everything will be // cached at .Content level - and that reference cache level will propagate to all // properties - public PublishedElement(PublishedContentType contentType, Guid key, Dictionary values, bool previewing) + public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary values, bool previewing) : this(contentType, key, values, previewing, PropertyCacheLevel.None, null) { } @@ -60,7 +60,7 @@ namespace Umbraco.Web.PublishedCache #region ContentType - public PublishedContentType ContentType { get; } + public IPublishedContentType ContentType { get; } #endregion diff --git a/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs b/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs index 62f72a27aa..ef62a3254d 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.PublishedCache // so making it configurable. private const bool FullCacheWhenPreviewing = true; - public PublishedElementPropertyBase(PublishedPropertyType propertyType, IPublishedElement element, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null, IPublishedSnapshotAccessor publishedSnapshotAccessor = null) + public PublishedElementPropertyBase(IPublishedPropertyType propertyType, IPublishedElement element, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null, IPublishedSnapshotAccessor publishedSnapshotAccessor = null) : base(propertyType, referenceCacheLevel) { _sourceValue = sourceValue; diff --git a/src/Umbraco.Web/PublishedCache/PublishedMember.cs b/src/Umbraco.Web/PublishedCache/PublishedMember.cs index 419c279d46..e7cdb65f2e 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedMember.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedMember.cs @@ -17,13 +17,11 @@ namespace Umbraco.Web.PublishedCache private readonly IMember _member; private readonly IMembershipUser _membershipUser; private readonly IPublishedProperty[] _properties; - private readonly PublishedContentType _publishedMemberType; + private readonly IPublishedContentType _publishedMemberType; public PublishedMember( IMember member, - PublishedContentType publishedMemberType, - IUmbracoContextAccessor umbracoContextAccessor) - :base(umbracoContextAccessor) + IPublishedContentType publishedMemberType) { _member = member ?? throw new ArgumentNullException(nameof(member)); _membershipUser = member; @@ -35,16 +33,13 @@ namespace Umbraco.Web.PublishedCache // if they are not part of the member type properties - in which case they are created as // simple raw properties - which are completely invariant - var _properties = PublishedProperty.MapProperties(_publishedMemberType.PropertyTypes, _member.Properties, - (t, v) => new RawValueProperty(t, this, v ?? string.Empty)); - var properties = new List(); foreach (var propertyType in _publishedMemberType.PropertyTypes) { var property = _member.Properties[propertyType.Alias]; if (property == null) continue; - //properties.Add(new FooProperty(propertyType, this, property.Values)); + properties.Add(new RawValueProperty(propertyType, this, property.GetValue())); } EnsureMemberProperties(properties); _properties = properties.ToArray(); @@ -88,6 +83,8 @@ namespace Umbraco.Web.PublishedCache public override IEnumerable Children => Enumerable.Empty(); + public override IEnumerable ChildrenForAllCultures => Enumerable.Empty(); + public override IEnumerable Properties => _properties; public override IPublishedProperty GetProperty(string alias) @@ -126,7 +123,7 @@ namespace Umbraco.Web.PublishedCache properties.Add(property); } - public override PublishedContentType ContentType => _publishedMemberType; + public override IPublishedContentType ContentType => _publishedMemberType; public override int Id => _member.Id; @@ -138,16 +135,16 @@ namespace Umbraco.Web.PublishedCache public override string Name => _member.Name; - public override PublishedCultureInfo GetCulture(string culture = null) => throw new NotSupportedException(); - public override IReadOnlyDictionary Cultures => throw new NotSupportedException(); public override string UrlSegment => throw new NotSupportedException(); // TODO: ARGH! need to fix this - this is not good because it uses ApplicationContext.Current + [Obsolete("Use WriterName(IUserService) extension instead")] public override string WriterName => _member.GetCreatorProfile().Name; // TODO: ARGH! need to fix this - this is not good because it uses ApplicationContext.Current + [Obsolete("Use CreatorName(IUserService) extension instead")] public override string CreatorName => _member.GetCreatorProfile().Name; public override int WriterId => _member.CreatorId; diff --git a/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs b/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs index 20d3e6d8e3..1b56f54569 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Cache; namespace Umbraco.Web.PublishedCache { - abstract class PublishedSnapshotServiceBase : IPublishedSnapshotService + internal abstract class PublishedSnapshotServiceBase : IPublishedSnapshotService { protected PublishedSnapshotServiceBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 6ac21e047b..750ffa4be6 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Services; using Umbraco.Examine; using Umbraco.Web.Composing; using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; namespace Umbraco.Web { @@ -20,134 +21,23 @@ namespace Umbraco.Web public static class PublishedContentExtensions { // see notes in PublishedElementExtensions + // (yes, this is not pretty, but works for now) // private static IPublishedValueFallback PublishedValueFallback => Current.PublishedValueFallback; private static IPublishedSnapshot PublishedSnapshot => Current.PublishedSnapshot; + private static UmbracoContext UmbracoContext => Current.UmbracoContext; + private static ISiteDomainHelper SiteDomainHelper => Current.Factory.GetInstance(); - #region Urls + #region Creator/Writer Names - /// - /// Gets the url for the content. - /// - /// The content. - /// The url for the content. - /// Better use the Url property but that method is here to complement UrlAbsolute(). - public static string Url(this IPublishedContent content) + public static string CreatorName(this IPublishedContent content, IUserService userService) { - return content.Url; + return userService.GetProfileById(content.CreatorId)?.Name; } - /// - /// Gets the absolute url for the content. - /// - /// The content. - /// The absolute url for the content. - public static string UrlAbsolute(this IPublishedContent content) => content.UrlAbsolute(null); - - /// - /// Gets the absolute url for the content. - /// - /// The content. - /// The culture to get the url for - /// The absolute url for the content. - public static string UrlAbsolute(this IPublishedContent content, string culture) + public static string WriterName(this IPublishedContent content, IUserService userService) { - // adapted from PublishedContentBase.Url - - if (Current.UmbracoContext == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); - if (Current.UmbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); - - switch (content.ItemType) - { - case PublishedItemType.Content: - return Current.UmbracoContext.UrlProvider.GetUrl(content.Id, true, culture); - case PublishedItemType.Media: - return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, Constants.Conventions.Media.File, true); - default: - throw new ArgumentOutOfRangeException(); - } - } - - /// - /// Gets the url for the media. - /// - /// The content. - /// The property alias to resolve the url from. - /// The variation language. - /// The url for the content. - /// Better use the GetMediaUrl method but that method is here to complement MediaUrlAbsolute(). - public static string MediaUrl(this IPublishedContent content, string propertyAlias, string culture = null) - { - if (Current.UmbracoContext == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); - if (Current.UmbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); - - return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, propertyAlias, culture); - } - - /// - /// Gets the absolute url for the media. - /// - /// The content. - /// The property alias to resolve the url from. - /// The variation language. - /// The absolute url for the media. - public static string MediaUrlAbsolute(this IPublishedContent content, string propertyAlias, string culture = null) - { - if (Current.UmbracoContext == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); - if (Current.UmbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); - - return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, propertyAlias, true, culture); - } - - /// - /// Gets the Url segment. - /// - /// - /// Gets the url segment for the document, taking its content type and a specified - /// culture in account. For invariant content types, the culture is ignored, else it is - /// used to try and find the segment corresponding to the culture. May return null. - /// - public static string GetUrlSegment(this IPublishedContent content, string culture = null) - { - // for invariant content, return the invariant url segment - if (!content.ContentType.VariesByCulture()) - return content.UrlSegment; - - // content.GetCulture(culture) will use the 'current' culture (via accessor) in case 'culture' - // is null (meaning, 'current') - and can return 'null' if that culture is not published - and - // will return 'null' if the content is variant and culture is invariant - - // else try and get the culture info - // return the corresponding url segment, or null if none - var cultureInfo = content.GetCulture(culture); - return cultureInfo?.UrlSegment; - } - - public static bool IsAllowedTemplate(this IPublishedContent content, int templateId) - { - if (Current.Configs.Settings().WebRouting.DisableAlternativeTemplates) - return content.TemplateId == templateId; - - if (content.TemplateId == templateId || !Current.Configs.Settings().WebRouting.ValidateAlternativeTemplates) - return true; - - var publishedContentContentType = Current.Services.ContentTypeService.Get(content.ContentType.Id); - if (publishedContentContentType == null) - throw new NullReferenceException("No content type returned for published content (contentType='" + content.ContentType.Id + "')"); - - return publishedContentContentType.IsAllowedTemplate(templateId); - - } - public static bool IsAllowedTemplate(this IPublishedContent content, string templateAlias) - { - var template = Current.Services.FileService.GetTemplate(templateAlias); - return template != null && content.IsAllowedTemplate(template.Id); + return userService.GetProfileById(content.WriterId)?.Name; } #endregion @@ -162,7 +52,7 @@ namespace Umbraco.Web /// A value indicating whether the content is of a content type composed of a content type identified by the alias. public static bool IsComposedOf(this IPublishedContent content, string alias) { - return content.ContentType.CompositionAliases.Contains(alias); + return content.ContentType.CompositionAliases.InvariantContains(alias); } #endregion @@ -185,6 +75,27 @@ namespace Umbraco.Web return template == null ? string.Empty : template.Alias; } + public static bool IsAllowedTemplate(this IPublishedContent content, int templateId) + { + if (Current.Configs.Settings().WebRouting.DisableAlternativeTemplates) + return content.TemplateId == templateId; + + if (content.TemplateId == templateId || !Current.Configs.Settings().WebRouting.ValidateAlternativeTemplates) + return true; + + var publishedContentContentType = Current.Services.ContentTypeService.Get(content.ContentType.Id); + if (publishedContentContentType == null) + throw new NullReferenceException("No content type returned for published content (contentType='" + content.ContentType.Id + "')"); + + return publishedContentContentType.IsAllowedTemplate(templateId); + + } + public static bool IsAllowedTemplate(this IPublishedContent content, string templateAlias) + { + var template = Current.Services.FileService.GetTemplate(templateAlias); + return template != null && content.IsAllowedTemplate(template.Id); + } + #endregion #region HasValue, Value, Value @@ -271,26 +182,24 @@ namespace Umbraco.Web #region Variations /// - /// Determines whether the content has a culture. + /// Gets the culture assigned to a document by domains, in the context of a current Uri. /// - /// Culture is case-insensitive. - public static bool HasCulture(this IPublishedContent content, string culture) - => content.Cultures.ContainsKey(culture ?? string.Empty); - - /// - /// Filters a sequence of to return invariant items, and items that are published for the specified culture. - /// - /// The content items. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null). - internal static IEnumerable WhereIsInvariantOrHasCulture(this IEnumerable contents, string culture = null) - where T : class, IPublishedContent + /// The document. + /// An optional current Uri. + /// The culture assigned to the document by domains. + /// + /// In 1:1 multilingual setup, a document contains several cultures (there is not + /// one document per culture), and domains, withing the context of a current Uri, assign + /// a culture to that document. + /// + public static string GetCultureFromDomains(this IPublishedContent content, Uri current = null) { - if (contents == null) throw new ArgumentNullException(nameof(contents)); + var umbracoContext = UmbracoContext; - culture = culture ?? Current.VariationContextAccessor.VariationContext?.Culture ?? ""; + if (umbracoContext == null) + throw new InvalidOperationException("A current UmbracoContext is required."); - // either does not vary by culture, or has the specified culture - return contents.Where(x => !x.ContentType.VariesByCulture() || x.HasCulture(culture)); + return DomainUtilities.GetCultureFromDomains(content.Id, content.Path, current, umbracoContext, SiteDomainHelper); } #endregion @@ -315,7 +224,7 @@ namespace Umbraco.Web .And() .ManagedQuery(term); - return query.Execute().ToPublishedSearchResults(Current.UmbracoContext.ContentCache); + return query.Execute().ToPublishedSearchResults(Current.UmbracoContext.Content); } public static IEnumerable SearchChildren(this IPublishedContent content, string term, string indexName = null) @@ -336,7 +245,7 @@ namespace Umbraco.Web .And() .ManagedQuery(term); - return query.Execute().ToPublishedSearchResults(Current.UmbracoContext.ContentCache); + return query.Execute().ToPublishedSearchResults(Current.UmbracoContext.Content); } #endregion @@ -526,7 +435,7 @@ namespace Umbraco.Web /// Does not consider the content itself. Returns all ancestors, of the specified content type. public static IEnumerable Ancestors(this IPublishedContent content, string contentTypeAlias) { - return content.AncestorsOrSelf(false, n => n.ContentType.Alias == contentTypeAlias); + return content.AncestorsOrSelf(false, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); } /// @@ -591,7 +500,7 @@ namespace Umbraco.Web /// May or may not begin with the content itself, depending on its content type. public static IEnumerable AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias) { - return content.AncestorsOrSelf(true, n => n.ContentType.Alias == contentTypeAlias); + return content.AncestorsOrSelf(true, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); } /// @@ -654,7 +563,7 @@ namespace Umbraco.Web /// Does not consider the content itself. May return null. public static IPublishedContent Ancestor(this IPublishedContent content, string contentTypeAlias) { - return content.EnumerateAncestors(false).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); + return content.EnumerateAncestors(false).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); } /// @@ -717,7 +626,7 @@ namespace Umbraco.Web /// May or may not return the content itself depending on its content type. May return null. public static IPublishedContent AncestorOrSelf(this IPublishedContent content, string contentTypeAlias) { - return content.EnumerateAncestors(true).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); + return content.EnumerateAncestors(true).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); } /// @@ -778,7 +687,7 @@ namespace Umbraco.Web /// The specific culture to filter for. If null is used the current culture is used. (Default is null) /// /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// This can be useful in order to return all nodes in an entire site by a type when combined with or similar. /// public static IEnumerable DescendantsOrSelfOfType(this IEnumerable parentNodes, string docTypeAlias, string culture = null) { @@ -792,7 +701,7 @@ namespace Umbraco.Web /// The specific culture to filter for. If null is used the current culture is used. (Default is null) /// /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// This can be useful in order to return all nodes in an entire site by a type when combined with or similar. /// public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes, string culture = null) where T : class, IPublishedContent @@ -832,7 +741,7 @@ namespace Umbraco.Web public static IEnumerable DescendantsOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.DescendantsOrSelf(false, p => p.ContentType.Alias == contentTypeAlias, culture); + return content.DescendantsOrSelf(false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); } public static IEnumerable Descendants(this IPublishedContent content, string culture = null) @@ -859,7 +768,7 @@ namespace Umbraco.Web public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.DescendantsOrSelf(true, p => p.ContentType.Alias == contentTypeAlias, culture); + return content.DescendantsOrSelf(true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); } public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string culture = null) @@ -886,7 +795,7 @@ namespace Umbraco.Web public static IPublishedContent DescendantOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.EnumerateDescendants(false, culture).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); + return content.EnumerateDescendants(false, culture).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); } public static T Descendant(this IPublishedContent content, string culture = null) @@ -913,7 +822,7 @@ namespace Umbraco.Web public static IPublishedContent DescendantOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.EnumerateDescendants(true, culture).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); + return content.EnumerateDescendants(true, culture).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); } public static T DescendantOrSelf(this IPublishedContent content, string culture = null) @@ -938,7 +847,7 @@ namespace Umbraco.Web if (content == null) throw new ArgumentNullException(nameof(content)); if (orSelf) yield return content; - foreach (var desc in content.Children(culture).SelectMany(x => x.EnumerateDescendants())) + foreach (var desc in content.Children(culture).SelectMany(x => x.EnumerateDescendants(culture))) yield return desc; } @@ -946,7 +855,7 @@ namespace Umbraco.Web { yield return content; - foreach (var desc in content.Children(culture).SelectMany(x => x.EnumerateDescendants())) + foreach (var desc in content.Children(culture).SelectMany(x => x.EnumerateDescendants(culture))) yield return desc; } @@ -973,23 +882,6 @@ namespace Umbraco.Web #region Axes: children - /// - /// Gets the children of the content. - /// - /// The content. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of the content. - /// - /// Children are sorted by their sortOrder. - /// This method exists for consistency, it is the same as calling content.Children as a property. - /// - public static IEnumerable Children(this IPublishedContent content, string culture = null) - { - if (content == null) throw new ArgumentNullException(nameof(content)); - - return content.Children.WhereIsInvariantOrHasCulture(culture); - } - /// /// Gets the children of the content, filtered by a predicate. /// @@ -1014,7 +906,7 @@ namespace Umbraco.Web /// The children of the content, of any of the specified types. public static IEnumerable ChildrenOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.Children(x => contentTypeAlias.InvariantContains(x.ContentType.Alias), culture); + return content.Children(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); } /// @@ -1095,7 +987,7 @@ namespace Umbraco.Web ? content.Children(culture).Any() ? content.Children(culture).ElementAt(0) : null - : content.Children(culture).FirstOrDefault(x => x.ContentType.Alias == contentTypeAliasFilter); + : content.Children(culture).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAliasFilter)); if (firstNode == null) return new DataTable(); //no children found @@ -1111,24 +1003,24 @@ namespace Umbraco.Web //create all row data var tableData = Core.DataTableExtensions.CreateTableData(); //loop through each child and create row data for it - foreach (var n in content.Children.OrderBy(x => x.SortOrder)) + foreach (var n in content.Children().OrderBy(x => x.SortOrder)) { if (contentTypeAliasFilter.IsNullOrWhiteSpace() == false) { - if (n.ContentType.Alias != contentTypeAliasFilter) + if (n.ContentType.Alias.InvariantEquals(contentTypeAliasFilter) == false) continue; //skip this one, it doesn't match the filter } var standardVals = new Dictionary { { "Id", n.Id }, - { "NodeName", n.Name }, + { "NodeName", n.Name() }, { "NodeTypeAlias", n.ContentType.Alias }, { "CreateDate", n.CreateDate }, { "UpdateDate", n.UpdateDate }, { "CreatorName", n.CreatorName }, { "WriterName", n.WriterName }, - { "Url", n.Url } + { "Url", n.Url() } }; var userVals = new Dictionary(); @@ -1189,7 +1081,7 @@ namespace Umbraco.Web /// /// Note that in V7 this method also return the content node self. /// - public static IEnumerable Siblings(this IPublishedContent content, string culture = null) + public static IEnumerable Siblings(this IPublishedContent content, string culture = null) where T : class, IPublishedContent { return SiblingsAndSelf(content, culture).Where(x => x.Id != content.Id); @@ -1300,5 +1192,43 @@ namespace Umbraco.Web } #endregion + + #region Url + + /// + /// Gets the url of the content item. + /// + /// + /// If the content item is a document, then this method returns the url of the + /// document. If it is a media, then this methods return the media url for the + /// 'umbracoFile' property. Use the MediaUrl() method to get the media url for other + /// properties. + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + public static string Url(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Default) + { + var umbracoContext = Composing.Current.UmbracoContext; + + if (umbracoContext == null) + throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext is null."); + if (umbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext.UrlProvider is null."); + + switch (content.ContentType.ItemType) + { + case PublishedItemType.Content: + return umbracoContext.UrlProvider.GetUrl(content, mode, culture); + + case PublishedItemType.Media: + return umbracoContext.UrlProvider.GetMediaUrl(content, mode, culture, Constants.Conventions.Media.File); + + default: + throw new NotSupportedException(); + } + } + + #endregion } } diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 61180580cb..d697898f33 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -20,14 +20,22 @@ namespace Umbraco.Web { private readonly IPublishedSnapshot _publishedSnapshot; private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IExamineManager _examineManager; + + [Obsolete("Use the constructor with all parameters instead")] + public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor) + : this (publishedSnapshot, variationContextAccessor, ExamineManager.Instance) + { + } /// /// Initializes a new instance of the class. /// - public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor) + public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor, IExamineManager examineManager) { _publishedSnapshot = publishedSnapshot ?? throw new ArgumentNullException(nameof(publishedSnapshot)); _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); } #region Content @@ -175,46 +183,62 @@ namespace Umbraco.Web #region Search /// - public IEnumerable Search(string term, string culture = null, string indexName = null) + public IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName) { return Search(term, 0, 0, out _, culture, indexName); } /// - public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = null, string indexName = null) + public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName) { - indexName = string.IsNullOrEmpty(indexName) - ? Constants.UmbracoIndexes.ExternalIndexName - : indexName; - - if (!ExamineManager.Instance.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) - throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); - - var searcher = umbIndex.GetSearcher(); - - // default to max 500 results - var count = skip == 0 && take == 0 ? 500 : skip + take; - - //set this to the specific culture or to the culture in the request - culture = culture ?? _variationContextAccessor.VariationContext.Culture; - - ISearchResults results; - if (culture.IsNullOrWhiteSpace()) + if (skip < 0) { - results = searcher.Search(term, count); + throw new ArgumentOutOfRangeException(nameof(skip), skip, "The value must be greater than or equal to zero."); + } + + if (take < 0) + { + throw new ArgumentOutOfRangeException(nameof(take), take, "The value must be greater than or equal to zero."); + } + + if (string.IsNullOrEmpty(indexName)) + { + indexName = Constants.UmbracoIndexes.ExternalIndexName; + } + + if (!_examineManager.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) + { + throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); + } + + var query = umbIndex.GetSearcher().CreateQuery(IndexTypes.Content); + + IQueryExecutor queryExecutor; + if (culture == "*") + { + // Search everything + queryExecutor = query.ManagedQuery(term); + } + else if (string.IsNullOrWhiteSpace(culture)) + { + // Only search invariant + queryExecutor = query.Field(UmbracoContentIndex.VariesByCultureFieldName, "n") // Must not vary by culture + .And().ManagedQuery(term); } else { - //get all index fields suffixed with the culture name supplied - var cultureFields = umbIndex.GetCultureFields(culture); - var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "y"); //must vary by culture - qry = qry.And().ManagedQuery(term, cultureFields.ToArray()); - results = qry.Execute(count); + // Only search the specified culture + var fields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); // Get all index fields suffixed with the culture name supplied + queryExecutor = query.ManagedQuery(term, fields); } + var results = skip == 0 && take == 0 + ? queryExecutor.Execute() + : queryExecutor.Execute(skip + take); + totalRecords = results.TotalItemCount; - return new CultureContextualSearchResults(results.ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); + return new CultureContextualSearchResults(results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); } /// @@ -226,12 +250,23 @@ namespace Umbraco.Web /// public IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords) { + if (skip < 0) + { + throw new ArgumentOutOfRangeException(nameof(skip), skip, "The value must be greater than or equal to zero."); + } + + if (take < 0) + { + throw new ArgumentOutOfRangeException(nameof(take), take, "The value must be greater than or equal to zero."); + } + var results = skip == 0 && take == 0 ? query.Execute() - : query.Execute(maxResults: skip + take); + : query.Execute(skip + take); totalRecords = results.TotalItemCount; - return results.ToPublishedSearchResults(_publishedSnapshot.Content); + + return results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot); } /// @@ -304,9 +339,6 @@ namespace Umbraco.Web } } - - - #endregion } } diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index de7c72d21a..f28581eb42 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web /// A value indicating whether the content is of a content type composed of a content type identified by the alias. public static bool IsComposedOf(this IPublishedElement content, string alias) { - return content.ContentType.CompositionAliases.Contains(alias); + return content.ContentType.CompositionAliases.InvariantContains(alias); } #endregion @@ -145,7 +145,7 @@ namespace Umbraco.Web } #endregion - + #region ToIndexedArray public static IndexedArrayItem[] ToIndexedArray(this IEnumerable source) @@ -165,8 +165,9 @@ namespace Umbraco.Web public static IEnumerable OfTypes(this IEnumerable contents, params string[] types) where T : IPublishedElement { - types = types.Select(x => x.ToLowerInvariant()).ToArray(); - return contents.Where(x => types.Contains(x.ContentType.Alias.ToLowerInvariant())); + if (types == null || types.Length == 0) return Enumerable.Empty(); + + return contents.Where(x => types.InvariantContains(x.ContentType.Alias)); } #endregion @@ -188,5 +189,34 @@ namespace Umbraco.Web } #endregion + + #region MediaUrl + + /// + /// Gets the url for a media. + /// + /// The content item. + /// The culture (use current culture by default). + /// The url mode (use site configuration by default). + /// The alias of the property (use 'umbracoFile' by default). + /// The url for the media. + /// + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + public static string MediaUrl(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Default, string propertyAlias = Constants.Conventions.Media.File) + { + var umbracoContext = Composing.Current.UmbracoContext; + + if (umbracoContext == null) + throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext is null."); + if (umbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext.UrlProvider is null."); + + return umbracoContext.UrlProvider.GetMediaUrl(content, mode, culture, propertyAlias); + } + + #endregion } } diff --git a/src/Umbraco.Web/RoutableDocumentFilter.cs b/src/Umbraco.Web/RoutableDocumentFilter.cs new file mode 100644 index 0000000000..9725ce0e14 --- /dev/null +++ b/src/Umbraco.Web/RoutableDocumentFilter.cs @@ -0,0 +1,207 @@ +using System; +using System.IO; +using System.Web; +using System.Web.Routing; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Configuration; +using System.Threading; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.IO; +using System.Collections.Concurrent; +using Umbraco.Core.Collections; + +namespace Umbraco.Web +{ + /// + /// Utility class used to check if the current request is for a front-end request + /// + /// + /// There are various checks to determine if this is a front-end request such as checking if the request is part of any reserved paths or existing MVC routes. + /// + public sealed class RoutableDocumentFilter + { + public RoutableDocumentFilter(IGlobalSettings globalSettings) + { + _globalSettings = globalSettings; + } + + private static readonly ConcurrentDictionary RouteChecks = new ConcurrentDictionary(); + private readonly IGlobalSettings _globalSettings; + private object _locker = new object(); + private bool _isInit = false; + private int? _routeCount; + private HashSet _reservedList; + + /// + /// Checks if the request is a document request (i.e. one that the module should handle) + /// + /// + /// + /// + public bool IsDocumentRequest(HttpContextBase httpContext, Uri uri) + { + var maybeDoc = true; + var lpath = uri.AbsolutePath.ToLowerInvariant(); + + // handle directory-urls used for asmx + // TODO: legacy - what's the point really? + var asmxPos = lpath.IndexOf(".asmx/", StringComparison.OrdinalIgnoreCase); + if (asmxPos >= 0) + { + // use uri.AbsolutePath, not path, 'cos path has been lowercased + httpContext.RewritePath(uri.AbsolutePath.Substring(0, asmxPos + 5), // filePath + uri.AbsolutePath.Substring(asmxPos + 5), // pathInfo + uri.Query.TrimStart('?')); + maybeDoc = false; + } + + // a document request should be + // /foo/bar/nil + // /foo/bar/nil/ + // /foo/bar/nil.aspx + // where /foo is not a reserved path + + // if the path contains an extension that is not .aspx + // then it cannot be a document request + var extension = Path.GetExtension(lpath); + if (maybeDoc && extension.IsNullOrWhiteSpace() == false && extension != ".aspx") + maybeDoc = false; + + // at that point, either we have no extension, or it is .aspx + + // if the path is reserved then it cannot be a document request + if (maybeDoc && IsReservedPathOrUrl(lpath, httpContext, RouteTable.Routes)) + maybeDoc = false; + + //NOTE: No need to warn, plus if we do we should log the document, as this message doesn't really tell us anything :) + //if (!maybeDoc) + //{ + // Logger.Warn("Not a document"); + //} + return maybeDoc; + } + + /// + /// 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 bool IsReservedPathOrUrl(string url) + { + LazyInitializer.EnsureInitialized(ref _reservedList, ref _isInit, ref _locker, () => + { + // store references to strings to determine changes + var reservedPathsCache = _globalSettings.ReservedPaths; + var 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 NormalizePaths(reservedPathsCache.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries))) + { + newReservedList.Add(reservedPathTrimmed); + } + + foreach (var reservedPathTrimmed in NormalizePaths(ReservedPaths)) + { + newReservedList.Add(reservedPathTrimmed); + } + + // use the new list from now on + return 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)); + } + + private IEnumerable NormalizePaths(IEnumerable paths) + { + return paths + .Select(x => x.Trim().ToLowerInvariant()) + .Where(x => x.IsNullOrWhiteSpace() == false) + .Select(reservedPath => IOHelper.ResolveUrl(reservedPath).Trim().EnsureStartsWith("/").EnsureEndsWith("/")) + .Where(reservedPathTrimmed => reservedPathTrimmed.IsNullOrWhiteSpace() == false); + } + + /// + /// 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 bool IsReservedPathOrUrl(string url, HttpContextBase httpContext, RouteCollection routes) + { + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + if (routes == null) throw new ArgumentNullException(nameof(routes)); + + //This is some rudimentary code to check if the route table has changed at runtime, we're basically just keeping a count + //of the routes. This isn't fail safe but there's no way to monitor changes to the route table. Else we need to create a hash + //of all routes and then recompare but that will be annoying to do on each request and then we might as well just do the whole MVC + //route on each request like we were doing before instead of caching the result of GetRouteData. + var changed = false; + using (routes.GetReadLock()) + { + if (!_routeCount.HasValue || _routeCount.Value != routes.Count) + { + //the counts are not set or have changed, need to reset + changed = true; + } + } + if (changed) + { + using (routes.GetWriteLock()) + { + _routeCount = routes.Count; + + //try clearing each entry + foreach(var r in RouteChecks.Keys.ToList()) + RouteChecks.TryRemove(r, out _); + } + } + + var absPath = httpContext?.Request?.Url.AbsolutePath; + + if (absPath.IsNullOrWhiteSpace()) + return false; + + //check if the current request matches a route, if so then it is reserved. + var hasRoute = RouteChecks.GetOrAdd(absPath, x => routes.GetRouteData(httpContext) != null); + if (hasRoute) + return true; + + //continue with the standard ignore routine + return IsReservedPathOrUrl(url); + } + + /// + /// This is used internally to track any registered callback paths for Identity providers. If the request path matches + /// any of the registered paths, then the module will let the request keep executing + /// + internal static readonly ConcurrentHashSet ReservedPaths = new ConcurrentHashSet(); + } +} diff --git a/src/Umbraco.Web/Routing/AliasUrlProvider.cs b/src/Umbraco.Web/Routing/AliasUrlProvider.cs index 4c879e931f..f84ac608d2 100644 --- a/src/Umbraco.Web/Routing/AliasUrlProvider.cs +++ b/src/Umbraco.Web/Routing/AliasUrlProvider.cs @@ -31,7 +31,7 @@ namespace Umbraco.Web.Routing #region GetUrl /// - public UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current) + public UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlMode mode, string culture, Uri current) { return null; // we have nothing to say } @@ -53,23 +53,21 @@ namespace Umbraco.Web.Routing /// public IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current) { - var node = umbracoContext.ContentCache.GetById(id); + var node = umbracoContext.Content.GetById(id); if (node == null) yield break; if (!node.HasProperty(Constants.Conventions.Content.UrlAlias)) yield break; - var domainHelper = umbracoContext.GetDomainHelper(_siteDomainHelper); - // look for domains, walking up the tree var n = node; - var domainUris = domainHelper.DomainsForNode(n.Id, current, false); + var domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainHelper, n.Id, current, false); while (domainUris == null && n != null) // n is null at root { // move to parent node n = n.Parent; - domainUris = n == null ? null : domainHelper.DomainsForNode(n.Id, current, excludeDefault: false); + domainUris = n == null ? null : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainHelper, n.Id, current, excludeDefault: false); } // determine whether the alias property varies @@ -86,12 +84,17 @@ namespace Umbraco.Web.Routing yield break; var umbracoUrlName = node.Value(Constants.Conventions.Content.UrlAlias); - if (string.IsNullOrWhiteSpace(umbracoUrlName)) + var aliases = umbracoUrlName?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + if (aliases == null || aliases.Any() == false) yield break; - var path = "/" + umbracoUrlName; - var uri = new Uri(path, UriKind.Relative); - yield return UrlInfo.Url(UriUtility.UriFromUmbraco(uri, _globalSettings, _requestConfig).ToString()); + foreach (var alias in aliases.Distinct()) + { + var path = "/" + alias; + var uri = new Uri(path, UriKind.Relative); + yield return UrlInfo.Url(UriUtility.UriFromUmbraco(uri, _globalSettings, _requestConfig).ToString()); + } } else { @@ -108,12 +111,17 @@ namespace Umbraco.Web.Routing ? node.Value(Constants.Conventions.Content.UrlAlias, culture: domainUri.Culture.Name) : node.Value(Constants.Conventions.Content.UrlAlias); - if (string.IsNullOrWhiteSpace(umbracoUrlName)) + var aliases = umbracoUrlName?.Split(new [] {','}, StringSplitOptions.RemoveEmptyEntries); + + if (aliases == null || aliases.Any() == false) continue; - var path = "/" + umbracoUrlName; - var uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); - yield return UrlInfo.Url(UriUtility.UriFromUmbraco(uri, _globalSettings, _requestConfig).ToString(), domainUri.Culture.Name); + foreach(var alias in aliases.Distinct()) + { + var path = "/" + alias; + var uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); + yield return UrlInfo.Url(UriUtility.UriFromUmbraco(uri, _globalSettings, _requestConfig).ToString(), domainUri.Culture.Name); + } } } } diff --git a/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs b/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs index 75af1fed84..eae198bb59 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs @@ -47,13 +47,13 @@ namespace Umbraco.Web.Routing while (pos > 1) { route = route.Substring(0, pos); - node = frequest.UmbracoContext.ContentCache.GetByRoute(route, culture: frequest?.Culture?.Name); + node = frequest.UmbracoContext.Content.GetByRoute(route, culture: frequest?.Culture?.Name); if (node != null) break; pos = route.LastIndexOf('/'); } if (node != null) { - var d = DomainHelper.FindWildcardDomainInPath(frequest.UmbracoContext.PublishedSnapshot.Domains.GetAll(true), node.Path, null); + var d = DomainUtilities.FindWildcardDomainInPath(frequest.UmbracoContext.PublishedSnapshot.Domains.GetAll(true), node.Path, null); if (d != null) errorCulture = d.Culture; } @@ -71,7 +71,7 @@ namespace Umbraco.Web.Routing { _logger.Debug("Got id={ErrorNodeId}.", error404.Value); - content = frequest.UmbracoContext.ContentCache.GetById(error404.Value); + content = frequest.UmbracoContext.Content.GetById(error404.Value); _logger.Debug(content == null ? "Could not find content with that id." diff --git a/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs b/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs index b3c060fe35..b339198928 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.Routing if (nodeId > 0) { _logger.Debug("Id={NodeId}", nodeId); - node = frequest.UmbracoContext.ContentCache.GetById(nodeId); + node = frequest.UmbracoContext.Content.GetById(nodeId); if (node != null) { diff --git a/src/Umbraco.Web/Routing/ContentFinderByPageIdQuery.cs b/src/Umbraco.Web/Routing/ContentFinderByPageIdQuery.cs index 54745b2529..70a920f6f0 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByPageIdQuery.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByPageIdQuery.cs @@ -14,7 +14,7 @@ int pageId; if (int.TryParse(frequest.UmbracoContext.HttpContext.Request["umbPageID"], out pageId)) { - var doc = frequest.UmbracoContext.ContentCache.GetById(pageId); + var doc = frequest.UmbracoContext.Content.GetById(pageId); if (doc != null) { diff --git a/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs b/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs index 254dedf979..46e44dc5a3 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.Routing public bool TryFindContent(PublishedRequest frequest) { var route = frequest.HasDomain - ? frequest.Domain.ContentId + DomainHelper.PathRelativeToDomain(frequest.Domain.Uri, frequest.Uri.GetAbsolutePathDecoded()) + ? frequest.Domain.ContentId + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.Uri.GetAbsolutePathDecoded()) : frequest.Uri.GetAbsolutePathDecoded(); var redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(route); @@ -44,8 +44,8 @@ namespace Umbraco.Web.Routing return false; } - var content = frequest.UmbracoContext.ContentCache.GetById(redirectUrl.ContentId); - var url = content == null ? "#" : content.GetUrl(redirectUrl.Culture); + var content = frequest.UmbracoContext.Content.GetById(redirectUrl.ContentId); + var url = content == null ? "#" : content.Url(redirectUrl.Culture); if (url.StartsWith("#")) { _logger.Debug("Route {Route} matches content {ContentId} which has no url.", route, redirectUrl.ContentId); diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrl.cs b/src/Umbraco.Web/Routing/ContentFinderByUrl.cs index 11f2717455..0a14dc97fe 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrl.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrl.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Routing { string route; if (frequest.HasDomain) - route = frequest.Domain.ContentId + DomainHelper.PathRelativeToDomain(frequest.Domain.Uri, frequest.Uri.GetAbsolutePathDecoded()); + route = frequest.Domain.ContentId + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.Uri.GetAbsolutePathDecoded()); else route = frequest.Uri.GetAbsolutePathDecoded(); @@ -48,7 +48,7 @@ namespace Umbraco.Web.Routing Logger.Debug("Test route {Route}", route); - var node = docreq.UmbracoContext.ContentCache.GetByRoute(docreq.UmbracoContext.InPreviewMode, route, culture: docreq.Culture?.Name); + var node = docreq.UmbracoContext.Content.GetByRoute(docreq.UmbracoContext.InPreviewMode, route, culture: docreq.Culture?.Name); if (node != null) { docreq.PublishedContent = node; diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs index a98dbf8a80..cf71611047 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.Routing if (frequest.Uri.AbsolutePath != "/") // no alias if "/" { - node = FindContentByAlias(frequest.UmbracoContext.ContentCache, + node = FindContentByAlias(frequest.UmbracoContext.Content, frequest.HasDomain ? frequest.Domain.ContentId : 0, frequest.Culture.Name, frequest.Uri.GetAbsolutePathDecoded()); diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs b/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs index 00cd5566de..16dfa63596 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs @@ -38,7 +38,7 @@ namespace Umbraco.Web.Routing var path = frequest.Uri.GetAbsolutePathDecoded(); if (frequest.HasDomain) - path = DomainHelper.PathRelativeToDomain(frequest.Domain.Uri, path); + path = DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, path); // no template if "/" if (path == "/") diff --git a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs index a9c961c280..89abde0576 100644 --- a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs @@ -1,7 +1,7 @@ using System; -using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.Routing { @@ -10,13 +10,26 @@ namespace Umbraco.Web.Routing /// public class DefaultMediaUrlProvider : IMediaUrlProvider { + private readonly PropertyEditorCollection _propertyEditors; + + public DefaultMediaUrlProvider(PropertyEditorCollection propertyEditors) + { + _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + } + + [Obsolete("Use the constructor with all parameters instead")] + public DefaultMediaUrlProvider() : this(Current.PropertyEditors) + { + } + /// public virtual UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, - string propertyAlias, - UrlProviderMode mode, string culture, Uri current) + string propertyAlias, UrlMode mode, string culture, Uri current) { var prop = content.GetProperty(propertyAlias); - var value = prop?.GetValue(culture); + + // get the raw source value since this is what is used by IDataEditorWithMediaPath for processing + var value = prop?.GetSourceValue(culture); if (value == null) { return null; @@ -25,38 +38,37 @@ namespace Umbraco.Web.Routing var propType = prop.PropertyType; string path = null; - switch (propType.EditorAlias) + if (_propertyEditors.TryGet(propType.EditorAlias, out var editor) + && editor is IDataEditorWithMediaPath dataEditor) { - case Constants.PropertyEditors.Aliases.UploadField: - path = value.ToString(); - break; - case Constants.PropertyEditors.Aliases.ImageCropper: - //get the url from the json format - path = value is ImageCropperValue stronglyTyped ? stronglyTyped.Src : value.ToString(); - break; + path = dataEditor.GetMediaPath(value); } var url = AssembleUrl(path, current, mode); return url == null ? null : UrlInfo.Url(url.ToString(), culture); } - private Uri AssembleUrl(string path, Uri current, UrlProviderMode mode) + private Uri AssembleUrl(string path, Uri current, UrlMode mode) { if (string.IsNullOrEmpty(path)) return null; + // the stored path is absolute so we just return it as is + if(Uri.IsWellFormedUriString(path, UriKind.Absolute)) + return new Uri(path); + Uri uri; if (current == null) - mode = UrlProviderMode.Relative; // best we can do + mode = UrlMode.Relative; // best we can do switch (mode) { - case UrlProviderMode.Absolute: + case UrlMode.Absolute: uri = new Uri(current?.GetLeftPart(UriPartial.Authority) + path); break; - case UrlProviderMode.Relative: - case UrlProviderMode.Auto: + case UrlMode.Relative: + case UrlMode.Auto: uri = new Uri(path, UriKind.Relative); break; default: diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index 9e13d3b9c1..4092538481 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -29,17 +29,17 @@ namespace Umbraco.Web.Routing #region GetUrl /// - public virtual UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current) + public virtual UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlMode mode, string culture, Uri current) { if (!current.IsAbsoluteUri) throw new ArgumentException("Current url must be absolute.", nameof(current)); // will not use cache if previewing - var route = umbracoContext.ContentCache.GetRouteById(content.Id, culture); + var route = umbracoContext.Content.GetRouteById(content.Id, culture); return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture); } - internal UrlInfo GetUrlFromRoute(string route, UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode, string culture) + internal UrlInfo GetUrlFromRoute(string route, UmbracoContext umbracoContext, int id, Uri current, UrlMode mode, string culture) { if (string.IsNullOrWhiteSpace(route)) { @@ -47,15 +47,13 @@ namespace Umbraco.Web.Routing return null; } - var domainHelper = umbracoContext.GetDomainHelper(_siteDomainHelper); - // extract domainUri and path // route is / or / var pos = route.IndexOf('/'); var path = pos == 0 ? route : route.Substring(pos); var domainUri = pos == 0 ? null - : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current, culture); + : DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainHelper, int.Parse(route.Substring(0, pos)), current, culture); // assemble the url from domainUri (maybe null) and path var url = AssembleUrl(domainUri, path, current, mode).ToString(); @@ -80,19 +78,17 @@ namespace Umbraco.Web.Routing /// public virtual IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current) { - var node = umbracoContext.ContentCache.GetById(id); + var node = umbracoContext.Content.GetById(id); if (node == null) yield break; - var domainHelper = umbracoContext.GetDomainHelper(_siteDomainHelper); - // look for domains, walking up the tree var n = node; - var domainUris = domainHelper.DomainsForNode(n.Id, current, false); + var domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainHelper, n.Id, current, false); while (domainUris == null && n != null) // n is null at root { n = n.Parent; // move to parent node - domainUris = n == null ? null : domainHelper.DomainsForNode(n.Id, current, excludeDefault: true); + domainUris = n == null ? null : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainHelper, n.Id, current, excludeDefault: true); } // no domains = exit @@ -104,7 +100,7 @@ namespace Umbraco.Web.Routing var culture = d?.Culture?.Name; //although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok - var route = umbracoContext.ContentCache.GetRouteById(id, culture); + var route = umbracoContext.Content.GetRouteById(id, culture); if (route == null) continue; //need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned) @@ -121,7 +117,7 @@ namespace Umbraco.Web.Routing #region Utilities - Uri AssembleUrl(DomainAndUri domainUri, string path, Uri current, UrlProviderMode mode) + Uri AssembleUrl(DomainAndUri domainUri, string path, Uri current, UrlMode mode) { Uri uri; @@ -130,15 +126,15 @@ namespace Umbraco.Web.Routing if (domainUri == null) // no domain was found { if (current == null) - mode = UrlProviderMode.Relative; // best we can do + mode = UrlMode.Relative; // best we can do switch (mode) { - case UrlProviderMode.Absolute: + case UrlMode.Absolute: uri = new Uri(current.GetLeftPart(UriPartial.Authority) + path); break; - case UrlProviderMode.Relative: - case UrlProviderMode.Auto: + case UrlMode.Relative: + case UrlMode.Auto: uri = new Uri(path, UriKind.Relative); break; default: @@ -147,21 +143,21 @@ namespace Umbraco.Web.Routing } else // a domain was found { - if (mode == UrlProviderMode.Auto) + if (mode == UrlMode.Auto) { //this check is a little tricky, we can't just compare domains if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) == current.GetLeftPart(UriPartial.Authority)) - mode = UrlProviderMode.Relative; + mode = UrlMode.Relative; else - mode = UrlProviderMode.Absolute; + mode = UrlMode.Absolute; } switch (mode) { - case UrlProviderMode.Absolute: + case UrlMode.Absolute: uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); break; - case UrlProviderMode.Relative: + case UrlMode.Relative: uri = new Uri(CombinePaths(domainUri.Uri.AbsolutePath, path), UriKind.Relative); break; default: diff --git a/src/Umbraco.Web/Routing/DomainAndUri.cs b/src/Umbraco.Web/Routing/DomainAndUri.cs index 1151055621..46dc085998 100644 --- a/src/Umbraco.Web/Routing/DomainAndUri.cs +++ b/src/Umbraco.Web/Routing/DomainAndUri.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.Routing { try { - Uri = DomainHelper.ParseUriFromDomainName(Name, currentUri); + Uri = DomainUtilities.ParseUriFromDomainName(Name, currentUri); } catch (UriFormatException) { diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainUtilities.cs similarity index 81% rename from src/Umbraco.Web/Routing/DomainHelper.cs rename to src/Umbraco.Web/Routing/DomainUtilities.cs index 95d97653a0..fb0c56b28d 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainUtilities.cs @@ -2,29 +2,69 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; -using Umbraco.Web.PublishedCache; // published snapshot +using Umbraco.Web.PublishedCache; namespace Umbraco.Web.Routing { /// /// Provides utilities to handle domains. /// - public class DomainHelper + public static class DomainUtilities { - private readonly IDomainCache _domainCache; - private readonly ISiteDomainHelper _siteDomainHelper; + #region Document Culture - public DomainHelper(IDomainCache domainCache, ISiteDomainHelper siteDomainHelper) + /// + /// Gets the culture assigned to a document by domains, in the context of a current Uri. + /// + /// The document identifier. + /// The document path. + /// An optional current Uri. + /// An Umbraco context. + /// The site domain helper. + /// The culture assigned to the document by domains. + /// + /// In 1:1 multilingual setup, a document contains several cultures (there is not + /// one document per culture), and domains, withing the context of a current Uri, assign + /// a culture to that document. + /// + internal static string GetCultureFromDomains(int contentId, string contentPath, Uri current, UmbracoContext umbracoContext, ISiteDomainHelper siteDomainHelper) { - _domainCache = domainCache; - _siteDomainHelper = siteDomainHelper; + if (umbracoContext == null) + throw new InvalidOperationException("A current UmbracoContext is required."); + + if (current == null) + current = umbracoContext.CleanedUmbracoUrl; + + // get the published route, else the preview route + // if both are null then the content does not exist + var route = umbracoContext.Content.GetRouteById(contentId) ?? + umbracoContext.Content.GetRouteById(true, contentId); + + if (route == null) + return null; + + var pos = route.IndexOf('/'); + var domain = pos == 0 + ? null + : DomainForNode(umbracoContext.Domains, siteDomainHelper, int.Parse(route.Substring(0, pos)), current); + + var rootContentId = domain?.ContentId ?? -1; + var wcDomain = FindWildcardDomainInPath(umbracoContext.Domains.GetAll(true), contentPath, rootContentId); + + if (wcDomain != null) return wcDomain.Culture.Name; + if (domain != null) return domain.Culture.Name; + return umbracoContext.Domains.DefaultCulture; } - #region Domain for Node + #endregion + + #region Domain for Document /// /// Finds the domain for the specified node, if any, that best matches a specified uri. /// + /// A domain cache. + /// The site domain helper. /// The node identifier. /// The uri, or null. /// The culture, or null. @@ -35,14 +75,14 @@ namespace Umbraco.Web.Routing /// If culture is null, uses the default culture for the installation instead. Otherwise, /// will try with the specified culture, else return null. /// - internal DomainAndUri DomainForNode(int nodeId, Uri current, string culture = null) + internal static DomainAndUri DomainForNode(IDomainCache domainCache, ISiteDomainHelper siteDomainHelper, int nodeId, Uri current, string culture = null) { // be safe if (nodeId <= 0) return null; // get the domains on that node - var domains = _domainCache.GetAssigned(nodeId, false).ToArray(); + var domains = domainCache.GetAssigned(nodeId).ToArray(); // none? if (domains.Length == 0) @@ -50,37 +90,28 @@ namespace Umbraco.Web.Routing // else filter // it could be that none apply (due to culture) - return SelectDomain(domains, current, culture, _domainCache.DefaultCulture, - (cdomainAndUris, ccurrent, cculture, cdefaultCulture) => _siteDomainHelper.MapDomain(cdomainAndUris, ccurrent, cculture, cdefaultCulture)); - } - - /// - /// Gets a value indicating whether a specified node has domains. - /// - /// The node identifier. - /// True if the node has domains, else false. - internal bool NodeHasDomains(int nodeId) - { - return nodeId > 0 && _domainCache.GetAssigned(nodeId, false).Any(); + return SelectDomain(domains, current, culture, domainCache.DefaultCulture, siteDomainHelper.MapDomain); } /// /// Find the domains for the specified node, if any, that match a specified uri. /// + /// A domain cache. + /// The site domain helper. /// The node identifier. /// The uri, or null. /// A value indicating whether to exclude the current/default domain. True by default. /// The domains and their uris, that match the specified uri, else null. /// If at least a domain is set on the node then the method returns the domains that /// best match the specified uri, else it returns null. - internal IEnumerable DomainsForNode(int nodeId, Uri current, bool excludeDefault = true) + internal static IEnumerable DomainsForNode(IDomainCache domainCache, ISiteDomainHelper siteDomainHelper, int nodeId, Uri current, bool excludeDefault = true) { // be safe if (nodeId <= 0) return null; // get the domains on that node - var domains = _domainCache.GetAssigned(nodeId, false).ToArray(); + var domains = domainCache.GetAssigned(nodeId).ToArray(); // none? if (domains.Length == 0) @@ -90,7 +121,7 @@ namespace Umbraco.Web.Routing var domainAndUris = SelectDomains(domains, current).ToArray(); // filter - return _siteDomainHelper.MapDomains(domainAndUris, current, excludeDefault, null, _domainCache.DefaultCulture).ToArray(); + return siteDomainHelper.MapDomains(domainAndUris, current, excludeDefault, null, domainCache.DefaultCulture).ToArray(); } #endregion @@ -252,7 +283,7 @@ namespace Umbraco.Web.Routing } /// - /// Parses a domain name into a URI. + /// Parses a domain name into a URI. /// /// The domain name to parse /// The currently requested URI. If the domain name is relative, the authority of URI will be used. diff --git a/src/Umbraco.Web/Routing/IContentFinder.cs b/src/Umbraco.Web/Routing/IContentFinder.cs index fe5699ac7b..2e388c4814 100644 --- a/src/Umbraco.Web/Routing/IContentFinder.cs +++ b/src/Umbraco.Web/Routing/IContentFinder.cs @@ -8,9 +8,9 @@ namespace Umbraco.Web.Routing /// /// Tries to find and assign an Umbraco document to a PublishedRequest. /// - /// The PublishedRequest. + /// The PublishedRequest. /// A value indicating whether an Umbraco document was found and assigned. /// Optionally, can also assign the template or anything else on the document request, although that is not required. - bool TryFindContent(PublishedRequest frequest); + bool TryFindContent(PublishedRequest request); } } diff --git a/src/Umbraco.Web/Routing/IMediaUrlProvider.cs b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs index 419e4d78df..8a81b27415 100644 --- a/src/Umbraco.Web/Routing/IMediaUrlProvider.cs +++ b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs @@ -26,6 +26,6 @@ namespace Umbraco.Web.Routing /// e.g. a cdn url provider will most likely always return an absolute url. /// If the provider is unable to provide a url, it returns null. /// - UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, string propertyAlias, UrlProviderMode mode, string culture, Uri current); + UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, string propertyAlias, UrlMode mode, string culture, Uri current); } } diff --git a/src/Umbraco.Web/Routing/IUrlProvider.cs b/src/Umbraco.Web/Routing/IUrlProvider.cs index 9459c2552d..c0ce1fef39 100644 --- a/src/Umbraco.Web/Routing/IUrlProvider.cs +++ b/src/Umbraco.Web/Routing/IUrlProvider.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.Routing /// when no culture is specified, the current culture. /// If the provider is unable to provide a url, it should return null. /// - UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current); + UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlMode mode, string culture, Uri current); /// /// Gets the other urls of a published content. diff --git a/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs b/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs index ae8da1fc6c..e1f9022f27 100644 --- a/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs +++ b/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs @@ -37,9 +37,9 @@ namespace Umbraco.Web.Routing reason = "No template exists to render the document at url '{0}'."; response.Write("

Page not found

"); - response.Write("

"); + response.Write("

"); response.Write(string.Format(reason, HttpUtility.HtmlEncode(Current.UmbracoContext.OriginalRequestUrl.PathAndQuery))); - response.Write("

"); + response.Write(""); if (string.IsNullOrWhiteSpace(_message) == false) response.Write("

" + _message + "

"); response.Write("

This page can be replaced with a custom 404. Check the documentation for \"custom 404\".

"); diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index b362b21a0c..6e768c28b6 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -284,7 +284,7 @@ namespace Umbraco.Web.Routing var defaultCulture = domainsCache.DefaultCulture; // try to find a domain matching the current request - var domainAndUri = DomainHelper.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); + var domainAndUri = DomainUtilities.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); // handle domain - always has a contentId and a culture if (domainAndUri != null) @@ -328,7 +328,7 @@ namespace Umbraco.Web.Routing var nodePath = request.PublishedContent.Path; _logger.Debug("{TracePrefix}Path={NodePath}", tracePrefix, nodePath); var rootNodeId = request.HasDomain ? request.Domain.ContentId : (int?)null; - var domain = DomainHelper.FindWildcardDomainInPath(request.UmbracoContext.PublishedSnapshot.Domains.GetAll(true), nodePath, rootNodeId); + var domain = DomainUtilities.FindWildcardDomainInPath(request.UmbracoContext.PublishedSnapshot.Domains.GetAll(true), nodePath, rootNodeId); // always has a contentId and a culture if (domain != null) @@ -517,7 +517,7 @@ namespace Umbraco.Web.Routing { // try and get the redirect node from a legacy integer ID valid = true; - internalRedirectNode = request.UmbracoContext.ContentCache.GetById(internalRedirectId); + internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectId); } else { @@ -526,7 +526,7 @@ namespace Umbraco.Web.Routing { // try and get the redirect node from a UDI Guid valid = true; - internalRedirectNode = request.UmbracoContext.ContentCache.GetById(udiInternalRedirectId.Guid); + internalRedirectNode = request.UmbracoContext.Content.GetById(udiInternalRedirectId.Guid); } } diff --git a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs index 0d82467179..fcf6f55f44 100644 --- a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs +++ b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs @@ -1,16 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; -using Umbraco.Core.Sync; -using Umbraco.Web.Cache; -using Current = Umbraco.Web.Composing.Current; +using Umbraco.Web.PublishedCache; namespace Umbraco.Web.Routing { @@ -23,64 +20,17 @@ namespace Umbraco.Web.Routing /// recycle bin = moving to and from does nothing: to = the node is gone, where would we redirect? from = same public sealed class RedirectTrackingComponent : IComponent { - private const string ContextKey1 = "Umbraco.Web.Redirects.RedirectTrackingEventHandler.1"; - private const string ContextKey2 = "Umbraco.Web.Redirects.RedirectTrackingEventHandler.2"; - private const string ContextKey3 = "Umbraco.Web.Redirects.RedirectTrackingEventHandler.3"; + private const string _eventStateKey = "Umbraco.Web.Redirects.RedirectTrackingEventHandler"; private readonly IUmbracoSettingsSection _umbracoSettings; + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IRedirectUrlService _redirectUrlService; - public RedirectTrackingComponent(IUmbracoSettingsSection umbracoSettings) + public RedirectTrackingComponent(IUmbracoSettingsSection umbracoSettings, IPublishedSnapshotAccessor publishedSnapshotAccessor, IRedirectUrlService redirectUrlService) { _umbracoSettings = umbracoSettings; - } - - private static Dictionary OldRoutes - { - get - { - var oldRoutes = (Dictionary) Current.UmbracoContext.HttpContext.Items[ContextKey3]; - if (oldRoutes == null) - Current.UmbracoContext.HttpContext.Items[ContextKey3] = oldRoutes = new Dictionary(); - return oldRoutes; - } - } - - private static bool HasOldRoutes - { - get - { - if (Current.UmbracoContext == null) return false; - if (Current.UmbracoContext.HttpContext == null) return false; - if (Current.UmbracoContext.HttpContext.Items[ContextKey3] == null) return false; - return true; - } - } - - private static bool LockedEvents - { - get => Moving && Current.UmbracoContext.HttpContext.Items[ContextKey2] != null; - set - { - if (Moving && value) - Current.UmbracoContext.HttpContext.Items[ContextKey2] = true; - else - Current.UmbracoContext.HttpContext.Items.Remove(ContextKey2); - } - } - - private static bool Moving - { - get => Current.UmbracoContext.HttpContext.Items[ContextKey1] != null; - set - { - if (value) - Current.UmbracoContext.HttpContext.Items[ContextKey1] = true; - else - { - Current.UmbracoContext.HttpContext.Items.Remove(ContextKey1); - Current.UmbracoContext.HttpContext.Items.Remove(ContextKey2); - } - } + _publishedSnapshotAccessor = publishedSnapshotAccessor; + _redirectUrlService = redirectUrlService; } public void Initialize() @@ -88,26 +38,11 @@ namespace Umbraco.Web.Routing // don't let the event handlers kick in if Redirect Tracking is turned off in the config if (_umbracoSettings.WebRouting.DisableRedirectUrlTracking) return; - // events are weird - // on 'published' we 'could' get the old or the new route depending on event handlers order - // so it is not reliable. getting the old route in 'publishing' to be sure and storing in http - // context. then for the same reason, we have to process these old items only when the cache - // is ready - // when moving, the moved node is also published, which is causing all sorts of troubles with - // descendants, so when moving, we lock events so that neither 'published' nor 'publishing' - // are processed more than once - // we cannot rely only on ContentCacheRefresher because when CacheUpdated triggers the old - // route is gone - // - // this is all very weird but it seems to work - ContentService.Publishing += ContentService_Publishing; ContentService.Published += ContentService_Published; ContentService.Moving += ContentService_Moving; ContentService.Moved += ContentService_Moved; - ContentCacheRefresher.CacheUpdated += ContentCacheRefresher_CacheUpdated; - // kill all redirects once a content is deleted //ContentService.Deleted += ContentService_Deleted; // BUT, doing it here would prevent content deletion due to FK @@ -119,102 +54,79 @@ namespace Umbraco.Web.Routing public void Terminate() { } - private static void ContentCacheRefresher_CacheUpdated(ContentCacheRefresher sender, CacheRefresherEventArgs args) + private void ContentService_Publishing(IContentService sender, PublishEventArgs args) { - // that event is a distributed even that triggers on all nodes - // BUT it should totally NOT run on nodes other that the one that handled the other events - // and besides, it cannot run on a background thread! - if (!HasOldRoutes) - return; - - // sanity checks - if (args.MessageType != MessageType.RefreshByPayload) - { - throw new InvalidOperationException("ContentCacheRefresher MessageType should be ByPayload."); - } - - if (args.MessageObject == null) - { - return; - } - - if (!(args.MessageObject is ContentCacheRefresher.JsonPayload[])) - { - throw new InvalidOperationException("ContentCacheRefresher MessageObject should be JsonPayload[]."); - } - - // manage routes - var removeKeys = new List(); - - foreach (var oldRoute in OldRoutes) - { - // assuming we cannot have 'CacheUpdated' for only part of the infos else we'd need - // to set a flag in 'Published' to indicate which entities have been refreshed ok - CreateRedirect(oldRoute.Key.ContentId, oldRoute.Key.Culture, oldRoute.Value.ContentKey, oldRoute.Value.OldRoute); - removeKeys.Add(oldRoute.Key); - } - - foreach (var k in removeKeys) - { - OldRoutes.Remove(k); - } - } - - private static void ContentService_Publishing(IContentService sender, PublishEventArgs args) - { - if (LockedEvents) return; - - var contentCache = Current.UmbracoContext.ContentCache; + var oldRoutes = GetOldRoutes(args.EventState); foreach (var entity in args.PublishedEntities) { - var entityContent = contentCache.GetById(entity.Id); - if (entityContent == null) continue; + StoreOldRoute(entity, oldRoutes); + } + } - // get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures) - var defaultCultures = entityContent.AncestorsOrSelf()?.FirstOrDefault(a => a.Cultures.Any())?.Cultures.Select(c => c.Key).ToArray() - ?? new[] {(string) null}; - foreach (var x in entityContent.DescendantsOrSelf()) - { - // if this entity defines specific cultures, use those instead of the default ones - var cultures = x.Cultures.Any() ? x.Cultures.Select(c => c.Key) : defaultCultures; + private void ContentService_Published(IContentService sender, ContentPublishedEventArgs args) + { + var oldRoutes = GetOldRoutes(args.EventState); + CreateRedirects(oldRoutes); + } - foreach (var culture in cultures) - { - var route = contentCache.GetRouteById(x.Id, culture); - if (IsNotRoute(route)) return; - OldRoutes[new ContentIdAndCulture(x.Id, culture)] = new ContentKeyAndOldRoute(x.Key, route); - } - } + private void ContentService_Moving(IContentService sender, MoveEventArgs args) + { + var oldRoutes = GetOldRoutes(args.EventState); + foreach (var info in args.MoveInfoCollection) + { + StoreOldRoute(info.Entity, oldRoutes); + } + } + + private void ContentService_Moved(IContentService sender, MoveEventArgs args) + { + var oldRoutes = GetOldRoutes(args.EventState); + CreateRedirects(oldRoutes); + } + + private OldRoutesDictionary GetOldRoutes(IDictionary eventState) + { + if (! eventState.ContainsKey(_eventStateKey)) + { + eventState[_eventStateKey] = new OldRoutesDictionary(); } - LockedEvents = true; // we only want to see the "first batch" + return eventState[_eventStateKey] as OldRoutesDictionary; } - private static void ContentService_Published(IContentService sender, PublishEventArgs e) + private void StoreOldRoute(IContent entity, OldRoutesDictionary oldRoutes) { - // look note in CacheUpdated - // we might want to set a flag on the entities we are seeing here + var contentCache = _publishedSnapshotAccessor.PublishedSnapshot.Content; + var entityContent = contentCache.GetById(entity.Id); + if (entityContent == null) return; + + // get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures) + var defaultCultures = entityContent.AncestorsOrSelf()?.FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() + ?? new[] { (string)null }; + foreach (var x in entityContent.DescendantsOrSelf()) + { + // if this entity defines specific cultures, use those instead of the default ones + var cultures = x.Cultures.Any() ? x.Cultures.Keys : defaultCultures; + + foreach (var culture in cultures) + { + var route = contentCache.GetRouteById(x.Id, culture); + if (IsNotRoute(route)) continue; + oldRoutes[new ContentIdAndCulture(x.Id, culture)] = new ContentKeyAndOldRoute(x.Key, route); + } + } } - private static void ContentService_Moving(IContentService sender, MoveEventArgs e) + private void CreateRedirects(OldRoutesDictionary oldRoutes) { - // TODO: Use the new e.EventState to track state between Moving/Moved events! - Moving = true; - } + var contentCache = _publishedSnapshotAccessor.PublishedSnapshot.Content; - private static void ContentService_Moved(IContentService sender, MoveEventArgs e) - { - Moving = false; - LockedEvents = false; - } - - private static void CreateRedirect(int contentId, string culture, Guid contentKey, string oldRoute) - { - var contentCache = Current.UmbracoContext.ContentCache; - var newRoute = contentCache.GetRouteById(contentId, culture); - if (IsNotRoute(newRoute) || oldRoute == newRoute) return; - var redirectUrlService = Current.Services.RedirectUrlService; - redirectUrlService.Register(oldRoute, contentKey, culture); + foreach (var oldRoute in oldRoutes) + { + var newRoute = contentCache.GetRouteById(oldRoute.Key.ContentId, oldRoute.Key.Culture); + if (IsNotRoute(newRoute) || oldRoute.Value.OldRoute == newRoute) continue; + _redirectUrlService.Register(oldRoute.Value.OldRoute, oldRoute.Value.ContentKey, oldRoute.Key.Culture); + } } private static bool IsNotRoute(string route) @@ -243,5 +155,8 @@ namespace Umbraco.Web.Routing public Guid ContentKey => Item1; public string OldRoute => Item2; } + + private class OldRoutesDictionary : Dictionary + { } } } diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index 525695c274..d42639b781 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -3,11 +3,8 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; using Umbraco.Web.Composing; -using Umbraco.Web.Models; namespace Umbraco.Web.Routing { @@ -34,10 +31,10 @@ namespace Umbraco.Web.Routing _urlProviders = urlProviders; _mediaUrlProviders = mediaUrlProviders; _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); - var provider = UrlProviderMode.Auto; + var provider = UrlMode.Auto; Mode = provider; - if (Enum.TryParse(routingSettings.UrlProviderMode, out provider)) + if (Enum.TryParse(routingSettings.UrlProviderMode, out provider)) { Mode = provider; } @@ -51,7 +48,7 @@ namespace Umbraco.Web.Routing /// The list of media url providers /// The current variation accessor. /// An optional provider mode. - public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor, UrlProviderMode mode = UrlProviderMode.Auto) + public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor, UrlMode mode = UrlMode.Auto) { _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); _urlProviders = urlProviders; @@ -69,65 +66,16 @@ namespace Umbraco.Web.Routing /// /// Gets or sets the provider url mode. /// - public UrlProviderMode Mode { get; set; } + public UrlMode Mode { get; set; } #endregion #region GetUrl - private UrlProviderMode GetMode(bool absolute) => absolute ? UrlProviderMode.Absolute : Mode; - private IPublishedContent GetDocument(int id) => _umbracoContext.ContentCache.GetById(id); - private IPublishedContent GetDocument(Guid id) => _umbracoContext.ContentCache.GetById(id); - - /// - /// Gets the url of a published content. - /// - /// The published content. - /// A culture. - /// The current absolute url. - /// The url for the published content. - public string GetUrl(IPublishedContent content, string culture = null, Uri current = null) - => GetUrl(content, Mode, culture, current); - - /// - /// Gets the url of a published content. - /// - /// The published content. - /// A value indicating whether the url should be absolute in any case. - /// A culture. - /// The current absolute url. - /// The url for the published content. - /// - /// The url is absolute or relative depending on Mode and on current, unless - /// absolute is true, in which case the url is always absolute. - /// - public string GetUrl(IPublishedContent content, bool absolute, string culture = null, Uri current = null) - => GetUrl(content, GetMode(absolute), culture, current); - - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// A culture. - /// The current absolute url. - /// The url for the published content. - public string GetUrl(Guid id, string culture = null, Uri current = null) - => GetUrl(GetDocument(id), Mode, culture, current); - - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// A value indicating whether the url should be absolute in any case. - /// A culture. - /// The current absolute url. - /// The url for the published content. - /// - /// The url is absolute or relative depending on Mode and on current, unless - /// absolute is true, in which case the url is always absolute. - /// - public string GetUrl(Guid id, bool absolute, string culture = null, Uri current = null) - => GetUrl(GetDocument(id), GetMode(absolute), culture, current); + private UrlMode GetMode(bool absolute) => absolute ? UrlMode.Absolute : Mode; + private IPublishedContent GetDocument(int id) => _umbracoContext.Content.GetById(id); + private IPublishedContent GetDocument(Guid id) => _umbracoContext.Content.GetById(id); + private IPublishedContent GetMedia(Guid id) => _umbracoContext.Media.GetById(id); /// /// Gets the url of a published content. @@ -137,34 +85,9 @@ namespace Umbraco.Web.Routing /// A culture. /// The current absolute url. /// The url for the published content. - public string GetUrl(Guid id, UrlProviderMode mode, string culture = null, Uri current = null) + public string GetUrl(Guid id, UrlMode mode = UrlMode.Default, string culture = null, Uri current = null) => GetUrl(GetDocument(id), mode, culture, current); - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// A culture. - /// The current absolute url. - /// The url for the published content. - public string GetUrl(int id, string culture = null, Uri current = null) - => GetUrl(GetDocument(id), Mode, culture, current); - - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// A value indicating whether the url should be absolute in any case. - /// A culture. - /// The current absolute url. - /// The url for the published content. - /// - /// The url is absolute or relative depending on Mode and on current, unless - /// absolute is true, in which case the url is always absolute. - /// - public string GetUrl(int id, bool absolute, string culture = null, Uri current = null) - => GetUrl(GetDocument(id), GetMode(absolute), culture, current); - /// /// Gets the url of a published content. /// @@ -173,7 +96,7 @@ namespace Umbraco.Web.Routing /// A culture. /// The current absolute url. /// The url for the published content. - public string GetUrl(int id, UrlProviderMode mode, string culture = null, Uri current = null) + public string GetUrl(int id, UrlMode mode = UrlMode.Default, string culture = null, Uri current = null) => GetUrl(GetDocument(id), mode, culture, current); /// @@ -190,11 +113,14 @@ namespace Umbraco.Web.Routing /// when no culture is specified, the current culture. /// If the provider is unable to provide a url, it returns "#". /// - public string GetUrl(IPublishedContent content, UrlProviderMode mode, string culture = null, Uri current = null) + public string GetUrl(IPublishedContent content, UrlMode mode = UrlMode.Default, string culture = null, Uri current = null) { - if (content == null || content.ItemType == PublishedItemType.Element) + if (content == null || content.ContentType.ItemType == PublishedItemType.Element) return "#"; + if (mode == UrlMode.Default) + mode = Mode; + // this the ONLY place where we deal with default culture - IUrlProvider always receive a culture // be nice with tests, assume things can be null, ultimately fall back to invariant // (but only for variant content of course) @@ -262,42 +188,20 @@ namespace Umbraco.Web.Routing /// /// Gets the url of a media item. /// - /// The published content. - /// The property alias to resolve the url from. - /// The variation language. - /// The current absolute url. - /// The url for the media. - /// - /// The url is absolute or relative depending on mode and on current. - /// If the media is multi-lingual, gets the url for the specified culture or, - /// when no culture is specified, the current culture. - /// If the provider is unable to provide a url, it returns . - /// - public string GetMediaUrl(IPublishedContent content, string propertyAlias, string culture = null, Uri current = null) - => GetMediaUrl(content, propertyAlias, Mode, culture, current); + /// + /// + /// + /// + /// + /// + public string GetMediaUrl(Guid id, UrlMode mode = UrlMode.Default, string culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri current = null) + => GetMediaUrl(GetMedia(id), mode, culture, propertyAlias, current); /// /// Gets the url of a media item. /// /// The published content. /// The property alias to resolve the url from. - /// A value indicating whether the url should be absolute in any case. - /// The variation language. - /// The current absolute url. - /// The url for the media. - /// - /// The url is absolute or relative depending on mode and on current. - /// If the media is multi-lingual, gets the url for the specified culture or, - /// when no culture is specified, the current culture. - /// If the provider is unable to provide a url, it returns . - /// - public string GetMediaUrl(IPublishedContent content, string propertyAlias, bool absolute, string culture = null, Uri current = null) - => GetMediaUrl(content, propertyAlias, GetMode(absolute), culture, current); - /// - /// Gets the url of a media item. - /// - /// The published content. - /// The property alias to resolve the url from. /// The url mode. /// The variation language. /// The current absolute url. @@ -308,15 +212,16 @@ namespace Umbraco.Web.Routing /// when no culture is specified, the current culture. /// If the provider is unable to provide a url, it returns . /// - public string GetMediaUrl(IPublishedContent content, - string propertyAlias, UrlProviderMode mode, - string culture = null, Uri current = null) + public string GetMediaUrl(IPublishedContent content, UrlMode mode = UrlMode.Default, string culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri current = null) { if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); if (content == null) return ""; + if (mode == UrlMode.Default) + mode = Mode; + // this the ONLY place where we deal with default culture - IMediaUrlProvider always receive a culture // be nice with tests, assume things can be null, ultimately fall back to invariant // (but only for variant content of course) diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index 2a840828b6..077680d2e2 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -109,7 +109,7 @@ namespace Umbraco.Web.Routing string url; try { - url = umbracoContext.UrlProvider.GetUrl(content.Id, culture); + url = umbracoContext.UrlProvider.GetUrl(content.Id, culture: culture); } catch (Exception ex) { @@ -187,7 +187,7 @@ namespace Umbraco.Web.Routing var l = new List(); while (o != null) { - l.Add(o.Name); + l.Add(o.Name()); o = o.Parent; } l.Reverse(); diff --git a/src/Umbraco.Web/Runtime/WebFinalComponent.cs b/src/Umbraco.Web/Runtime/WebFinalComponent.cs index 42ff0ee5e6..2ba78b2080 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComponent.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComponent.cs @@ -1,37 +1,18 @@ using System; -using System.Linq; using System.Web.Http; -using System.Web.Mvc; -using System.Web.Routing; -using Umbraco.Core; using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Web.Install; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; namespace Umbraco.Web.Runtime { public class WebFinalComponent : IComponent { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly SurfaceControllerTypeCollection _surfaceControllerTypes; - private readonly UmbracoApiControllerTypeCollection _apiControllerTypes; - private readonly IGlobalSettings _globalSettings; - public WebFinalComponent(IUmbracoContextAccessor umbracoContextAccessor, SurfaceControllerTypeCollection surfaceControllerTypes, UmbracoApiControllerTypeCollection apiControllerTypes, IGlobalSettings globalSettings) + public WebFinalComponent() { - _umbracoContextAccessor = umbracoContextAccessor; - _surfaceControllerTypes = surfaceControllerTypes; - _apiControllerTypes = apiControllerTypes; - _globalSettings = globalSettings; } public void Initialize() { - // set routes - CreateRoutes(_umbracoContextAccessor, _globalSettings, _surfaceControllerTypes, _apiControllerTypes); - // ensure WebAPI is initialized, after everything GlobalConfiguration.Configuration.EnsureInitialized(); } @@ -39,92 +20,5 @@ namespace Umbraco.Web.Runtime public void Terminate() { } - // internal for tests - internal static void CreateRoutes( - IUmbracoContextAccessor umbracoContextAccessor, - IGlobalSettings globalSettings, - SurfaceControllerTypeCollection surfaceControllerTypes, - UmbracoApiControllerTypeCollection apiControllerTypes) - { - var umbracoPath = globalSettings.GetUmbracoMvcArea(); - - // create the front-end route - var defaultRoute = RouteTable.Routes.MapRoute( - "Umbraco_default", - umbracoPath + "/RenderMvc/{action}/{id}", - new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional } - ); - defaultRoute.RouteHandler = new RenderRouteHandler(umbracoContextAccessor, ControllerBuilder.Current.GetControllerFactory()); - - // register install routes - RouteTable.Routes.RegisterArea(); - - // register all back office routes - RouteTable.Routes.RegisterArea(new BackOfficeArea(globalSettings)); - - // plugin controllers must come first because the next route will catch many things - RoutePluginControllers(globalSettings, surfaceControllerTypes, apiControllerTypes); - } - - private static void RoutePluginControllers( - IGlobalSettings globalSettings, - SurfaceControllerTypeCollection surfaceControllerTypes, - UmbracoApiControllerTypeCollection apiControllerTypes) - { - var umbracoPath = globalSettings.GetUmbracoMvcArea(); - - // need to find the plugin controllers and route them - var pluginControllers = surfaceControllerTypes.Concat(apiControllerTypes).ToArray(); - - // local controllers do not contain the attribute - var localControllers = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace()); - foreach (var s in localControllers) - { - if (TypeHelper.IsTypeAssignableFrom(s)) - RouteLocalSurfaceController(s, umbracoPath); - else if (TypeHelper.IsTypeAssignableFrom(s)) - RouteLocalApiController(s, umbracoPath); - } - - // get the plugin controllers that are unique to each area (group by) - var pluginSurfaceControlleres = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace() == false); - var groupedAreas = pluginSurfaceControlleres.GroupBy(controller => PluginController.GetMetadata(controller).AreaName); - // loop through each area defined amongst the controllers - foreach (var g in groupedAreas) - { - // create & register an area for the controllers (this will throw an exception if all controllers are not in the same area) - var pluginControllerArea = new PluginControllerArea(globalSettings, g.Select(PluginController.GetMetadata)); - RouteTable.Routes.RegisterArea(pluginControllerArea); - } - } - - private static void RouteLocalApiController(Type controller, string umbracoPath) - { - var meta = PluginController.GetMetadata(controller); - var url = umbracoPath + (meta.IsBackOffice ? "/BackOffice" : "") + "/Api/" + meta.ControllerName + "/{action}/{id}"; - var route = RouteTable.Routes.MapHttpRoute( - $"umbraco-api-{meta.ControllerName}", - url, // url to match - new { controller = meta.ControllerName, id = UrlParameter.Optional }, - new[] { meta.ControllerNamespace }); - if (route.DataTokens == null) // web api routes don't set the data tokens object - route.DataTokens = new RouteValueDictionary(); - route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "api"); //ensure the umbraco token is set - } - - private static void RouteLocalSurfaceController(Type controller, string umbracoPath) - { - var meta = PluginController.GetMetadata(controller); - var url = umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}"; - var route = RouteTable.Routes.MapRoute( - $"umbraco-surface-{meta.ControllerName}", - url, // url to match - new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, - new[] { meta.ControllerNamespace }); // look in this namespace to create the controller - route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); // ensure the umbraco token is set - route.DataTokens.Add("UseNamespaceFallback", false); // don't look anywhere else except this namespace! - // make it use our custom/special SurfaceMvcHandler - route.RouteHandler = new SurfaceRouteHandler(); - } } } diff --git a/src/Umbraco.Web/Runtime/WebInitialComponent.cs b/src/Umbraco.Web/Runtime/WebInitialComponent.cs index e1e0f4d80a..ac813d7196 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComponent.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComponent.cs @@ -8,12 +8,14 @@ using System.Web.Configuration; using System.Web.Http; using System.Web.Http.Dispatcher; using System.Web.Mvc; +using System.Web.Routing; using ClientDependency.Core.CompositeFiles.Providers; using ClientDependency.Core.Config; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Web.Install; using Umbraco.Web.JavaScript; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; @@ -24,10 +26,16 @@ namespace Umbraco.Web.Runtime { public sealed class WebInitialComponent : IComponent { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly SurfaceControllerTypeCollection _surfaceControllerTypes; + private readonly UmbracoApiControllerTypeCollection _apiControllerTypes; private readonly IGlobalSettings _globalSettings; - public WebInitialComponent(IGlobalSettings globalSettings) + public WebInitialComponent(IUmbracoContextAccessor umbracoContextAccessor, SurfaceControllerTypeCollection surfaceControllerTypes, UmbracoApiControllerTypeCollection apiControllerTypes, IGlobalSettings globalSettings) { + _umbracoContextAccessor = umbracoContextAccessor; + _surfaceControllerTypes = surfaceControllerTypes; + _apiControllerTypes = apiControllerTypes; _globalSettings = globalSettings; } @@ -51,6 +59,9 @@ namespace Umbraco.Web.Runtime // add global filters ConfigureGlobalFilters(); + + // set routes + CreateRoutes(_umbracoContextAccessor, _globalSettings, _surfaceControllerTypes, _apiControllerTypes); } public void Terminate() @@ -131,5 +142,94 @@ namespace Umbraco.Web.Runtime ClientDependencySettings.Instance.MvcRendererCollection.Add(renderer); } + + // internal for tests + internal static void CreateRoutes( + IUmbracoContextAccessor umbracoContextAccessor, + IGlobalSettings globalSettings, + SurfaceControllerTypeCollection surfaceControllerTypes, + UmbracoApiControllerTypeCollection apiControllerTypes) + { + var umbracoPath = globalSettings.GetUmbracoMvcArea(); + + // create the front-end route + var defaultRoute = RouteTable.Routes.MapRoute( + "Umbraco_default", + umbracoPath + "/RenderMvc/{action}/{id}", + new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional } + ); + defaultRoute.RouteHandler = new RenderRouteHandler(umbracoContextAccessor, ControllerBuilder.Current.GetControllerFactory()); + + // register install routes + RouteTable.Routes.RegisterArea(); + + // register all back office routes + RouteTable.Routes.RegisterArea(new BackOfficeArea(globalSettings)); + + // plugin controllers must come first because the next route will catch many things + RoutePluginControllers(globalSettings, surfaceControllerTypes, apiControllerTypes); + } + + private static void RoutePluginControllers( + IGlobalSettings globalSettings, + SurfaceControllerTypeCollection surfaceControllerTypes, + UmbracoApiControllerTypeCollection apiControllerTypes) + { + var umbracoPath = globalSettings.GetUmbracoMvcArea(); + + // need to find the plugin controllers and route them + var pluginControllers = surfaceControllerTypes.Concat(apiControllerTypes).ToArray(); + + // local controllers do not contain the attribute + var localControllers = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace()); + foreach (var s in localControllers) + { + if (TypeHelper.IsTypeAssignableFrom(s)) + RouteLocalSurfaceController(s, umbracoPath); + else if (TypeHelper.IsTypeAssignableFrom(s)) + RouteLocalApiController(s, umbracoPath); + } + + // get the plugin controllers that are unique to each area (group by) + var pluginSurfaceControlleres = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace() == false); + var groupedAreas = pluginSurfaceControlleres.GroupBy(controller => PluginController.GetMetadata(controller).AreaName); + // loop through each area defined amongst the controllers + foreach (var g in groupedAreas) + { + // create & register an area for the controllers (this will throw an exception if all controllers are not in the same area) + var pluginControllerArea = new PluginControllerArea(globalSettings, g.Select(PluginController.GetMetadata)); + RouteTable.Routes.RegisterArea(pluginControllerArea); + } + } + + private static void RouteLocalApiController(Type controller, string umbracoPath) + { + var meta = PluginController.GetMetadata(controller); + var url = umbracoPath + (meta.IsBackOffice ? "/BackOffice" : "") + "/Api/" + meta.ControllerName + "/{action}/{id}"; + var route = RouteTable.Routes.MapHttpRoute( + $"umbraco-api-{meta.ControllerName}", + url, // url to match + new { controller = meta.ControllerName, id = UrlParameter.Optional }, + new[] { meta.ControllerNamespace }); + if (route.DataTokens == null) // web api routes don't set the data tokens object + route.DataTokens = new RouteValueDictionary(); + route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "api"); //ensure the umbraco token is set + } + + private static void RouteLocalSurfaceController(Type controller, string umbracoPath) + { + var meta = PluginController.GetMetadata(controller); + var url = umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}"; + var route = RouteTable.Routes.MapRoute( + $"umbraco-surface-{meta.ControllerName}", + url, // url to match + new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, + new[] { meta.ControllerNamespace }); // look in this namespace to create the controller + route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); // ensure the umbraco token is set + route.DataTokens.Add("UseNamespaceFallback", false); // don't look anywhere else except this namespace! + // make it use our custom/special SurfaceMvcHandler + route.RouteHandler = new SurfaceRouteHandler(); + } + } } diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 5a464701e0..203bae4854 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -9,9 +9,7 @@ using Umbraco.Core.Dashboards; using Umbraco.Core.Dictionary; using Umbraco.Core.Events; using Umbraco.Core.Migrations.PostMigrations; -using Umbraco.Web.Migrations.PostMigrations; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Runtime; using Umbraco.Core.Services; @@ -19,7 +17,6 @@ using Umbraco.Web.Actions; using Umbraco.Web.Cache; using Umbraco.Web.Composing.CompositionExtensions; using Umbraco.Web.ContentApps; -using Umbraco.Web.Dashboards; using Umbraco.Web.Dictionary; using Umbraco.Web.Editors; using Umbraco.Web.Features; @@ -37,10 +34,12 @@ using Umbraco.Web.Security.Providers; using Umbraco.Web.Services; using Umbraco.Web.SignalR; using Umbraco.Web.Templates; -using Umbraco.Web.Tour; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Current = Umbraco.Web.Composing.Current; +using Umbraco.Web.PropertyEditors; +using Umbraco.Core.Models; +using Umbraco.Web.Models; namespace Umbraco.Web.Runtime { @@ -92,10 +91,10 @@ namespace Umbraco.Web.Runtime // we should stop injecting UmbracoContext and always inject IUmbracoContextAccessor, however at the moment // there are tons of places (controllers...) which require UmbracoContext in their ctor - so let's register - // a way to inject the UmbracoContext - and register it per-request to be more efficient - // TODO: stop doing this - composition.Register(factory => factory.GetInstance().UmbracoContext, Lifetime.Request); - + // a way to inject the UmbracoContext - DO NOT register this as Lifetime.Request since LI will dispose the context + // in it's own way but we don't want that to happen, we manage its lifetime ourselves. + composition.Register(factory => factory.GetInstance().UmbracoContext); + composition.RegisterUnique(); composition.Register(factory => { var umbCtx = factory.GetInstance(); @@ -107,6 +106,11 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + // register the umbraco helper - this is Transient! very important! // also, if not level.Run, we cannot really use the helper (during upgrade...) // so inject a "void" helper (not exactly pretty but...) @@ -125,6 +129,8 @@ namespace Umbraco.Web.Runtime // register distributed cache composition.RegisterUnique(f => new DistributedCache()); + composition.RegisterUnique(); + // replace some services composition.RegisterUnique(); composition.RegisterUnique(); @@ -142,19 +148,19 @@ namespace Umbraco.Web.Runtime .ComposeUmbracoControllers(GetType().Assembly) .SetDefaultRenderMvcController(); // default controller for template views - composition.WithCollectionBuilder() + composition.SearchableTrees() .Add(() => composition.TypeLoader.GetTypes()); composition.Register(Lifetime.Request); - composition.WithCollectionBuilder() + composition.EditorValidators() .Add(() => composition.TypeLoader.GetTypes()); - composition.WithCollectionBuilder(); + composition.TourFilters(); composition.RegisterUnique(); - composition.WithCollectionBuilder() + composition.Actions() .Add(() => composition.TypeLoader.GetTypes()); //we need to eagerly scan controller types since they will need to be routed @@ -169,26 +175,28 @@ namespace Umbraco.Web.Runtime // here because there cannot be two converters for one property editor - and we want the full // RteMacroRenderingValueConverter that converts macros, etc. So remove TinyMceValueConverter. // (the limited one, defined in Core, is there for tests) - same for others - composition.WithCollectionBuilder() + composition.PropertyValueConverters() .Remove() .Remove() .Remove(); - + // add all known factories, devs can then modify this list on application // startup either by binding to events or in their own global.asax - composition.WithCollectionBuilder() + composition.FilteredControllerFactory() .Append(); - composition.WithCollectionBuilder() + composition.UrlProviders() .Append() .Append(); - composition.WithCollectionBuilder() + composition.MediaUrlProviders() .Append(); + composition.RegisterUnique(); + composition.RegisterUnique(); - composition.WithCollectionBuilder() + composition.ContentFinders() // all built-in finders in the correct order, // devs can then modify this list on application startup .Append() @@ -203,7 +211,7 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); // register *all* checks, except those marked [HideFromTypeFinder] of course - composition.WithCollectionBuilder() + composition.HealthChecks() .Add(() => composition.TypeLoader.GetTypes()); composition.WithCollectionBuilder() @@ -223,13 +231,13 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); // register known content apps - composition.WithCollectionBuilder() + composition.ContentApps() .Append() .Append() .Append(); // register back office sections in the order we want them rendered - composition.WithCollectionBuilder() + composition.Sections() .Append() .Append() .Append() @@ -240,18 +248,18 @@ namespace Umbraco.Web.Runtime .Append(); // register core CMS dashboards and 3rd party types - will be ordered by weight attribute & merged with package.manifest dashboards - composition.WithCollectionBuilder() + composition.Dashboards() .Add(composition.TypeLoader.GetTypes()); // register back office trees // the collection builder only accepts types inheriting from TreeControllerBase // and will filter out those that are not attributed with TreeAttribute - composition.WithCollectionBuilder() + composition.Trees() .AddTreeControllers(umbracoApiControllerTypes.Where(x => typeof(TreeControllerBase).IsAssignableFrom(x))); // register OEmbed providers - no type scanning - all explicit opt-in of adding types // note: IEmbedProvider is not IDiscoverable - think about it if going for type scanning - composition.WithCollectionBuilder() + composition.OEmbedProviders() .Append() .Append() .Append() @@ -264,7 +272,9 @@ namespace Umbraco.Web.Runtime .Append() .Append() .Append() - .Append(); + .Append() + .Append(); + // replace with web implementation composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Runtime/WebRuntime.cs b/src/Umbraco.Web/Runtime/WebRuntime.cs index 13cd717fd1..ffcd2343ed 100644 --- a/src/Umbraco.Web/Runtime/WebRuntime.cs +++ b/src/Umbraco.Web/Runtime/WebRuntime.cs @@ -1,4 +1,6 @@ -using System.Web; +using System; +using System.Web; +using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; @@ -17,11 +19,18 @@ namespace Umbraco.Web.Runtime private readonly UmbracoApplicationBase _umbracoApplication; private IProfiler _webProfiler; + [Obsolete("Use the ctor with all parameters instead")] + public WebRuntime(UmbracoApplicationBase umbracoApplication) + : this(umbracoApplication, null, null) + { + } + /// /// Initializes a new instance of the class. /// /// - public WebRuntime(UmbracoApplicationBase umbracoApplication) + public WebRuntime(UmbracoApplicationBase umbracoApplication, ILogger logger, IMainDom mainDom) + : base(logger, mainDom) { _umbracoApplication = umbracoApplication; } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 47e97593f0..c0475b1f79 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -199,7 +199,7 @@ namespace Umbraco.Web.Scheduling { lock (_locker) { - var task = _runningTask ?? Task.FromResult(0); + var task = _runningTask ?? Task.CompletedTask; return new ThreadingTaskImmutable(task); } } @@ -211,8 +211,9 @@ namespace Umbraco.Web.Scheduling /// An awaitable object. /// /// Used to wait until the runner has terminated. - /// This is for unit tests and should not be used otherwise. In most cases when the runner - /// has terminated, the application domain is going down and it is not the right time to do things. + /// + /// The only time the runner will be terminated is by the Hosting Environment when the application is being shutdown. + /// /// internal ThreadingTaskImmutable TerminatedAwaitable { @@ -338,29 +339,37 @@ namespace Umbraco.Web.Scheduling if (_isRunning == false) return; // done already } + var hasTasks = TaskCount > 0; + + if (!force && hasTasks) + _logger.Info("{LogPrefix} Waiting for tasks to complete", _logPrefix); + // complete the queue // will stop waiting on the queue or on a latch _tasks.Complete(); if (force) { - // we must bring everything down, now - Thread.Sleep(100); // give time to Complete() + // we must bring everything down, now lock (_locker) { // was Complete() enough? - if (_isRunning == false) return; + // if _tasks.Complete() ended up triggering code to stop the runner and reset + // the _isRunning flag, then there's no need to initiate a cancel on the cancelation token. + if (_isRunning == false) + return; } + // try to cancel running async tasks (cannot do much about sync tasks) // break latched tasks // stop processing the queue - _shutdownTokenSource.Cancel(false); // false is the default - _shutdownTokenSource.Dispose(); + _shutdownTokenSource?.Cancel(false); // false is the default + _shutdownTokenSource?.Dispose(); _shutdownTokenSource = null; } // tasks in the queue will be executed... - if (wait == false) return; + if (!wait) return; _runningTask?.Wait(CancellationToken.None); // wait for whatever is running to end... } @@ -428,7 +437,7 @@ namespace Umbraco.Web.Scheduling lock (_locker) { // deal with race condition - if (_shutdownToken.IsCancellationRequested == false && _tasks.Count > 0) continue; + if (_shutdownToken.IsCancellationRequested == false && TaskCount > 0) continue; // if we really have nothing to do, stop _logger.Debug("{LogPrefix} Stopping", _logPrefix); @@ -453,7 +462,7 @@ namespace Umbraco.Web.Scheduling // if KeepAlive is false then don't block, exit if there is // no task in the buffer - yes, there is a race condition, which // we'll take care of - if (_options.KeepAlive == false && _tasks.Count == 0) + if (_options.KeepAlive == false && TaskCount == 0) return null; try @@ -503,15 +512,19 @@ namespace Umbraco.Web.Scheduling // returns the task that completed // - latched.Latch completes when the latch releases // - _tasks.Completion completes when the runner completes - // - tokenTaskSource.Task completes when this task, or the whole runner, is cancelled + // - tokenTaskSource.Task completes when this task, or the whole runner is cancelled var task = await Task.WhenAny(latched.Latch, _tasks.Completion, tokenTaskSource.Task); // ok to run now if (task == latched.Latch) return bgTask; + // we are shutting down if the _tasks.Complete(); was called or the shutdown token was cancelled + var isShuttingDown = _shutdownToken.IsCancellationRequested || task == _tasks.Completion; + // if shutting down, return the task only if it runs on shutdown - if (_shutdownToken.IsCancellationRequested == false && latched.RunsOnShutdown) return bgTask; + if (isShuttingDown && latched.RunsOnShutdown) + return bgTask; // else, either it does not run on shutdown or it's been cancelled, dispose latched.Dispose(); @@ -578,17 +591,18 @@ namespace Umbraco.Web.Scheduling // triggers when the hosting environment requests that the runner terminates internal event TypedEventHandler, EventArgs> Terminating; - // triggers when the runner has terminated (no task can be added, no task is running) + // triggers when the hosting environment has terminated (no task can be added, no task is running) internal event TypedEventHandler, EventArgs> Terminated; private void OnEvent(TypedEventHandler, EventArgs> handler, string name) { - if (handler == null) return; OnEvent(handler, name, EventArgs.Empty); } private void OnEvent(TypedEventHandler, TArgs> handler, string name, TArgs e) { + _logger.Debug("{LogPrefix} OnEvent {EventName}", _logPrefix, name); + if (handler == null) return; try @@ -664,17 +678,16 @@ namespace Umbraco.Web.Scheduling #endregion + #region IRegisteredObject.Stop + /// - /// Requests a registered object to un-register. + /// Used by IRegisteredObject.Stop and shutdown on threadpool threads to not block shutdown times. /// - /// true to indicate the registered object should un-register from the hosting - /// environment before returning; otherwise, false. - /// - /// "When the application manager needs to stop a registered object, it will call the Stop method." - /// The application manager will call the Stop method to ask a registered object to un-register. During - /// processing of the Stop method, the registered object must call the HostingEnvironment.UnregisterObject method. - /// - public void Stop(bool immediate) + /// + /// + /// An awaitable Task that is used to handle the shutdown. + /// + internal Task StopInternal(bool immediate) { // the first time the hosting environment requests that the runner terminates, // raise the Terminating event - that could be used to prevent any process that @@ -693,33 +706,90 @@ namespace Umbraco.Web.Scheduling if (onTerminating) OnEvent(Terminating, "Terminating"); - if (immediate == false) + // Run the Stop commands on another thread since IRegisteredObject.Stop calls are called sequentially + // with a single aspnet thread during shutdown and we don't want to delay other calls to IRegisteredObject.Stop. + if (!immediate) { - // The Stop method is first called with the immediate parameter set to false. The object can either complete - // processing, call the UnregisterObject method, and then return or it can return immediately and complete - // processing asynchronously before calling the UnregisterObject method. + return Task.Run(StopInitial, CancellationToken.None); + } + else + { + lock (_locker) + { + if (_terminated) return Task.CompletedTask; + return Task.Run(StopImmediate, CancellationToken.None); + } + } + } - _logger.Info("{LogPrefix} Waiting for tasks to complete", _logPrefix); + /// + /// Requests a registered object to un-register. + /// + /// true to indicate the registered object should un-register from the hosting + /// environment before returning; otherwise, false. + /// + /// "When the application manager needs to stop a registered object, it will call the Stop method." + /// The application manager will call the Stop method to ask a registered object to un-register. During + /// processing of the Stop method, the registered object must call the HostingEnvironment.UnregisterObject method. + /// + public void Stop(bool immediate) => StopInternal(immediate); + + /// + /// Called when immediate == false for IRegisteredObject.Stop(bool immediate) + /// + /// + /// Called on a threadpool thread + /// + private void StopInitial() + { + // immediate == false when the app is trying to wind down, immediate == true will be called either: + // after a call with immediate == false or if the app is not trying to wind down and needs to immediately stop. + // So Stop may be called twice or sometimes only once. + + try + { Shutdown(false, false); // do not accept any more tasks, flush the queue, do not wait - + } + finally + { // raise the completed event only after the running threading task has completed lock (_locker) { if (_runningTask != null) - _runningTask.ContinueWith(_ => Terminate(false)); + _runningTask.ContinueWith(_ => StopImmediate()); else - Terminate(false); + StopImmediate(); } } - else - { - // If the registered object does not complete processing before the application manager's time-out - // period expires, the Stop method is called again with the immediate parameter set to true. When the - // immediate parameter is true, the registered object must call the UnregisterObject method before returning; - // otherwise, its registration will be removed by the application manager. - _logger.Info("{LogPrefix} Canceling tasks", _logPrefix); + // If the shutdown token was not canceled in the Shutdown call above, it means there was still tasks + // being processed, in which case we'll give it a couple seconds + if (!_shutdownToken.IsCancellationRequested) + { + // If we are called with immediate == false, wind down above and then shutdown within 2 seconds, + // we want to shut down the app as quick as possible, if we wait until immediate == true, this can + // take a very long time since immediate will only be true when a new request is received on the new + // appdomain (or another iis timeout occurs ... which can take some time). + Thread.Sleep(2000); //we are already on a threadpool thread + StopImmediate(); + } + } + + /// + /// Called when immediate == true for IRegisteredObject.Stop(bool immediate) + /// + /// + /// Called on a threadpool thread + /// + private void StopImmediate() + { + _logger.Info("{LogPrefix} Canceling tasks", _logPrefix); + try + { Shutdown(true, true); // cancel all tasks, wait for the current one to end + } + finally + { Terminate(true); } } @@ -732,7 +802,13 @@ namespace Umbraco.Web.Scheduling // raise the Terminated event // complete the awaitable completion source, if any - HostingEnvironment.UnregisterObject(this); + if (immediate) + { + //only unregister when it's the final call, else we won't be notified of the final call + HostingEnvironment.UnregisterObject(this); + } + + if (_terminated) return; // already taken care of TaskCompletionSource terminatedSource; lock (_locker) @@ -747,7 +823,9 @@ namespace Umbraco.Web.Scheduling OnEvent(Terminated, "Terminated"); - terminatedSource.SetResult(0); + terminatedSource.TrySetResult(0); } + + #endregion } } diff --git a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs index 4f66af11bd..746bc61e34 100644 --- a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs +++ b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs @@ -51,7 +51,7 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } - using (_logger.DebugDuration("Health checks executing", "Health checks complete")) + using (_logger.DebugDuration("Health checks executing", "Health checks complete")) { var healthCheckConfig = Current.Configs.HealthChecks(); diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index 6dd7572b87..9a22c59566 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -3,6 +3,8 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Sync; @@ -11,14 +13,16 @@ namespace Umbraco.Web.Scheduling internal class KeepAlive : RecurringTaskBase { private readonly IRuntimeState _runtime; + private readonly IKeepAliveSection _keepAliveSection; private readonly IProfilingLogger _logger; private static HttpClient _httpClient; public KeepAlive(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - IRuntimeState runtime, IProfilingLogger logger) + IRuntimeState runtime, IKeepAliveSection keepAliveSection, IProfilingLogger logger) : base(runner, delayMilliseconds, periodMilliseconds) { _runtime = runtime; + _keepAliveSection = keepAliveSection; _logger = logger; if (_httpClient == null) _httpClient = new HttpClient(); @@ -46,25 +50,27 @@ namespace Umbraco.Web.Scheduling using (_logger.DebugDuration("Keep alive executing", "Keep alive complete")) { - string umbracoAppUrl = null; - + var keepAlivePingUrl = _keepAliveSection.KeepAlivePingUrl; try { - umbracoAppUrl = _runtime.ApplicationUrl.ToString(); - if (umbracoAppUrl.IsNullOrWhiteSpace()) + if (keepAlivePingUrl.Contains("{umbracoApplicationUrl}")) { - _logger.Warn("No url for service (yet), skip."); - return true; // repeat + var umbracoAppUrl = _runtime.ApplicationUrl.ToString(); + if (umbracoAppUrl.IsNullOrWhiteSpace()) + { + _logger.Warn("No umbracoApplicationUrl for service (yet), skip."); + return true; // repeat + } + + keepAlivePingUrl = keepAlivePingUrl.Replace("{umbracoApplicationUrl}", umbracoAppUrl.TrimEnd('/')); } - var url = umbracoAppUrl.TrimEnd('/') + "/api/keepalive/ping"; - - var request = new HttpRequestMessage(HttpMethod.Get, url); + var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); var result = await _httpClient.SendAsync(request, token); } catch (Exception ex) { - _logger.Error(ex, "Failed (at '{UmbracoAppUrl}').", umbracoAppUrl); + _logger.Error(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl); } } diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index c39f9df1b3..db13a80f9b 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -1,16 +1,14 @@ using System; -using System.Threading; -using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Sync; namespace Umbraco.Web.Scheduling { + internal class LogScrubber : RecurringTaskBase { private readonly IRuntimeState _runtime; diff --git a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs index 5e29b17f56..a08289186f 100644 --- a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.HealthChecks; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; using Umbraco.Core.Services; @@ -16,6 +18,11 @@ namespace Umbraco.Web.Scheduling { internal sealed class SchedulerComponent : IComponent { + private const int DefaultDelayMilliseconds = 180000; // 3 mins + private const int OneMinuteMilliseconds = 60000; + private const int FiveMinuteMilliseconds = 300000; + private const int OneHourMilliseconds = 3600000; + private readonly IRuntimeState _runtime; private readonly IContentService _contentService; private readonly IAuditService _auditService; @@ -29,6 +36,7 @@ namespace Umbraco.Web.Scheduling private BackgroundTaskRunner _publishingRunner; private BackgroundTaskRunner _tasksRunner; private BackgroundTaskRunner _scrubberRunner; + private BackgroundTaskRunner _fileCleanupRunner; private BackgroundTaskRunner _healthCheckRunner; private bool _started; @@ -58,6 +66,7 @@ namespace Umbraco.Web.Scheduling _publishingRunner = new BackgroundTaskRunner("ScheduledPublishing", _logger); _tasksRunner = new BackgroundTaskRunner("ScheduledTasks", _logger); _scrubberRunner = new BackgroundTaskRunner("LogScrubber", _logger); + _fileCleanupRunner = new BackgroundTaskRunner("TempFileCleanup", _logger); _healthCheckRunner = new BackgroundTaskRunner("HealthCheckNotifier", _logger); // we will start the whole process when a successful request is made @@ -90,9 +99,14 @@ namespace Umbraco.Web.Scheduling var tasks = new List(); - tasks.Add(RegisterKeepAlive()); + if (settings.KeepAlive.DisableKeepAliveTask == false) + { + tasks.Add(RegisterKeepAlive(settings.KeepAlive)); + } + tasks.Add(RegisterScheduledPublishing()); tasks.Add(RegisterLogScrubber(settings)); + tasks.Add(RegisterTempFileCleanup()); var healthCheckConfig = Current.Configs.HealthChecks(); if (healthCheckConfig.NotificationSettings.Enabled) @@ -102,11 +116,11 @@ namespace Umbraco.Web.Scheduling }); } - private IBackgroundTask RegisterKeepAlive() + private IBackgroundTask RegisterKeepAlive(IKeepAliveSection keepAliveSection) { // ping/keepalive // on all servers - var task = new KeepAlive(_keepAliveRunner, 60000, 300000, _runtime, _logger); + var task = new KeepAlive(_keepAliveRunner, DefaultDelayMilliseconds, FiveMinuteMilliseconds, _runtime, keepAliveSection, _logger); _keepAliveRunner.TryAdd(task); return task; } @@ -115,7 +129,7 @@ namespace Umbraco.Web.Scheduling { // scheduled publishing/unpublishing // install on all, will only run on non-replica servers - var task = new ScheduledPublishing(_publishingRunner, 60000, 60000, _runtime, _contentService, _umbracoContextFactory, _logger); + var task = new ScheduledPublishing(_publishingRunner, DefaultDelayMilliseconds, OneMinuteMilliseconds, _runtime, _contentService, _umbracoContextFactory, _logger); _publishingRunner.TryAdd(task); return task; } @@ -128,15 +142,15 @@ namespace Umbraco.Web.Scheduling int delayInMilliseconds; if (string.IsNullOrEmpty(healthCheckConfig.NotificationSettings.FirstRunTime)) { - delayInMilliseconds = 60000; + delayInMilliseconds = DefaultDelayMilliseconds; } else { // Otherwise start at scheduled time delayInMilliseconds = DateTime.Now.PeriodicMinutesFrom(healthCheckConfig.NotificationSettings.FirstRunTime) * 60 * 1000; - if (delayInMilliseconds < 60000) + if (delayInMilliseconds < DefaultDelayMilliseconds) { - delayInMilliseconds = 60000; + delayInMilliseconds = DefaultDelayMilliseconds; } } @@ -150,7 +164,19 @@ namespace Umbraco.Web.Scheduling { // log scrubbing // install on all, will only run on non-replica servers - var task = new LogScrubber(_scrubberRunner, 60000, LogScrubber.GetLogScrubbingInterval(settings, _logger), _runtime, _auditService, settings, _scopeProvider, _logger); + var task = new LogScrubber(_scrubberRunner, DefaultDelayMilliseconds, LogScrubber.GetLogScrubbingInterval(settings, _logger), _runtime, _auditService, settings, _scopeProvider, _logger); + _scrubberRunner.TryAdd(task); + return task; + } + + private IBackgroundTask RegisterTempFileCleanup() + { + // temp file cleanup, will run on all servers - even though file upload should only be handled on the master, this will + // ensure that in the case it happes on replicas that they are cleaned up. + var task = new TempFileCleanup(_fileCleanupRunner, DefaultDelayMilliseconds, OneHourMilliseconds, + new[] { new DirectoryInfo(IOHelper.MapPath(SystemDirectories.TempFileUploads)) }, + TimeSpan.FromDays(1), //files that are over a day old + _runtime, _logger); _scrubberRunner.TryAdd(task); return task; } diff --git a/src/Umbraco.Web/Scheduling/TempFileCleanup.cs b/src/Umbraco.Web/Scheduling/TempFileCleanup.cs new file mode 100644 index 0000000000..5d28bbfdb1 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/TempFileCleanup.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Used to cleanup temporary file locations + /// + internal class TempFileCleanup : RecurringTaskBase + { + private readonly DirectoryInfo[] _tempFolders; + private readonly TimeSpan _age; + private readonly IRuntimeState _runtime; + private readonly IProfilingLogger _logger; + + public TempFileCleanup(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + IEnumerable tempFolders, TimeSpan age, + IRuntimeState runtime, IProfilingLogger logger) + : base(runner, delayMilliseconds, periodMilliseconds) + { + //SystemDirectories.TempFileUploads + + _tempFolders = tempFolders.ToArray(); + _age = age; + _runtime = runtime; + _logger = logger; + } + + public override bool PerformRun() + { + // ensure we do not run if not main domain + if (_runtime.IsMainDom == false) + { + _logger.Debug("Does not run if not MainDom."); + return false; // do NOT repeat, going down + } + + foreach (var dir in _tempFolders) + CleanupFolder(dir); + + return true; //repeat + } + + private void CleanupFolder(DirectoryInfo dir) + { + dir.Refresh(); //in case it's changed during runtime + if (!dir.Exists) + { + _logger.Debug("The cleanup folder doesn't exist {Folder}", dir.FullName); + } + + var files = dir.GetFiles("*.*", SearchOption.AllDirectories); + foreach (var file in files) + { + if (DateTime.UtcNow - file.LastWriteTimeUtc > _age) + { + try + { + file.Delete(); + } + catch (Exception ex) + { + _logger.Error(ex, "Could not delete temp file {FileName}", file.FullName); + } + } + } + } + + public override bool IsAsync => false; + } +} diff --git a/src/Umbraco.Web/Search/BackgroundIndexRebuilder.cs b/src/Umbraco.Web/Search/BackgroundIndexRebuilder.cs new file mode 100644 index 0000000000..0ae5ceade9 --- /dev/null +++ b/src/Umbraco.Web/Search/BackgroundIndexRebuilder.cs @@ -0,0 +1,123 @@ +using System; +using System.Threading; +using Umbraco.Core.Logging; +using Umbraco.Examine; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Web.Scheduling; + +namespace Umbraco.Web.Search +{ + /// + /// Utility to rebuild all indexes on a background thread + /// + public sealed class BackgroundIndexRebuilder + { + private static readonly object RebuildLocker = new object(); + private readonly IndexRebuilder _indexRebuilder; + private readonly IMainDom _mainDom; + private readonly IProfilingLogger _logger; + private static BackgroundTaskRunner _rebuildOnStartupRunner; + + public BackgroundIndexRebuilder(IMainDom mainDom, IProfilingLogger logger, IndexRebuilder indexRebuilder) + { + _mainDom = mainDom; + _logger = logger; + _indexRebuilder = indexRebuilder; + } + + /// + /// Called to rebuild empty indexes on startup + /// + /// + /// + /// + /// + public void RebuildIndexes(bool onlyEmptyIndexes, int waitMilliseconds = 0) + { + // TODO: need a way to disable rebuilding on startup + + lock (RebuildLocker) + { + if (_rebuildOnStartupRunner != null && _rebuildOnStartupRunner.IsRunning) + { + _logger.Warn("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); + return; + } + + _logger.Info("Starting initialize async background thread."); + //do the rebuild on a managed background thread + var task = new RebuildOnStartupTask(_mainDom, _indexRebuilder, _logger, onlyEmptyIndexes, waitMilliseconds); + + _rebuildOnStartupRunner = new BackgroundTaskRunner( + "RebuildIndexesOnStartup", + _logger); + + _rebuildOnStartupRunner.TryAdd(task); + } + } + + /// + /// Background task used to rebuild empty indexes on startup + /// + private class RebuildOnStartupTask : IBackgroundTask + { + private readonly IMainDom _mainDom; + + private readonly IndexRebuilder _indexRebuilder; + private readonly ILogger _logger; + private readonly bool _onlyEmptyIndexes; + private readonly int _waitMilliseconds; + + public RebuildOnStartupTask(IMainDom mainDom, + IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) + { + _mainDom = mainDom; + _indexRebuilder = indexRebuilder ?? throw new ArgumentNullException(nameof(indexRebuilder)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _onlyEmptyIndexes = onlyEmptyIndexes; + _waitMilliseconds = waitMilliseconds; + } + + public bool IsAsync => false; + + public void Dispose() + { + } + + public void Run() + { + try + { + // rebuilds indexes + RebuildIndexes(); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to rebuild empty indexes."); + } + } + + public Task RunAsync(CancellationToken token) + { + throw new NotImplementedException(); + } + + /// + /// Used to rebuild indexes on startup or cold boot + /// + private void RebuildIndexes() + { + //do not attempt to do this if this has been disabled since we are not the main dom. + //this can be called during a cold boot + if (!_mainDom.IsMainDom) return; + + if (_waitMilliseconds > 0) + Thread.Sleep(_waitMilliseconds); + + _indexRebuilder.ExamineManager.ConfigureIndexes(_mainDom, _logger); + _indexRebuilder.RebuildIndexes(_onlyEmptyIndexes); + } + } + } +} diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index 3583e5b7f9..149b4d1436 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading; using Examine; using Umbraco.Core; using Umbraco.Core.Cache; @@ -15,45 +14,42 @@ using Umbraco.Core.Sync; using Umbraco.Web.Cache; using Umbraco.Examine; using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Web.Scheduling; -using System.Threading.Tasks; using Examine.LuceneEngine.Directories; using Umbraco.Core.Composing; +using System.ComponentModel; namespace Umbraco.Web.Search { - public sealed class ExamineComponent : IComponent + public sealed class ExamineComponent : Umbraco.Core.Composing.IComponent { private readonly IExamineManager _examineManager; private readonly IContentValueSetBuilder _contentValueSetBuilder; private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; private readonly IValueSetBuilder _mediaValueSetBuilder; private readonly IValueSetBuilder _memberValueSetBuilder; - private static bool _disableExamineIndexing = false; - private static volatile bool _isConfigured = false; - private static readonly object IsConfiguredLocker = new object(); + private readonly BackgroundIndexRebuilder _backgroundIndexRebuilder; + private static object _isConfiguredLocker = new object(); private readonly IScopeProvider _scopeProvider; - private readonly ServiceContext _services; - private static BackgroundTaskRunner _rebuildOnStartupRunner; - private static readonly object RebuildLocker = new object(); + private readonly ServiceContext _services; private readonly IMainDom _mainDom; private readonly IProfilingLogger _logger; private readonly IUmbracoIndexesCreator _indexCreator; - private readonly IndexRebuilder _indexRebuilder; + // the default enlist priority is 100 // enlist with a lower priority to ensure that anything "default" runs after us // but greater that SafeXmlReaderWriter priority which is 60 private const int EnlistPriority = 80; - + public ExamineComponent(IMainDom mainDom, IExamineManager examineManager, IProfilingLogger profilingLogger, IScopeProvider scopeProvider, IUmbracoIndexesCreator indexCreator, - IndexRebuilder indexRebuilder, ServiceContext services, + ServiceContext services, IContentValueSetBuilder contentValueSetBuilder, IPublishedContentValueSetBuilder publishedContentValueSetBuilder, IValueSetBuilder mediaValueSetBuilder, - IValueSetBuilder memberValueSetBuilder) + IValueSetBuilder memberValueSetBuilder, + BackgroundIndexRebuilder backgroundIndexRebuilder) { _services = services; _scopeProvider = scopeProvider; @@ -62,11 +58,10 @@ namespace Umbraco.Web.Search _publishedContentValueSetBuilder = publishedContentValueSetBuilder; _mediaValueSetBuilder = mediaValueSetBuilder; _memberValueSetBuilder = memberValueSetBuilder; - + _backgroundIndexRebuilder = backgroundIndexRebuilder; _mainDom = mainDom; _logger = profilingLogger; _indexCreator = indexCreator; - _indexRebuilder = indexRebuilder; } public void Initialize() @@ -91,11 +86,10 @@ namespace Umbraco.Web.Search if (!examineShutdownRegistered) { - _logger.Debug("Examine shutdown not registered, this AppDomain is not the MainDom, Examine will be disabled"); + _logger.Info("Examine shutdown not registered, this AppDomain is not the MainDom, Examine will be disabled"); //if we could not register the shutdown examine ourselves, it means we are not maindom! in this case all of examine should be disabled! Suspendable.ExamineEvents.SuspendIndexers(_logger); - _disableExamineIndexing = true; return; //exit, do not continue } @@ -116,71 +110,18 @@ namespace Umbraco.Web.Search // bind to distributed cache events - this ensures that this logic occurs on ALL servers // that are taking part in a load balanced environment. ContentCacheRefresher.CacheUpdated += ContentCacheRefresherUpdated; - ContentTypeCacheRefresher.CacheUpdated += ContentTypeCacheRefresherUpdated; ; + ContentTypeCacheRefresher.CacheUpdated += ContentTypeCacheRefresherUpdated; MediaCacheRefresher.CacheUpdated += MediaCacheRefresherUpdated; MemberCacheRefresher.CacheUpdated += MemberCacheRefresherUpdated; - - EnsureUnlocked(_logger, _examineManager); - - // TODO: Instead of waiting 5000 ms, we could add an event handler on to fulfilling the first request, then start? - RebuildIndexes(_indexRebuilder, _logger, true, 5000); + LanguageCacheRefresher.CacheUpdated += LanguageCacheRefresherUpdated; } public void Terminate() { } - /// - /// Called to rebuild empty indexes on startup - /// - /// - /// - /// - /// - public static void RebuildIndexes(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) - { - // TODO: need a way to disable rebuilding on startup - - lock(RebuildLocker) - { - if (_rebuildOnStartupRunner != null && _rebuildOnStartupRunner.IsRunning) - { - logger.Warn("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); - return; - } - - logger.Info("Starting initialize async background thread."); - //do the rebuild on a managed background thread - var task = new RebuildOnStartupTask(indexRebuilder, logger, onlyEmptyIndexes, waitMilliseconds); - - _rebuildOnStartupRunner = new BackgroundTaskRunner( - "RebuildIndexesOnStartup", - logger); - - _rebuildOnStartupRunner.TryAdd(task); - } - } - - /// - /// Must be called to each index is unlocked before any indexing occurs - /// - /// - /// Indexing rebuilding can occur on a normal boot if the indexes are empty or on a cold boot by the database server messenger. Before - /// either of these happens, we need to configure the indexes. - /// - private static void EnsureUnlocked(ILogger logger, IExamineManager examineManager) - { - if (_disableExamineIndexing) return; - if (_isConfigured) return; - - lock (IsConfiguredLocker) - { - //double check - if (_isConfigured) return; - - _isConfigured = true; - examineManager.UnlockLuceneIndexes(logger); - } - } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method should not be used and will be removed in future versions, rebuilding indexes can be done with the IndexRebuilder or the BackgroundIndexRebuilder")] + public static void RebuildIndexes(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) => Current.Factory.GetInstance().RebuildIndexes(onlyEmptyIndexes, waitMilliseconds); #region Cache refresher updated event handlers @@ -382,6 +323,25 @@ namespace Umbraco.Web.Search } } + private void LanguageCacheRefresherUpdated(LanguageCacheRefresher sender, CacheRefresherEventArgs e) + { + if (!(e.MessageObject is LanguageCacheRefresher.JsonPayload[] payloads)) + return; + + if (payloads.Length == 0) return; + + var removedOrCultureChanged = payloads.Any(x => + x.ChangeType == LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture + || x.ChangeType == LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); + + if (removedOrCultureChanged) + { + //if a lang is removed or it's culture has changed, we need to rebuild the indexes since + //field names and values in the index have a string culture value. + _backgroundIndexRebuilder.RebuildIndexes(false); + } + } + /// /// Updates indexes based on content type changes /// @@ -737,7 +697,7 @@ namespace Umbraco.Web.Search { var strId = id.ToString(CultureInfo.InvariantCulture); foreach (var index in examineComponent._examineManager.Indexes.OfType() - .Where(x => (keepIfUnpublished && !x.PublishedValuesOnly) || !keepIfUnpublished) + .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) .Where(x => x.EnableDefaultEventHandler)) { index.DeleteFromIndex(strId); @@ -746,63 +706,6 @@ namespace Umbraco.Web.Search } #endregion - /// - /// Background task used to rebuild empty indexes on startup - /// - private class RebuildOnStartupTask : IBackgroundTask - { - private readonly IndexRebuilder _indexRebuilder; - private readonly ILogger _logger; - private readonly bool _onlyEmptyIndexes; - private readonly int _waitMilliseconds; - - public RebuildOnStartupTask(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) - { - _indexRebuilder = indexRebuilder ?? throw new ArgumentNullException(nameof(indexRebuilder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _onlyEmptyIndexes = onlyEmptyIndexes; - _waitMilliseconds = waitMilliseconds; - } - - public bool IsAsync => false; - - public void Dispose() - { - } - - public void Run() - { - try - { - // rebuilds indexes - RebuildIndexes(); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to rebuild empty indexes."); - } - } - - public Task RunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } - - /// - /// Used to rebuild indexes on startup or cold boot - /// - private void RebuildIndexes() - { - //do not attempt to do this if this has been disabled since we are not the main dom. - //this can be called during a cold boot - if (_disableExamineIndexing) return; - - if (_waitMilliseconds > 0) - Thread.Sleep(_waitMilliseconds); - - EnsureUnlocked(_logger, _indexRebuilder.ExamineManager); - _indexRebuilder.RebuildIndexes(_onlyEmptyIndexes); - } - } + } } diff --git a/src/Umbraco.Web/Search/ExamineComposer.cs b/src/Umbraco.Web/Search/ExamineComposer.cs index 6e74f0e89d..64eeb6978a 100644 --- a/src/Umbraco.Web/Search/ExamineComposer.cs +++ b/src/Umbraco.Web/Search/ExamineComposer.cs @@ -4,12 +4,14 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Examine; namespace Umbraco.Web.Search { + /// /// Configures and installs Examine. /// @@ -28,21 +30,25 @@ namespace Umbraco.Web.Search composition.Register(Lifetime.Singleton); composition.Register(Lifetime.Singleton); + composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(factory => new ContentValueSetBuilder( factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), + factory.GetInstance(), true)); composition.RegisterUnique(factory => new ContentValueSetBuilder( factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), + factory.GetInstance(), false)); composition.RegisterUnique, MediaValueSetBuilder>(); composition.RegisterUnique, MemberValueSetBuilder>(); + composition.RegisterUnique(); //We want to manage Examine's AppDomain shutdown sequence ourselves so first we'll disable Examine's default behavior //and then we'll use MainDom to control Examine's shutdown - this MUST be done in Compose ie before ExamineManager diff --git a/src/Umbraco.Web/Search/ExamineFinalComponent.cs b/src/Umbraco.Web/Search/ExamineFinalComponent.cs new file mode 100644 index 0000000000..95000b2b46 --- /dev/null +++ b/src/Umbraco.Web/Search/ExamineFinalComponent.cs @@ -0,0 +1,42 @@ +using Examine; +using Umbraco.Core.Logging; +using Umbraco.Examine; +using Umbraco.Core.Composing; +using Umbraco.Core; + +namespace Umbraco.Web.Search +{ + + /// + /// Executes after all other examine components have executed + /// + public sealed class ExamineFinalComponent : IComponent + { + private readonly IProfilingLogger _logger; + private readonly IExamineManager _examineManager; + BackgroundIndexRebuilder _indexRebuilder; + private readonly IMainDom _mainDom; + + public ExamineFinalComponent(IProfilingLogger logger, IExamineManager examineManager, BackgroundIndexRebuilder indexRebuilder, IMainDom mainDom) + { + _logger = logger; + _examineManager = examineManager; + _indexRebuilder = indexRebuilder; + _mainDom = mainDom; + } + + public void Initialize() + { + if (!_mainDom.IsMainDom) return; + + _examineManager.ConfigureIndexes(_mainDom, _logger); + + // TODO: Instead of waiting 5000 ms, we could add an event handler on to fulfilling the first request, then start? + _indexRebuilder.RebuildIndexes(true, 5000); + } + + public void Terminate() + { + } + } +} diff --git a/src/Umbraco.Web/Search/ExamineFinalComposer.cs b/src/Umbraco.Web/Search/ExamineFinalComposer.cs new file mode 100644 index 0000000000..5b6334f1f6 --- /dev/null +++ b/src/Umbraco.Web/Search/ExamineFinalComposer.cs @@ -0,0 +1,13 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Search +{ + // examine's final composer composes after all user composers + // and *also* after ICoreComposer (in case IUserComposer is disabled) + [ComposeAfter(typeof(IUserComposer))] + [ComposeAfter(typeof(ICoreComposer))] + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public class ExamineFinalComposer : ComponentComposer + { } +} diff --git a/src/Umbraco.Web/Search/ExamineUserComponent.cs b/src/Umbraco.Web/Search/ExamineUserComponent.cs new file mode 100644 index 0000000000..35bc3f59ea --- /dev/null +++ b/src/Umbraco.Web/Search/ExamineUserComponent.cs @@ -0,0 +1,37 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Search +{ + /// + /// An abstract class for custom index authors to inherit from + /// + public abstract class ExamineUserComponent : IComponent + { + private readonly IMainDom _mainDom; + + public ExamineUserComponent(IMainDom mainDom) + { + _mainDom = mainDom; + } + + /// + /// Initialize the component, eagerly exits if ExamineComponent.ExamineEnabled == false + /// + public void Initialize() + { + if (!_mainDom.IsMainDom) return; + + InitializeComponent(); + } + + /// + /// Abstract method which executes to initialize this component if ExamineComponent.ExamineEnabled == true + /// + protected abstract void InitializeComponent(); + + public virtual void Terminate() + { + } + } +} diff --git a/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs b/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs index 560fb19f09..cb25e1242a 100644 --- a/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs +++ b/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs @@ -8,6 +8,7 @@ using Umbraco.Examine; namespace Umbraco.Web.Search { + /// /// Used to return diagnostic data for any index /// diff --git a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs new file mode 100644 index 0000000000..c5a6c53d19 --- /dev/null +++ b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Umbraco.Web.Search +{ + /// + /// Used to propagate hardcoded internal Field lists + /// + public interface IUmbracoTreeSearcherFields + { + /// + /// Propagate list of searchable fields for all node types + /// + IEnumerable GetBackOfficeFields(); + /// + /// Propagate list of searchable fields for Members + /// + IEnumerable GetBackOfficeMembersFields(); + /// + /// Propagate list of searchable fields for Media + /// + IEnumerable GetBackOfficeMediaFields(); + /// + /// Propagate list of searchable fields for Documents + /// + IEnumerable GetBackOfficeDocumentFields(); + } +} diff --git a/src/Umbraco.Web/Search/SearchableApplicationTree.cs b/src/Umbraco.Web/Search/SearchableApplicationTree.cs index 997d8ce8e8..346f106b84 100644 --- a/src/Umbraco.Web/Search/SearchableApplicationTree.cs +++ b/src/Umbraco.Web/Search/SearchableApplicationTree.cs @@ -4,15 +4,21 @@ namespace Umbraco.Web.Search { public class SearchableApplicationTree { - public SearchableApplicationTree(string appAlias, string treeAlias, ISearchableTree searchableTree) + public SearchableApplicationTree(string appAlias, string treeAlias, int sortOrder, string formatterService, string formatterMethod, ISearchableTree searchableTree) { AppAlias = appAlias; TreeAlias = treeAlias; + SortOrder = sortOrder; + FormatterService = formatterService; + FormatterMethod = formatterMethod; SearchableTree = searchableTree; } public string AppAlias { get; } public string TreeAlias { get; } + public int SortOrder { get; } + public string FormatterService { get; } + public string FormatterMethod { get; } public ISearchableTree SearchableTree { get; } } } diff --git a/src/Umbraco.Web/Search/SearchableTreeAttribute.cs b/src/Umbraco.Web/Search/SearchableTreeAttribute.cs index 6c44b8946d..d81a85bb4b 100644 --- a/src/Umbraco.Web/Search/SearchableTreeAttribute.cs +++ b/src/Umbraco.Web/Search/SearchableTreeAttribute.cs @@ -1,36 +1,53 @@ using System; -using Umbraco.Core.Exceptions; namespace Umbraco.Web.Search { [AttributeUsage(AttributeTargets.Class)] public sealed class SearchableTreeAttribute : Attribute { - /// - /// This constructor defines both the angular service and method name to use - /// - /// - /// - public SearchableTreeAttribute(string serviceName, string methodName) - { - if (string.IsNullOrWhiteSpace(serviceName)) throw new ArgumentNullOrEmptyException(nameof(serviceName)); - if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentNullOrEmptyException(nameof(methodName)); - MethodName = methodName; - ServiceName = serviceName; - } + public const int DefaultSortOrder = 1000; - /// - /// This constructor will assume that the method name equals `format(searchResult, appAlias, treeAlias)` - /// - /// - public SearchableTreeAttribute(string serviceName) - { - if (string.IsNullOrWhiteSpace(serviceName)) throw new ArgumentNullOrEmptyException(nameof(serviceName)); - MethodName = ""; - ServiceName = serviceName; - } + public string ServiceName { get; } public string MethodName { get; } - public string ServiceName { get; } + + public int SortOrder { get; } + + /// + /// This constructor will assume that the method name equals `format(searchResult, appAlias, treeAlias)`. + /// + /// Name of the service. + public SearchableTreeAttribute(string serviceName) + : this(serviceName, string.Empty) + { } + + /// + /// This constructor defines both the Angular service and method name to use. + /// + /// Name of the service. + /// Name of the method. + public SearchableTreeAttribute(string serviceName, string methodName) + : this(serviceName, methodName, DefaultSortOrder) + { } + + /// + /// This constructor defines both the Angular service and method name to use and explicitly defines a sort order for the results + /// + /// Name of the service. + /// Name of the method. + /// The sort order. + /// serviceName + /// or + /// methodName + /// Value can't be empty or consist only of white-space characters. - serviceName + public SearchableTreeAttribute(string serviceName, string methodName, int sortOrder) + { + if (serviceName == null) throw new ArgumentNullException(nameof(serviceName)); + if (string.IsNullOrWhiteSpace(serviceName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(serviceName)); + + ServiceName = serviceName; + MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); + SortOrder = sortOrder; + } } } diff --git a/src/Umbraco.Web/Search/SearchableTreeCollection.cs b/src/Umbraco.Web/Search/SearchableTreeCollection.cs index 8f7c6ece0b..0e7b1cd268 100644 --- a/src/Umbraco.Web/Search/SearchableTreeCollection.cs +++ b/src/Umbraco.Web/Search/SearchableTreeCollection.cs @@ -32,7 +32,15 @@ namespace Umbraco.Web.Search var found = searchableTrees.FirstOrDefault(x => x.TreeAlias.InvariantEquals(appTree.TreeAlias)); if (found != null) { - dictionary[found.TreeAlias] = new SearchableApplicationTree(appTree.SectionAlias, appTree.TreeAlias, found); + var searchableTreeAttribute = found.GetType().GetCustomAttribute(false); + dictionary[found.TreeAlias] = new SearchableApplicationTree( + appTree.SectionAlias, + appTree.TreeAlias, + searchableTreeAttribute?.SortOrder ?? SearchableTreeAttribute.DefaultSortOrder, + searchableTreeAttribute?.ServiceName ?? string.Empty, + searchableTreeAttribute?.MethodName ?? string.Empty, + found + ); } } return dictionary; diff --git a/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs b/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs index 5ac5529be5..ec536b9d75 100644 --- a/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs +++ b/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Logging; using Umbraco.Core.Services; using Umbraco.Examine; @@ -19,18 +20,20 @@ namespace Umbraco.Web.Search public UmbracoIndexesCreator(IProfilingLogger profilingLogger, ILocalizationService languageService, IPublicAccessService publicAccessService, - IMemberService memberService) + IMemberService memberService, IUmbracoIndexConfig umbracoIndexConfig) { ProfilingLogger = profilingLogger ?? throw new System.ArgumentNullException(nameof(profilingLogger)); LanguageService = languageService ?? throw new System.ArgumentNullException(nameof(languageService)); PublicAccessService = publicAccessService ?? throw new System.ArgumentNullException(nameof(publicAccessService)); MemberService = memberService ?? throw new System.ArgumentNullException(nameof(memberService)); + UmbracoIndexConfig = umbracoIndexConfig; } protected IProfilingLogger ProfilingLogger { get; } protected ILocalizationService LanguageService { get; } protected IPublicAccessService PublicAccessService { get; } protected IMemberService MemberService { get; } + protected IUmbracoIndexConfig UmbracoIndexConfig { get; } /// /// Creates the Umbraco indexes @@ -55,7 +58,8 @@ namespace Umbraco.Web.Search new CultureInvariantWhitespaceAnalyzer(), ProfilingLogger, LanguageService, - GetContentValueSetValidator()); + GetContentValueSetValidator() + ); return index; } @@ -80,28 +84,31 @@ namespace Umbraco.Web.Search CreateFileSystemLuceneDirectory(Constants.UmbracoIndexes.MembersIndexPath), new CultureInvariantWhitespaceAnalyzer(), ProfilingLogger, - GetMemberValueSetValidator()); + GetMemberValueSetValidator() + ); return index; } - + [Obsolete("This method should not be used and will be removed in future versions. GetContentValueSetValidator was moved to IUmbracoIndexConfig")] public virtual IContentValueSetValidator GetContentValueSetValidator() { - return new ContentValueSetValidator(false, true, PublicAccessService); + return UmbracoIndexConfig.GetContentValueSetValidator(); } - + [Obsolete("This method should not be used and will be removed in future versions. GetPublishedContentValueSetValidator was moved to IUmbracoIndexConfig")] public virtual IContentValueSetValidator GetPublishedContentValueSetValidator() { - return new ContentValueSetValidator(true, false, PublicAccessService); + return UmbracoIndexConfig.GetPublishedContentValueSetValidator(); } /// /// Returns the for the member indexer /// /// + [Obsolete("This method should not be used and will be removed in future versions. GetMemberValueSetValidator was moved to IUmbracoIndexConfig")] public virtual IValueSetValidator GetMemberValueSetValidator() { - return new MemberValueSetValidator(); + return UmbracoIndexConfig.GetMemberValueSetValidator(); } + } } diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 7fb51a61eb..dcc156e356 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -7,13 +7,17 @@ using Examine; using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Examine; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Mapping; using Umbraco.Web.Trees; namespace Umbraco.Web.Search { + /// /// Used for internal Umbraco implementations of /// @@ -24,47 +28,28 @@ namespace Umbraco.Web.Search private readonly ILocalizationService _languageService; private readonly IEntityService _entityService; private readonly UmbracoMapper _mapper; + private readonly ISqlContext _sqlContext; + private readonly IUmbracoTreeSearcherFields _umbracoTreeSearcherFields; + public UmbracoTreeSearcher(IExamineManager examineManager, UmbracoContext umbracoContext, ILocalizationService languageService, IEntityService entityService, - UmbracoMapper mapper) + UmbracoMapper mapper, + ISqlContext sqlContext,IUmbracoTreeSearcherFields umbracoTreeSearcherFields) { _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); _umbracoContext = umbracoContext; _languageService = languageService; _entityService = entityService; _mapper = mapper; + _sqlContext = sqlContext; + _umbracoTreeSearcherFields = umbracoTreeSearcherFields; } /// - /// This method is obsolete, use the overload with ignoreUserStartNodes instead - /// Searches for results based on the entity type - /// - /// - /// - /// - /// - /// A starting point for the search, generally a node id, but for members this is a member type alias - /// - /// - /// - /// - [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] - public IEnumerable ExamineSearch( - string query, - UmbracoEntityTypes entityType, - int pageSize, - long pageIndex, - out long totalFound, - string searchFrom = null) - { - return ExamineSearch(query, entityType, pageSize, pageIndex, out totalFound, ignoreUserStartNodes: false, searchFrom); - } - - /// - /// Searches for results based on the entity type + /// Searches Examine for results based on the entity type /// /// /// @@ -80,16 +65,23 @@ namespace Umbraco.Web.Search string query, UmbracoEntityTypes entityType, int pageSize, - long pageIndex, - out long totalFound, - bool ignoreUserStartNodes, - string searchFrom = null) + long pageIndex, out long totalFound, string searchFrom = null, bool ignoreUserStartNodes = false) + { + return ExamineSearch(query, entityType, pageSize, pageIndex, culture: null, out totalFound, searchFrom, ignoreUserStartNodes); + } + + public IEnumerable ExamineSearch( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, string culture, + out long totalFound, string searchFrom = null, bool ignoreUserStartNodes = false) { var sb = new StringBuilder(); string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = new[] { "id", "__NodeId" }; + var fields = _umbracoTreeSearcherFields.GetBackOfficeFields().ToList(); // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... @@ -98,12 +90,18 @@ namespace Umbraco.Web.Search //} + //special GUID check since if a user searches on one specifically we need to escape it + if (Guid.TryParse(query, out var g)) + { + query = "\"" + g.ToString() + "\""; + } + switch (entityType) { case UmbracoEntityTypes.Member: indexName = Constants.UmbracoIndexes.MembersIndexName; type = "member"; - fields = new[] { "id", "__NodeId", "email", "loginName" }; + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMembersFields()); if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") { sb.Append("+__NodeTypeAlias:"); @@ -113,11 +111,13 @@ namespace Umbraco.Web.Search break; case UmbracoEntityTypes.Media: type = "media"; + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMediaFields()); var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeDocumentFields()); var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; @@ -151,13 +151,33 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Media: return MediaFromSearchResults(pagedResult); case UmbracoEntityTypes.Document: - return ContentFromSearchResults(pagedResult); + return ContentFromSearchResults(pagedResult, culture); default: throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); } } - private bool BuildQuery(StringBuilder sb, string query, string searchFrom, string[] fields, string type) + /// + /// Searches with the for results based on the entity type + /// + /// + /// + /// + /// + /// + /// + /// + public IEnumerable EntitySearch(UmbracoObjectTypes objectType, string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) + { + //if it's a GUID, match it + Guid.TryParse(query, out var g); + + var results = _entityService.GetPagedDescendants(objectType, pageIndex, pageSize, out totalFound, + filter: _sqlContext.Query().Where(x => x.Name.Contains(query) || x.Key == g)); + return _mapper.MapEnumerable(results); + } + + private bool BuildQuery(StringBuilder sb, string query, string searchFrom, List fields, string type) { //build a lucene query: // the nodeName will be boosted 10x without wildcards @@ -230,11 +250,26 @@ namespace Umbraco.Web.Search foreach (var f in fields) { + var queryWordsReplaced = new string[querywords.Length]; + + // when searching file names containing hyphens we need to replace the hyphens with spaces + if (f.Equals(UmbracoExamineIndex.UmbracoFileFieldName)) + { + for (var index = 0; index < querywords.Length; index++) + { + queryWordsReplaced[index] = querywords[index].Replace("\\-", " ").Replace("_", " ").Trim(" "); + } + } + else + { + queryWordsReplaced = querywords; + } + //additional fields normally sb.Append(f); sb.Append(":"); sb.Append("("); - foreach (var w in querywords) + foreach (var w in queryWordsReplaced) { sb.Append(w.ToLower()); sb.Append("* "); @@ -385,9 +420,9 @@ namespace Umbraco.Web.Search var m = _mapper.Map(result); //if no icon could be mapped, it will be set to document, so change it to picture - if (m.Icon == "icon-document") + if (m.Icon == Constants.Icons.DefaultIcon) { - m.Icon = "icon-user"; + m.Icon = Constants.Icons.Member; } if (result.Values.ContainsKey("email") && result.Values["email"] != null) @@ -412,32 +447,24 @@ namespace Umbraco.Web.Search /// /// private IEnumerable MediaFromSearchResults(IEnumerable results) - { - //add additional data - foreach (var result in results) - { - var m = _mapper.Map(result); - //if no icon could be mapped, it will be set to document, so change it to picture - if (m.Icon == "icon-document") - { - m.Icon = "icon-picture"; - } - yield return m; - } - } + => _mapper.Map>(results); /// /// Returns a collection of entities for content based on search results /// /// /// - private IEnumerable ContentFromSearchResults(IEnumerable results) + private IEnumerable ContentFromSearchResults(IEnumerable results, string culture = null) { var defaultLang = _languageService.GetDefaultLanguageIsoCode(); - foreach (var result in results) { - var entity = _mapper.Map(result); + var entity = _mapper.Map(result, context => { + if(culture != null) { + context.SetCulture(culture); + } + } + ); var intId = entity.Id.TryConvertTo(); if (intId.Success) @@ -445,7 +472,7 @@ namespace Umbraco.Web.Search //if it varies by culture, return the default language URL if (result.Values.TryGetValue(UmbracoContentIndex.VariesByCultureFieldName, out var varies) && varies == "y") { - entity.AdditionalData["Url"] = _umbracoContext.Url(intId.Result, defaultLang); + entity.AdditionalData["Url"] = _umbracoContext.Url(intId.Result, culture ?? defaultLang); } else { diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs new file mode 100644 index 0000000000..f90d7bc6b6 --- /dev/null +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Examine; + +namespace Umbraco.Web.Search +{ + public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields + { + private IReadOnlyList _backOfficeFields = new List {"id", "__NodeId", "__Key"}; + public IEnumerable GetBackOfficeFields() + { + return _backOfficeFields; + } + + + private IReadOnlyList _backOfficeMembersFields = new List {"email", "loginName"}; + public IEnumerable GetBackOfficeMembersFields() + { + return _backOfficeMembersFields; + } + private IReadOnlyList _backOfficeMediaFields = new List {UmbracoExamineIndex.UmbracoFileFieldName }; + public IEnumerable GetBackOfficeMediaFields() + { + return _backOfficeMediaFields; + } + public IEnumerable GetBackOfficeDocumentFields() + { + return Enumerable.Empty(); + } + } +} diff --git a/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs b/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs index 33aabbaf94..568bfc1a26 100644 --- a/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs +++ b/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs @@ -75,7 +75,8 @@ namespace Umbraco.Web.Security /// public static void ForUmbracoBackOffice(this AuthenticationOptions options, string style, string icon, string callbackPath = null) { - if (string.IsNullOrEmpty(options.AuthenticationType)) throw new ArgumentNullOrEmptyException("options.AuthenticationType"); + if (options == null) throw new ArgumentNullException(nameof(options)); + if (string.IsNullOrEmpty(options.AuthenticationType)) throw new InvalidOperationException("The authentication type can't be null or empty."); //Ensure the prefix is set if (options.AuthenticationType.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix) == false) @@ -101,7 +102,7 @@ namespace Umbraco.Web.Security var path = (PathString) prop.GetValue(options); if (path.HasValue) { - UmbracoModule.ReservedPaths.TryAdd(path.ToString()); + RoutableDocumentFilter.ReservedPaths.TryAdd(path.ToString()); } } } @@ -112,7 +113,7 @@ namespace Umbraco.Web.Security } else { - UmbracoModule.ReservedPaths.TryAdd(callbackPath); + RoutableDocumentFilter.ReservedPaths.TryAdd(callbackPath); } } } diff --git a/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs b/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs index e7aef4be03..ed6abb3fbe 100644 --- a/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs +++ b/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs @@ -41,6 +41,9 @@ namespace Umbraco.Web.Security : Guid.NewGuid(); backOfficeIdentity.SessionId = session.ToString(); + + //since it is a cookie-based authentication add that claim + backOfficeIdentity.AddClaim(new Claim(ClaimTypes.CookiePath, "/", ClaimValueTypes.String, UmbracoBackOfficeIdentity.Issuer, UmbracoBackOfficeIdentity.Issuer, backOfficeIdentity)); } base.ResponseSignIn(context); diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs index b33487bc8d..5f1c1012f3 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs @@ -112,6 +112,21 @@ namespace Umbraco.Web.Security return SignInStatus.LockedOut; } + // We need to verify that the user belongs to one or more groups that define content and media start nodes. + // To do so we have to create the user claims identity and validate the calculated start nodes. + var userIdentity = await CreateUserIdentityAsync(user); + if (userIdentity is UmbracoBackOfficeIdentity backOfficeIdentity) + { + if (backOfficeIdentity.StartContentNodes.Length == 0 || backOfficeIdentity.StartMediaNodes.Length == 0) + { + _logger.WriteCore(TraceEventType.Information, 0, + $"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, no content and/or media start nodes could be found for any of the user's groups", null, null); + + // We will say its a sucessful login which it is, but they have no node access + return SignInStatus.Success; + } + } + await UserManager.ResetAccessFailedCountAsync(user.Id); return await SignInOrTwoFactor(user, isPersistent); } diff --git a/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs index 44fbdda6c7..2a9c3a5f07 100644 --- a/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs @@ -23,7 +23,7 @@ namespace Umbraco.Web.Security string[] defaultUserGroups = null, string defaultCulture = null) { - _defaultUserGroups = defaultUserGroups ?? new[] { "editor" }; + _defaultUserGroups = defaultUserGroups ?? new[] { Constants.Security.EditorGroupAlias }; _autoLinkExternalAccount = autoLinkExternalAccount; _defaultCulture = defaultCulture ?? Current.Configs.Global().DefaultUILanguage; } diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index cdf696c520..f74897d565 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -542,7 +542,7 @@ namespace Umbraco.Web.Security } /// - /// Returns the login status model of the currently logged in member, if no member is logged in it returns null; + /// Returns the login status model of the currently logged in member. /// /// public virtual LoginStatusModel GetCurrentLoginStatus() diff --git a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs index f3aa5dd838..10a293a3e7 100644 --- a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Security.Providers } private readonly IMemberTypeService _memberTypeService; - private string _defaultMemberTypeAlias = "writer"; + private string _defaultMemberTypeAlias = Constants.Security.WriterGroupAlias; private volatile bool _hasDefaultMember = false; private static readonly object Locker = new object(); diff --git a/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs new file mode 100644 index 0000000000..12d7cce753 --- /dev/null +++ b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs @@ -0,0 +1,91 @@ +using System; +using Umbraco.Web.Mvc; +using Umbraco.Core; +using System.Web.Helpers; +using System.Web; +using Newtonsoft.Json; +using System.ComponentModel; + +namespace Umbraco.Web.Security +{ + [Obsolete("This is no longer used and will be removed from the codebase in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + public class UmbracoAntiForgeryAdditionalDataProvider : IAntiForgeryAdditionalDataProvider + { + private readonly IAntiForgeryAdditionalDataProvider _defaultProvider; + + /// + /// Constructor, allows wrapping a default provider + /// + /// + public UmbracoAntiForgeryAdditionalDataProvider(IAntiForgeryAdditionalDataProvider defaultProvider) + { + _defaultProvider = defaultProvider; + } + + public string GetAdditionalData(HttpContextBase context) + { + return JsonConvert.SerializeObject(new AdditionalData + { + Stamp = DateTime.UtcNow.Ticks, + //this value will be here if this is a BeginUmbracoForms form + Ufprt = context.Items["ufprt"]?.ToString(), + //if there was a wrapped provider, add it's value to the json, else just a static value + WrappedValue = _defaultProvider?.GetAdditionalData(context) ?? "default" + }); + } + + public bool ValidateAdditionalData(HttpContextBase context, string additionalData) + { + if (!additionalData.DetectIsJson()) + return false; //must be json + + AdditionalData json; + try + { + json = JsonConvert.DeserializeObject(additionalData); + } + catch + { + return false; //couldn't parse + } + + if (json.Stamp == default) return false; + + //if there was a wrapped provider, validate it, else validate the static value + var validateWrapped = _defaultProvider?.ValidateAdditionalData(context, json.WrappedValue) ?? json.WrappedValue == "default"; + if (!validateWrapped) + return false; + + var ufprtRequest = context.Request["ufprt"]?.ToString(); + + //if the custom BeginUmbracoForms route value is not there, then it's nothing more to validate + if (ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) + return true; + + //if one or the other is null then something is wrong + if (!ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) return false; + if (ufprtRequest.IsNullOrWhiteSpace() && !json.Ufprt.IsNullOrWhiteSpace()) return false; + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(json.Ufprt, out var additionalDataParts)) + return false; + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprtRequest, out var requestParts)) + return false; + + //ensure they all match + return additionalDataParts.Count == requestParts.Count + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Action] + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Area]; + } + + internal class AdditionalData + { + public string Ufprt { get; set; } + public long Stamp { get; set; } + public string WrappedValue { get; set; } + } + + } +} diff --git a/src/Umbraco.Web/Services/DashboardService.cs b/src/Umbraco.Web/Services/DashboardService.cs index 71969df475..794c6fa671 100644 --- a/src/Umbraco.Web/Services/DashboardService.cs +++ b/src/Umbraco.Web/Services/DashboardService.cs @@ -104,8 +104,9 @@ namespace Umbraco.Web.Services } } - if (!hasAccess || denyRules.Length == 0) - return true; + // No need to check denyRules if there aren't any, just return current state + if (denyRules.Length == 0) + return hasAccess; // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will // be denied to see it no matter what diff --git a/src/Umbraco.Web/Services/SectionService.cs b/src/Umbraco.Web/Services/SectionService.cs index c001233152..2696186301 100644 --- a/src/Umbraco.Web/Services/SectionService.cs +++ b/src/Umbraco.Web/Services/SectionService.cs @@ -39,6 +39,6 @@ namespace Umbraco.Web.Services /// public ISection GetByAlias(string appAlias) - => GetSections().FirstOrDefault(t => t.Alias == appAlias); + => GetSections().FirstOrDefault(t => t.Alias.Equals(appAlias, StringComparison.OrdinalIgnoreCase)); } } diff --git a/src/Umbraco.Web/Suspendable.cs b/src/Umbraco.Web/Suspendable.cs index 86c83120ea..cfe4d28e8b 100644 --- a/src/Umbraco.Web/Suspendable.cs +++ b/src/Umbraco.Web/Suspendable.cs @@ -50,6 +50,8 @@ namespace Umbraco.Web } } + //This is really needed at all since the only place this is used is in ExamineComponent and that already maintains a flag of whether it suspsended or not + // AHH... but Deploy probably uses this? public static class ExamineEvents { private static bool _tried, _suspended; diff --git a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs new file mode 100644 index 0000000000..539ce303fd --- /dev/null +++ b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Umbraco.Core; + +namespace Umbraco.Web.Templates +{ + + public sealed class HtmlImageSourceParser + { + public HtmlImageSourceParser(Func getMediaUrl) + { + this._getMediaUrl = getMediaUrl; + } + + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + public HtmlImageSourceParser(IUmbracoContextAccessor umbracoContextAccessor) + { + _umbracoContextAccessor = umbracoContextAccessor; + } + + private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)((?:\?[^""]*)?""[^>]*data-udi="")([^""]*)(""[^>]*>)", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + private static readonly Regex DataUdiAttributeRegex = new Regex(@"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + private Func _getMediaUrl; + + /// + /// Parses out media UDIs from an html string based on 'data-udi' html attributes + /// + /// + /// + public IEnumerable FindUdisFromDataAttributes(string text) + { + var matches = DataUdiAttributeRegex.Matches(text); + if (matches.Count == 0) + yield break; + + foreach (Match match in matches) + { + if (match.Groups.Count == 2 && Udi.TryParse(match.Groups[1].Value, out var udi)) + yield return udi; + } + } + + /// + /// Parses the string looking for Umbraco image tags and updates them to their up-to-date image sources. + /// + /// + /// + /// Umbraco image tags are identified by their data-udi attributes + public string EnsureImageSources(string text) + { + if(_getMediaUrl == null) + _getMediaUrl = (guid) => _umbracoContextAccessor.UmbracoContext.UrlProvider.GetMediaUrl(guid); + + return ResolveImgPattern.Replace(text, match => + { + // match groups: + // - 1 = from the beginning of the image tag until src attribute value begins + // - 2 = the src attribute value excluding the querystring (if present) + // - 3 = anything after group 2 and before the data-udi attribute value begins + // - 4 = the data-udi attribute value + // - 5 = anything after group 4 until the image tag is closed + var udi = match.Groups[4].Value; + if (udi.IsNullOrWhiteSpace() || GuidUdi.TryParse(udi, out var guidUdi) == false) + { + return match.Value; + } + var mediaUrl = _getMediaUrl(guidUdi.Guid); + if (mediaUrl == null) + { + // image does not exist - we could choose to remove the image entirely here (return empty string), + // but that would leave the editors completely in the dark as to why the image doesn't show + return match.Value; + } + + return $"{match.Groups[1].Value}{mediaUrl}{match.Groups[3].Value}{udi}{match.Groups[5].Value}"; + }); + } + + /// + /// Removes media urls from <img> tags where a data-udi attribute is present + /// + /// + /// + public string RemoveImageSources(string text) + // see comment in ResolveMediaFromTextString for group reference + => ResolveImgPattern.Replace(text, "$1$3$4$5"); + } +} diff --git a/src/Umbraco.Web/Templates/HtmlLocalLinkParser.cs b/src/Umbraco.Web/Templates/HtmlLocalLinkParser.cs new file mode 100644 index 0000000000..f65a7183b7 --- /dev/null +++ b/src/Umbraco.Web/Templates/HtmlLocalLinkParser.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Templates +{ + /// + /// Utility class used to parse internal links + /// + public sealed class HtmlLocalLinkParser + { + + private static readonly Regex LocalLinkPattern = new Regex(@"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", + RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public HtmlLocalLinkParser(IUmbracoContextAccessor umbracoContextAccessor) + { + _umbracoContextAccessor = umbracoContextAccessor; + } + + internal IEnumerable FindUdisFromLocalLinks(string text) + { + foreach ((int? intId, GuidUdi udi, string tagValue) in FindLocalLinkIds(text)) + { + if (udi != null) + yield return udi; // In v8, we only care abuot UDIs + } + } + + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + /// + public string EnsureInternalLinks(string text, bool preview) + { + if (_umbracoContextAccessor.UmbracoContext == null) + throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); + + if (!preview) + return EnsureInternalLinks(text); + + using (_umbracoContextAccessor.UmbracoContext.ForcedPreview(preview)) // force for url provider + { + return EnsureInternalLinks(text); + } + } + + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + /// + public string EnsureInternalLinks(string text) + { + if (_umbracoContextAccessor.UmbracoContext == null) + throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); + + var urlProvider = _umbracoContextAccessor.UmbracoContext.UrlProvider; + + foreach((int? intId, GuidUdi udi, string tagValue) in FindLocalLinkIds(text)) + { + if (udi != null) + { + var newLink = "#"; + if (udi.EntityType == Constants.UdiEntityType.Document) + newLink = urlProvider.GetUrl(udi.Guid); + else if (udi.EntityType == Constants.UdiEntityType.Media) + newLink = urlProvider.GetMediaUrl(udi.Guid); + + if (newLink == null) + newLink = "#"; + + text = text.Replace(tagValue, "href=\"" + newLink); + } + else if (intId.HasValue) + { + var newLink = urlProvider.GetUrl(intId.Value); + text = text.Replace(tagValue, "href=\"" + newLink); + } + } + + return text; + } + + private IEnumerable<(int? intId, GuidUdi udi, string tagValue)> FindLocalLinkIds(string text) + { + // Parse internal links + var tags = LocalLinkPattern.Matches(text); + foreach (Match tag in tags) + { + if (tag.Groups.Count > 0) + { + var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); + + //The id could be an int or a UDI + if (Udi.TryParse(id, out var udi)) + { + var guidUdi = udi as GuidUdi; + if (guidUdi != null) + yield return (null, guidUdi, tag.Value); + } + + if (int.TryParse(id, out var intId)) + { + yield return (intId, null, tag.Value); + } + } + } + + } + } +} diff --git a/src/Umbraco.Web/Templates/HtmlUrlParser.cs b/src/Umbraco.Web/Templates/HtmlUrlParser.cs new file mode 100644 index 0000000000..5b78477579 --- /dev/null +++ b/src/Umbraco.Web/Templates/HtmlUrlParser.cs @@ -0,0 +1,61 @@ +using System.Text.RegularExpressions; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Templates +{ + public sealed class HtmlUrlParser + { + private readonly IContentSection _contentSection; + private readonly IProfilingLogger _logger; + + private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + public HtmlUrlParser(IContentSection contentSection, IProfilingLogger logger) + { + _contentSection = contentSection; + _logger = logger; + } + + /// + /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path. + /// + /// + /// + /// + /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. + /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs. + /// + public string EnsureUrls(string text) + { + if (_contentSection.ResolveUrlsFromTextString == false) return text; + + using (var timer = _logger.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) + { + // find all relative urls (ie. urls that contain ~) + var tags = ResolveUrlPattern.Matches(text); + _logger.Debug(typeof(IOHelper), "After regex: {Duration} matched: {TagsCount}", timer.Stopwatch.ElapsedMilliseconds, tags.Count); + foreach (Match tag in tags) + { + var url = ""; + if (tag.Groups[1].Success) + url = tag.Groups[1].Value; + + // The richtext editor inserts a slash in front of the url. That's why we need this little fix + // if (url.StartsWith("/")) + // text = text.Replace(url, ResolveUrl(url.Substring(1))); + // else + if (string.IsNullOrEmpty(url) == false) + { + var resolvedUrl = (url.Substring(0, 1) == "/") ? IOHelper.ResolveUrl(url.Substring(1)) : IOHelper.ResolveUrl(url); + text = text.Replace(url, resolvedUrl); + } + } + } + + return text; + } + } +} diff --git a/src/Umbraco.Web/Templates/TemplateRenderer.cs b/src/Umbraco.Web/Templates/TemplateRenderer.cs index 5123eb49ac..4602942be1 100644 --- a/src/Umbraco.Web/Templates/TemplateRenderer.cs +++ b/src/Umbraco.Web/Templates/TemplateRenderer.cs @@ -49,7 +49,7 @@ namespace Umbraco.Web.Templates // terribly much for this implementation since we are just creating a doc content request to modify it's properties manually. var contentRequest = _publishedRouter.CreateRequest(_umbracoContextAccessor.UmbracoContext); - var doc = contentRequest.UmbracoContext.ContentCache.GetById(pageId); + var doc = contentRequest.UmbracoContext.Content.GetById(pageId); if (doc == null) { diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index 98aacffe42..f08abfaf12 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -1,22 +1,25 @@ -using System; -using System.Text.RegularExpressions; +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.IO; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Web.Composing; +using Umbraco.Web.PropertyEditors; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; +using File = System.IO.File; namespace Umbraco.Web.Templates { - //NOTE: I realize there is only one class in this namespace but I'm pretty positive that there will be more classes in - //this namespace once we start migrating and cleaning up more code. - /// - /// Utility class used for templates - /// + [Obsolete("This class is obsolete, all methods have been moved to other classes: " + nameof(HtmlLocalLinkParser) + ", " + nameof(HtmlUrlParser) + " and " + nameof(HtmlImageSourceParser))] public static class TemplateUtilities { + [Obsolete("Inject and use an instance of " + nameof(HtmlLocalLinkParser) + " instead")] internal static string ParseInternalLinks(string text, bool preview, UmbracoContext umbracoContext) { using (umbracoContext.ForcedPreview(preview)) // force for url provider @@ -27,153 +30,28 @@ namespace Umbraco.Web.Templates return text; } - /// - /// Parses the string looking for the {localLink} syntax and updates them to their correct links. - /// - /// - /// - /// - public static string ParseInternalLinks(string text, UrlProvider urlProvider) => - ParseInternalLinks(text, urlProvider, Current.UmbracoContext.MediaCache); + [Obsolete("Inject and use an instance of " + nameof(HtmlLocalLinkParser) + " instead")] + public static string ParseInternalLinks(string text, UrlProvider urlProvider) + => Current.Factory.GetInstance().EnsureInternalLinks(text); - // TODO: Replace mediaCache with media url provider - internal static string ParseInternalLinks(string text, UrlProvider urlProvider, IPublishedMediaCache mediaCache) - { - if (urlProvider == null) throw new ArgumentNullException(nameof(urlProvider)); - if (mediaCache == null) throw new ArgumentNullException(nameof(mediaCache)); - - // Parse internal links - var tags = LocalLinkPattern.Matches(text); - foreach (Match tag in tags) - { - if (tag.Groups.Count > 0) - { - var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); - - //The id could be an int or a UDI - if (Udi.TryParse(id, out var udi)) - { - var guidUdi = udi as GuidUdi; - if (guidUdi != null) - { - var newLink = "#"; - if (guidUdi.EntityType == Constants.UdiEntityType.Document) - newLink = urlProvider.GetUrl(guidUdi.Guid); - else if (guidUdi.EntityType == Constants.UdiEntityType.Media) - newLink = mediaCache.GetById(guidUdi.Guid)?.Url; - - if (newLink == null) - newLink = "#"; - - text = text.Replace(tag.Value, "href=\"" + newLink); - } - } - - if (int.TryParse(id, out var intId)) - { - var newLink = urlProvider.GetUrl(intId); - text = text.Replace(tag.Value, "href=\"" + newLink); - } - } - } - - return text; - } - - - // static compiled regex for faster performance - private static readonly Regex LocalLinkPattern = new Regex(@"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", - RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)([^""]*""[^>]*data-udi="")([^""]*)(""[^>]*>)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - /// - /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path. - /// - /// - /// - /// - /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. - /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs. - /// + [Obsolete("Inject and use an instance of " + nameof(HtmlUrlParser))] public static string ResolveUrlsFromTextString(string text) - { - if (Current.Configs.Settings().Content.ResolveUrlsFromTextString == false) return text; - - using (var timer = Current.ProfilingLogger.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) - { - // find all relative urls (ie. urls that contain ~) - var tags = ResolveUrlPattern.Matches(text); - Current.Logger.Debug(typeof(IOHelper), "After regex: {Duration} matched: {TagsCount}", timer.Stopwatch.ElapsedMilliseconds, tags.Count); - foreach (Match tag in tags) - { - var url = ""; - if (tag.Groups[1].Success) - url = tag.Groups[1].Value; - - // The richtext editor inserts a slash in front of the url. That's why we need this little fix - // if (url.StartsWith("/")) - // text = text.Replace(url, ResolveUrl(url.Substring(1))); - // else - if (String.IsNullOrEmpty(url) == false) - { - var resolvedUrl = (url.Substring(0, 1) == "/") ? IOHelper.ResolveUrl(url.Substring(1)) : IOHelper.ResolveUrl(url); - text = text.Replace(url, resolvedUrl); - } - } - } - - return text; - } + => Current.Factory.GetInstance().EnsureUrls(text); + [Obsolete("Use " + nameof(StringExtensions) + "." + nameof(StringExtensions.CleanForXss) + " instead")] public static string CleanForXss(string text, params char[] ignoreFromClean) - { - return text.CleanForXss(ignoreFromClean); - } + => text.CleanForXss(ignoreFromClean); - /// - /// Parses the string looking for Umbraco image tags and updates them to their up-to-date image sources. - /// - /// - /// - /// Umbraco image tags are identified by their data-udi attributes + [Obsolete("Use " + nameof(HtmlImageSourceParser) + "." + nameof(HtmlImageSourceParser.EnsureImageSources) + " instead")] public static string ResolveMediaFromTextString(string text) - { - // don't attempt to proceed without a context - if (Current.UmbracoContext == null || Current.UmbracoContext.MediaCache == null) - { - return text; - } + => Current.Factory.GetInstance().EnsureImageSources(text); + + [Obsolete("Use " + nameof(HtmlImageSourceParser) + "." + nameof(HtmlImageSourceParser.RemoveImageSources) + " instead")] + internal static string RemoveMediaUrlsFromTextString(string text) + => Current.Factory.GetInstance().RemoveImageSources(text); - return ResolveImgPattern.Replace(text, match => - { - // match groups: - // - 1 = from the beginning of the image tag until src attribute value begins - // - 2 = the src attribute value excluding the querystring (if present) - // - 3 = anything after group 2 and before the data-udi attribute value begins - // - 4 = the data-udi attribute value - // - 5 = anything after group 4 until the image tag is closed - var src = match.Groups[2].Value; - var udi = match.Groups[4].Value; - if(src.IsNullOrWhiteSpace() || udi.IsNullOrWhiteSpace() || GuidUdi.TryParse(udi, out var guidUdi) == false) - { - return match.Value; - } - var media = Current.UmbracoContext.MediaCache.GetById(guidUdi.Guid); - if(media == null) - { - // image does not exist - we could choose to remove the image entirely here (return empty string), - // but that would leave the editors completely in the dark as to why the image doesn't show - return match.Value; - } - - var url = media.Url; - return $"{match.Groups[1].Value}{url}{match.Groups[3].Value}{udi}{match.Groups[5].Value}"; - }); - } + [Obsolete("Use " + nameof(HtmlImageSourceParser) + "." + nameof(RichTextEditorPastedImages.FindAndPersistPastedTempImages) + " instead")] + internal static string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, ILogger logger) + => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId, Current.Factory.GetInstance()); } } diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index 8c23a91d5a..fd05f7cfbd 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees if (id == Constants.System.RootString) { //get all blueprint content types - var contentTypeAliases = entities.Select(x => ((ContentEntitySlim) x).ContentTypeAlias).Distinct(); + var contentTypeAliases = entities.Select(x => ((IContentEntitySlim) x).ContentTypeAlias).Distinct(); //get the ids var contentTypeIds = Services.ContentTypeService.GetAllContentTypeIds(contentTypeAliases.ToArray()).ToArray(); @@ -59,7 +59,7 @@ namespace Umbraco.Web.Trees nodes.AddRange(docTypeEntities .Select(entity => { - var treeNode = CreateTreeNode(entity, Constants.ObjectTypes.DocumentBlueprint, id, queryStrings, "icon-item-arrangement", true); + var treeNode = CreateTreeNode(entity, Constants.ObjectTypes.DocumentBlueprint, id, queryStrings, Constants.Icons.ContentType, true); treeNode.Path = $"-1,{entity.Id}"; treeNode.NodeType = "document-type-blueprints"; // TODO: This isn't the best way to ensure a no operation process for clicking a node but it works for now. @@ -75,7 +75,7 @@ namespace Umbraco.Web.Trees var ct = Services.ContentTypeService.Get(intId.Result); if (ct == null) return nodes; - var blueprintsForDocType = entities.Where(x => ct.Alias == ((ContentEntitySlim) x).ContentTypeAlias); + var blueprintsForDocType = entities.Where(x => ct.Alias == ((IContentEntitySlim) x).ContentTypeAlias); nodes.AddRange(blueprintsForDocType .Select(entity => { diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 4249d1639e..85ff30487d 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -15,6 +15,10 @@ using Umbraco.Web.WebApi.Filters; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Search; using Constants = Umbraco.Core.Constants; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; namespace Umbraco.Web.Trees { @@ -30,26 +34,27 @@ namespace Umbraco.Web.Trees [Tree(Constants.Applications.Content, Constants.Trees.Content)] [PluginController("UmbracoTrees")] [CoreTree] - [SearchableTree("searchResultFormatter", "configureContentResult")] + [SearchableTree("searchResultFormatter", "configureContentResult", 10)] public class ContentTreeController : ContentTreeControllerBase, ISearchableTree { private readonly UmbracoTreeSearcher _treeSearcher; private readonly ActionCollection _actions; - public ContentTreeController(UmbracoTreeSearcher treeSearcher, ActionCollection actions) - { - _treeSearcher = treeSearcher; - _actions = actions; - } - protected override int RecycleBinId => Constants.System.RecycleBinContent; protected override bool RecycleBinSmells => Services.ContentService.RecycleBinSmells(); private int[] _userStartNodes; + protected override int[] UserStartNodes => _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService)); + public ContentTreeController(UmbracoTreeSearcher treeSearcher, ActionCollection actions, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + _treeSearcher = treeSearcher; + _actions = actions; + } + /// protected override TreeNode GetSingleTreeNode(IEntitySlim entity, string parentId, FormDataCollection queryStrings) { @@ -124,8 +129,8 @@ namespace Umbraco.Web.Trees menu.DefaultMenuAlias = ActionNew.ActionAlias; // we need to get the default permissions as you can't set permissions on the very root node - var permission = Services.UserService.GetPermissions(Security.CurrentUser, Constants.System.Root).First(); - var nodeActions = _actions.FromEntityPermission(permission) + var assignedPermissions = Services.UserService.GetAssignedPermissions(Security.CurrentUser, Constants.System.Root); + var nodeActions = _actions.GetByLetters(assignedPermissions) .Select(x => new MenuItem(x)); //these two are the standard items @@ -201,9 +206,6 @@ namespace Umbraco.Web.Trees return HasPathAccess(entity, queryStrings); } - internal override IEnumerable GetChildrenFromEntityService(int entityId) - => Services.EntityService.GetChildren(entityId, UmbracoObjectType).ToList(); - protected override IEnumerable GetChildEntities(string id, FormDataCollection queryStrings) { var result = base.GetChildEntities(id, queryStrings); @@ -220,7 +222,6 @@ namespace Umbraco.Web.Trees return result; } - /// /// Returns a collection of all menu items that can be on a content node /// @@ -248,8 +249,11 @@ namespace Umbraco.Web.Trees OpensDialog = true }); } - - menu.Items.Add(new RefreshNode(Services.TextService, true)); + + if((item is DocumentEntitySlim documentEntity && documentEntity.IsContainer) == false) + { + menu.Items.Add(new RefreshNode(Services.TextService, true)); + } return menu; } diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index d01e9fffb4..95de72b7bf 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -17,12 +17,24 @@ using Umbraco.Core.Models.Entities; using System.Web.Http.ModelBinding; using Umbraco.Web.Actions; using Umbraco.Web.Composing; - +using Umbraco.Core.Security; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Persistence; namespace Umbraco.Web.Trees { public abstract class ContentTreeControllerBase : TreeController { + + protected ContentTreeControllerBase(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + } + + protected ContentTreeControllerBase() + { + } + #region Actions /// @@ -87,20 +99,45 @@ namespace Umbraco.Web.Trees /// /// /// - internal TreeNode GetSingleTreeNodeWithAccessCheck(IEntitySlim e, string parentId, FormDataCollection queryStrings) + internal TreeNode GetSingleTreeNodeWithAccessCheck(IEntitySlim e, string parentId, FormDataCollection queryStrings, + int[] startNodeIds, string[] startNodePaths) { - var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out var hasPathAccess); - if (IgnoreUserStartNodes(queryStrings) == false && entityIsAncestorOfStartNodes == false) + var entityIsAncestorOfStartNodes = ContentPermissionsHelper.IsInBranchOfStartNode(e.Path, startNodeIds, startNodePaths, out var hasPathAccess); + var ignoreUserStartNodes = IgnoreUserStartNodes(queryStrings); + if (ignoreUserStartNodes == false && entityIsAncestorOfStartNodes == false) return null; var treeNode = GetSingleTreeNode(e, parentId, queryStrings); - if (IgnoreUserStartNodes(queryStrings) == false && hasPathAccess == false) + if (treeNode == null) + { + //this means that the user has NO access to this node via permissions! They at least need to have browse permissions to see + //the node so we need to return null; + return null; + } + if (!ignoreUserStartNodes && !hasPathAccess) { treeNode.AdditionalData["noAccess"] = true; } return treeNode; } + private void GetUserStartNodes(out int[] startNodeIds, out string[] startNodePaths) + { + switch (RecycleBinId) + { + case Constants.System.RecycleBinMedia: + startNodeIds = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + startNodePaths = Security.CurrentUser.GetMediaStartNodePaths(Services.EntityService); + break; + case Constants.System.RecycleBinContent: + startNodeIds = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + startNodePaths = Security.CurrentUser.GetContentStartNodePaths(Services.EntityService); + break; + default: + throw new NotSupportedException("Path access is only determined on content or media"); + } + } + /// /// Returns the /// @@ -127,6 +164,8 @@ namespace Umbraco.Web.Trees ? queryStrings.GetValue(TreeQueryStringParameters.StartNodeId) : string.Empty; + var ignoreUserStartNodes = IgnoreUserStartNodes(queryStrings); + if (string.IsNullOrEmpty(startNodeId) == false && startNodeId != "undefined" && startNodeId != rootIdString) { // request has been made to render from a specific, non-root, start node @@ -134,7 +173,7 @@ namespace Umbraco.Web.Trees // ensure that the user has access to that node, otherwise return the empty tree nodes collection // TODO: in the future we could return a validation statement so we can have some UI to notify the user they don't have access - if (IgnoreUserStartNodes(queryStrings) == false && HasPathAccess(id, queryStrings) == false) + if (ignoreUserStartNodes == false && HasPathAccess(id, queryStrings) == false) { Logger.Warn("User {Username} does not have access to node with id {Id}", Security.CurrentUser.Username, id); return nodes; @@ -152,20 +191,34 @@ namespace Umbraco.Web.Trees // get child entities - if id is root, but user's start nodes do not contain the // root node, this returns the start nodes instead of root's children var entities = GetChildEntities(id, queryStrings).ToList(); - nodes.AddRange(entities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != null)); + + //get the current user start node/paths + GetUserStartNodes(out var userStartNodes, out var userStartNodePaths); // if the user does not have access to the root node, what we have is the start nodes, - // but to provide some context we also need to add their topmost nodes when they are not + // but to provide some context we need to add their topmost nodes when they are not // topmost nodes themselves (level > 1). if (id == rootIdString && hasAccessToRoot == false) { - var topNodeIds = entities.Where(x => x.Level > 1).Select(GetTopNodeId).Where(x => x != 0).Distinct().ToArray(); + // first add the entities that are topmost to the nodes collection + var topMostEntities = entities.Where(x => x.Level == 1).ToArray(); + nodes.AddRange(topMostEntities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings, userStartNodes, userStartNodePaths)).Where(x => x != null)); + + // now add the topmost nodes of the entities that aren't topmost to the nodes collection as well + // - these will appear as "no-access" nodes in the tree, but will allow the editors to drill down through the tree + // until they reach their start nodes + var topNodeIds = entities.Except(topMostEntities).Select(GetTopNodeId).Where(x => x != 0).Distinct().ToArray(); if (topNodeIds.Length > 0) { var topNodes = Services.EntityService.GetAll(UmbracoObjectType, topNodeIds.ToArray()); - nodes.AddRange(topNodes.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != null)); + nodes.AddRange(topNodes.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings, userStartNodes, userStartNodePaths)).Where(x => x != null)); } } + else + { + // the user has access to the root, just add the entities + nodes.AddRange(entities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings, userStartNodes, userStartNodePaths)).Where(x => x != null)); + } return nodes; } @@ -187,7 +240,7 @@ namespace Umbraco.Web.Trees { // try to parse id as an integer else use GetEntityFromId // which will grok Guids, Udis, etc and let use obtain the id - if (int.TryParse(id, out var entityId) == false) + if (!int.TryParse(id, out var entityId)) { var entity = GetEntityFromId(id); if (entity == null) @@ -196,11 +249,13 @@ namespace Umbraco.Web.Trees entityId = entity.Id; } + var ignoreUserStartNodes = IgnoreUserStartNodes(queryStrings); + IEntitySlim[] result; // if a request is made for the root node but user has no access to // root node, return start nodes instead - if (entityId == Constants.System.Root && UserStartNodes.Contains(Constants.System.Root) == false) + if (!ignoreUserStartNodes && entityId == Constants.System.Root && UserStartNodes.Contains(Constants.System.Root) == false) { result = UserStartNodes.Length > 0 ? Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes).ToArray() @@ -214,12 +269,8 @@ namespace Umbraco.Web.Trees return result; } - /// - /// Abstract method to fetch the entities from the entity service - /// - /// - /// - internal abstract IEnumerable GetChildrenFromEntityService(int entityId); + private IEnumerable GetChildrenFromEntityService(int entityId) + => Services.EntityService.GetChildren(entityId, UmbracoObjectType).ToList(); /// /// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access @@ -255,9 +306,8 @@ namespace Umbraco.Web.Trees /// protected sealed override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { - var ignoreUserStartNodes = queryStrings.GetValue(TreeQueryStringParameters.IgnoreUserStartNodes); //check if we're rendering the root - if (id == Constants.System.RootString && UserStartNodes.Contains(Constants.System.Root) || ignoreUserStartNodes) + if (id == Constants.System.RootString && UserStartNodes.Contains(Constants.System.Root)) { var altStartId = string.Empty; @@ -272,8 +322,10 @@ namespace Umbraco.Web.Trees var nodes = GetTreeNodesInternal(id, queryStrings); - //only render the recycle bin if we are not in dialog and the start id id still the root - if (IsDialog(queryStrings) == false && id == Constants.System.RootString) + //only render the recycle bin if we are not in dialog and the start id is still the root + //we need to check for the "application" key in the queryString because its value is required here, + //and for some reason when there are no dashboards, this parameter is missing + if (IsDialog(queryStrings) == false && id == Constants.System.RootString && queryStrings.HasKey("application")) { nodes.Add(CreateTreeNode( RecycleBinId.ToInvariantString(), @@ -310,11 +362,16 @@ namespace Umbraco.Web.Trees //Here we need to figure out if the node is a container and if so check if the user has a custom start node, then check if that start node is a child // of this container node. If that is true, the HasChildren must be true so that the tree node still renders even though this current node is a container/list view. if (isContainer && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) - { + { var startNodes = Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes); //if any of these start nodes' parent is current, then we need to render children normally so we need to switch some logic and tell // the UI that this node does have children and that it isn't a container - if (startNodes.Any(x => x.ParentId == e.Id)) + + if (startNodes.Any(x => + { + var pathParts = x.Path.Split(','); + return pathParts.Contains(e.Id.ToInvariantString()); + })) { renderChildren = true; } @@ -368,7 +425,7 @@ namespace Umbraco.Web.Trees } var menu = new MenuItemCollection(); - // only add empty recycle bin if the current user is allowed to delete by default + // only add empty recycle bin if the current user is allowed to delete by default if (deleteAllowed) { menu.Items.Add(new MenuItem("emptyRecycleBin", Services.TextService) @@ -414,19 +471,8 @@ namespace Umbraco.Web.Trees internal IEnumerable GetAllowedUserMenuItemsForNode(IUmbracoEntity dd) { - var permission = Services.UserService.GetPermissions(Security.CurrentUser, dd.Path); - // TODO: inject - var actions = Current.Actions.FromEntityPermission(permission) - .ToList(); - - var actionDelete = Current.Actions.GetAction(); - - // A user is allowed to delete their own stuff - var tryGetCurrentUserId = Security.GetUserId(); - if (tryGetCurrentUserId && dd.CreatorId == tryGetCurrentUserId.Result && actions.Contains(actionDelete) == false) - actions.Add(actionDelete); - - return actions.Select(x => new MenuItem(x)); + var permissionsForPath = Services.UserService.GetPermissionsForPath(Security.CurrentUser, dd.Path).GetAllPermissions(); + return Current.Actions.GetByLetters(permissionsForPath).Select(x => new MenuItem(x)); } /// @@ -509,5 +555,24 @@ namespace Umbraco.Web.Trees } private readonly ConcurrentDictionary _entityCache = new ConcurrentDictionary(); + + private bool? _ignoreUserStartNodes; + + + + /// + /// If the request should allows a user to choose nodes that they normally don't have access to + /// + /// + /// + internal bool IgnoreUserStartNodes(FormDataCollection queryStrings) + { + if (_ignoreUserStartNodes.HasValue) return _ignoreUserStartNodes.Value; + + var dataTypeKey = queryStrings.GetValue(TreeQueryStringParameters.DataTypeKey); + _ignoreUserStartNodes = dataTypeKey.HasValue && Services.DataTypeService.IsDataTypeIgnoringUserStartNodes(dataTypeKey.Value); + + return _ignoreUserStartNodes.Value; + } } } diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index 4a8cfba9a5..ece16229b2 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -3,11 +3,17 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http.Formatting; using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Trees; +using Umbraco.Web.Search; using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Trees @@ -18,6 +24,13 @@ namespace Umbraco.Web.Trees [CoreTree] public class ContentTypeTreeController : TreeController, ISearchableTree { + private readonly UmbracoTreeSearcher _treeSearcher; + + public ContentTypeTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + _treeSearcher = treeSearcher; + } + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) { var root = base.CreateRootNode(queryStrings); @@ -49,17 +62,28 @@ namespace Umbraco.Web.Trees //if the request is for folders only then just return if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + var children = Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DocumentType).ToArray(); + var contentTypes = Services.ContentTypeService.GetAll(children.Select(c => c.Id).ToArray()).ToDictionary(c => c.Id); nodes.AddRange( - Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DocumentType) + children .OrderBy(entity => entity.Name) .Select(dt => { + // get the content type here so we can get the icon from it to use when we create the tree node + // and we can enrich the result with content type data that's not available in the entity service output + var contentType = contentTypes[dt.Id]; + // since 7.4+ child type creation is enabled by a config option. It defaults to on, but can be disabled if we decide to. // need this check to keep supporting sites where children have already been created. var hasChildren = dt.HasChildren; - var node = CreateTreeNode(dt, Constants.ObjectTypes.DocumentType, id, queryStrings, "icon-item-arrangement", hasChildren); + var node = CreateTreeNode(dt, Constants.ObjectTypes.DocumentType, id, queryStrings, contentType?.Icon ?? Constants.Icons.ContentType, hasChildren); node.Path = dt.Path; + + // now we can enrich the result with content type data that's not available in the entity service output + node.Alias = contentType.Alias; + node.AdditionalData["isElement"] = contentType.IsElement; + return node; })); @@ -135,10 +159,7 @@ namespace Umbraco.Web.Trees } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) - { - var results = Services.EntityService.GetPagedDescendants(UmbracoObjectTypes.DocumentType, pageIndex, pageSize, out totalFound, - filter: SqlContext.Query().Where(x => x.Name.Contains(query))); - return Mapper.MapEnumerable(results); - } + => _treeSearcher.EntitySearch(UmbracoObjectTypes.DocumentType, query, pageSize, pageIndex, out totalFound, searchFrom); + } } diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index 75260b586d..6a7fb7f5ad 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -12,6 +12,11 @@ using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Web.Search; namespace Umbraco.Web.Trees { @@ -21,6 +26,13 @@ namespace Umbraco.Web.Trees [CoreTree] public class DataTypeTreeController : TreeController, ISearchableTree { + private readonly UmbracoTreeSearcher _treeSearcher; + + public DataTypeTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + _treeSearcher = treeSearcher; + } + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { var intId = id.TryConvertTo(); @@ -48,19 +60,20 @@ namespace Umbraco.Web.Trees //System ListView nodes var systemListViewDataTypeIds = GetNonDeletableSystemListViewDataTypeIds(); + var children = Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DataType).ToArray(); + var dataTypes = Services.DataTypeService.GetAll(children.Select(c => c.Id).ToArray()).ToDictionary(dt => dt.Id); + nodes.AddRange( - Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DataType) + children .OrderBy(entity => entity.Name) .Select(dt => { - var node = CreateTreeNode(dt.Id.ToInvariantString(), id, queryStrings, dt.Name, "icon-autofill", false); + var dataType = dataTypes[dt.Id]; + var node = CreateTreeNode(dt.Id.ToInvariantString(), id, queryStrings, dt.Name, dataType.Editor.Icon, false); node.Path = dt.Path; - if (systemListViewDataTypeIds.Contains(dt.Id)) - { - node.Icon = "icon-thumbnail-list"; - } return node; - })); + }) + ); return nodes; } @@ -72,7 +85,15 @@ namespace Umbraco.Web.Trees { var systemIds = new[] { - Constants.System.DefaultLabelDataTypeId + Constants.DataTypes.Boolean, // Used by the Member Type: "Member" + Constants.DataTypes.Textarea, // Used by the Member Type: "Member" + Constants.DataTypes.LabelBigint, // Used by the Media Type: "Image"; Used by the Media Type: "File" + Constants.DataTypes.LabelDateTime, // Used by the Member Type: "Member" + Constants.DataTypes.LabelDecimal, // Used by the Member Type: "Member" + Constants.DataTypes.LabelInt, // Used by the Media Type: "Image"; Used by the Member Type: "Member" + Constants.DataTypes.LabelString, // Used by the Media Type: "Image"; Used by the Media Type: "File" + Constants.DataTypes.ImageCropper, // Used by the Media Type: "Image" + Constants.DataTypes.Upload, // Used by the Media Type: "File" }; return systemIds.Concat(GetNonDeletableSystemListViewDataTypeIds()); @@ -140,10 +161,6 @@ namespace Umbraco.Web.Trees } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) - { - var results = Services.EntityService.GetPagedDescendants(UmbracoObjectTypes.DataType, pageIndex, pageSize, out totalFound, - filter: SqlContext.Query().Where(x => x.Name.Contains(query))); - return Mapper.MapEnumerable(results); - } + => _treeSearcher.EntitySearch(UmbracoObjectTypes.DataType, query, pageSize, pageIndex, out totalFound, searchFrom); } } diff --git a/src/Umbraco.Web/Trees/FilesTreeController.cs b/src/Umbraco.Web/Trees/FilesTreeController.cs index 60adb5048e..ae951bebf4 100644 --- a/src/Umbraco.Web/Trees/FilesTreeController.cs +++ b/src/Umbraco.Web/Trees/FilesTreeController.cs @@ -14,6 +14,6 @@ namespace Umbraco.Web.Trees protected override string[] Extensions => ExtensionsStatic; - protected override string FileIcon => "icon-document"; + protected override string FileIcon => Constants.Icons.MediaFile; } } diff --git a/src/Umbraco.Web/Trees/MacrosTreeController.cs b/src/Umbraco.Web/Trees/MacrosTreeController.cs index 02636cbea8..cbe1946779 100644 --- a/src/Umbraco.Web/Trees/MacrosTreeController.cs +++ b/src/Umbraco.Web/Trees/MacrosTreeController.cs @@ -1,8 +1,6 @@ -using System; -using System.Linq; +using System.Linq; using System.Net.Http.Formatting; using Umbraco.Core; -using Umbraco.Core.Models.Entities; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; @@ -31,14 +29,14 @@ namespace Umbraco.Web.Trees if (id == Constants.System.RootString) { - foreach (var macro in Services.MacroService.GetAll()) + foreach (var macro in Services.MacroService.GetAll().OrderBy(m => m.Name)) { nodes.Add(CreateTreeNode( macro.Id.ToString(), id, queryStrings, macro.Name, - "icon-settings-alt", + Constants.Icons.Macro, false)); } } diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index fc3d23f092..df44a809c9 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -16,6 +16,10 @@ using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Search; using Constants = Umbraco.Core.Constants; using Umbraco.Core.Services.Implement; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; namespace Umbraco.Web.Trees { @@ -30,12 +34,12 @@ namespace Umbraco.Web.Trees [Tree(Constants.Applications.Media, Constants.Trees.Media)] [PluginController("UmbracoTrees")] [CoreTree] - [SearchableTree("searchResultFormatter", "configureMediaResult")] + [SearchableTree("searchResultFormatter", "configureMediaResult", 20)] public class MediaTreeController : ContentTreeControllerBase, ISearchableTree { private readonly UmbracoTreeSearcher _treeSearcher; - public MediaTreeController(UmbracoTreeSearcher treeSearcher) + public MediaTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { _treeSearcher = treeSearcher; } @@ -166,12 +170,6 @@ namespace Umbraco.Web.Trees { return _treeSearcher.ExamineSearch(query, UmbracoEntityTypes.Media, pageSize, pageIndex, out totalFound, searchFrom); } - - internal override IEnumerable GetChildrenFromEntityService(int entityId) - // Not pretty having to cast the service, but it is the only way to get to use an internal method that we - // do not want to make public on the interface. Unfortunately also prevents this from being unit tested. - // See this issue for details on why we need this: - // https://github.com/umbraco/Umbraco-CMS/issues/3457 - => ((EntityService)Services.EntityService).GetMediaChildrenWithoutPropertyData(entityId).ToList(); + } } diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index 5798c546fc..7a9a80c8fc 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -10,6 +10,11 @@ using Umbraco.Web.WebApi.Filters; using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Web.Search; namespace Umbraco.Web.Trees { @@ -19,6 +24,15 @@ namespace Umbraco.Web.Trees [CoreTree] public class MediaTypeTreeController : TreeController, ISearchableTree { + private readonly UmbracoTreeSearcher _treeSearcher; + private readonly IMediaTypeService _mediaTypeService; + + public MediaTypeTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper, IMediaTypeService mediaTypeService) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + _treeSearcher = treeSearcher; + _mediaTypeService = mediaTypeService; + } + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { var intId = id.TryConvertTo(); @@ -42,6 +56,8 @@ namespace Umbraco.Web.Trees // if the request is for folders only then just return if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + var mediaTypes = _mediaTypeService.GetAll(); + nodes.AddRange( Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.MediaType) .OrderBy(entity => entity.Name) @@ -50,7 +66,8 @@ namespace Umbraco.Web.Trees // since 7.4+ child type creation is enabled by a config option. It defaults to on, but can be disabled if we decide to. // need this check to keep supporting sites where children have already been created. var hasChildren = dt.HasChildren; - var node = CreateTreeNode(dt, Constants.ObjectTypes.MediaType, id, queryStrings, "icon-thumbnails", hasChildren); + var mt = mediaTypes.FirstOrDefault(x => x.Id == dt.Id); + var node = CreateTreeNode(dt, Constants.ObjectTypes.MediaType, id, queryStrings, mt?.Icon ?? Constants.Icons.MediaType, hasChildren); node.Path = dt.Path; return node; @@ -108,7 +125,10 @@ namespace Umbraco.Web.Trees } menu.Items.Add(Services.TextService, opensDialog: true); - menu.Items.Add(Services.TextService, opensDialog: true); + if(ct.IsSystemMediaType() == false) + { + menu.Items.Add(Services.TextService, opensDialog: true); + } menu.Items.Add(new RefreshNode(Services.TextService, true)); } @@ -117,10 +137,7 @@ namespace Umbraco.Web.Trees } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) - { - var results = Services.EntityService.GetPagedDescendants(UmbracoObjectTypes.MediaType, pageIndex, pageSize, out totalFound, - filter: SqlContext.Query().Where(x => x.Name.Contains(query))); - return Mapper.MapEnumerable(results); - } + => _treeSearcher.EntitySearch(UmbracoObjectTypes.MediaType, query, pageSize, pageIndex, out totalFound, searchFrom); + } } diff --git a/src/Umbraco.Web/Trees/MemberGroupTreeController.cs b/src/Umbraco.Web/Trees/MemberGroupTreeController.cs index ea2412e4bd..54c499d717 100644 --- a/src/Umbraco.Web/Trees/MemberGroupTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberGroupTreeController.cs @@ -17,7 +17,7 @@ namespace Umbraco.Web.Trees { return Services.MemberGroupService.GetAll() .OrderBy(x => x.Name) - .Select(dt => CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-item-arrangement", false)); + .Select(dt => CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, Constants.Icons.MemberGroup, false)); } protected override TreeNode CreateRootNode(FormDataCollection queryStrings) diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index bb0091af54..c0a9d15cfa 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -80,7 +80,7 @@ namespace Umbraco.Web.Trees "-1", queryStrings, member.Name, - "icon-user", + Constants.Icons.Member, false, "", Udi.Create(ObjectTypes.GetUdiType(Constants.ObjectTypes.Member), member.Key)); @@ -110,7 +110,7 @@ namespace Umbraco.Web.Trees "-1", queryStrings, member.UserName, - "icon-user", + Constants.Icons.Member, false); return node; @@ -124,14 +124,14 @@ namespace Umbraco.Web.Trees if (id == Constants.System.RootString) { nodes.Add( - CreateTreeNode(Constants.Conventions.MemberTypes.AllMembersListId, id, queryStrings, Services.TextService.Localize("member/allMembers"), "icon-users", true, + CreateTreeNode(Constants.Conventions.MemberTypes.AllMembersListId, id, queryStrings, Services.TextService.Localize("member/allMembers"), Constants.Icons.MemberType, true, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + Constants.Conventions.MemberTypes.AllMembersListId)); if (_isUmbracoProvider) { nodes.AddRange(Services.MemberTypeService.GetAll() .Select(memberType => - CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, "icon-users", true, + CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, memberType.Icon.IfNullOrWhiteSpace(Constants.Icons.Member), true, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + memberType.Alias))); } } diff --git a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs index 3a72460963..5db9088f20 100644 --- a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs @@ -2,7 +2,10 @@ using System.Linq; using System.Net.Http.Formatting; using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Trees; +using Umbraco.Web.Search; using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Trees @@ -10,8 +13,15 @@ namespace Umbraco.Web.Trees [CoreTree] [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] [Tree(Constants.Applications.Settings, Constants.Trees.MemberTypes, SortOrder = 2, TreeGroup = Constants.Trees.Groups.Settings)] - public class MemberTypeTreeController : MemberTypeAndGroupTreeControllerBase + public class MemberTypeTreeController : MemberTypeAndGroupTreeControllerBase, ISearchableTree { + private readonly UmbracoTreeSearcher _treeSearcher; + + public MemberTypeTreeController(UmbracoTreeSearcher treeSearcher) + { + _treeSearcher = treeSearcher; + } + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) { var root = base.CreateRootNode(queryStrings); @@ -23,7 +33,11 @@ namespace Umbraco.Web.Trees { return Services.MemberTypeService.GetAll() .OrderBy(x => x.Name) - .Select(dt => CreateTreeNode(dt, Constants.ObjectTypes.MemberType, id, queryStrings, "icon-item-arrangement", false)); + .Select(dt => CreateTreeNode(dt, Constants.ObjectTypes.MemberType, id, queryStrings, dt?.Icon ?? Constants.Icons.MemberType, false)); } + + public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) + => _treeSearcher.EntitySearch(UmbracoObjectTypes.MemberType, query, pageSize, pageIndex, out totalFound, searchFrom); + } } diff --git a/src/Umbraco.Web/Trees/RelationTypeTreeController.cs b/src/Umbraco.Web/Trees/RelationTypeTreeController.cs index ab6dd39820..aa3206b5e4 100644 --- a/src/Umbraco.Web/Trees/RelationTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/RelationTypeTreeController.cs @@ -16,6 +16,8 @@ namespace Umbraco.Web.Trees { protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { + //TODO: Do not allow deleting built in types + var menu = new MenuItemCollection(); if (id == Constants.System.RootString) diff --git a/src/Umbraco.Web/Trees/TemplatesTreeController.cs b/src/Umbraco.Web/Trees/TemplatesTreeController.cs index 8c37718c9b..d0ca8d1b2e 100644 --- a/src/Umbraco.Web/Trees/TemplatesTreeController.cs +++ b/src/Umbraco.Web/Trees/TemplatesTreeController.cs @@ -3,12 +3,18 @@ using System.Globalization; using System.Linq; using System.Net.Http.Formatting; using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; +using Umbraco.Web.Search; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; @@ -20,6 +26,13 @@ namespace Umbraco.Web.Trees [CoreTree] public class TemplatesTreeController : TreeController, ISearchableTree { + private readonly UmbracoTreeSearcher _treeSearcher; + + public TemplatesTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + _treeSearcher = treeSearcher; + } + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) { var root = base.CreateRootNode(queryStrings); @@ -119,10 +132,6 @@ namespace Umbraco.Web.Trees } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) - { - var results = Services.EntityService.GetPagedDescendants(UmbracoObjectTypes.Template, pageIndex, pageSize, out totalFound, - filter: SqlContext.Query().Where(x => x.Name.Contains(query))); - return Mapper.MapEnumerable(results); - } + => _treeSearcher.EntitySearch(UmbracoObjectTypes.Template, query, pageSize, pageIndex, out totalFound, searchFrom); } } diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 2e409c2820..77e5ebef62 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Models.Trees; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Services; namespace Umbraco.Web.Trees { @@ -264,6 +265,7 @@ namespace Umbraco.Web.Trees treeNode.Path = entity.Path; treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key); treeNode.HasChildren = hasChildren; + treeNode.Trashed = entity.Trashed; return treeNode; } @@ -369,16 +371,6 @@ namespace Umbraco.Web.Trees return queryStrings.GetValue(TreeQueryStringParameters.Use) == "dialog"; } - /// - /// If the request should allows a user to choose nodes that they normally don't have access to - /// - /// - /// - protected bool IgnoreUserStartNodes(FormDataCollection queryStrings) - { - return queryStrings.GetValue(TreeQueryStringParameters.IgnoreUserStartNodes); - } - /// /// An event that allows developers to modify the tree node collection that is being rendered /// diff --git a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs index 0fcf5321e4..02a198401b 100644 --- a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs +++ b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs @@ -8,7 +8,7 @@ public const string Use = "use"; public const string Application = "application"; public const string StartNodeId = "startNodeId"; - public const string IgnoreUserStartNodes = "ignoreUserStartNodes"; + public const string DataTypeKey = "dataTypeKey"; //public const string OnNodeClick = "OnNodeClick"; //public const string RenderParent = "RenderParent"; } diff --git a/src/Umbraco.Web/Trees/UserTreeController.cs b/src/Umbraco.Web/Trees/UserTreeController.cs index 7da0b689af..55d98def86 100644 --- a/src/Umbraco.Web/Trees/UserTreeController.cs +++ b/src/Umbraco.Web/Trees/UserTreeController.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.Trees //this will load in a custom UI instead of the dashboard for the root node root.RoutePath = $"{Constants.Applications.Users}/{Constants.Trees.Users}/users"; - root.Icon = "icon-users"; + root.Icon = Constants.Icons.UserGroup; root.HasChildren = false; return root; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e565f354c8..e39687bed8 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -13,10 +13,11 @@ true Off + $(AdditionalFileItemNames);Content true - full + portable false bin\Debug\ DEBUG;TRACE @@ -26,7 +27,7 @@ latest - pdbonly + portable true bin\Release\ TRACE @@ -60,9 +61,9 @@ - + - + 2.7.0.100 @@ -80,9 +81,19 @@ + + 1.0.0-beta2-19324-01 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 3.3.0 + runtime; build; native; contentfiles; analyzers + all + @@ -132,16 +143,21 @@ + + + + + @@ -149,6 +165,7 @@ + @@ -203,29 +220,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -328,6 +369,7 @@ + @@ -462,8 +504,6 @@ - - @@ -779,7 +819,6 @@ - @@ -787,7 +826,6 @@ - @@ -825,7 +863,6 @@ - @@ -892,11 +929,6 @@ - - True - True - Reference.map - @@ -1021,7 +1053,6 @@ - @@ -1142,7 +1173,7 @@ - + @@ -1160,12 +1191,6 @@ - - True - True - Reference.map - - @@ -1189,54 +1214,22 @@ - - - + - + - - - - MSDiscoCodeGenerator - Reference.cs - Designer - Mvc\web.config - - MSDiscoCodeGenerator - Reference.cs - - - - - Reference.map - - - Reference.map - SettingsSingleFileGenerator Settings.Designer.cs - - Dynamic - Web References\org.umbraco.our\ - http://our.umbraco.org/umbraco/webservices/api/repository.asmx - - - - - Settings - umbraco_org_umbraco_our_Repository - Dynamic Web References\org.umbraco.update\ diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index 191fb9dcd6..f8ee238da7 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -1,6 +1,9 @@ -using System.Threading; +using System.Configuration; +using System.Threading; using System.Web; using Umbraco.Core; +using Umbraco.Core.Logging.Serilog; +using Umbraco.Core.Runtime; using Umbraco.Web.Runtime; namespace Umbraco.Web @@ -12,7 +15,18 @@ namespace Umbraco.Web { protected override IRuntime GetRuntime() { - return new WebRuntime(this); + var logger = SerilogLogger.CreateWithDefaultConfiguration(); + + // Determine if we should use the sql main dom or the default + var appSettingMainDomLock = ConfigurationManager.AppSettings[Constants.AppSettings.MainDomLock]; + + var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" + ? (IMainDomLock)new SqlMainDomLock(logger) + : new MainDomSemaphoreLock(logger); + + var runtime = new WebRuntime(this, logger, new MainDom(logger, mainDomLock)); + + return runtime; } /// diff --git a/src/Umbraco.Web/UmbracoComponentRenderer.cs b/src/Umbraco.Web/UmbracoComponentRenderer.cs index 25c2d78731..0373c73724 100644 --- a/src/Umbraco.Web/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Web/UmbracoComponentRenderer.cs @@ -27,12 +27,14 @@ namespace Umbraco.Web private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IMacroRenderer _macroRenderer; private readonly ITemplateRenderer _templateRenderer; + private readonly HtmlLocalLinkParser _linkParser; - public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer) + public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer, HtmlLocalLinkParser linkParser) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; _templateRenderer = templateRenderer ?? throw new ArgumentNullException(nameof(templateRenderer)); + _linkParser = linkParser; } /// @@ -77,7 +79,7 @@ namespace Umbraco.Web /// public IHtmlString RenderMacro(int contentId, string alias, object parameters) { - return RenderMacro(contentId, alias, parameters.ToDictionary()); + return RenderMacro(contentId, alias, parameters?.ToDictionary()); } /// @@ -92,12 +94,12 @@ namespace Umbraco.Web if (contentId == default) throw new ArgumentException("Invalid content id " + contentId); - var content = _umbracoContextAccessor.UmbracoContext.ContentCache?.GetById(true, contentId); + var content = _umbracoContextAccessor.UmbracoContext.Content?.GetById(contentId); if (content == null) throw new InvalidOperationException("Cannot render a macro, no content found by id " + contentId); - return RenderMacro(alias, parameters, content); + return RenderMacro(content, alias, parameters); } /// @@ -107,61 +109,17 @@ namespace Umbraco.Web /// The parameters. /// The content used for macro rendering /// - private IHtmlString RenderMacro(string alias, IDictionary parameters, IPublishedContent content) + private IHtmlString RenderMacro(IPublishedContent content, string alias, IDictionary parameters) { if (content == null) throw new ArgumentNullException(nameof(content)); // TODO: We are doing at ToLower here because for some insane reason the UpdateMacroModel method looks for a lower case match. the whole macro concept needs to be rewritten. //NOTE: the value could have HTML encoded values, so we need to deal with that - var macroProps = parameters.ToDictionary( + var macroProps = parameters?.ToDictionary( x => x.Key.ToLowerInvariant(), i => (i.Value is string) ? HttpUtility.HtmlDecode(i.Value.ToString()) : i.Value); - var macroControl = _macroRenderer.Render(alias, content, macroProps).GetAsControl(); - - string html; - if (macroControl is LiteralControl control) - { - // no need to execute, we already have text - html = control.Text; - } - else - { - using (var containerPage = new FormlessPage()) - { - containerPage.Controls.Add(macroControl); - - using (var output = new StringWriter()) - { - // .Execute() does a PushTraceContext/PopTraceContext and writes trace output straight into 'output' - // and I do not see how we could wire the trace context to the current context... so it creates dirty - // trace output right in the middle of the page. - // - // The only thing we can do is fully disable trace output while .Execute() runs and restore afterwards - // which means trace output is lost if the macro is a control (.ascx or user control) that is invoked - // from within Razor -- which makes sense anyway because the control can _not_ run correctly from - // within Razor since it will never be inserted into the page pipeline (which may even not exist at all - // if we're running MVC). - // - // I'm sure there's more things that will get lost with this context changing but I guess we'll figure - // those out as we go along. One thing we lose is the content type response output. - // http://issues.umbraco.org/issue/U4-1599 if it is setup during the macro execution. So - // here we'll save the content type response and reset it after execute is called. - - var contentType = _umbracoContextAccessor.UmbracoContext.HttpContext.Response.ContentType; - var traceIsEnabled = containerPage.Trace.IsEnabled; - containerPage.Trace.IsEnabled = false; - _umbracoContextAccessor.UmbracoContext.HttpContext.Server.Execute(containerPage, output, true); - containerPage.Trace.IsEnabled = traceIsEnabled; - //reset the content type - _umbracoContextAccessor.UmbracoContext.HttpContext.Response.ContentType = contentType; - - //Now, we need to ensure that local links are parsed - html = TemplateUtilities.ParseInternalLinks(output.ToString(), _umbracoContextAccessor.UmbracoContext.UrlProvider); - } - } - - } + string html = _macroRenderer.Render(alias, content, macroProps).GetAsText(); return new HtmlString(html); } diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index e68e6e2c77..347f79e51b 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -1,18 +1,21 @@ using System; using System.Collections.Generic; +using System.IO; using System.Web; +using System.Web.Routing; using Umbraco.Core; using Umbraco.Core.Configuration; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Events; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; -using Umbraco.Web.Runtime; using Umbraco.Web.Security; namespace Umbraco.Web { + /// /// Class that encapsulates Umbraco information of a specific HTTP request /// @@ -20,7 +23,6 @@ namespace Umbraco.Web { private readonly IGlobalSettings _globalSettings; private readonly Lazy _publishedSnapshot; - private DomainHelper _domainHelper; private string _previewToken; private bool? _previewing; @@ -109,19 +111,33 @@ namespace Umbraco.Web /// public IPublishedSnapshot PublishedSnapshot => _publishedSnapshot.Value; - // for unit tests - internal bool HasPublishedSnapshot => _publishedSnapshot.IsValueCreated; + /// + /// Gets the published content cache. + /// + [Obsolete("Use the Content property.")] + public IPublishedContentCache ContentCache => PublishedSnapshot.Content; /// /// Gets the published content cache. /// - public IPublishedContentCache ContentCache => PublishedSnapshot.Content; + public IPublishedContentCache Content => PublishedSnapshot.Content; /// /// Gets the published media cache. /// + [Obsolete("Use the Media property.")] public IPublishedMediaCache MediaCache => PublishedSnapshot.Media; + /// + /// Gets the published media cache. + /// + public IPublishedMediaCache Media => PublishedSnapshot.Media; + + /// + /// Gets the domains cache. + /// + public IDomainCache Domains => PublishedSnapshot.Domains; + /// /// Boolean value indicating whether the current request is a front-end umbraco request /// @@ -147,20 +163,6 @@ namespace Umbraco.Web /// public IVariationContextAccessor VariationContextAccessor { get; } - /// - /// Creates and caches an instance of a DomainHelper - /// - /// - /// We keep creating new instances of DomainHelper, it would be better if we didn't have to do that so instead we can - /// have one attached to the UmbracoContext. This method accepts an external ISiteDomainHelper otherwise the UmbracoContext - /// ctor will have to have another parameter added only for this one method which is annoying and doesn't make a ton of sense - /// since the UmbracoContext itself doesn't use this. - /// - /// TODO: The alternative is to have a IDomainHelperAccessor singleton which is cached per UmbracoContext - /// - internal DomainHelper GetDomainHelper(ISiteDomainHelper siteDomainHelper) - => _domainHelper ?? (_domainHelper = new DomainHelper(PublishedSnapshot.Domains, siteDomainHelper)); - /// /// Gets a value indicating whether the request has debugging enabled /// @@ -174,7 +176,8 @@ namespace Umbraco.Web return GlobalSettings.DebugMode && request != null && (string.IsNullOrEmpty(request["umbdebugshowtrace"]) == false - || string.IsNullOrEmpty(request["umbdebug"]) == false); + || string.IsNullOrEmpty(request["umbdebug"]) == false + || string.IsNullOrEmpty(request.Cookies["UMB-DEBUG"]?.Value) == false); } } @@ -201,7 +204,7 @@ namespace Umbraco.Web /// The url for the content. public string Url(int contentId, string culture = null) { - return UrlProvider.GetUrl(contentId, culture); + return UrlProvider.GetUrl(contentId, culture: culture); } /// @@ -212,7 +215,7 @@ namespace Umbraco.Web /// The url for the content. public string Url(Guid contentId, string culture = null) { - return UrlProvider.GetUrl(contentId, culture); + return UrlProvider.GetUrl(contentId, culture: culture); } /// @@ -222,7 +225,7 @@ namespace Umbraco.Web /// The mode. /// /// The url for the content. - public string Url(int contentId, UrlProviderMode mode, string culture = null) + public string Url(int contentId, UrlMode mode, string culture = null) { return UrlProvider.GetUrl(contentId, mode, culture); } @@ -234,7 +237,7 @@ namespace Umbraco.Web /// The mode. /// /// The url for the content. - public string Url(Guid contentId, UrlProviderMode mode, string culture = null) + public string Url(Guid contentId, UrlMode mode, string culture = null) { return UrlProvider.GetUrl(contentId, mode, culture); } @@ -245,9 +248,10 @@ namespace Umbraco.Web /// The content identifier. /// /// The absolute url for the content. + [Obsolete("Use the Url() method with UrlMode.Absolute.")] public string UrlAbsolute(int contentId, string culture = null) { - return UrlProvider.GetUrl(contentId, true, culture); + return UrlProvider.GetUrl(contentId, UrlMode.Absolute, culture); } /// @@ -256,9 +260,10 @@ namespace Umbraco.Web /// The content identifier. /// /// The absolute url for the content. + [Obsolete("Use the Url() method with UrlMode.Absolute.")] public string UrlAbsolute(Guid contentId, string culture = null) { - return UrlProvider.GetUrl(contentId, true, culture); + return UrlProvider.GetUrl(contentId, UrlMode.Absolute, culture); } #endregion @@ -285,7 +290,7 @@ namespace Umbraco.Web _previewing = _previewToken.IsNullOrWhiteSpace() == false; } - + // say we render a macro or RTE in a give 'preview' mode that might not be the 'current' one, // then due to the way it all works at the moment, the 'current' published snapshot need to be in the proper // default 'preview' mode - somehow we have to force it. and that could be recursive. diff --git a/src/Umbraco.Web/UmbracoContextFactory.cs b/src/Umbraco.Web/UmbracoContextFactory.cs index 2a812036bf..11d8952fa6 100644 --- a/src/Umbraco.Web/UmbracoContextFactory.cs +++ b/src/Umbraco.Web/UmbracoContextFactory.cs @@ -53,7 +53,15 @@ namespace Umbraco.Web { // make sure we have a variation context if (_variationContextAccessor.VariationContext == null) + { + // TODO: By using _defaultCultureAccessor.DefaultCulture this means that the VariationContext will always return a variant culture, it will never + // return an empty string signifying that the culture is invariant. But does this matter? Are we actually expecting this to return an empty string + // for invariant routes? From what i can tell throughout the codebase is that whenever we are checking against the VariationContext.Culture we are + // also checking if the content type varies by culture or not. This is fine, however the code in the ctor of VariationContext is then misleading + // since it's assuming that the Culture can be empty (invariant) when in reality of a website this will never be empty since a real culture is always set here. _variationContextAccessor.VariationContext = new VariationContext(_defaultCultureAccessor.DefaultCulture); + } + var webSecurity = new WebSecurity(httpContext, _userService, _globalSettings); diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 1c6eb28b92..6a2fba1152 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -5,10 +5,14 @@ using System.Web; using System.Xml.XPath; using Umbraco.Core; using Umbraco.Core.Dictionary; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; +using Umbraco.Web.Composing; +using Umbraco.Web.Mvc; using Umbraco.Web.Security; +using Constants = Umbraco.Core.Constants; namespace Umbraco.Web { @@ -66,24 +70,31 @@ namespace Umbraco.Web #endregion // ensures that we can return the specified value + [Obsolete("This method is only used in Obsolete properties")] T Ensure(T o) where T : class => o ?? throw new InvalidOperationException("This UmbracoHelper instance has not been initialized."); + [Obsolete("Inject and use an instance of " + nameof(IUmbracoComponentRenderer) + " in the constructor for using it in classes or get it from Current.UmbracoComponentRenderer in views.")] private IUmbracoComponentRenderer ComponentRenderer => Ensure(_componentRenderer); + + [Obsolete("Inject and use an instance of " + nameof(ICultureDictionaryFactory) + " in the constructor for using it in classes or get it from Current.CultureDictionaryFactory in views.")] private ICultureDictionaryFactory CultureDictionaryFactory => Ensure(_cultureDictionaryFactory); /// /// Gets the tag context. /// + [Obsolete("Inject and use an instance of " + nameof(ITagQuery) + " in the constructor for using it in classes or get it from Current.TagQuery in views.")] public ITagQuery TagQuery => Ensure(_tagQuery); /// /// Gets the query context. /// + [Obsolete("Inject and use an instance of " + nameof(IPublishedContentQuery) + " in the constructor for using it in classes or get it from Current.PublishedContentQuery in views")] public IPublishedContentQuery ContentQuery => Ensure(_publishedContentQuery); /// /// Gets the membership helper. /// + [Obsolete("Inject and use an instance of " + nameof(Security.MembershipHelper) + " in the constructor instead. In views you can use @Members.")] public MembershipHelper MembershipHelper => Ensure(_membershipHelper); /// @@ -142,7 +153,7 @@ namespace Umbraco.Web /// public IHtmlString RenderMacro(string alias) { - return ComponentRenderer.RenderMacro(AssignedContentItem?.Id ?? 0, alias, new { }); + return ComponentRenderer.RenderMacro(AssignedContentItem?.Id ?? 0, alias, null); } /// @@ -153,7 +164,7 @@ namespace Umbraco.Web /// public IHtmlString RenderMacro(string alias, object parameters) { - return ComponentRenderer.RenderMacro(AssignedContentItem?.Id ?? 0, alias, parameters.ToDictionary()); + return ComponentRenderer.RenderMacro(AssignedContentItem?.Id ?? 0, alias, parameters?.ToDictionary()); } /// @@ -228,7 +239,7 @@ namespace Umbraco.Web #endregion - + #region Member/Content/Media from Udi @@ -495,7 +506,7 @@ namespace Umbraco.Web /// The existing contents corresponding to the identifiers. /// If an identifier does not match an existing content, it will be missing in the returned value. public IEnumerable Content(IEnumerable ids) - { + { return ids.Select(id => ContentQuery.Content(id)).WhereNotNull(); } @@ -809,11 +820,37 @@ namespace Umbraco.Web } #endregion + internal static bool DecryptAndValidateEncryptedRouteString(string ufprt, out IDictionary parts) + { + string decryptedString; + try + { + decryptedString = ufprt.DecryptWithMachineKey(); + } + catch (Exception ex) when (ex is FormatException || ex is ArgumentException) + { + Current.Logger.Warn(typeof(UmbracoHelper), "A value was detected in the ufprt parameter but Umbraco could not decrypt the string"); + parts = null; + return false; + } + var parsedQueryString = HttpUtility.ParseQueryString(decryptedString); + parts = new Dictionary(); + foreach (var key in parsedQueryString.AllKeys) + { + parts[key] = parsedQueryString[key]; + } + //validate all required keys exist + //the controller + if (parts.All(x => x.Key != RenderRouteHandler.ReservedAdditionalKeys.Controller)) + return false; + //the action + if (parts.All(x => x.Key != RenderRouteHandler.ReservedAdditionalKeys.Action)) + return false; + //the area + if (parts.All(x => x.Key != RenderRouteHandler.ReservedAdditionalKeys.Area)) + return false; - - - - - + return true; + } } } diff --git a/src/Umbraco.Web/UmbracoInjectedModule.cs b/src/Umbraco.Web/UmbracoInjectedModule.cs index 6a786ed6bf..6dc41d39ee 100644 --- a/src/Umbraco.Web/UmbracoInjectedModule.cs +++ b/src/Umbraco.Web/UmbracoInjectedModule.cs @@ -1,8 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.IO; -using System.Text; using System.Web; using System.Web.Routing; using Umbraco.Core; @@ -10,14 +8,9 @@ using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Web.Routing; -using Umbraco.Web.Security; using Umbraco.Core.Exceptions; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Persistence.FaultHandling; using Umbraco.Core.Security; -using Umbraco.Core.Services; using Umbraco.Web.Composing; -using Umbraco.Web.PublishedCache; namespace Umbraco.Web { @@ -39,40 +32,26 @@ namespace Umbraco.Web public class UmbracoInjectedModule : IHttpModule { private readonly IGlobalSettings _globalSettings; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IUserService _userService; - private readonly UrlProviderCollection _urlProviders; private readonly IRuntimeState _runtime; private readonly ILogger _logger; private readonly IPublishedRouter _publishedRouter; - private readonly IVariationContextAccessor _variationContextAccessor; private readonly IUmbracoContextFactory _umbracoContextFactory; + private readonly RoutableDocumentFilter _routableDocumentLookup; public UmbracoInjectedModule( IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - IPublishedSnapshotService publishedSnapshotService, - IUserService userService, - UrlProviderCollection urlProviders, IRuntimeState runtime, ILogger logger, IPublishedRouter publishedRouter, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextFactory umbracoContextFactory) + IUmbracoContextFactory umbracoContextFactory, + RoutableDocumentFilter routableDocumentLookup) { - _combinedRouteCollection = new Lazy(CreateRouteCollection); - _globalSettings = globalSettings; - _umbracoContextAccessor = umbracoContextAccessor; - _publishedSnapshotService = publishedSnapshotService; - _userService = userService; - _urlProviders = urlProviders; _runtime = runtime; _logger = logger; _publishedRouter = publishedRouter; - _variationContextAccessor = variationContextAccessor; _umbracoContextFactory = umbracoContextFactory; + _routableDocumentLookup = routableDocumentLookup; } #region HttpModule event handlers @@ -182,18 +161,18 @@ namespace Umbraco.Web var reason = EnsureRoutableOutcome.IsRoutable; // ensure this is a document request - if (EnsureDocumentRequest(httpContext, uri) == false) + if (!_routableDocumentLookup.IsDocumentRequest(httpContext, context.OriginalRequestUrl)) { reason = EnsureRoutableOutcome.NotDocumentRequest; } // ensure the runtime is in the proper state // and deal with needed redirects, etc - else if (EnsureRuntime(httpContext, uri) == false) + else if (!EnsureRuntime(httpContext, uri)) { reason = EnsureRoutableOutcome.NotReady; } // ensure Umbraco has documents to serve - else if (EnsureHasContent(context, httpContext) == false) + else if (!EnsureHasContent(context, httpContext)) { reason = EnsureRoutableOutcome.NoContent; } @@ -201,55 +180,7 @@ namespace Umbraco.Web return Attempt.If(reason == EnsureRoutableOutcome.IsRoutable, reason); } - /// - /// Ensures that the request is a document request (i.e. one that the module should handle) - /// - /// - /// - /// - private bool EnsureDocumentRequest(HttpContextBase httpContext, Uri uri) - { - var maybeDoc = true; - var lpath = uri.AbsolutePath.ToLowerInvariant(); - - // handle directory-urls used for asmx - // TODO: legacy - what's the point really? - var asmxPos = lpath.IndexOf(".asmx/", StringComparison.OrdinalIgnoreCase); - if (asmxPos >= 0) - { - // use uri.AbsolutePath, not path, 'cos path has been lowercased - httpContext.RewritePath(uri.AbsolutePath.Substring(0, asmxPos + 5), // filePath - uri.AbsolutePath.Substring(asmxPos + 5), // pathInfo - uri.Query.TrimStart('?')); - maybeDoc = false; - } - - // a document request should be - // /foo/bar/nil - // /foo/bar/nil/ - // /foo/bar/nil.aspx - // where /foo is not a reserved path - - // if the path contains an extension that is not .aspx - // then it cannot be a document request - var extension = Path.GetExtension(lpath); - if (maybeDoc && extension.IsNullOrWhiteSpace() == false && extension != ".aspx") - maybeDoc = false; - - // at that point, either we have no extension, or it is .aspx - - // if the path is reserved then it cannot be a document request - if (maybeDoc && _globalSettings.IsReservedPathOrUrl(lpath, httpContext, _combinedRouteCollection.Value)) - maybeDoc = false; - - //NOTE: No need to warn, plus if we do we should log the document, as this message doesn't really tell us anything :) - //if (!maybeDoc) - //{ - // Logger.Warn("Not a document"); - //} - - return maybeDoc; - } + private bool EnsureRuntime(HttpContextBase httpContext, Uri uri) { @@ -264,7 +195,7 @@ namespace Umbraco.Web case RuntimeLevel.Unknown: case RuntimeLevel.Boot: case RuntimeLevel.BootFailed: - throw new Exception($"panic: Unexpected runtime level: {level}."); + throw new PanicException($"Unexpected runtime level: {level}."); case RuntimeLevel.Run: // ok @@ -300,7 +231,7 @@ namespace Umbraco.Web // if yes, return true private bool EnsureHasContent(UmbracoContext context, HttpContextBase httpContext) { - if (context.ContentCache.HasContent()) + if (context.Content.HasContent()) return true; _logger.Warn("Umbraco has no content"); @@ -505,36 +436,6 @@ namespace Umbraco.Web #endregion - /// - /// This is used to be passed into the GlobalSettings.IsReservedPathOrUrl and will include some 'fake' routes - /// used to determine if a path is reserved. - /// - /// - /// This is basically used to reserve paths dynamically - /// - private readonly Lazy _combinedRouteCollection; - - private RouteCollection CreateRouteCollection() - { - var routes = new RouteCollection(); - - foreach (var route in RouteTable.Routes) - routes.Add(route); - - foreach (var reservedPath in UmbracoModule.ReservedPaths) - { - try - { - routes.Add("_umbreserved_" + reservedPath.ReplaceNonAlphanumericChars(""), - new Route(reservedPath.TrimStart('/'), new StopRoutingHandler())); - } - catch (Exception ex) - { - _logger.Error("Could not add reserved path route", ex); - } - } - - return routes; - } + } } diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 048e521969..2ec4141f5a 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -1,7 +1,6 @@ using System; using System.Web; using Umbraco.Core; -using Umbraco.Core.Collections; using Umbraco.Core.Logging; using Umbraco.Web.Composing; using Umbraco.Web.Routing; @@ -102,11 +101,5 @@ namespace Umbraco.Web return end; } - - /// - /// This is used internally to track any registered callback paths for Identity providers. If the request path matches - /// any of the registered paths, then the module will let the request keep executing - /// - internal static readonly ConcurrentHashSet ReservedPaths = new ConcurrentHashSet(); } } diff --git a/src/Umbraco.Web/UrlHelperExtensions.cs b/src/Umbraco.Web/UrlHelperExtensions.cs index 249ce76193..7e63ff16b8 100644 --- a/src/Umbraco.Web/UrlHelperExtensions.cs +++ b/src/Umbraco.Web/UrlHelperExtensions.cs @@ -2,15 +2,12 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; -using System.Management.Instrumentation; using System.Web.Mvc; using System.Web.Routing; using ClientDependency.Core.Config; using Umbraco.Core; using Umbraco.Core.Configuration; -using Umbraco.Core.Exceptions; using Umbraco.Web.Composing; -using Umbraco.Web.Editors; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; @@ -70,7 +67,8 @@ namespace Umbraco.Web /// public static string GetUmbracoApiService(this UrlHelper url, string actionName, Type apiControllerType, RouteValueDictionary routeVals = null) { - if (string.IsNullOrEmpty(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); + if (actionName == null) throw new ArgumentNullException(nameof(actionName)); + if (string.IsNullOrEmpty(actionName)) throw new ArgumentException("Value can't be empty.", nameof(actionName)); if (apiControllerType == null) throw new ArgumentNullException(nameof(apiControllerType)); var area = ""; @@ -99,8 +97,10 @@ namespace Umbraco.Web /// public static string GetUmbracoApiService(this UrlHelper url, string actionName, string controllerName, string area, RouteValueDictionary routeVals = null) { - if (string.IsNullOrEmpty(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); - if (string.IsNullOrEmpty(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); + if (actionName == null) throw new ArgumentNullException(nameof(actionName)); + if (string.IsNullOrEmpty(actionName)) throw new ArgumentException("Value can't be empty.", nameof(actionName)); + if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); + if (string.IsNullOrEmpty(controllerName)) throw new ArgumentException("Value can't be empty.", nameof(controllerName)); if (routeVals == null) { diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 6f7fbacf7a..8e410bf584 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -4,8 +4,6 @@ using System.Linq; using System.Web; using System.Web.Mvc; using Umbraco.Core; -using Umbraco.Core.Exceptions; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Web.Composing; @@ -20,6 +18,8 @@ namespace Umbraco.Web public static class UrlHelperRenderExtensions { + private static readonly IHtmlString EmptyHtmlString = new HtmlString(string.Empty); + #region GetCropUrl /// @@ -39,6 +39,8 @@ namespace Umbraco.Web /// public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent mediaItem, string cropAlias, bool htmlEncode = true) { + if (mediaItem == null) return EmptyHtmlString; + var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true); return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); } @@ -65,6 +67,8 @@ namespace Umbraco.Web /// public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true) { + if (mediaItem == null) return EmptyHtmlString; + var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); } @@ -144,6 +148,8 @@ namespace Umbraco.Web bool upScale = true, bool htmlEncode = true) { + if (mediaItem == null) return EmptyHtmlString; + var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); @@ -247,6 +253,8 @@ namespace Umbraco.Web bool upScale = true, bool htmlEncode = true) { + if (imageCropperValue == null) return EmptyHtmlString; + var imageUrl = imageCropperValue.Src; var url = imageUrl.GetCropUrl(imageCropperValue, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, @@ -292,8 +300,10 @@ namespace Umbraco.Web /// public static string SurfaceAction(this UrlHelper url, string action, string controllerName, string area, object additionalRouteVals) { - if (string.IsNullOrEmpty(action)) throw new ArgumentNullOrEmptyException(nameof(action)); - if (string.IsNullOrEmpty(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); + if (action == null) throw new ArgumentNullException(nameof(action)); + if (string.IsNullOrEmpty(action)) throw new ArgumentException("Value can't be empty.", nameof(action)); + if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); + if (string.IsNullOrEmpty(controllerName)) throw new ArgumentException("Value can't be empty.", nameof(controllerName)); var encryptedRoute = CreateEncryptedRouteString(controllerName, action, area, additionalRouteVals); @@ -323,7 +333,8 @@ namespace Umbraco.Web /// public static string SurfaceAction(this UrlHelper url, string action, Type surfaceType, object additionalRouteVals) { - if (string.IsNullOrEmpty(action)) throw new ArgumentNullOrEmptyException(nameof(action)); + if (action == null) throw new ArgumentNullException(nameof(action)); + if (string.IsNullOrEmpty(action)) throw new ArgumentException("Value can't be empty.", nameof(action)); if (surfaceType == null) throw new ArgumentNullException(nameof(surfaceType)); var area = ""; @@ -371,25 +382,6 @@ namespace Umbraco.Web return url.SurfaceAction(action, typeof (T), additionalRouteVals); } - /// - /// Generates a Absolute Media Item URL based on the current context - /// - /// - /// - /// - public static string GetAbsoluteMediaUrl(this UrlHelper urlHelper, IPublishedContent mediaItem) - { - if (urlHelper == null) throw new ArgumentNullException("urlHelper"); - if (mediaItem == null) throw new ArgumentNullException("mediaItem"); - - if (urlHelper.RequestContext.HttpContext.Request.Url != null) - { - var requestUrl = urlHelper.RequestContext.HttpContext.Request.Url.GetLeftPart(UriPartial.Authority); - return string.Format("{0}{1}", requestUrl, mediaItem.Url); - } - return null; - } - /// /// This is used in methods like BeginUmbracoForm and SurfaceAction to generate an encrypted string which gets submitted in a request for which /// Umbraco can decrypt during the routing process in order to delegate the request to a specific MVC Controller. @@ -401,8 +393,10 @@ namespace Umbraco.Web /// internal static string CreateEncryptedRouteString(string controllerName, string controllerAction, string area, object additionalRouteVals = null) { - if (string.IsNullOrEmpty(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); - if (string.IsNullOrEmpty(controllerAction)) throw new ArgumentNullOrEmptyException(nameof(controllerAction)); + if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); + if (string.IsNullOrEmpty(controllerName)) throw new ArgumentException("Value can't be empty.", nameof(controllerName)); + if (controllerAction == null) throw new ArgumentNullException(nameof(controllerAction)); + if (string.IsNullOrEmpty(controllerAction)) throw new ArgumentException("Value can't be empty.", nameof(controllerAction)); if (area == null) throw new ArgumentNullException(nameof(area)); //need to create a params string as Base64 to put into our hidden field to use during the routes diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs b/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs deleted file mode 100644 index caa2887797..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs +++ /dev/null @@ -1,1046 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -// -// This source code was auto-generated by Microsoft.VSDesigner, Version 4.0.30319.42000. -// -#pragma warning disable 1591 - -namespace Umbraco.Web.org.umbraco.our { - using System; - using System.Web.Services; - using System.Diagnostics; - using System.Web.Services.Protocols; - using System.Xml.Serialization; - using System.ComponentModel; - - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Web.Services.WebServiceBindingAttribute(Name="RepositorySoap", Namespace="http://packages.umbraco.org/webservices/")] - public partial class Repository : System.Web.Services.Protocols.SoapHttpClientProtocol { - - private System.Threading.SendOrPostCallback CategoriesOperationCompleted; - - private System.Threading.SendOrPostCallback ModulesOperationCompleted; - - private System.Threading.SendOrPostCallback ModulesCategorizedOperationCompleted; - - private System.Threading.SendOrPostCallback NitrosOperationCompleted; - - private System.Threading.SendOrPostCallback NitrosCategorizedOperationCompleted; - - private System.Threading.SendOrPostCallback authenticateOperationCompleted; - - private System.Threading.SendOrPostCallback fetchPackageOperationCompleted; - - private System.Threading.SendOrPostCallback fetchPackageByVersionOperationCompleted; - - private System.Threading.SendOrPostCallback fetchProtectedPackageOperationCompleted; - - private System.Threading.SendOrPostCallback SubmitPackageOperationCompleted; - - private System.Threading.SendOrPostCallback PackageByGuidOperationCompleted; - - private bool useDefaultCredentialsSetExplicitly; - - /// - public Repository() { - this.Url = "http://our.umbraco.org/umbraco/webservices/api/repository.asmx"; - if ((this.IsLocalFileSystemWebService(this.Url) == true)) { - this.UseDefaultCredentials = true; - this.useDefaultCredentialsSetExplicitly = false; - } - else { - this.useDefaultCredentialsSetExplicitly = true; - } - } - - public new string Url { - get { - return base.Url; - } - set { - if ((((this.IsLocalFileSystemWebService(base.Url) == true) - && (this.useDefaultCredentialsSetExplicitly == false)) - && (this.IsLocalFileSystemWebService(value) == false))) { - base.UseDefaultCredentials = false; - } - base.Url = value; - } - } - - public new bool UseDefaultCredentials { - get { - return base.UseDefaultCredentials; - } - set { - base.UseDefaultCredentials = value; - this.useDefaultCredentialsSetExplicitly = true; - } - } - - /// - public event CategoriesCompletedEventHandler CategoriesCompleted; - - /// - public event ModulesCompletedEventHandler ModulesCompleted; - - /// - public event ModulesCategorizedCompletedEventHandler ModulesCategorizedCompleted; - - /// - public event NitrosCompletedEventHandler NitrosCompleted; - - /// - public event NitrosCategorizedCompletedEventHandler NitrosCategorizedCompleted; - - /// - public event authenticateCompletedEventHandler authenticateCompleted; - - /// - public event fetchPackageCompletedEventHandler fetchPackageCompleted; - - /// - public event fetchPackageByVersionCompletedEventHandler fetchPackageByVersionCompleted; - - /// - public event fetchProtectedPackageCompletedEventHandler fetchProtectedPackageCompleted; - - /// - public event SubmitPackageCompletedEventHandler SubmitPackageCompleted; - - /// - public event PackageByGuidCompletedEventHandler PackageByGuidCompleted; - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Categories", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] Categories(string repositoryGuid) { - object[] results = this.Invoke("Categories", new object[] { - repositoryGuid}); - return ((Category[])(results[0])); - } - - /// - public void CategoriesAsync(string repositoryGuid) { - this.CategoriesAsync(repositoryGuid, null); - } - - /// - public void CategoriesAsync(string repositoryGuid, object userState) { - if ((this.CategoriesOperationCompleted == null)) { - this.CategoriesOperationCompleted = new System.Threading.SendOrPostCallback(this.OnCategoriesOperationCompleted); - } - this.InvokeAsync("Categories", new object[] { - repositoryGuid}, this.CategoriesOperationCompleted, userState); - } - - private void OnCategoriesOperationCompleted(object arg) { - if ((this.CategoriesCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.CategoriesCompleted(this, new CategoriesCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Modules", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package[] Modules() { - object[] results = this.Invoke("Modules", new object[0]); - return ((Package[])(results[0])); - } - - /// - public void ModulesAsync() { - this.ModulesAsync(null); - } - - /// - public void ModulesAsync(object userState) { - if ((this.ModulesOperationCompleted == null)) { - this.ModulesOperationCompleted = new System.Threading.SendOrPostCallback(this.OnModulesOperationCompleted); - } - this.InvokeAsync("Modules", new object[0], this.ModulesOperationCompleted, userState); - } - - private void OnModulesOperationCompleted(object arg) { - if ((this.ModulesCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.ModulesCompleted(this, new ModulesCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/ModulesCategorized", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] ModulesCategorized() { - object[] results = this.Invoke("ModulesCategorized", new object[0]); - return ((Category[])(results[0])); - } - - /// - public void ModulesCategorizedAsync() { - this.ModulesCategorizedAsync(null); - } - - /// - public void ModulesCategorizedAsync(object userState) { - if ((this.ModulesCategorizedOperationCompleted == null)) { - this.ModulesCategorizedOperationCompleted = new System.Threading.SendOrPostCallback(this.OnModulesCategorizedOperationCompleted); - } - this.InvokeAsync("ModulesCategorized", new object[0], this.ModulesCategorizedOperationCompleted, userState); - } - - private void OnModulesCategorizedOperationCompleted(object arg) { - if ((this.ModulesCategorizedCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.ModulesCategorizedCompleted(this, new ModulesCategorizedCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Nitros", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package[] Nitros() { - object[] results = this.Invoke("Nitros", new object[0]); - return ((Package[])(results[0])); - } - - /// - public void NitrosAsync() { - this.NitrosAsync(null); - } - - /// - public void NitrosAsync(object userState) { - if ((this.NitrosOperationCompleted == null)) { - this.NitrosOperationCompleted = new System.Threading.SendOrPostCallback(this.OnNitrosOperationCompleted); - } - this.InvokeAsync("Nitros", new object[0], this.NitrosOperationCompleted, userState); - } - - private void OnNitrosOperationCompleted(object arg) { - if ((this.NitrosCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.NitrosCompleted(this, new NitrosCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/NitrosCategorized", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] NitrosCategorized() { - object[] results = this.Invoke("NitrosCategorized", new object[0]); - return ((Category[])(results[0])); - } - - /// - public void NitrosCategorizedAsync() { - this.NitrosCategorizedAsync(null); - } - - /// - public void NitrosCategorizedAsync(object userState) { - if ((this.NitrosCategorizedOperationCompleted == null)) { - this.NitrosCategorizedOperationCompleted = new System.Threading.SendOrPostCallback(this.OnNitrosCategorizedOperationCompleted); - } - this.InvokeAsync("NitrosCategorized", new object[0], this.NitrosCategorizedOperationCompleted, userState); - } - - private void OnNitrosCategorizedOperationCompleted(object arg) { - if ((this.NitrosCategorizedCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.NitrosCategorizedCompleted(this, new NitrosCategorizedCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/authenticate", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public string authenticate(string email, string md5Password) { - object[] results = this.Invoke("authenticate", new object[] { - email, - md5Password}); - return ((string)(results[0])); - } - - /// - public void authenticateAsync(string email, string md5Password) { - this.authenticateAsync(email, md5Password, null); - } - - /// - public void authenticateAsync(string email, string md5Password, object userState) { - if ((this.authenticateOperationCompleted == null)) { - this.authenticateOperationCompleted = new System.Threading.SendOrPostCallback(this.OnauthenticateOperationCompleted); - } - this.InvokeAsync("authenticate", new object[] { - email, - md5Password}, this.authenticateOperationCompleted, userState); - } - - private void OnauthenticateOperationCompleted(object arg) { - if ((this.authenticateCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.authenticateCompleted(this, new authenticateCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchPackage(string packageGuid) { - object[] results = this.Invoke("fetchPackage", new object[] { - packageGuid}); - return ((byte[])(results[0])); - } - - /// - public void fetchPackageAsync(string packageGuid) { - this.fetchPackageAsync(packageGuid, null); - } - - /// - public void fetchPackageAsync(string packageGuid, object userState) { - if ((this.fetchPackageOperationCompleted == null)) { - this.fetchPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchPackageOperationCompleted); - } - this.InvokeAsync("fetchPackage", new object[] { - packageGuid}, this.fetchPackageOperationCompleted, userState); - } - - private void OnfetchPackageOperationCompleted(object arg) { - if ((this.fetchPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchPackageCompleted(this, new fetchPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchPackageByVersion", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchPackageByVersion(string packageGuid, string repoVersion) { - object[] results = this.Invoke("fetchPackageByVersion", new object[] { - packageGuid, - repoVersion}); - return ((byte[])(results[0])); - } - - /// - public void fetchPackageByVersionAsync(string packageGuid, string repoVersion) { - this.fetchPackageByVersionAsync(packageGuid, repoVersion, null); - } - - /// - public void fetchPackageByVersionAsync(string packageGuid, string repoVersion, object userState) { - if ((this.fetchPackageByVersionOperationCompleted == null)) { - this.fetchPackageByVersionOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchPackageByVersionOperationCompleted); - } - this.InvokeAsync("fetchPackageByVersion", new object[] { - packageGuid, - repoVersion}, this.fetchPackageByVersionOperationCompleted, userState); - } - - private void OnfetchPackageByVersionOperationCompleted(object arg) { - if ((this.fetchPackageByVersionCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchPackageByVersionCompleted(this, new fetchPackageByVersionCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchProtectedPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchProtectedPackage(string packageGuid, string memberKey) { - object[] results = this.Invoke("fetchProtectedPackage", new object[] { - packageGuid, - memberKey}); - return ((byte[])(results[0])); - } - - /// - public void fetchProtectedPackageAsync(string packageGuid, string memberKey) { - this.fetchProtectedPackageAsync(packageGuid, memberKey, null); - } - - /// - public void fetchProtectedPackageAsync(string packageGuid, string memberKey, object userState) { - if ((this.fetchProtectedPackageOperationCompleted == null)) { - this.fetchProtectedPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchProtectedPackageOperationCompleted); - } - this.InvokeAsync("fetchProtectedPackage", new object[] { - packageGuid, - memberKey}, this.fetchProtectedPackageOperationCompleted, userState); - } - - private void OnfetchProtectedPackageOperationCompleted(object arg) { - if ((this.fetchProtectedPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchProtectedPackageCompleted(this, new fetchProtectedPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/SubmitPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public SubmitStatus SubmitPackage(string repositoryGuid, string authorGuid, string packageGuid, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageFile, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageDoc, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageThumbnail, string name, string author, string authorUrl, string description) { - object[] results = this.Invoke("SubmitPackage", new object[] { - repositoryGuid, - authorGuid, - packageGuid, - packageFile, - packageDoc, - packageThumbnail, - name, - author, - authorUrl, - description}); - return ((SubmitStatus)(results[0])); - } - - /// - public void SubmitPackageAsync(string repositoryGuid, string authorGuid, string packageGuid, byte[] packageFile, byte[] packageDoc, byte[] packageThumbnail, string name, string author, string authorUrl, string description) { - this.SubmitPackageAsync(repositoryGuid, authorGuid, packageGuid, packageFile, packageDoc, packageThumbnail, name, author, authorUrl, description, null); - } - - /// - public void SubmitPackageAsync(string repositoryGuid, string authorGuid, string packageGuid, byte[] packageFile, byte[] packageDoc, byte[] packageThumbnail, string name, string author, string authorUrl, string description, object userState) { - if ((this.SubmitPackageOperationCompleted == null)) { - this.SubmitPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnSubmitPackageOperationCompleted); - } - this.InvokeAsync("SubmitPackage", new object[] { - repositoryGuid, - authorGuid, - packageGuid, - packageFile, - packageDoc, - packageThumbnail, - name, - author, - authorUrl, - description}, this.SubmitPackageOperationCompleted, userState); - } - - private void OnSubmitPackageOperationCompleted(object arg) { - if ((this.SubmitPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.SubmitPackageCompleted(this, new SubmitPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/PackageByGuid", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package PackageByGuid(string packageGuid) { - object[] results = this.Invoke("PackageByGuid", new object[] { - packageGuid}); - return ((Package)(results[0])); - } - - /// - public void PackageByGuidAsync(string packageGuid) { - this.PackageByGuidAsync(packageGuid, null); - } - - /// - public void PackageByGuidAsync(string packageGuid, object userState) { - if ((this.PackageByGuidOperationCompleted == null)) { - this.PackageByGuidOperationCompleted = new System.Threading.SendOrPostCallback(this.OnPackageByGuidOperationCompleted); - } - this.InvokeAsync("PackageByGuid", new object[] { - packageGuid}, this.PackageByGuidOperationCompleted, userState); - } - - private void OnPackageByGuidOperationCompleted(object arg) { - if ((this.PackageByGuidCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.PackageByGuidCompleted(this, new PackageByGuidCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - public new void CancelAsync(object userState) { - base.CancelAsync(userState); - } - - private bool IsLocalFileSystemWebService(string url) { - if (((url == null) - || (url == string.Empty))) { - return false; - } - System.Uri wsUri = new System.Uri(url); - if (((wsUri.Port >= 1024) - && (string.Compare(wsUri.Host, "localHost", System.StringComparison.OrdinalIgnoreCase) == 0))) { - return true; - } - return false; - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public partial class Category { - - private string textField; - - private string descriptionField; - - private string urlField; - - private int idField; - - private Package[] packagesField; - - /// - public string Text { - get { - return this.textField; - } - set { - this.textField = value; - } - } - - /// - public string Description { - get { - return this.descriptionField; - } - set { - this.descriptionField = value; - } - } - - /// - public string Url { - get { - return this.urlField; - } - set { - this.urlField = value; - } - } - - /// - public int Id { - get { - return this.idField; - } - set { - this.idField = value; - } - } - - /// - public Package[] Packages { - get { - return this.packagesField; - } - set { - this.packagesField = value; - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public partial class Package { - - private System.Guid repoGuidField; - - private string textField; - - private string descriptionField; - - private string iconField; - - private string thumbnailField; - - private string documentationField; - - private string demoField; - - private bool acceptedField; - - private bool isModuleField; - - private bool editorsPickField; - - private bool protectedField; - - private bool hasUpgradeField; - - private string upgradeVersionField; - - private string upgradeReadMeField; - - private string urlField; - - /// - public System.Guid RepoGuid { - get { - return this.repoGuidField; - } - set { - this.repoGuidField = value; - } - } - - /// - public string Text { - get { - return this.textField; - } - set { - this.textField = value; - } - } - - /// - public string Description { - get { - return this.descriptionField; - } - set { - this.descriptionField = value; - } - } - - /// - public string Icon { - get { - return this.iconField; - } - set { - this.iconField = value; - } - } - - /// - public string Thumbnail { - get { - return this.thumbnailField; - } - set { - this.thumbnailField = value; - } - } - - /// - public string Documentation { - get { - return this.documentationField; - } - set { - this.documentationField = value; - } - } - - /// - public string Demo { - get { - return this.demoField; - } - set { - this.demoField = value; - } - } - - /// - public bool Accepted { - get { - return this.acceptedField; - } - set { - this.acceptedField = value; - } - } - - /// - public bool IsModule { - get { - return this.isModuleField; - } - set { - this.isModuleField = value; - } - } - - /// - public bool EditorsPick { - get { - return this.editorsPickField; - } - set { - this.editorsPickField = value; - } - } - - /// - public bool Protected { - get { - return this.protectedField; - } - set { - this.protectedField = value; - } - } - - /// - public bool HasUpgrade { - get { - return this.hasUpgradeField; - } - set { - this.hasUpgradeField = value; - } - } - - /// - public string UpgradeVersion { - get { - return this.upgradeVersionField; - } - set { - this.upgradeVersionField = value; - } - } - - /// - public string UpgradeReadMe { - get { - return this.upgradeReadMeField; - } - set { - this.upgradeReadMeField = value; - } - } - - /// - public string Url { - get { - return this.urlField; - } - set { - this.urlField = value; - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public enum SubmitStatus { - - /// - Complete, - - /// - Exists, - - /// - NoAccess, - - /// - Error, - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void CategoriesCompletedEventHandler(object sender, CategoriesCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class CategoriesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal CategoriesCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void ModulesCompletedEventHandler(object sender, ModulesCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class ModulesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal ModulesCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void ModulesCategorizedCompletedEventHandler(object sender, ModulesCategorizedCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class ModulesCategorizedCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal ModulesCategorizedCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void NitrosCompletedEventHandler(object sender, NitrosCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class NitrosCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal NitrosCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void NitrosCategorizedCompletedEventHandler(object sender, NitrosCategorizedCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class NitrosCategorizedCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal NitrosCategorizedCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void authenticateCompletedEventHandler(object sender, authenticateCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class authenticateCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal authenticateCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public string Result { - get { - this.RaiseExceptionIfNecessary(); - return ((string)(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void fetchPackageCompletedEventHandler(object sender, fetchPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void fetchPackageByVersionCompletedEventHandler(object sender, fetchPackageByVersionCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchPackageByVersionCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchPackageByVersionCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void fetchProtectedPackageCompletedEventHandler(object sender, fetchProtectedPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchProtectedPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchProtectedPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void SubmitPackageCompletedEventHandler(object sender, SubmitPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class SubmitPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal SubmitPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public SubmitStatus Result { - get { - this.RaiseExceptionIfNecessary(); - return ((SubmitStatus)(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void PackageByGuidCompletedEventHandler(object sender, PackageByGuidCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class PackageByGuidCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal PackageByGuidCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package)(this.results[0])); - } - } - } -} - -#pragma warning restore 1591 \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map b/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map deleted file mode 100644 index 8d133063a5..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco b/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco deleted file mode 100644 index 4bf1a61fca..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl b/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl deleted file mode 100644 index 2a3f3502db..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl +++ /dev/null @@ -1,995 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/Reference.cs b/src/Umbraco.Web/Web References/org.umbraco.update/Reference.cs deleted file mode 100644 index 230594fcbd..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.update/Reference.cs +++ /dev/null @@ -1,286 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -// -// This source code was auto-generated by Microsoft.VSDesigner, Version 4.0.30319.42000. -// -#pragma warning disable 1591 - -namespace Umbraco.Web.org.umbraco.update { - using System; - using System.Web.Services; - using System.Diagnostics; - using System.Web.Services.Protocols; - using System.Xml.Serialization; - using System.ComponentModel; - - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Web.Services.WebServiceBindingAttribute(Name="CheckForUpgradeSoap", Namespace="http://update.umbraco.org/")] - public partial class CheckForUpgrade : System.Web.Services.Protocols.SoapHttpClientProtocol { - - private System.Threading.SendOrPostCallback InstallOperationCompleted; - - private System.Threading.SendOrPostCallback CheckUpgradeOperationCompleted; - - private bool useDefaultCredentialsSetExplicitly; - - /// - public CheckForUpgrade() { - this.Url = "http://update.umbraco.org/checkforupgrade.asmx"; - if ((this.IsLocalFileSystemWebService(this.Url) == true)) { - this.UseDefaultCredentials = true; - this.useDefaultCredentialsSetExplicitly = false; - } - else { - this.useDefaultCredentialsSetExplicitly = true; - } - } - - public new string Url { - get { - return base.Url; - } - set { - if ((((this.IsLocalFileSystemWebService(base.Url) == true) - && (this.useDefaultCredentialsSetExplicitly == false)) - && (this.IsLocalFileSystemWebService(value) == false))) { - base.UseDefaultCredentials = false; - } - base.Url = value; - } - } - - public new bool UseDefaultCredentials { - get { - return base.UseDefaultCredentials; - } - set { - base.UseDefaultCredentials = value; - this.useDefaultCredentialsSetExplicitly = true; - } - } - - /// - public event InstallCompletedEventHandler InstallCompleted; - - /// - public event CheckUpgradeCompletedEventHandler CheckUpgradeCompleted; - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://update.umbraco.org/Install", RequestNamespace="http://update.umbraco.org/", ResponseNamespace="http://update.umbraco.org/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public void Install(System.Guid installId, bool isUpgrade, bool installCompleted, System.DateTime timestamp, int versionMajor, int versionMinor, int versionPatch, string versionComment, string error, string userAgent, string dbProvider) { - this.Invoke("Install", new object[] { - installId, - isUpgrade, - installCompleted, - timestamp, - versionMajor, - versionMinor, - versionPatch, - versionComment, - error, - userAgent, - dbProvider}); - } - - /// - public void InstallAsync(System.Guid installId, bool isUpgrade, bool installCompleted, System.DateTime timestamp, int versionMajor, int versionMinor, int versionPatch, string versionComment, string error, string userAgent, string dbProvider) { - this.InstallAsync(installId, isUpgrade, installCompleted, timestamp, versionMajor, versionMinor, versionPatch, versionComment, error, userAgent, dbProvider, null); - } - - /// - public void InstallAsync(System.Guid installId, bool isUpgrade, bool installCompleted, System.DateTime timestamp, int versionMajor, int versionMinor, int versionPatch, string versionComment, string error, string userAgent, string dbProvider, object userState) { - if ((this.InstallOperationCompleted == null)) { - this.InstallOperationCompleted = new System.Threading.SendOrPostCallback(this.OnInstallOperationCompleted); - } - this.InvokeAsync("Install", new object[] { - installId, - isUpgrade, - installCompleted, - timestamp, - versionMajor, - versionMinor, - versionPatch, - versionComment, - error, - userAgent, - dbProvider}, this.InstallOperationCompleted, userState); - } - - private void OnInstallOperationCompleted(object arg) { - if ((this.InstallCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.InstallCompleted(this, new System.ComponentModel.AsyncCompletedEventArgs(invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://update.umbraco.org/CheckUpgrade", RequestNamespace="http://update.umbraco.org/", ResponseNamespace="http://update.umbraco.org/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public UpgradeResult CheckUpgrade(int versionMajor, int versionMinor, int versionPatch, string versionComment) { - object[] results = this.Invoke("CheckUpgrade", new object[] { - versionMajor, - versionMinor, - versionPatch, - versionComment}); - return ((UpgradeResult)(results[0])); - } - - /// - public void CheckUpgradeAsync(int versionMajor, int versionMinor, int versionPatch, string versionComment) { - this.CheckUpgradeAsync(versionMajor, versionMinor, versionPatch, versionComment, null); - } - - /// - public void CheckUpgradeAsync(int versionMajor, int versionMinor, int versionPatch, string versionComment, object userState) { - if ((this.CheckUpgradeOperationCompleted == null)) { - this.CheckUpgradeOperationCompleted = new System.Threading.SendOrPostCallback(this.OnCheckUpgradeOperationCompleted); - } - this.InvokeAsync("CheckUpgrade", new object[] { - versionMajor, - versionMinor, - versionPatch, - versionComment}, this.CheckUpgradeOperationCompleted, userState); - } - - private void OnCheckUpgradeOperationCompleted(object arg) { - if ((this.CheckUpgradeCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.CheckUpgradeCompleted(this, new CheckUpgradeCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - public new void CancelAsync(object userState) { - base.CancelAsync(userState); - } - - private bool IsLocalFileSystemWebService(string url) { - if (((url == null) - || (url == string.Empty))) { - return false; - } - System.Uri wsUri = new System.Uri(url); - if (((wsUri.Port >= 1024) - && (string.Compare(wsUri.Host, "localHost", System.StringComparison.OrdinalIgnoreCase) == 0))) { - return true; - } - return false; - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://update.umbraco.org/")] - public partial class UpgradeResult { - - private string commentField; - - private UpgradeType upgradeTypeField; - - private string upgradeUrlField; - - /// - public string Comment { - get { - return this.commentField; - } - set { - this.commentField = value; - } - } - - /// - public UpgradeType UpgradeType { - get { - return this.upgradeTypeField; - } - set { - this.upgradeTypeField = value; - } - } - - /// - public string UpgradeUrl { - get { - return this.upgradeUrlField; - } - set { - this.upgradeUrlField = value; - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://update.umbraco.org/")] - public enum UpgradeType { - - /// - None, - - /// - Patch, - - /// - Minor, - - /// - Major, - - /// - Critical, - - /// - Error, - - /// - OutOfSync, - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void InstallCompletedEventHandler(object sender, System.ComponentModel.AsyncCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void CheckUpgradeCompletedEventHandler(object sender, CheckUpgradeCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class CheckUpgradeCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal CheckUpgradeCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public UpgradeResult Result { - get { - this.RaiseExceptionIfNecessary(); - return ((UpgradeResult)(this.results[0])); - } - } - } -} - -#pragma warning restore 1591 \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/Reference.map b/src/Umbraco.Web/Web References/org.umbraco.update/Reference.map deleted file mode 100644 index ded6650264..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.update/Reference.map +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult.datasource b/src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult.datasource deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult1.datasource b/src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult1.datasource deleted file mode 100644 index c42c98b0b7..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult1.datasource +++ /dev/null @@ -1,10 +0,0 @@ - - - - umbraco.presentation.org.umbraco.update.UpgradeResult, Web References.org.umbraco.update.Reference.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.disco b/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.disco deleted file mode 100644 index 366f4fdd6e..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.disco +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.wsdl b/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.wsdl deleted file mode 100644 index e2aba65c7c..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.wsdl +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs index 0c53838592..28f09b46b7 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs @@ -1,17 +1,13 @@ using System; +using System.Net; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; -using Umbraco.Core.Exceptions; -using Umbraco.Web.Composing; -using Umbraco.Web.Editors; - using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Web.Actions; using Umbraco.Core.Security; -using System.Net; -using System.Web; +using Umbraco.Web.Actions; +using Umbraco.Web.Composing; namespace Umbraco.Web.WebApi.Filters { @@ -47,7 +43,9 @@ namespace Umbraco.Web.WebApi.Filters public EnsureUserPermissionForContentAttribute(string paramName) { - if (string.IsNullOrEmpty(paramName)) throw new ArgumentNullOrEmptyException(nameof(paramName)); + if (paramName == null) throw new ArgumentNullException(nameof(paramName)); + if (string.IsNullOrEmpty(paramName)) throw new ArgumentException("Value can't be empty.", nameof(paramName)); + _paramName = paramName; _permissionToCheck = ActionBrowse.ActionLetter; } @@ -67,7 +65,7 @@ namespace Umbraco.Web.WebApi.Filters //not logged in throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); } - + int nodeId; if (_nodeId.HasValue == false) { @@ -117,29 +115,24 @@ namespace Umbraco.Web.WebApi.Filters nodeId = _nodeId.Value; } - var queryStringCollection = HttpUtility.ParseQueryString(actionContext.Request.RequestUri.Query); - bool.TryParse(queryStringCollection["ignoreUserStartNodes"], out var ignoreUserStartNodes); - if (ignoreUserStartNodes == false) + var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, + Current.UmbracoContext.Security.CurrentUser, + Current.Services.UserService, + Current.Services.ContentService, + Current.Services.EntityService, + out var contentItem, + _permissionToCheck.HasValue ? new[] { _permissionToCheck.Value } : null); + + if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound) + throw new HttpResponseException(HttpStatusCode.NotFound); + + if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) + throw new HttpResponseException(actionContext.Request.CreateUserNoAccessResponse()); + + if (contentItem != null) { - var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, - Current.UmbracoContext.Security.CurrentUser, - Current.Services.UserService, - Current.Services.ContentService, - Current.Services.EntityService, - out var contentItem, - _permissionToCheck.HasValue ? new[] {_permissionToCheck.Value} : null); - - if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound) - throw new HttpResponseException(HttpStatusCode.NotFound); - - if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) - throw new HttpResponseException(actionContext.Request.CreateUserNoAccessResponse()); - - if (contentItem != null) - { - //store the content item in request cache so it can be resolved in the controller without re-looking it up - actionContext.Request.Properties[typeof(IContent).ToString()] = contentItem; - } + //store the content item in request cache so it can be resolved in the controller without re-looking it up + actionContext.Request.Properties[typeof(IContent).ToString()] = contentItem; } base.OnActionExecuting(actionContext); diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs index 24bf2ea9a8..60e2889fd5 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs @@ -3,7 +3,6 @@ using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; using Umbraco.Core; -using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Web.Composing; using Umbraco.Web.Editors; @@ -38,14 +37,18 @@ namespace Umbraco.Web.WebApi.Filters public EnsureUserPermissionForMediaAttribute(string paramName) { - if (string.IsNullOrEmpty(paramName)) throw new ArgumentNullOrEmptyException(nameof(paramName)); + if (paramName == null) throw new ArgumentNullException(nameof(paramName)); + if (string.IsNullOrEmpty(paramName)) throw new ArgumentException("Value can't be empty.", nameof(paramName)); + _paramName = paramName; } // TODO: v8 guess this is not used anymore, source is ignored?! public EnsureUserPermissionForMediaAttribute(string paramName, DictionarySource source) { - if (string.IsNullOrEmpty(paramName)) throw new ArgumentNullOrEmptyException(nameof(paramName)); + if (paramName == null) throw new ArgumentNullException(nameof(paramName)); + if (string.IsNullOrEmpty(paramName)) throw new ArgumentException("Value can't be empty.", nameof(paramName)); + _paramName = paramName; } diff --git a/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs index 6e6ec35b1b..1646c8a426 100644 --- a/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs @@ -146,28 +146,6 @@ namespace Umbraco.Web.WebApi.Filters Current.Logger.Warn("The actionExecutedContext.Request.Content is not ObjectContent, it is {RequestObjectType}", actionExecutedContext.Request.Content.GetType()); } } - - //Now remove all old files so that the temp folder(s) never grow - foreach (var tempFolder in tempFolders.Distinct()) - { - var files = Directory.GetFiles(tempFolder); - foreach (var file in files) - { - if (DateTime.UtcNow - File.GetLastWriteTimeUtc(file) > TimeSpan.FromDays(1)) - { - try - { - File.Delete(file); - } - catch (System.Exception ex) - { - Current.Logger.Error(ex, "Could not delete temp file {FileName}", file); - } - } - } - - } - } } } diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs index f8b02f08ca..21dc60e6cc 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -3,14 +3,12 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Web; using System.Web.Http.Filters; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Composing; using Umbraco.Core.Security; -using Umbraco.Web.Trees; namespace Umbraco.Web.WebApi.Filters { @@ -74,12 +72,7 @@ namespace Umbraco.Web.WebApi.Filters protected virtual void FilterItems(IUser user, IList items) { - bool.TryParse(HttpContext.Current.Request.QueryString.Get(TreeQueryStringParameters.IgnoreUserStartNodes), out var ignoreUserStartNodes); - - if (ignoreUserStartNodes == false) - { - FilterBasedOnStartNode(items, user); - } + FilterBasedOnStartNode(items, user); } internal void FilterBasedOnStartNode(IList items, IUser user) diff --git a/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs b/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs index 713bce7d1b..7f05d59a18 100644 --- a/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs @@ -1,10 +1,8 @@ -using Newtonsoft.Json.Converters; -using System; +using System; using System.Linq; using System.Net.Http.Formatting; using System.Web.Http.Controllers; -using Umbraco.Core; -using Umbraco.Core.Exceptions; +using Newtonsoft.Json.Converters; namespace Umbraco.Web.WebApi.Filters { @@ -21,7 +19,9 @@ namespace Umbraco.Web.WebApi.Filters /// public JsonDateTimeFormatAttributeAttribute(string format) { - if (string.IsNullOrEmpty(format)) throw new ArgumentNullOrEmptyException(nameof(format)); + if (format == null) throw new ArgumentNullException(nameof(format)); + if (string.IsNullOrEmpty(format)) throw new ArgumentException("Value can't be empty.", nameof(format)); + _format = format; } diff --git a/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs index 0abdfb5d2f..f147a2a4cb 100644 --- a/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web.WebApi.Filters var userIdentity = ((ApiController) actionContext.ControllerContext.Controller).User.Identity as ClaimsIdentity; if (userIdentity != null) { - //if there is not CookiePath claim, then exist + //if there is not CookiePath claim, then exit if (userIdentity.HasClaim(x => x.Type == ClaimTypes.CookiePath) == false) { base.OnActionExecuting(actionContext); diff --git a/src/Umbraco.Web/WebApi/UmbracoApiController.cs b/src/Umbraco.Web/WebApi/UmbracoApiController.cs index 8a2e54ca02..4600c02ac1 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiController.cs @@ -1,8 +1,10 @@ -using Umbraco.Core; +using System; +using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Mapping; using Umbraco.Core.Persistence; using Umbraco.Core.Services; @@ -17,9 +19,15 @@ namespace Umbraco.Web.WebApi { } + [Obsolete("This constructor is obsolete since it doesn't inject the UmbracoMapper. The UmbracoMapper will be resolved from the service locator Current.Mapper, which is not good for testability. Inject the UmbracoMapper using full constructor injection instead.")] protected UmbracoApiController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { } + + protected UmbracoApiController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper, UmbracoMapper umbracoMapper) + : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper, umbracoMapper) + { + } } } diff --git a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs index 89a630fb5d..a1cd7708e2 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs @@ -40,13 +40,28 @@ namespace Umbraco.Web.WebApi Current.Factory.GetInstance(), Current.Factory.GetInstance(), Current.Factory.GetInstance(), - Current.Factory.GetInstance() + Current.Factory.GetInstance(), + Current.Factory.GetInstance() ) { } /// /// Initializes a new instance of the class with all its dependencies. /// + protected UmbracoApiControllerBase(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper, UmbracoMapper umbracoMapper) + { + UmbracoContextAccessor = umbracoContextAccessor; + GlobalSettings = globalSettings; + SqlContext = sqlContext; + Services = services; + AppCaches = appCaches; + Logger = logger; + RuntimeState = runtimeState; + Umbraco = umbracoHelper; + Mapper = umbracoMapper; + } + + [Obsolete("This constructor is obsolete since it doesn't inject the UmbracoMapper. The UmbracoMapper will be resolved from the service locator Current.Mapper, which is not good for testability. Inject the UmbracoMapper using full constructor injection instead.")] protected UmbracoApiControllerBase(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) { UmbracoContextAccessor = umbracoContextAccessor; @@ -58,7 +73,7 @@ namespace Umbraco.Web.WebApi RuntimeState = runtimeState; Umbraco = umbracoHelper; - // fixme - can we break all ctors? + // not good for testability, hence the obsolete. Mapper = Current.Mapper; } diff --git a/src/umbraco.sln b/src/umbraco.sln index 7313e06e49..a747f21d19 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2005 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29209.152 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI", "Umbraco.Web.UI\Umbraco.Web.UI.csproj", "{4C4C194C-B5E4-4991-8F87-4373E24CC19F}" EndProject @@ -70,7 +70,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{E3F9F378 ProjectSection(SolutionItems) = preProject ..\build\NuSpecs\tools\applications.config.install.xdt = ..\build\NuSpecs\tools\applications.config.install.xdt ..\build\NuSpecs\tools\ClientDependency.config.install.xdt = ..\build\NuSpecs\tools\ClientDependency.config.install.xdt - ..\build\NuSpecs\tools\install.core.ps1 = ..\build\NuSpecs\tools\install.core.ps1 ..\build\NuSpecs\tools\install.ps1 = ..\build\NuSpecs\tools\install.ps1 ..\build\NuSpecs\tools\Readme.txt = ..\build\NuSpecs\tools\Readme.txt ..\build\NuSpecs\tools\ReadmeUpgrade.txt = ..\build\NuSpecs\tools\ReadmeUpgrade.txt @@ -103,6 +102,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp ..\.github\ISSUE_TEMPLATE\5_Security_issue.md = ..\.github\ISSUE_TEMPLATE\5_Security_issue.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.ModelsBuilder.Embedded", "Umbraco.ModelsBuilder.Embedded\Umbraco.ModelsBuilder.Embedded.csproj", "{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.TestData", "Umbraco.TestData\Umbraco.TestData.csproj", "{FB5676ED-7A69-492C-B802-E7B24144C0FC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -135,6 +138,14 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.Build.0 = Release|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.Build.0 = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -147,6 +158,7 @@ Global {53594E5B-64A2-4545-8367-E3627D266AE8} = {FD962632-184C-4005-A5F3-E705D92FC645} {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} + {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC}