diff --git a/.github/BUILD.md b/.github/BUILD.md index c89a1be460..a9c26f3a9b 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -1,4 +1,4 @@ -# Umbraco Cms Build +# Umbraco CMS Build ## Are you sure? @@ -66,7 +66,7 @@ The Visual Studio object is `null` when Visual Studio has not been detected (eg * `Path`: Visual Studio installation path (eg some place under `Program Files`) * `Major`: Visual Studio major version (eg `15` for VS 2017) * `Minor`: Visual Studio minor version -* `MsBUild`: the absolute path to the MsBuild executable +* `MsBuild`: the absolute path to the MsBuild executable #### GetUmbracoVersion diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 0e79851c0b..1526c54656 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -29,4 +29,4 @@ Don't rest on your laurels and never accept the status quo. Contribute and give ## Friendly -Don’t judge upon mistakes made but rather upon the speed and quality with which mistakes are corrected. Friendly posts and contributions generate smiles and builds long lasting relationships. \ No newline at end of file +Don’t judge upon mistakes made but rather upon the speed and quality with which mistakes are corrected. Friendly posts and contributions generate smiles and build long lasting relationships. \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 84115b946a..2679eaa411 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,15 +2,15 @@ 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 -The following is a set of guidelines for contributing to Umbraco CMS. +The following is a set of guidelines, for contributing to Umbraco CMS. -These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. +These are mostly guidelines, not rules. Use your best judgement, and feel free to propose changes to this document in a pull request. Remember, we're a friendly bunch and are happy with whatever contribution you might provide. Below are guidelines for success that we've gathered over the years. If you choose to ignore them then we still love you 💖. **Code of conduct** -This project and everyone participating in it is governed by the [our Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Sebastiaan Janssen - sj@umbraco.dk](mailto:sj@umbraco.dk). +This project and everyone participating in it, is governed by the [our Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Sebastiaan Janssen - sj@umbraco.dk](mailto:sj@umbraco.dk). **Table of contents** @@ -38,11 +38,11 @@ This document gives you a quick overview on how to get started. ### Guidelines for contributions we welcome -Not all changes are wanted, so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valuable time. +Not all changes are wanted, so on occasion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valuable time. -We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes. +We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes, so we can ensure that you don't put all your hard work into something we would not be able to merge. -Remember, if an issue is in the `Up for grabs` list or you've asked for some feedback before you sent us a PR, your PR will not be closed as unwanted. +Remember, it is always worth working on an issue from the `Up for grabs` list or even asking for some feedback before you send us a PR. This way, your PR will not be closed as unwanted. ### What can I start with? @@ -64,32 +64,32 @@ Great question! The short version goes like this: * **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions) * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/dev`, create a new branch first. * **Push** - great, now you can push the changes up to your fork on GitHub - * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. + * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here] (https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. ![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, linking your pull request to an issue on the [issue tracker,](https://github.com/umbraco/Umbraco-CMS/issues) if applicable. * Include screenshots and animated GIFs in your pull request whenever possible. - * Unit tests, while optional are awesome, thank you! - * New code is commented with documentation from which [the reference documentation](https://our.umbraco.com/documentation/Reference/) is generated + * Unit tests, while optional, are awesome. Thank you! + * New code is commented with documentation from which [the reference documentation](https://our.umbraco.com/documentation/Reference/) is generated. -Again, these are guidelines, not strict requirements. +Again, these are guidelines, not strict requirements. However, the more information that you give to us, the more we have to work with when considering your contributions. Good documentation of a pull request can really speed up the time it takes to review and merge your work! ## Reviews -You've sent us your first contribution, congratulations! Now what? +You've sent us your first contribution - congratulations! Now what? -The [pull request team](#the-pr-team) can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request you to make some additional changes. +The [pull request team](#the-pr-team) can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request that you make some additional changes. We have [a process in place which you can read all about](REVIEW_PROCESS.md). The very abbreviated version is: - Your PR will get a reply within 48 hours - An in-depth reply will be added within at most 2 weeks - The PR will be either merged or rejected within at most 4 weeks -- Sometimes it is difficult to meet these timelines and we'll talk to you +- Sometimes it is difficult to meet these timelines and we'll talk to you if this is the case. ### Styleguides @@ -99,21 +99,21 @@ That said, the Umbraco development team likes to follow the hints that ReSharper ### The PR team -The pull request team consists of a member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members +The pull request team consists of one member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members who have comitted to volunteering their free time: - [Anders Bjerner](https://github.com/abjerner) - [Dave Woestenborghs](https://github.com/dawoe) - [Emma Burstow](https://github.com/emmaburstow) - [Poornima Nayar](https://github.com/poornimanayar) -These wonderful volunteers will provide you with a first reply to your PR, review and test out your changes and might ask more questions. After that they'll let Umbraco HQ know if everything seems okay. +These wonderful people aim to provide you with a first reply to your PR, review and test out your changes and on occasions, they might ask more questions. If they are happy with your work, they'll let Umbraco HQ know by approving the PR. Hq will have final sign-off and will check the work again before it is merged. ### Questions? -You can get in touch with [the PR team](#the-pr-team) in multiple ways, we love open conversations and we are a friendly bunch. No question you have is stupid. Any questions you have usually helps out multiple people with the same question. Ask away: +You can get in touch with [the PR team](#the-pr-team) in multiple ways; we love open conversations and we are a friendly bunch. No question you have is stupid. Any question you have usually helps out multiple people with the same question. Ask away: -- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward -- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.com/forum/contributing-to-umbraco-cms/) forum, the team monitors that one closely +- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward. +- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.com/forum/contributing-to-umbraco-cms/) forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction. ## Working with the code @@ -125,19 +125,19 @@ In order to build the Umbraco source code locally, first make sure you have the * Node v10+ * npm v6.4.1+ -The easiest way to get started is to run `build.ps1` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details. +The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2017 (version 15.9.7 or higher, [the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile. -Alternatively, you can open `src\umbraco.sln` in Visual Studio 2017 (version 15.9.7 or higher, [the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile. +Alternatively, you can run `build.ps1` from the Powershell command line, which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details. ![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 +146,34 @@ There's two big areas that you should know about: npm install npm run build ``` + The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to disable caching in the browser to help you to see the changes you're making. + 2. "The rest" is a C# based codebase, which is mostly ASP.NET MVC based. You can make changes, build them in Visual Studio, and hit `F5` to see the result. -To find the general areas of something you're looking to fix or improve, have a look at the following two parts of the API documentation. +To find the general areas for something you're looking to fix or improve, have a look at the following two parts of the API documentation. * [The AngularJS based backoffice files](https://our.umbraco.com/apidocs/ui/#/api) (to be found in `src\Umbraco.Web.UI.Client\src`) * [The C# application](https://our.umbraco.com/apidocs/csharp/) ### Which branch should I target for my contributions? -We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/dev`. Whatever the default is, that's where we'd like you to target your contributions. +We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/dev`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'. + +Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise. ![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 @@ -185,3 +189,7 @@ git rebase upstream/v8/dev In this command we're syncing with the `v8/dev` branch, but you can of course choose another one if needed. (More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated)) + +### And finally + +We welcome all kinds of contributions to this repository. If you don't feel you'd like to make code changes here, you can visit our [documentation repository](https://github.com/umbraco/UmbracoDocs) and use your experience to contribute to making the docs we have, even better. We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the PR team and HQ. So, if you see something on the issue tracker or pull requests you feel you can add to, please don't be shy. diff --git a/.github/CONTRIBUTION_GUIDELINES.md b/.github/CONTRIBUTION_GUIDELINES.md index 7d2afb46bf..0ac35e6897 100644 --- a/.github/CONTRIBUTION_GUIDELINES.md +++ b/.github/CONTRIBUTION_GUIDELINES.md @@ -13,7 +13,7 @@ We’re usually able to handle small PRs pretty quickly. A community volunteer w 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 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. +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.). @@ -30,6 +30,6 @@ It is highly recommended that you speak to the HQ before making large, complex c ### 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 to fix bugs. +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/README.md b/.github/README.md index bdf9ef9f67..d6d978c3d6 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,4 +1,4 @@ -# [Umbraco CMS](https://umbraco.com) · [![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/dev)](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/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index b7bfaaff5b..97e9ef3df2 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -30,7 +30,6 @@ - diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt index 2f52d03776..ba88f808c0 100644 --- a/build/NuSpecs/tools/ReadmeUpgrade.txt +++ b/build/NuSpecs/tools/ReadmeUpgrade.txt @@ -26,6 +26,6 @@ The following items will now be automatically included when creating a deploy pa system: umbraco, config\splashes and global.asax. Please read the release notes on our.umbraco.com: -http://our.umbraco.com/contribute/releases +https://our.umbraco.com/contribute/releases - Umbraco diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index f0bfb01585..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 @@ - + diff --git a/src/Umbraco.Abstractions/Cache/CacheKeys.cs b/src/Umbraco.Abstractions/Cache/CacheKeys.cs index e8f93d636a..b8ee0e97c4 100644 --- a/src/Umbraco.Abstractions/Cache/CacheKeys.cs +++ b/src/Umbraco.Abstractions/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.Abstractions/Configuration/Grid/IGridEditorConfig.cs b/src/Umbraco.Abstractions/Configuration/Grid/IGridEditorConfig.cs index 4f35a16cbe..0edd2f10c5 100644 --- a/src/Umbraco.Abstractions/Configuration/Grid/IGridEditorConfig.cs +++ b/src/Umbraco.Abstractions/Configuration/Grid/IGridEditorConfig.cs @@ -5,6 +5,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.Abstractions/Models/ContentEditing/ContentApp.cs b/src/Umbraco.Abstractions/Models/ContentEditing/ContentApp.cs index bf28c28c9e..64e4b41186 100644 --- a/src/Umbraco.Abstractions/Models/ContentEditing/ContentApp.cs +++ b/src/Umbraco.Abstractions/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/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/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index b55dc0ca18..eb2b3525a7 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -85,12 +85,7 @@ namespace Umbraco.Core /// ListView. /// public const string ListView = "Umbraco.ListView"; - - /// - /// Macro Container. - /// - public const string MacroContainer = "Umbraco.MacroContainer"; - + /// /// Media Picker. /// diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index d1012fb669..5da1062275 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Web.Hosting; using Umbraco.Core.Logging; @@ -65,7 +65,7 @@ namespace Umbraco.Core // a new process for the same application path var appPath = HostingEnvironment.ApplicationPhysicalPath; - var hash = (appId + ":::" + appPath).ToSHA1(); + var hash = (appId + ":::" + appPath).GenerateHash(); var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK"; _asyncLock = new AsyncLock(lockName); diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index e41a40e3d9..e62825101c 100644 --- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs @@ -343,16 +343,20 @@ namespace Umbraco.Core.Mapping if (ctor == null) return null; - if (_ctors.ContainsKey(sourceType)) + _ctors.AddOrUpdate(sourceType, sourceCtor, (k, v) => { + // Add missing constructors foreach (var c in sourceCtor) { - if (!_ctors[sourceType].TryGetValue(c.Key, out _)) - _ctors[sourceType].Add(c.Key, c.Value); - } - } - else - _ctors[sourceType] = sourceCtor; + if (!v.ContainsKey(c.Key)) + { + v.Add(c.Key, c.Value); + } + } + + return v; + }); + return ctor; } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 7d5c05d584..94d8cfbc62 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -227,8 +227,8 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs index 7b2daa99ef..95b272dcb4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs @@ -74,9 +74,18 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 .From() .Where(x => x.NodeId == group.Key)).First(); + // check for duplicate aliases + var aliases = group.Select(x => x.Alias).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + if (aliases.Distinct().Count() != aliases.Length) + throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataType.NodeId}, editor={dataType.EditorAlias}: duplicate alias."); + + // handle null/empty aliases + int index = 0; + var dictionary = group.ToDictionary(x => string.IsNullOrWhiteSpace(x.Alias) ? index++.ToString() : x.Alias); + // migrate the preValues to configuration var migrator = _preValueMigrators.GetMigrator(dataType.EditorAlias) ?? new DefaultPreValueMigrator(); - var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, group.ToDictionary(x => x.Alias, x => x)); + var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, dictionary); var json = JsonConvert.SerializeObject(config); // validate - and kill the migration if it fails diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs index 7112679de2..0c8161c9ef 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs @@ -24,8 +24,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes } // assuming we don't want to fall back to array - if (aliases.Length != preValuesA.Count || aliases.Any(string.IsNullOrWhiteSpace)) - throw new InvalidOperationException($"Cannot migrate datatype w/ id={dataTypeId} preValues: duplicate or null/empty alias."); + if (aliases.Any(string.IsNullOrWhiteSpace)) + throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataTypeId}, editor={editorAlias}: null/empty alias."); // dictionary-base prevalues return GetPreValues(preValuesA).ToDictionary(x => x.Alias, GetPreValueValue); diff --git a/src/Umbraco.Core/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/Entities/IMemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs new file mode 100644 index 0000000000..050a999cc2 --- /dev/null +++ b/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Core.Models.Entities +{ + public interface IMemberEntitySlim : IContentEntitySlim + { + + } +} diff --git a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs new file mode 100644 index 0000000000..335e269467 --- /dev/null +++ b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Models.Entities +{ + public class MemberEntitySlim : EntitySlim, IMemberEntitySlim + { + public string ContentTypeAlias { get; set; } + + /// + public string ContentTypeIcon { get; set; } + + /// + public string ContentTypeThumbnail { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index cf7df4fb86..e00ac4ba15 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -67,7 +67,7 @@ namespace Umbraco.Core.Models if (user.Avatar.IsNullOrWhiteSpace()) { - var gravatarHash = user.Email.ToMd5(); + var gravatarHash = user.Email.GenerateHash(); var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404"; //try Gravatar diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 59b5ffc407..78ad60f763 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -1,15 +1,504 @@ -using Newtonsoft.Json; -using System; +using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; +using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; +using System.Xml; +using Newtonsoft.Json; +using Umbraco.Core.Collections; namespace Umbraco.Core { + /// + /// Provides object extension methods. + /// public static class ObjectExtensions { private static readonly ConcurrentDictionary> ToObjectTypes = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary NullableGenericCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary InputTypeConverterCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary DestinationTypeConverterCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary AssignableTypeCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary BoolConvertCache = new ConcurrentDictionary(); + + private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' }; + private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new CustomBooleanTypeConverter(); + + //private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>(); + + /// + /// + /// + /// + /// + /// + public static IEnumerable AsEnumerableOfOne(this T input) + { + return Enumerable.Repeat(input, 1); + } + + /// + /// + /// + /// + public static void DisposeIfDisposable(this object input) + { + if (input is IDisposable disposable) + disposable.Dispose(); + } + + /// + /// Provides a shortcut way of safely casting an input when you cannot guarantee the is + /// an instance type (i.e., when the C# AS keyword is not applicable). + /// + /// + /// The input. + /// + internal static T SafeCast(this object input) + { + if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) return default; + if (input is T variable) return variable; + return default; + } + + /// + /// Attempts to convert the input object to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The type to convert to + /// The input. + /// The + public static Attempt TryConvertTo(this object input) + { + var result = TryConvertTo(input, typeof(T)); + + if (result.Success) + return Attempt.Succeed((T)result.Result); + + // just try to cast + try + { + return Attempt.Succeed((T)input); + } + catch (Exception e) + { + return Attempt.Fail(e); + } + } + + /// + /// Attempts to convert the input object to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The input. + /// The type to convert to + /// The + public static Attempt TryConvertTo(this object input, Type target) + { + if (target == null) + { + return Attempt.Fail(); + } + + try + { + if (input == null) + { + // Nullable is ok + if (target.IsGenericType && GetCachedGenericNullableType(target) != null) + { + return Attempt.Succeed(null); + } + + // Reference types are ok + return Attempt.If(target.IsValueType == false, null); + } + + var inputType = input.GetType(); + + // Easy + if (target == typeof(object) || inputType == target) + { + return Attempt.Succeed(input); + } + + // Check for string so that overloaders of ToString() can take advantage of the conversion. + if (target == typeof(string)) + { + return Attempt.Succeed(input.ToString()); + } + + // If we've got a nullable of something, we try to convert directly to that thing. + // We cache the destination type and underlying nullable types + // Any other generic types need to fall through + if (target.IsGenericType) + { + var underlying = GetCachedGenericNullableType(target); + if (underlying != null) + { + // Special case for empty strings for bools/dates which should return null if an empty string. + if (input is string inputString) + { + // TODO: Why the check against only bool/date when a string is null/empty? In what scenario can we convert to another type when the string is null or empty other than just being null? + if (string.IsNullOrEmpty(inputString) && (underlying == typeof(DateTime) || underlying == typeof(bool))) + { + return Attempt.Succeed(null); + } + } + + // Recursively call into this method with the inner (not-nullable) type and handle the outcome + var inner = input.TryConvertTo(underlying); + + // And if successful, fall on through to rewrap in a nullable; if failed, pass on the exception + if (inner.Success) + { + input = inner.Result; // Now fall on through... + } + else + { + return Attempt.Fail(inner.Exception); + } + } + } + else + { + // target is not a generic type + + if (input is string inputString) + { + // Try convert from string, returns an Attempt if the string could be + // processed (either succeeded or failed), else null if we need to try + // other methods + var result = TryConvertToFromString(inputString, target); + if (result.HasValue) + { + return result.Value; + } + } + + // TODO: Do a check for destination type being IEnumerable and source type implementing IEnumerable with + // the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it. + if (GetCachedCanAssign(input, inputType, target)) + { + return Attempt.Succeed(Convert.ChangeType(input, target)); + } + } + + if (target == typeof(bool)) + { + if (GetCachedCanConvertToBoolean(inputType)) + { + return Attempt.Succeed(CustomBooleanTypeConverter.ConvertFrom(input)); + } + } + + var inputConverter = GetCachedSourceTypeConverter(inputType, target); + if (inputConverter != null) + { + return Attempt.Succeed(inputConverter.ConvertTo(input, target)); + } + + var outputConverter = GetCachedTargetTypeConverter(inputType, target); + if (outputConverter != null) + { + return Attempt.Succeed(outputConverter.ConvertFrom(input)); + } + + if (target.IsGenericType && GetCachedGenericNullableType(target) != null) + { + // cannot Convert.ChangeType as that does not work with nullable + // input has already been converted to the underlying type - just + // return input, there's an implicit conversion from T to T? anyways + return Attempt.Succeed(input); + } + + // Re-check convertibles since we altered the input through recursion + if (input is IConvertible convertible2) + { + return Attempt.Succeed(Convert.ChangeType(convertible2, target)); + } + } + catch (Exception e) + { + return Attempt.Fail(e); + } + + return Attempt.Fail(); + } + + /// + /// Attempts to convert the input string to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The input. + /// The type to convert to + /// The + private static Attempt? TryConvertToFromString(this string input, Type target) + { + // Easy + if (target == typeof(string)) + { + return Attempt.Succeed(input); + } + + // Null, empty, whitespaces + if (string.IsNullOrWhiteSpace(input)) + { + if (target == typeof(bool)) + { + // null/empty = bool false + return Attempt.Succeed(false); + } + + if (target == typeof(DateTime)) + { + // null/empty = min DateTime value + return Attempt.Succeed(DateTime.MinValue); + } + + // Cannot decide here, + // Any of the types below will fail parsing and will return a failed attempt + // but anything else will not be processed and will return null + // so even though the string is null/empty we have to proceed. + } + + // Look for type conversions in the expected order of frequency of use. + // + // By using a mixture of ordered if statements and switches we can optimize both for + // fast conditional checking for most frequently used types and the branching + // that does not depend on previous values available to switch statements. + if (target.IsPrimitive) + { + if (target == typeof(int)) + { + if (int.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Because decimal 100.01m will happily convert to integer 100, it + // makes sense that string "100.01" *also* converts to integer 100. + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt32(value2)); + } + + if (target == typeof(long)) + { + if (long.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Same as int + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2)); + } + + // TODO: Should we do the decimal trick for short, byte, unsigned? + + if (target == typeof(bool)) + { + if (bool.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Don't declare failure so the CustomBooleanTypeConverter can try + return null; + } + + // Calling this method directly is faster than any attempt to cache it. + switch (Type.GetTypeCode(target)) + { + case TypeCode.Int16: + return Attempt.If(short.TryParse(input, out var value), value); + + case TypeCode.Double: + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(double.TryParse(input2, out var valueD), valueD); + + case TypeCode.Single: + var input3 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(float.TryParse(input3, out var valueF), valueF); + + case TypeCode.Char: + return Attempt.If(char.TryParse(input, out var valueC), valueC); + + case TypeCode.Byte: + return Attempt.If(byte.TryParse(input, out var valueB), valueB); + + case TypeCode.SByte: + return Attempt.If(sbyte.TryParse(input, out var valueSb), valueSb); + + case TypeCode.UInt32: + return Attempt.If(uint.TryParse(input, out var valueU), valueU); + + case TypeCode.UInt16: + return Attempt.If(ushort.TryParse(input, out var valueUs), valueUs); + + case TypeCode.UInt64: + return Attempt.If(ulong.TryParse(input, out var valueUl), valueUl); + } + } + else if (target == typeof(Guid)) + { + return Attempt.If(Guid.TryParse(input, out var value), value); + } + else if (target == typeof(DateTime)) + { + if (DateTime.TryParse(input, out var value)) + { + switch (value.Kind) + { + case DateTimeKind.Unspecified: + case DateTimeKind.Utc: + return Attempt.Succeed(value); + + case DateTimeKind.Local: + return Attempt.Succeed(value.ToUniversalTime()); + + default: + throw new ArgumentOutOfRangeException(); + } + } + + return Attempt.Fail(); + } + else if (target == typeof(DateTimeOffset)) + { + return Attempt.If(DateTimeOffset.TryParse(input, out var value), value); + } + else if (target == typeof(TimeSpan)) + { + return Attempt.If(TimeSpan.TryParse(input, out var value), value); + } + else if (target == typeof(decimal)) + { + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value), value); + } + else if (input != null && target == typeof(Version)) + { + return Attempt.If(Version.TryParse(input, out var value), value); + } + + // E_NOTIMPL IPAddress, BigInteger + return null; // we can't decide... + } + internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname) + { + // TODO: Localize this exception + if (isDisposed) + throw new ObjectDisposedException(objectname); + } + + //public enum PropertyNamesCaseType + //{ + // CamelCase, + // CaseInsensitive + //} + + ///// + ///// Convert an object to a JSON string with camelCase formatting + ///// + ///// + ///// + //public static string ToJsonString(this object obj) + //{ + // return obj.ToJsonString(PropertyNamesCaseType.CamelCase); + //} + + ///// + ///// Convert an object to a JSON string with the specified formatting + ///// + ///// The obj. + ///// Type of the property names case. + ///// + //public static string ToJsonString(this object obj, PropertyNamesCaseType propertyNamesCaseType) + //{ + // var type = obj.GetType(); + // var dateTimeStyle = "yyyy-MM-dd HH:mm:ss"; + + // if (type.IsPrimitive || typeof(string).IsAssignableFrom(type)) + // { + // return obj.ToString(); + // } + + // if (typeof(DateTime).IsAssignableFrom(type) || typeof(DateTimeOffset).IsAssignableFrom(type)) + // { + // return Convert.ToDateTime(obj).ToString(dateTimeStyle); + // } + + // var serializer = new JsonSerializer(); + + // switch (propertyNamesCaseType) + // { + // case PropertyNamesCaseType.CamelCase: + // serializer.ContractResolver = new CamelCasePropertyNamesContractResolver(); + // break; + // } + + // var dateTimeConverter = new IsoDateTimeConverter + // { + // DateTimeStyles = System.Globalization.DateTimeStyles.None, + // DateTimeFormat = dateTimeStyle + // }; + + // if (typeof(IDictionary).IsAssignableFrom(type)) + // { + // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + // } + + // if (type.IsArray || (typeof(IEnumerable).IsAssignableFrom(type))) + // { + // return JArray.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + // } + + // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + //} + + /// + /// Converts an object into a dictionary + /// + /// + /// + /// + /// + /// + /// + public static IDictionary ToDictionary(this T o, params Expression>[] ignoreProperties) + { + return o.ToDictionary(ignoreProperties.Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray()); + } + + /// + /// Turns object into dictionary + /// + /// + /// Properties to ignore + /// + public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) + { + if (o != null) + { + var props = TypeDescriptor.GetProperties(o); + var d = new Dictionary(); + foreach (var prop in props.Cast().Where(x => ignoreProperties.Contains(x.Name) == false)) + { + var val = prop.GetValue(o); + if (val != null) + { + d.Add(prop.Name, (TVal)val); + } + } + return d; + } + return new Dictionary(); + } /// /// Converts an object's properties into a dictionary. @@ -17,12 +506,10 @@ namespace Umbraco.Core /// The object to convert. /// A property namer function. /// A dictionary containing each properties. - public static Dictionary ToObjectDictionary(this T obj, Func namer = null) + public static Dictionary ToObjectDictionary(T obj, Func namer = null) { if (obj == null) return new Dictionary(); - //fixme: This has a hard reference to Newtonsoft - string DefaultNamer(PropertyInfo property) { var jsonProperty = property.GetCustomAttribute(); @@ -43,7 +530,264 @@ namespace Umbraco.Core ToObjectTypes[t] = properties; } - return properties.ToDictionary(x => x.Key, x => ((Func)x.Value)(obj)); + return properties.ToDictionary(x => x.Key, x => ((Func) x.Value)(obj)); } + + internal static string ToDebugString(this object obj, int levels = 0) + { + if (obj == null) return "{null}"; + try + { + if (obj is string) + { + return "\"{0}\"".InvariantFormat(obj); + } + 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); + } + if (obj is Enum) + { + return "[{0}]".InvariantFormat(obj); + } + if (obj is IEnumerable) + { + var enumerable = (obj as IEnumerable); + + var items = (from object enumItem in enumerable let value = GetEnumPropertyDebugString(enumItem, levels) where value != null select value).Take(10).ToList(); + + return items.Any() + ? "{{ {0} }}".InvariantFormat(string.Join(", ", items)) + : null; + } + + var props = obj.GetType().GetProperties(); + if ((props.Length == 2) && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) + { + try + { + var key = props[0].GetValue(obj, null) as string; + var value = props[1].GetValue(obj, null).ToDebugString(levels - 1); + return "{0}={1}".InvariantFormat(key, value); + } + catch (Exception) + { + return "[KeyValuePropertyException]"; + } + } + if (levels > -1) + { + var items = + (from propertyInfo in props + let value = GetPropertyDebugString(propertyInfo, obj, levels) + where value != null + select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray(); + + return items.Any() + ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, String.Join(", ", items)) + : null; + } + } + catch (Exception ex) + { + return "[Exception:{0}]".InvariantFormat(ex.Message); + } + return null; + } + + /// + /// Attempts to serialize the value to an XmlString using ToXmlString + /// + /// + /// + /// + internal static Attempt TryConvertToXmlString(this object value, Type type) + { + try + { + var output = value.ToXmlString(type); + return Attempt.Succeed(output); + } + catch (NotSupportedException ex) + { + return Attempt.Fail(ex); + } + } + + /// + /// Returns an XmlSerialized safe string representation for the value + /// + /// + /// The Type can only be a primitive type or Guid and byte[] otherwise an exception is thrown + /// + internal static string ToXmlString(this object value, Type type) + { + if (value == null) return string.Empty; + if (type == typeof(string)) return (value.ToString().IsNullOrWhiteSpace() ? "" : value.ToString()); + if (type == typeof(bool)) return XmlConvert.ToString((bool)value); + if (type == typeof(byte)) return XmlConvert.ToString((byte)value); + if (type == typeof(char)) return XmlConvert.ToString((char)value); + if (type == typeof(DateTime)) return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.Unspecified); + if (type == typeof(DateTimeOffset)) return XmlConvert.ToString((DateTimeOffset)value); + if (type == typeof(decimal)) return XmlConvert.ToString((decimal)value); + if (type == typeof(double)) return XmlConvert.ToString((double)value); + if (type == typeof(float)) return XmlConvert.ToString((float)value); + if (type == typeof(Guid)) return XmlConvert.ToString((Guid)value); + if (type == typeof(int)) return XmlConvert.ToString((int)value); + if (type == typeof(long)) return XmlConvert.ToString((long)value); + if (type == typeof(sbyte)) return XmlConvert.ToString((sbyte)value); + if (type == typeof(short)) return XmlConvert.ToString((short)value); + if (type == typeof(TimeSpan)) return XmlConvert.ToString((TimeSpan)value); + if (type == typeof(bool)) return XmlConvert.ToString((bool)value); + if (type == typeof(uint)) return XmlConvert.ToString((uint)value); + if (type == typeof(ulong)) return XmlConvert.ToString((ulong)value); + if (type == typeof(ushort)) return XmlConvert.ToString((ushort)value); + + throw new NotSupportedException("Cannot convert type " + type.FullName + " to a string using ToXmlString as it is not supported by XmlConvert"); + } + + /// + /// Returns an XmlSerialized safe string representation for the value and type + /// + /// + /// + /// + internal static string ToXmlString(this object value) + { + return value.ToXmlString(typeof (T)); + } + + private static string GetEnumPropertyDebugString(object enumItem, int levels) + { + try + { + return enumItem.ToDebugString(levels - 1); + } + catch (Exception) + { + return "[GetEnumPartException]"; + } + } + + private static string GetPropertyDebugString(PropertyInfo propertyInfo, object obj, int levels) + { + try + { + return propertyInfo.GetValue(obj, null).ToDebugString(levels - 1); + } + catch (Exception) + { + return "[GetPropertyValueException]"; + } + } + + internal static Guid AsGuid(this object value) + { + return value is Guid guid ? guid : Guid.Empty; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string NormalizeNumberDecimalSeparator(string s) + { + var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; + return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); + } + + // gets a converter for source, that can convert to target, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TypeConverter GetCachedSourceTypeConverter(Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + + if (InputTypeConverterCache.TryGetValue(key, out var typeConverter)) + { + return typeConverter; + } + + var converter = TypeDescriptor.GetConverter(source); + if (converter.CanConvertTo(target)) + { + return InputTypeConverterCache[key] = converter; + } + + return InputTypeConverterCache[key] = null; + } + + // gets a converter for target, that can convert from source, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TypeConverter GetCachedTargetTypeConverter(Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + + if (DestinationTypeConverterCache.TryGetValue(key, out var typeConverter)) + { + return typeConverter; + } + + var converter = TypeDescriptor.GetConverter(target); + if (converter.CanConvertFrom(source)) + { + return DestinationTypeConverterCache[key] = converter; + } + + return DestinationTypeConverterCache[key] = null; + } + + // gets the underlying type of a nullable type, or null if the type is not nullable + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Type GetCachedGenericNullableType(Type type) + { + if (NullableGenericCache.TryGetValue(type, out var underlyingType)) + { + return underlyingType; + } + + if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + Type underlying = Nullable.GetUnderlyingType(type); + return NullableGenericCache[type] = underlying; + } + + return NullableGenericCache[type] = null; + } + + // gets an IConvertible from source to target type, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool GetCachedCanAssign(object input, Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + if (AssignableTypeCache.TryGetValue(key, out var canConvert)) + { + return canConvert; + } + + // "object is" is faster than "Type.IsAssignableFrom. + // We can use it to very quickly determine whether true/false + if (input is IConvertible && target.IsAssignableFrom(source)) + { + return AssignableTypeCache[key] = true; + } + + return AssignableTypeCache[key] = false; + } + + // determines whether a type can be converted to boolean + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool GetCachedCanConvertToBoolean(Type type) + { + if (BoolConvertCache.TryGetValue(type, out var result)) + { + return result; + } + + if (CustomBooleanTypeConverter.CanConvertFrom(type)) + { + return BoolConvertCache[type] = true; + } + + return BoolConvertCache[type] = false; + } + + } } diff --git a/src/Umbraco.Core/Persistence/BulkDataReader.cs b/src/Umbraco.Core/Persistence/BulkDataReader.cs index 1eaa88ee88..7dbe74922a 100644 --- a/src/Umbraco.Core/Persistence/BulkDataReader.cs +++ b/src/Umbraco.Core/Persistence/BulkDataReader.cs @@ -470,7 +470,7 @@ namespace Umbraco.Core.Persistence break; case SqlDbType.SmallInt: - dataType = typeof(Int16); + dataType = typeof(short); dataTypeName = "smallint"; break; @@ -688,34 +688,34 @@ namespace Umbraco.Core.Persistence DataColumnCollection columns = _schemaTable.Columns; - columns.Add(SchemaTableColumn.ColumnName, typeof(System.String)); - columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(System.Int32)); - columns.Add(SchemaTableColumn.ColumnSize, typeof(System.Int32)); - columns.Add(SchemaTableColumn.NumericPrecision, typeof(System.Int16)); - columns.Add(SchemaTableColumn.NumericScale, typeof(System.Int16)); - columns.Add(SchemaTableColumn.IsUnique, typeof(System.Boolean)); - columns.Add(SchemaTableColumn.IsKey, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(System.String)); - columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(System.String)); - columns.Add(SchemaTableColumn.BaseColumnName, typeof(System.String)); - columns.Add(SchemaTableColumn.BaseSchemaName, typeof(System.String)); - columns.Add(SchemaTableColumn.BaseTableName, typeof(System.String)); - columns.Add(SchemaTableColumn.DataType, typeof(System.Type)); - columns.Add(SchemaTableColumn.AllowDBNull, typeof(System.Boolean)); - columns.Add(SchemaTableColumn.ProviderType, typeof(System.Int32)); - columns.Add(SchemaTableColumn.IsAliased, typeof(System.Boolean)); - columns.Add(SchemaTableColumn.IsExpression, typeof(System.Boolean)); - columns.Add(BulkDataReader.IsIdentitySchemaColumn, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(System.Boolean)); - columns.Add(SchemaTableColumn.IsLong, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(System.Type)); - columns.Add(BulkDataReader.DataTypeNameSchemaColumn, typeof(System.String)); - columns.Add(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, typeof(System.String)); - columns.Add(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(System.String)); - columns.Add(BulkDataReader.XmlSchemaCollectionNameSchemaColumn, typeof(System.String)); + columns.Add(SchemaTableColumn.ColumnName, typeof(string)); + columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int)); + columns.Add(SchemaTableColumn.ColumnSize, typeof(int)); + columns.Add(SchemaTableColumn.NumericPrecision, typeof(short)); + columns.Add(SchemaTableColumn.NumericScale, typeof(short)); + columns.Add(SchemaTableColumn.IsUnique, typeof(bool)); + columns.Add(SchemaTableColumn.IsKey, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string)); + columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string)); + columns.Add(SchemaTableColumn.BaseColumnName, typeof(string)); + columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string)); + columns.Add(SchemaTableColumn.BaseTableName, typeof(string)); + columns.Add(SchemaTableColumn.DataType, typeof(Type)); + columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool)); + columns.Add(SchemaTableColumn.ProviderType, typeof(int)); + columns.Add(SchemaTableColumn.IsAliased, typeof(bool)); + columns.Add(SchemaTableColumn.IsExpression, typeof(bool)); + columns.Add(BulkDataReader.IsIdentitySchemaColumn, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool)); + columns.Add(SchemaTableColumn.IsLong, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(Type)); + columns.Add(BulkDataReader.DataTypeNameSchemaColumn, typeof(string)); + columns.Add(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, typeof(string)); + columns.Add(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(string)); + columns.Add(BulkDataReader.XmlSchemaCollectionNameSchemaColumn, typeof(string)); } #endregion @@ -1090,7 +1090,7 @@ namespace Umbraco.Core.Persistence /// public decimal GetDecimal(int i) { - return (Decimal)GetValue(i); + return (decimal)GetValue(i); } /// diff --git a/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs b/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs deleted file mode 100644 index 48edee3c94..0000000000 --- a/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Runtime.CompilerServices; - -namespace Umbraco.Core.Persistence -{ - internal static class DatabaseNodeLockExtensions - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ValidateDatabase(IUmbracoDatabase database) - { - if (database == null) - throw new ArgumentNullException("database"); - if (database.GetCurrentTransactionIsolationLevel() < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - } - - // updating a record within a repeatable-read transaction gets an exclusive lock on - // that record which will be kept until the transaction is ended, effectively locking - // out all other accesses to that record - thus obtaining an exclusive lock over the - // protected resources. - public static void AcquireLockNodeWriteLock(this IUmbracoDatabase database, int nodeId) - { - ValidateDatabase(database); - - database.Execute("UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", - new { @id = nodeId }); - } - - // reading a record within a repeatable-read transaction gets a shared lock on - // that record which will be kept until the transaction is ended, effectively preventing - // other write accesses to that record - thus obtaining a shared lock over the protected - // resources. - public static void AcquireLockNodeReadLock(this IUmbracoDatabase database, int nodeId) - { - ValidateDatabase(database); - - database.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", - new { @id = nodeId }); - } - } -} diff --git a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs index afb419ebd6..3a44cb10b4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs @@ -7,5 +7,12 @@ namespace Umbraco.Core.Persistence.Repositories public interface IDataTypeRepository : IReadWriteQueryRepository { IEnumerable> Move(IDataType toMove, EntityContainer container); + + /// + /// Returns a dictionary of content type s and the property type aliases that use a + /// + /// + /// + IReadOnlyDictionary> FindUsages(int id); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 0aa2ea8bba..3e4b6b17d9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -217,6 +218,7 @@ AND umbracoNode.nodeObjectType = @objectType", protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) { + CorrectPropertyTypeVariations(entity); ValidateVariations(entity); var dto = ContentTypeFactory.BuildContentTypeDto(entity); @@ -409,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 @@ -454,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); } } @@ -511,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, @@ -523,23 +502,40 @@ 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) { - //if the entity does not vary at all, then the property cannot have a variance value greater than it - if (entity.Variations == ContentVariation.Nothing) + foreach (var prop in entity.PropertyTypes) { - foreach (var prop in entity.PropertyTypes) - { - if (prop.IsPropertyDirty(nameof(prop.Variations)) && prop.Variations > entity.Variations) - throw new InvalidOperationException($"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); - } - + // 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) @@ -660,27 +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); - RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); - break; - case ContentVariation.Nothing: - CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); - CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); - RenormalizeDocumentEditedFlags(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); } } } @@ -692,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 } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs index 45aa7603a3..4ec259b24d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -279,6 +279,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return moveInfo; } + public IReadOnlyDictionary> FindUsages(int id) + { + if (id == default) + return new Dictionary>(); + + var sql = Sql() + .Select(ct => ct.Select(node => node.NodeDto)) + .AndSelect(pt => Alias(pt.Alias, "ptAlias"), pt => Alias(pt.Name, "ptName")) + .From() + .InnerJoin().On(ct => ct.NodeId, pt => pt.ContentTypeId) + .InnerJoin().On(n => n.NodeId, ct => ct.NodeId) + .Where(pt => pt.DataTypeId == id) + .OrderBy(node => node.NodeId) + .AndBy(pt => pt.Alias); + + var dtos = Database.FetchOneToMany(ct => ct.PropertyTypes, sql); + + return dtos.ToDictionary( + x => (Udi)new GuidUdi(ObjectTypes.GetUdiType(x.NodeDto.NodeObjectType.Value), x.NodeDto.UniqueId).EnsureClosed(), + x => (IEnumerable)x.PropertyTypes.Select(p => p.Alias).ToList()); + } + private string EnsureUniqueNodeName(string nodeName, int id = 0) { var template = SqlContext.Templates.Get("Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName", tsql => tsql @@ -291,5 +313,24 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return SimilarNodeName.GetUniqueName(names, id, nodeName); } + + + [TableName(Constants.DatabaseSchema.Tables.ContentType)] + private class ContentTypeReferenceDto : ContentTypeDto + { + [ResultColumn] + [Reference(ReferenceType.Many)] + public List PropertyTypes { get; set; } + } + + [TableName(Constants.DatabaseSchema.Tables.PropertyType)] + private class PropertyTypeReferenceDto + { + [Column("ptAlias")] + public string Alias { get; set; } + + [Column("ptName")] + public string Name { get; set; } + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 344557d815..2649b9993f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -248,14 +248,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 diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 3292f41d0c..09c427e230 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -41,8 +41,9 @@ 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 = GetBaseWhere(isContent, isMedia, false, x => + var sql = GetBaseWhere(isContent, isMedia, isMember, false, x => { if (filter == null) return; foreach (var filterClause in filter.GetWhereClauses()) @@ -53,7 +54,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement 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) { @@ -80,6 +81,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement dtos = page.Items; totalRecords = page.TotalItems; } + else if (isMember) + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } else { var page = Database.Page(pageIndexToFetch, pageSize, sql); @@ -87,7 +94,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement totalRecords = page.TotalItems; } - var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray(); + var entities = dtos.Select(x => BuildEntity(isContent, isMedia, isMember, x)).ToArray(); if (isContent) BuildVariants(entities.Cast()); @@ -97,13 +104,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement 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(false, false, false, 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) @@ -119,7 +126,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (dto == null) return null; - var entity = BuildEntity(false, isMedia, dto); + var entity = BuildEntity(false, isMedia, isMember, dto); return entity; } @@ -128,25 +135,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(false, false, false, 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) @@ -163,7 +172,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement : PerformGetAll(objectType); } - private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia) + private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool isMember) { //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) @@ -179,7 +188,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ? (IEnumerable)Database.Fetch(sql) : Database.Fetch(sql); - var entities = dtos.Select(x => BuildEntity(false, isMedia, x)).ToArray(); + var entities = dtos.Select(x => BuildEntity(false, isMedia, isMember, x)).ToArray(); return entities; } @@ -188,9 +197,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; - var sql = GetFullSqlForEntityType(isContent, isMedia, objectType, filter); - return GetEntities(sql, isContent, isMedia); + var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, filter); + return GetEntities(sql, isContent, isMedia, isMember); } public IEnumerable GetAllPaths(Guid objectType, params int[] ids) @@ -217,26 +227,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(x => BuildEntity(false, false, false, x)).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, objectType); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, sql, true); + sql = AddGroupBy(isContent, isMedia, isMember, sql, true); - return GetEntities(sql, isContent, isMedia); + return GetEntities(sql, isContent, isMedia, isMember); } public UmbracoObjectTypes GetObjectType(int id) @@ -328,29 +339,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); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, 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(); @@ -365,7 +376,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); @@ -386,7 +397,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql .From(); - if (isContent || isMedia) + if (isContent || isMedia || isMember) { sql .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) @@ -421,49 +432,49 @@ 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 objectType) { - return GetBase(isContent, isMedia, filter, isCount) + return GetBase(isContent, isMedia, isMember, filter, isCount) .Where(x => x.NodeObjectType == objectType); } // 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) @@ -482,7 +493,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } - 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); @@ -527,6 +538,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public string MediaPath { get; set; } } + private class MemberEntityDto : BaseDto + { + } + public class VariantInfoDto { public int NodeId { get; set; } @@ -573,12 +588,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Factory - private EntitySlim BuildEntity(bool isContent, bool isMedia, BaseDto dto) + private EntitySlim BuildEntity(bool isContent, bool isMedia, bool isMember, BaseDto dto) { if (isContent) return BuildDocumentEntity(dto); if (isMedia) return BuildMediaEntity(dto); + if (isMember) + return BuildMemberEntity(dto); // EntitySlim does not track changes var entity = new EntitySlim(); @@ -643,6 +660,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/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 1a8b2b8821..a905294417 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -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 diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 1605f4f672..0f7984a1bd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -133,7 +133,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); @@ -546,6 +546,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/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/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 55625ff04e..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; @@ -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); @@ -121,5 +127,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// 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 cb4b7a5176..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 @@ -152,6 +156,39 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() 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; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index fab7526a6b..3d0adf175e 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())"); @@ -246,6 +249,41 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) 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.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 1800;"); + 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); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 0c27ac2d50..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(); @@ -225,6 +227,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax 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; diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 51e0172f35..072813b4e6 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; @@ -54,7 +51,7 @@ 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; 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/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/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs index b86494adb5..51e5d756eb 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. @@ -40,6 +40,7 @@ namespace Umbraco.Core.Services int Count(); IEnumerable GetAll(params int[] ids); + IEnumerable GetAll(IEnumerable ids); IEnumerable GetDescendants(int id, bool andSelf); // parent-child axis IEnumerable GetComposedOf(int id); // composition axis diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index bb56e110cd..3ebfa95bfb 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -10,6 +10,13 @@ namespace Umbraco.Core.Services /// 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/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index ce63be9f2c..720713e9ad 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1848,7 +1848,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)); diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 6ac8e1404a..705a876d83 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()); } } diff --git a/src/Umbraco.Core/Services/Implement/DataTypeService.cs b/src/Umbraco.Core/Services/Implement/DataTypeService.cs index dc998b18dd..5a93fb91b1 100644 --- a/src/Umbraco.Core/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Core/Services/Implement/DataTypeService.cs @@ -466,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/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/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 77ffb10192..9bb61c7f2e 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -1,11 +1,16 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; using System.Web.Security; +using Newtonsoft.Json; +using Umbraco.Core.Configuration; using Umbraco.Core.Composing; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; @@ -13,8 +18,69 @@ using Umbraco.Core.Strings; namespace Umbraco.Core { + + /// + /// String extension methods + /// public static class StringExtensions { + + private static readonly char[] ToCSharpHexDigitLower = "0123456789abcdef".ToCharArray(); + private static readonly char[] ToCSharpEscapeChars; + + static StringExtensions() + { + var escapes = new[] { "\aa", "\bb", "\ff", "\nn", "\rr", "\tt", "\vv", "\"\"", "\\\\", "??", "\00" }; + ToCSharpEscapeChars = new char[escapes.Max(e => e[0]) + 1]; + foreach (var escape in escapes) + ToCSharpEscapeChars[escape[0]] = escape[1]; + } + + /// + /// Convert a path to node ids in the order from right to left (deepest to shallowest) + /// + /// + /// + internal static int[] GetIdsFromPathReversed(this string path) + { + var nodeIds = path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.TryConvertTo()) + .Where(x => x.Success) + .Select(x => x.Result) + .Reverse() + .ToArray(); + return nodeIds; + } + + /// + /// Removes new lines and tabs + /// + /// + /// + internal static string StripWhitespace(this string txt) + { + return Regex.Replace(txt, @"\s", string.Empty); + } + + internal static string StripFileExtension(this string fileName) + { + //filenames cannot contain line breaks + if (fileName.Contains(Environment.NewLine) || fileName.Contains("\r") || fileName.Contains("\n")) return fileName; + + var lastIndex = fileName.LastIndexOf('.'); + if (lastIndex > 0) + { + var ext = fileName.Substring(lastIndex); + //file extensions cannot contain whitespace + if (ext.Contains(" ")) return fileName; + + return string.Format("{0}", fileName.Substring(0, fileName.IndexOf(ext, StringComparison.Ordinal))); + } + return fileName; + + + } + /// /// 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 @@ -59,6 +125,27 @@ namespace Umbraco.Core return Attempt.Fail(input); } + /// + /// This tries to detect a json string, this is not a fail safe way but it is quicker than doing + /// a try/catch when deserializing when it is not json. + /// + /// + /// + public static bool DetectIsJson(this string input) + { + if (input.IsNullOrWhiteSpace()) return false; + input = input.Trim(); + return (input.StartsWith("{") && input.EndsWith("}")) + || (input.StartsWith("[") && input.EndsWith("]")); + } + + internal static readonly Lazy Whitespace = new Lazy(() => new Regex(@"\s+", RegexOptions.Compiled)); + internal static readonly string[] JsonEmpties = { "[]", "{}" }; + internal static bool DetectIsEmptyJson(this string input) + { + return JsonEmpties.Contains(Whitespace.Value.Replace(input, string.Empty)); + } + /// /// Returns a JObject/JArray instance if the string can be converted to json, otherwise returns the string /// @@ -81,6 +168,93 @@ namespace Umbraco.Core } } + internal static string ReplaceNonAlphanumericChars(this string input, string replacement) + { + //any character that is not alphanumeric, convert to a hyphen + var mName = input; + foreach (var c in mName.ToCharArray().Where(c => !char.IsLetterOrDigit(c))) + { + mName = mName.Replace(c.ToString(CultureInfo.InvariantCulture), replacement); + } + return mName; + } + + internal static string ReplaceNonAlphanumericChars(this string input, char replacement) + { + var inputArray = input.ToCharArray(); + var outputArray = new char[input.Length]; + for (var i = 0; i < inputArray.Length; i++) + outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement; + return new string(outputArray); + } + private static readonly char[] CleanForXssChars = "*?(){}[];:%<>/\\|&'\"".ToCharArray(); + + /// + /// Cleans string to aid in preventing xss attacks. + /// + /// + /// + /// + public static string CleanForXss(this string input, params char[] ignoreFromClean) + { + //remove any HTML + input = input.StripHtml(); + //strip out any potential chars involved with XSS + return input.ExceptChars(new HashSet(CleanForXssChars.Except(ignoreFromClean))); + } + + public static string ExceptChars(this string str, HashSet toExclude) + { + var sb = new StringBuilder(str.Length); + foreach (var c in str.Where(c => toExclude.Contains(c) == false)) + { + sb.Append(c); + } + return sb.ToString(); + } + + /// + /// Returns a stream from a string + /// + /// + /// + internal static Stream GenerateStreamFromString(this string s) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(s); + writer.Flush(); + stream.Position = 0; + return stream; + } + + /// + /// This will append the query string to the URL + /// + /// + /// + /// + /// + /// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are + /// delimited properly with '&' + /// + internal static string AppendQueryStringToUrl(this string url, params string[] queryStrings) + { + //remove any prefixed '&' or '?' + for (var i = 0; i < queryStrings.Length; i++) + { + queryStrings[i] = queryStrings[i].TrimStart('?', '&').TrimEnd('&'); + } + + var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); + + if (url.Contains("?")) + { + return url + string.Join("&", nonEmpty).EnsureStartsWith('&'); + } + return url + string.Join("&", nonEmpty).EnsureStartsWith('?'); + } + /// /// Encrypt the string using the MachineKey in medium trust /// @@ -140,6 +314,738 @@ namespace Umbraco.Core return decryptedValue.ToString(); } + //this is from SqlMetal and just makes it a bit of fun to allow pluralization + public static string MakePluralName(this string name) + { + if ((name.EndsWith("x", StringComparison.OrdinalIgnoreCase) || name.EndsWith("ch", StringComparison.OrdinalIgnoreCase)) || (name.EndsWith("s", StringComparison.OrdinalIgnoreCase) || name.EndsWith("sh", StringComparison.OrdinalIgnoreCase))) + { + name = name + "es"; + return name; + } + if ((name.EndsWith("y", StringComparison.OrdinalIgnoreCase) && (name.Length > 1)) && !IsVowel(name[name.Length - 2])) + { + name = name.Remove(name.Length - 1, 1); + name = name + "ies"; + return name; + } + if (!name.EndsWith("s", StringComparison.OrdinalIgnoreCase)) + { + name = name + "s"; + } + return name; + } + + public static bool IsVowel(this char c) + { + switch (c) + { + case 'O': + case 'U': + case 'Y': + case 'A': + case 'E': + case 'I': + case 'o': + case 'u': + case 'y': + case 'a': + case 'e': + case 'i': + return true; + } + return false; + } + + /// + /// Trims the specified value from a string; accepts a string input whereas the in-built implementation only accepts char or char[]. + /// + /// The value. + /// For removing. + /// + public static string Trim(this string value, string forRemoving) + { + if (string.IsNullOrEmpty(value)) return value; + return value.TrimEnd(forRemoving).TrimStart(forRemoving); + } + + public static string EncodeJsString(this string s) + { + var sb = new StringBuilder(); + foreach (var c in s) + { + switch (c) + { + case '\"': + sb.Append("\\\""); + break; + case '\\': + sb.Append("\\\\"); + break; + case '\b': + sb.Append("\\b"); + break; + case '\f': + sb.Append("\\f"); + break; + case '\n': + sb.Append("\\n"); + break; + case '\r': + sb.Append("\\r"); + break; + case '\t': + sb.Append("\\t"); + break; + default: + int i = (int)c; + if (i < 32 || i > 127) + { + sb.AppendFormat("\\u{0:X04}", i); + } + else + { + sb.Append(c); + } + break; + } + } + return sb.ToString(); + } + + public static string TrimEnd(this string value, string forRemoving) + { + if (string.IsNullOrEmpty(value)) return value; + if (string.IsNullOrEmpty(forRemoving)) return value; + + while (value.EndsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) + { + value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase)); + } + return value; + } + + public static string TrimStart(this string value, string forRemoving) + { + if (string.IsNullOrEmpty(value)) return value; + if (string.IsNullOrEmpty(forRemoving)) return value; + + while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) + { + value = value.Substring(forRemoving.Length); + } + return value; + } + + public static string EnsureStartsWith(this string input, string toStartWith) + { + if (input.StartsWith(toStartWith)) return input; + return toStartWith + input.TrimStart(toStartWith); + } + + public static string EnsureStartsWith(this string input, char value) + { + return input.StartsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : value + input; + } + + public static string EnsureEndsWith(this string input, char value) + { + return input.EndsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : input + value; + } + + public static string EnsureEndsWith(this string input, string toEndWith) + { + return input.EndsWith(toEndWith.ToString(CultureInfo.InvariantCulture)) ? input : input + toEndWith; + } + + public static bool IsLowerCase(this char ch) + { + return ch.ToString(CultureInfo.InvariantCulture) == ch.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(); + } + + public static bool IsUpperCase(this char ch) + { + return ch.ToString(CultureInfo.InvariantCulture) == ch.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(); + } + + /// Indicates whether a specified string is null, empty, or + /// consists only of white-space characters. + /// The value to check. + /// Returns if the value is null, + /// empty, or consists only of white-space characters, otherwise + /// returns . + public static bool IsNullOrWhiteSpace(this string value) => string.IsNullOrWhiteSpace(value); + + public static string IfNullOrWhiteSpace(this string str, string defaultValue) + { + return str.IsNullOrWhiteSpace() ? defaultValue : str; + } + + /// The to delimited list. + /// The list. + /// The delimiter. + /// the list + [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "By design")] + public static IList ToDelimitedList(this string list, string delimiter = ",") + { + var delimiters = new[] { delimiter }; + return !list.IsNullOrWhiteSpace() + ? list.Split(delimiters, StringSplitOptions.RemoveEmptyEntries) + .Select(i => i.Trim()) + .ToList() + : new List(); + } + + /// enum try parse. + /// The str type. + /// The ignore case. + /// The result. + /// The type + /// The enum try parse. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] + public static bool EnumTryParse(this string strType, bool ignoreCase, out T result) + { + try + { + result = (T)Enum.Parse(typeof(T), strType, ignoreCase); + return true; + } + catch + { + result = default(T); + return false; + } + } + + /// + /// Parse string to Enum + /// + /// The enum type + /// The string to parse + /// The ignore case + /// The parsed enum + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] + public static T EnumParse(this string strType, bool ignoreCase) + { + return (T)Enum.Parse(typeof(T), strType, ignoreCase); + } + + /// + /// Strips all HTML from a string. + /// + /// The text. + /// Returns the string without any HTML tags. + public static string StripHtml(this string text) + { + const string pattern = @"<(.|\n)*?>"; + return Regex.Replace(text, pattern, string.Empty, RegexOptions.Compiled); + } + + /// + /// Encodes as GUID. + /// + /// The input. + /// + public static Guid EncodeAsGuid(this string input) + { + if (string.IsNullOrWhiteSpace(input)) throw new ArgumentNullException("input"); + + var convertToHex = input.ConvertToHex(); + var hexLength = convertToHex.Length < 32 ? convertToHex.Length : 32; + var hex = convertToHex.Substring(0, hexLength).PadLeft(32, '0'); + var output = Guid.Empty; + return Guid.TryParse(hex, out output) ? output : Guid.Empty; + } + + /// + /// Converts to hex. + /// + /// The input. + /// + public static string ConvertToHex(this string input) + { + if (string.IsNullOrEmpty(input)) return string.Empty; + + var sb = new StringBuilder(input.Length); + foreach (var c in input) + { + sb.AppendFormat("{0:x2}", Convert.ToUInt32(c)); + } + return sb.ToString(); + } + + public static string DecodeFromHex(this string hexValue) + { + var strValue = ""; + while (hexValue.Length > 0) + { + strValue += Convert.ToChar(Convert.ToUInt32(hexValue.Substring(0, 2), 16)).ToString(); + hexValue = hexValue.Substring(2, hexValue.Length - 2); + } + return strValue; + } + + /// + /// Encodes a string to a safe URL base64 string + /// + /// + /// + public static string ToUrlBase64(this string input) + { + if (input == null) throw new ArgumentNullException(nameof(input)); + + if (string.IsNullOrEmpty(input)) + return string.Empty; + + //return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ","); + var bytes = Encoding.UTF8.GetBytes(input); + return UrlTokenEncode(bytes); + } + + /// + /// Decodes a URL safe base64 string back + /// + /// + /// + public static string FromUrlBase64(this string input) + { + if (input == null) throw new ArgumentNullException(nameof(input)); + + //if (input.IsInvalidBase64()) return null; + + try + { + //var decodedBytes = Convert.FromBase64String(input.Replace("-", ".").Replace("_", "/").Replace(",", "=")); + var decodedBytes = UrlTokenDecode(input); + return decodedBytes != null ? Encoding.UTF8.GetString(decodedBytes) : null; + } + catch (FormatException) + { + return null; + } + } + + /// + /// formats the string with invariant culture + /// + /// The format. + /// The args. + /// + public static string InvariantFormat(this string format, params object[] args) + { + return String.Format(CultureInfo.InvariantCulture, format, args); + } + + /// + /// Converts an integer to an invariant formatted string + /// + /// + /// + public static string ToInvariantString(this int str) + { + return str.ToString(CultureInfo.InvariantCulture); + } + + public static string ToInvariantString(this long str) + { + return str.ToString(CultureInfo.InvariantCulture); + } + + /// + /// Compares 2 strings with invariant culture and case ignored + /// + /// The compare. + /// The compare to. + /// + public static bool InvariantEquals(this string compare, string compareTo) + { + return String.Equals(compare, compareTo, StringComparison.InvariantCultureIgnoreCase); + } + + public static bool InvariantStartsWith(this string compare, string compareTo) + { + return compare.StartsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); + } + + public static bool InvariantEndsWith(this string compare, string compareTo) + { + return compare.EndsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); + } + + public static bool InvariantContains(this string compare, string compareTo) + { + return compare.IndexOf(compareTo, StringComparison.OrdinalIgnoreCase) >= 0; + } + + public static bool InvariantContains(this IEnumerable compare, string compareTo) + { + return compare.Contains(compareTo, StringComparer.InvariantCultureIgnoreCase); + } + + public static int InvariantIndexOf(this string s, string value) + { + return s.IndexOf(value, StringComparison.OrdinalIgnoreCase); + } + + public static int InvariantLastIndexOf(this string s, string value) + { + return s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase); + } + + + /// + /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method + /// + /// + /// + /// + public static T ParseInto(this string val) + { + return (T)val.ParseInto(typeof(T)); + } + + /// + /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method + /// + /// + /// + /// + public static object ParseInto(this string val, Type type) + { + if (string.IsNullOrEmpty(val) == false) + { + TypeConverter tc = TypeDescriptor.GetConverter(type); + return tc.ConvertFrom(val); + } + return val; + } + + /// + /// Generates a hash of a string based on the FIPS compliance setting. + /// + /// The to hash. + /// + /// The hashed string. + /// + public static string GenerateHash(this string str) + { + return str.GenerateHash(CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5"); + } + + /// + /// Generate a hash of a string based on the specified hash algorithm. + /// + /// The hash algorithm implementation to use. + /// The to hash. + /// + /// The hashed string. + /// + internal static string GenerateHash(this string str) + where T : HashAlgorithm + { + return str.GenerateHash(typeof(T).FullName); + } + + /// + /// Generate a hash of a string based on the specified . + /// + /// 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) + { + var hasher = HashAlgorithm.Create(hashType); + if (hasher == null) throw new InvalidOperationException($"No hashing type found by name {hashType}."); + + using (hasher) + { + var byteArray = Encoding.UTF8.GetBytes(str); + var hashedByteArray = hasher.ComputeHash(byteArray); + + var sb = new StringBuilder(); + foreach (var b in hashedByteArray) + { + sb.Append(b.ToString("x2")); + } + + return sb.ToString(); + } + } + + /// + /// Decodes a string that was encoded with UrlTokenEncode + /// + /// + /// + internal static byte[] UrlTokenDecode(string input) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + if (input.Length == 0) + return Array.Empty(); + + // calc array size - must be groups of 4 + var arrayLength = input.Length; + var remain = arrayLength % 4; + if (remain != 0) arrayLength += 4 - remain; + + var inArray = new char[arrayLength]; + for (var i = 0; i < input.Length; i++) + { + var ch = input[i]; + switch (ch) + { + case '-': // restore '-' as '+' + inArray[i] = '+'; + break; + + case '_': // restore '_' as '/' + inArray[i] = '/'; + break; + + default: // keep char unchanged + inArray[i] = ch; + break; + } + } + + // pad with '=' + for (var j = input.Length; j < inArray.Length; j++) + inArray[j] = '='; + + return Convert.FromBase64CharArray(inArray, 0, inArray.Length); + } + + /// + /// Encodes a string so that it is 'safe' for URLs, files, etc.. + /// + /// + /// + internal static string UrlTokenEncode(byte[] input) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + if (input.Length == 0) + return string.Empty; + + // base-64 digits are A-Z, a-z, 0-9, + and / + // the = char is used for trailing padding + + var str = Convert.ToBase64String(input); + + var pos = str.IndexOf('='); + if (pos < 0) pos = str.Length; + + // replace chars that would cause problems in urls + var chArray = new char[pos]; + for (var i = 0; i < pos; i++) + { + var ch = str[i]; + switch (ch) + { + case '+': // replace '+' with '-' + chArray[i] = '-'; + break; + + case '/': // replace '/' with '_' + chArray[i] = '_'; + break; + + default: // keep char unchanged + chArray[i] = ch; + break; + } + } + + return new string(chArray); + } + + /// + /// Ensures that the folder path ends with a DirectorySeparatorChar + /// + /// + /// + public static string NormaliseDirectoryPath(this string currentFolder) + { + currentFolder = currentFolder + .IfNull(x => String.Empty) + .TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + return currentFolder; + } + + /// + /// Truncates the specified text string. + /// + /// The text. + /// Length of the max. + /// The suffix. + /// + public static string Truncate(this string text, int maxLength, string suffix = "...") + { + // replaces the truncated string to a ... + var truncatedString = text; + + if (maxLength <= 0) return truncatedString; + var strLength = maxLength - suffix.Length; + + if (strLength <= 0) return truncatedString; + + if (text == null || text.Length <= maxLength) return truncatedString; + + truncatedString = text.Substring(0, strLength); + truncatedString = truncatedString.TrimEnd(); + truncatedString += suffix; + + return truncatedString; + } + + /// + /// Strips carrage returns and line feeds from the specified text. + /// + /// The input. + /// + public static string StripNewLines(this string input) + { + return input.Replace("\r", "").Replace("\n", ""); + } + + /// + /// Converts to single line by replacing line breaks with spaces. + /// + public static string ToSingleLine(this string text) + { + if (string.IsNullOrEmpty(text)) return text; + text = text.Replace("\r\n", " "); // remove CRLF + text = text.Replace("\r", " "); // remove CR + text = text.Replace("\n", " "); // remove LF + return text; + } + + public static string OrIfNullOrWhiteSpace(this string input, string alternative) + { + return !string.IsNullOrWhiteSpace(input) + ? input + : alternative; + } + + /// + /// Returns a copy of the string with the first character converted to uppercase. + /// + /// The string. + /// The converted string. + public static string ToFirstUpper(this string input) + { + return string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToUpper() + input.Substring(1); + } + + /// + /// Returns a copy of the string with the first character converted to lowercase. + /// + /// The string. + /// The converted string. + public static string ToFirstLower(this string input) + { + return string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToLower() + input.Substring(1); + } + + /// + /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the specified culture. + /// + /// The string. + /// The culture. + /// The converted string. + public static string ToFirstUpper(this string input, CultureInfo culture) + { + return string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToUpper(culture) + input.Substring(1); + } + + /// + /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the specified culture. + /// + /// The string. + /// The culture. + /// The converted string. + public static string ToFirstLower(this string input, CultureInfo culture) + { + return string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToLower(culture) + input.Substring(1); + } + + /// + /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the invariant culture. + /// + /// The string. + /// The converted string. + public static string ToFirstUpperInvariant(this string input) + { + return string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToUpperInvariant() + input.Substring(1); + } + + /// + /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the invariant culture. + /// + /// The string. + /// The converted string. + public static string ToFirstLowerInvariant(this string input) + { + return string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToLowerInvariant() + input.Substring(1); + } + + /// + /// Returns a new string in which all occurrences of specified strings are replaced by other specified strings. + /// + /// The string to filter. + /// The replacements definition. + /// The filtered string. + public static string ReplaceMany(this string text, IDictionary replacements) + { + if (text == null) throw new ArgumentNullException(nameof(text)); + if (replacements == null) throw new ArgumentNullException(nameof(replacements)); + + + foreach (KeyValuePair item in replacements) + text = text.Replace(item.Key, item.Value); + + return text; + } + + /// + /// Returns a new string in which all occurrences of specified characters are replaced by a specified character. + /// + /// The string to filter. + /// The characters to replace. + /// The replacement character. + /// The filtered string. + public static string ReplaceMany(this string text, char[] chars, char replacement) + { + if (text == null) throw new ArgumentNullException(nameof(text)); + if (chars == null) throw new ArgumentNullException(nameof(chars)); + + + for (int i = 0; i < chars.Length; i++) + text = text.Replace(chars[i], replacement); + + return text; + } + // FORMAT STRINGS /// @@ -304,5 +1210,270 @@ namespace Umbraco.Core { return Current.ShortStringHelper.CleanStringForSafeFileName(text, culture); } + + /// + /// An extension method that returns a new string in which all occurrences of a + /// specified string in the current instance are replaced with another specified string. + /// StringComparison specifies the type of search to use for the specified string. + /// + /// Current instance of the string + /// Specified string to replace + /// Specified string to inject + /// String Comparison object to specify search type + /// Updated string + public static string Replace(this string source, string oldString, string newString, StringComparison stringComparison) + { + // This initialization ensures the first check starts at index zero of the source. On successive checks for + // a match, the source is skipped to immediately after the last replaced occurrence for efficiency + // and to avoid infinite loops when oldString and newString compare equal. + int index = -1 * newString.Length; + + // Determine if there are any matches left in source, starting from just after the result of replacing the last match. + while ((index = source.IndexOf(oldString, index + newString.Length, stringComparison)) >= 0) + { + // Remove the old text. + source = source.Remove(index, oldString.Length); + + // Add the replacement text. + source = source.Insert(index, newString); + } + + return source; + } + + /// + /// Converts a literal string into a C# expression. + /// + /// Current instance of the string. + /// The string in a C# format. + public static string ToCSharpString(this string s) + { + if (s == null) return ""; + + // http://stackoverflow.com/questions/323640/can-i-convert-a-c-sharp-string-value-to-an-escaped-string-literal + + var sb = new StringBuilder(s.Length + 2); + for (var rp = 0; rp < s.Length; rp++) + { + var c = s[rp]; + if (c < ToCSharpEscapeChars.Length && '\0' != ToCSharpEscapeChars[c]) + sb.Append('\\').Append(ToCSharpEscapeChars[c]); + else if ('~' >= c && c >= ' ') + sb.Append(c); + else + sb.Append(@"\x") + .Append(ToCSharpHexDigitLower[c >> 12 & 0x0F]) + .Append(ToCSharpHexDigitLower[c >> 8 & 0x0F]) + .Append(ToCSharpHexDigitLower[c >> 4 & 0x0F]) + .Append(ToCSharpHexDigitLower[c & 0x0F]); + } + + return sb.ToString(); + + // requires full trust + /* + using (var writer = new StringWriter()) + using (var provider = CodeDomProvider.CreateProvider("CSharp")) + { + provider.GenerateCodeFromExpression(new CodePrimitiveExpression(s), writer, null); + return writer.ToString().Replace(string.Format("\" +{0}\t\"", Environment.NewLine), ""); + } + */ + } + + public static string EscapeRegexSpecialCharacters(this string text) + { + var regexSpecialCharacters = new Dictionary + { + {".", @"\."}, + {"(", @"\("}, + {")", @"\)"}, + {"]", @"\]"}, + {"[", @"\["}, + {"{", @"\{"}, + {"}", @"\}"}, + {"?", @"\?"}, + {"!", @"\!"}, + {"$", @"\$"}, + {"^", @"\^"}, + {"+", @"\+"}, + {"*", @"\*"}, + {"|", @"\|"}, + {"<", @"\<"}, + {">", @"\>"} + }; + return ReplaceMany(text, regexSpecialCharacters); + } + + /// + /// Checks whether a string "haystack" contains within it any of the strings in the "needles" collection and returns true if it does or false if it doesn't + /// + /// The string to check + /// The collection of strings to check are contained within the first string + /// The type of comparison to perform - defaults to + /// True if any of the needles are contained with haystack; otherwise returns false + /// Added fix to ensure the comparison is used - see http://issues.umbraco.org/issue/U4-11313 + public static bool ContainsAny(this string haystack, IEnumerable needles, StringComparison comparison = StringComparison.CurrentCulture) + { + if (haystack == null) + throw new ArgumentNullException("haystack"); + + if (string.IsNullOrEmpty(haystack) || needles == null || !needles.Any()) + { + return false; + } + + return needles.Any(value => haystack.IndexOf(value, comparison) >= 0); + } + + public static bool CsvContains(this string csv, string value) + { + if (string.IsNullOrEmpty(csv)) + { + return false; + } + var idCheckList = csv.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + return idCheckList.Contains(value); + } + + /// + /// Converts a file name to a friendly name for a content item + /// + /// + /// + public static string ToFriendlyName(this string fileName) + { + // strip the file extension + fileName = fileName.StripFileExtension(); + + // underscores and dashes to spaces + fileName = fileName.ReplaceMany(new[] { '_', '-' }, ' '); + + // any other conversions ? + + // Pascalcase (to be done last) + fileName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(fileName); + + // Replace multiple consecutive spaces with a single space + fileName = string.Join(" ", fileName.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); + + return fileName; + } + + // From: http://stackoverflow.com/a/961504/5018 + // filters control characters but allows only properly-formed surrogate sequences + private static readonly Lazy InvalidXmlChars = new Lazy(() => + new Regex( + @"(? + /// An extension method that returns a new string in which all occurrences of an + /// unicode characters that are invalid in XML files are replaced with an empty string. + /// + /// Current instance of the string + /// Updated string + /// + /// + /// removes any unusual unicode characters that can't be encoded into XML + /// + internal static string ToValidXmlString(this string text) + { + return string.IsNullOrEmpty(text) ? text : InvalidXmlChars.Value.Replace(text, ""); + } + + /// + /// Converts a string to a Guid - WARNING, depending on the string, this may not be unique + /// + /// + /// + internal static Guid ToGuid(this string text) + { + return CreateGuidFromHash(UrlNamespace, + text, + CryptoConfig.AllowOnlyFipsAlgorithms + ? 5 // SHA1 + : 3); // MD5 + } + + /// + /// The namespace for URLs (from RFC 4122, Appendix C). + /// + /// See RFC 4122 + /// + internal static readonly Guid UrlNamespace = new Guid("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); + + /// + /// Creates a name-based UUID using the algorithm from RFC 4122 §4.3. + /// + /// See GuidUtility.cs for original implementation. + /// + /// The ID of the namespace. + /// The name (within that namespace). + /// The version number of the UUID to create; this value must be either + /// 3 (for MD5 hashing) or 5 (for SHA-1 hashing). + /// A UUID derived from the namespace and name. + /// See Generating a deterministic GUID. + internal static Guid CreateGuidFromHash(Guid namespaceId, string name, int version) + { + if (name == null) + throw new ArgumentNullException("name"); + if (version != 3 && version != 5) + throw new ArgumentOutOfRangeException("version", "version must be either 3 or 5."); + + // convert the name to a sequence of octets (as defined by the standard or conventions of its namespace) (step 3) + // ASSUME: UTF-8 encoding is always appropriate + byte[] nameBytes = Encoding.UTF8.GetBytes(name); + + // convert the namespace UUID to network order (step 3) + byte[] namespaceBytes = namespaceId.ToByteArray(); + SwapByteOrder(namespaceBytes); + + // comput the hash of the name space ID concatenated with the name (step 4) + byte[] hash; + using (HashAlgorithm algorithm = version == 3 ? (HashAlgorithm)MD5.Create() : SHA1.Create()) + { + algorithm.TransformBlock(namespaceBytes, 0, namespaceBytes.Length, null, 0); + algorithm.TransformFinalBlock(nameBytes, 0, nameBytes.Length); + hash = algorithm.Hash; + } + + // most bytes from the hash are copied straight to the bytes of the new GUID (steps 5-7, 9, 11-12) + byte[] newGuid = new byte[16]; + Array.Copy(hash, 0, newGuid, 0, 16); + + // set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to the appropriate 4-bit version number from Section 4.1.3 (step 8) + newGuid[6] = (byte)((newGuid[6] & 0x0F) | (version << 4)); + + // set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively (step 10) + newGuid[8] = (byte)((newGuid[8] & 0x3F) | 0x80); + + // convert the resulting UUID to local byte order (step 13) + SwapByteOrder(newGuid); + return new Guid(newGuid); + } + + // Converts a GUID (expressed as a byte array) to/from network order (MSB-first). + internal static void SwapByteOrder(byte[] guid) + { + SwapBytes(guid, 0, 3); + SwapBytes(guid, 1, 2); + SwapBytes(guid, 4, 5); + SwapBytes(guid, 6, 7); + } + + private static void SwapBytes(byte[] guid, int left, int right) + { + byte temp = guid[left]; + guid[left] = guid[right]; + guid[right] = temp; + } + + /// + /// Turns an null-or-whitespace string into a null string. + /// + public static string NullOrWhiteSpaceAsNull(this string text) + => string.IsNullOrWhiteSpace(text) ? null : text; } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index b4c819761a..b33924069d 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -10,6 +10,7 @@ Umbraco.Core ..\ + $(AdditionalFileItemNames);Content true @@ -60,6 +61,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + 3.3.0 + runtime; build; native; contentfiles; analyzers + all + 1.3.0 @@ -241,6 +247,8 @@ + + @@ -283,7 +291,9 @@ + + @@ -744,7 +754,6 @@ - @@ -1240,4 +1249,4 @@ - \ No newline at end of file + 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.Examine/ContentIndexPopulator.cs b/src/Umbraco.Examine/ContentIndexPopulator.cs index 51b9de4a0b..dd4176a774 100644 --- a/src/Umbraco.Examine/ContentIndexPopulator.cs +++ b/src/Umbraco.Examine/ContentIndexPopulator.cs @@ -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/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/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 8765bb9966..83d64f7910 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -10,6 +10,7 @@ Umbraco.Examine ..\ + $(AdditionalFileItemNames);Content true @@ -56,6 +57,11 @@ + + 3.3.0 + runtime; build; native; contentfiles; analyzers + all + @@ -64,9 +70,11 @@ + + @@ -106,4 +114,4 @@ - \ No newline at end of file + 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.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/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 9cd4f39c17..7459ae848b 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -268,7 +268,7 @@ AnotherContentFinder public void GetDataEditors() { var types = _typeLoader.GetDataEditors(); - Assert.AreEqual(39, types.Count()); + Assert.AreEqual(38, types.Count()); } /// diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs index 26e6a1ad8a..0245159c6e 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs @@ -67,7 +67,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [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() 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]]> @@ -64,63 +67,75 @@
  • + ng-click="vm.pickDataType(dataType)" + class="cursor-pointer">
    - + {{ dataType.name }} - +
  • + -
    - + + + + 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.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index 704b61e333..6ba2ec0270 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 @@ -23,6 +23,7 @@ placeholder="@general_url" class="umb-property-editor umb-textstring" ng-model="model.target.url" + umb-auto-focus ng-disabled="model.target.id || model.target.udi" /> 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..dfc77f786c 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 @@ -41,8 +41,6 @@ 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) { 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 57a9c22b0b..ba103a2761 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 @@ -28,6 +28,7 @@ angular.module("umbraco") var dialogOptions = $scope.model; $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; @@ -37,7 +38,7 @@ angular.module("umbraco") $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); $scope.lockedFolder = true; $scope.allowMediaEdit = dialogOptions.allowMediaEdit ? dialogOptions.allowMediaEdit : false; - + var userStartNodes = []; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; @@ -137,7 +138,8 @@ angular.module("umbraco") $scope.target = node; if (ensureWithinStartNode(node)) { selectMedia(node); - $scope.target.url = mediaHelper.resolveFile(node); + $scope.target.url = mediaHelper.resolveFileFromEntity(node); + $scope.target.thumbnail = mediaHelper.resolveFileFromEntity(node, true); $scope.target.altText = altText; openDetailsDialog(); } @@ -226,7 +228,7 @@ angular.module("umbraco") } function clickHandler(media, event, index) { - + if (media.isFolder) { if ($scope.disableFolderSelect) { gotoFolder(media); @@ -333,22 +335,26 @@ angular.module("umbraco") } 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); - vm.mediaPickerDetailsOverlay = { - show: true, - submit: function (model) { + vm.mediaPickerDetailsOverlay.show = false; + vm.mediaPickerDetailsOverlay = null; + }, + close: function (oldModel) { + vm.mediaPickerDetailsOverlay.show = false; + vm.mediaPickerDetailsOverlay = null; - $scope.model.selection.push($scope.target); - $scope.model.submit($scope.model); - - vm.mediaPickerDetailsOverlay.show = false; - vm.mediaPickerDetailsOverlay = null; - }, - close: function (oldModel) { - vm.mediaPickerDetailsOverlay.show = false; - vm.mediaPickerDetailsOverlay = null; - } - }; + close(); + } + }; + }); }; var debounceSearchMedia = _.debounce(function () { @@ -453,21 +459,25 @@ angular.module("umbraco") function getChildren(id) { vm.loading = true; return entityResource.getChildren(id, "Media", vm.searchOptions).then(function (data) { - - 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); - } + + 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); } - vm.searchOptions.filter = ""; - $scope.images = data ? data : []; + data[i].filtered = allowedTypes && allowedTypes.indexOf(data[i].metaData.ContentTypeAlias) < 0; + } - // set already selected medias to selected - preSelectMedia(); - vm.loading = false; - }); + vm.searchOptions.filter = ""; + $scope.images = data ? data : []; + + // set already selected medias to selected + preSelectMedia(); + vm.loading = false; + }); } function preSelectMedia() { 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 a08395143f..656c5f2ac1 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,198 +1,224 @@
      - + - - + + - + -
      + -
      +
      -
      +
      - - + + - diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.controller.js index faca3b3fa0..4d537bd73c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.controller.js @@ -31,6 +31,7 @@ vm.datePickerChange = datePickerChange; vm.submit = submit; vm.close = close; + vm.copyQuery = copyQuery; function onInit() { @@ -120,6 +121,11 @@ query.filters.push({}); } + function copyQuery() { + var copyText = $scope.model.result.queryExpression; + navigator.clipboard.writeText(copyText); + } + function trashFilter(query, filter) { for (var i = 0; i < query.filters.length; i++) { if (query.filters[i] == filter) { 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 779ca739d2..f01f325265 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,160 @@ -
      +
      -
      -
      +
      +
      - 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}}
      + + copy to clipboard + -
      +
      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..f71eb2c51e 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; + }); + }); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.controller.js index 36d7c0f4ed..8c728150da 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.controller.js @@ -1,26 +1,42 @@ (function () { "use strict"; - function TemplateSectionsController($scope, formHelper) { + function TemplateSectionsController($scope, formHelper, localizationService) { var vm = this; + vm.labels = {}; + vm.select = select; vm.submit = submit; vm.close = close; $scope.model.mandatoryRenderSection = false; - if(!$scope.model.title) { - $scope.model.title = "Sections"; - } - function onInit() { - if($scope.model.hasMaster) { + if ($scope.model.hasMaster) { $scope.model.insertType = 'addSection'; } else { $scope.model.insertType = 'renderBody'; } + + var labelKeys = [ + "template_insertSections", + "template_sectionMandatory" + ]; + + localizationService.localizeMany(labelKeys).then(function (data) { + vm.labels.title = data[0]; + vm.labels.sectionMandatory = data[1]; + + setTitle(vm.labels.title); + }); + } + + function setTitle(value) { + if (!$scope.model.title) { + $scope.model.title = value; + } } function select(type) { @@ -34,13 +50,12 @@ } function close() { - if($scope.model.close) { + if ($scope.model.close) { $scope.model.close(); } } onInit(); - } angular.module("umbraco").controller("Umbraco.Editors.TemplateSectionsController", TemplateSectionsController); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html index 5b946976d7..045a1403e2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html @@ -44,10 +44,10 @@
      - - +
      + +
      +
      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 737253feb2..0aab35ca21 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 @@ -6,13 +6,31 @@