diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6e421da5d7..96014f65b7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,4 +1,4 @@ -_Looking for Umbraco version 8? [Click here](https://github.com/umbraco/Umbraco-CMS/blob/temp8/docs/CONTRIBUTING.md) to go to the v8 branch_ +_Looking for Umbraco version 8? [Click here](https://github.com/umbraco/Umbraco-CMS/blob/temp8/.github/V8_GETTING_STARTED.md) to go to the v8 branch_ # Contributing to Umbraco CMS 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 @@ -16,9 +16,9 @@ This document gives you a quick overview on how to get started, we will link to ## 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 valueable time. +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 valueable 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. 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. @@ -36,7 +36,7 @@ Great question! The short version goes like this: * **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md) * **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions) - * **Commit** - done? Yay! 🎉 It is recommended to create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-U4-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `U4-12345` + * **Commit** - done? Yay! 🎉 It is recommended to 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` * **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. @@ -72,7 +72,6 @@ The pull request team consists of a member of Umbraco HQ, [Sebastiaan](https://g - [Anders Bjerner](https://github.com/abjerner) - [Dave Woestenborghs](https://github.com/dawoe) - [Emma Burstow](https://github.com/emmaburstow) -- [Kyle Weems](https://github.com/cssquirrel) - [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. diff --git a/.github/CONTRIBUTING_DETAILED.md b/.github/CONTRIBUTING_DETAILED.md index a07539da6a..8c2bfffd87 100644 --- a/.github/CONTRIBUTING_DETAILED.md +++ b/.github/CONTRIBUTING_DETAILED.md @@ -19,13 +19,13 @@ When contributing code to Umbraco there's plenty of things you'll want to know, * [What branch should I target for my contributions?](#what-branch-should-i-target-for-my-contributions) * [Building Umbraco from source code](#building-umbraco-from-source-code) * [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository) - + ## How Can I Contribute? ### Reporting Bugs This section guides you through submitting a bug report for Umbraco CMS. Following these guidelines helps maintainers and the community understand your report 📝, reproduce the behavior 💻 💻, and find related reports 🔎. -Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](http://issues.umbraco.org/issues#newissue=61-30118), the information it asks for helps us resolve issues faster. +Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](https://github.com/umbraco/Umbraco-CMS/issues/new/choose), the information it asks for helps us resolve issues faster. > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. @@ -52,7 +52,7 @@ Provide more context by answering these questions: Include details about your configuration and environment: - * **Which version of Umbraco are you using?** + * **Which version of Umbraco are you using?** * **What is the environment you're using Umbraco in?** Is this a problem on your local machine or on a server. Tell us about your configuration: Windows version, IIS/IISExpress, database type, etc. * **Which packages do you have installed?** @@ -80,7 +80,7 @@ The most successful pull requests usually look a like this: * 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. ## Making changes after the PR was opened @@ -90,7 +90,7 @@ If you make the corrections we ask for in the same branch and push them to your To be honest, we don't like rules very much. We trust you have the best of intentions and we encourage you to create working code. If it doesn't look perfect then we'll happily help clean it up. -That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc. +That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc. ## What should I know before I get started? @@ -125,6 +125,12 @@ We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-fl ### Building Umbraco from source code +In order to build the Umbraco source code locally, first make sure you have the following installed. + + * Visual Studio 2017 v15.3+ + * Node v10+ (Installed via `build.bat` script. If you already have it installed, make sure you're running at least v10) + * npm v6.4.1+ (Installed via `build.bat` script. If you already have it installed, make sure you're running at least v6.4.1) + The easiest way to get started is to run `build.bat` 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. Alternatively, you can open `src\umbraco.sln` in Visual Studio 2017 (version 15.3 or higher, [the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6275d161dc..8cb9017518 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,11 +1,12 @@ ### Prerequisites -- [ ] I have [created an issue](https://github.com/umbraco/Umbraco-CMS/issues) for the proposed changes in this PR, the link is: - [ ] I have added steps to test this contribution in the description below +If there's an existing issue for this PR then this fixes: + ### Description - - + + diff --git a/.github/README.md b/.github/README.md index cf29f4e527..5a1340006e 100644 --- a/.github/README.md +++ b/.github/README.md @@ -15,7 +15,7 @@ Once a track is done, we start releasing previews where we ask people to test th Umbraco CMS =========== -The friendliest, most flexible and fastest growing ASP.NET CMS used by more than 443,000 websites worldwide: [https://umbraco.com](https://umbraco.com) +The friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 443,000 websites worldwide: [https://umbraco.com](https://umbraco.com) [![ScreenShot](img/vimeo.png)](https://vimeo.com/172382998/) @@ -28,34 +28,34 @@ Umbraco is a free open source Content Management System built on the ASP.NET pla ## Umbraco - The Friendly CMS -For the first time on the Microsoft platform, there is a free user and developer friendly CMS that makes it quick and easy to create websites - or a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, out of the box. +For the first time on the Microsoft platform, there is a free user- and developer-friendly CMS that makes it quick and easy to create websites - and a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, right out of the box. -Umbraco is not only loved by developers, but is a content editors dream. Enjoy intuitive editing tools, media management, responsive views and approval workflows to send your content live. +Umbraco is not only loved by developers, but is a content editor's dream. Enjoy intuitive editing tools, media management, responsive views, and approval workflows to send your content live. -Used by more than 443,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scales. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 220,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay. +Used by more than 443,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scalable. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 220,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay. To view more examples, please visit [https://umbraco.com/case-studies-testimonials/](https://umbraco.com/case-studies-testimonials/) ## Why Open Source? -As an Open Source platform, Umbraco is more than just a CMS. We are transparent with our roadmap for future versions, our incremental sprint planning notes are publicly accessible and community contributions and packages are available for all to use. +As an Open Source platform, Umbraco is more than just a CMS. We are transparent with our roadmap for future versions, our incremental sprint planning notes are publicly accessible, and community contributions and packages are available for all to use. ## Trying out Umbraco CMS -[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 intergrations. 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. +[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 yourself and handling deployments and upgrades is all down to you. +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. ## Community -Our friendly community is available 24/7 at the community hub we call ["Our Umbraco"](https://our.umbraco.com). Our Umbraco feature 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. ## Contribute to Umbraco -Umbraco is contribution focused and community driven. If you want to contribute back to Umbraco please check out our [guide to contributing](CONTRIBUTING.md). +Umbraco is contribution-focused and community-driven. If you want to contribute back to Umbraco, please check out our [guide to contributing](CONTRIBUTING.md). ## Found a bug? Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](CONTRIBUTING_DETAILED.md#reporting-bugs). You can comment and report issues on the [github issue tracker](https://github.com/umbraco/Umbraco-CMS/issues). -Since [September 2018](https://umbraco.com/blog/a-second-take-on-umbraco-issue-tracker-hello-github-issues/) the old issue tracker is in read only mode, but can still be found at [http://issues.umbraco.org](http://issues.umbraco.org). +Since [September 2018](https://umbraco.com/blog/a-second-take-on-umbraco-issue-tracker-hello-github-issues/), the old issue tracker is in read-only mode, but can still be found at [http://issues.umbraco.org](http://issues.umbraco.org). diff --git a/.github/V8_GETTING_STARTED.md b/.github/V8_GETTING_STARTED.md index 62b376b0e7..1cc33bb126 100644 --- a/.github/V8_GETTING_STARTED.md +++ b/.github/V8_GETTING_STARTED.md @@ -33,5 +33,5 @@ We recommend running the site with the Visual Studio since you'll be able to rem We are keeping track of [known issues and limitations here](http://issues.umbraco.org/issue/U4-11279). These line items will eventually be turned into actual tasks to be worked on. Feel free to help us keep this list updated if you find issues and even help fix some of these items. If there is a particular item you'd like to help fix please mention this on the task and we'll create a sub task for the item to continue discussion there. -There's [a list of tasks for v8 that haven't been completed](https://issues.umbraco.org/issues?q=&project=U4&tagValue=&release=8.0.0&issueType=&resolvedState=open&search=search). If you are interested in helping out with any of these please mention this on the task. This list will be constantly updated as we begin to document and design some of the other tasks that still need to get done. +There's [a list of tasks for v8 that haven't been completed](https://github.com/umbraco/Umbraco-CMS/labels/release%2F8.0.0). If you are interested in helping out with any of these please mention this on the task. This list will be constantly updated as we begin to document and design some of the other tasks that still need to get done. diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index daa0018668..dd565aa1d4 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -36,7 +36,7 @@ - + diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index e9bd8ca6ea..adf090c69b 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -25,7 +25,7 @@ - + diff --git a/build/NuSpecs/tools/Dashboard.config.install.xdt b/build/NuSpecs/tools/Dashboard.config.install.xdt index 036beeba29..a81af8c365 100644 --- a/build/NuSpecs/tools/Dashboard.config.install.xdt +++ b/build/NuSpecs/tools/Dashboard.config.install.xdt @@ -3,7 +3,7 @@
- + views/dashboard/settings/settingsdashboardintro.html @@ -14,7 +14,7 @@ forms - + views/dashboard/forms/formsdashboardintro.html @@ -28,7 +28,7 @@
- + views/dashboard/developer/developerdashboardvideos.html @@ -47,7 +47,7 @@ - + views/dashboard/media/mediafolderbrowser.html @@ -56,7 +56,7 @@
- + views/dashboard/members/membersdashboardvideos.html @@ -92,4 +92,4 @@
- \ No newline at end of file + diff --git a/build/build.ps1 b/build/build.ps1 index 8548cbb1ac..1066c62876 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -43,14 +43,6 @@ $release = "" + $semver.Major + "." + $semver.Minor + "." + $semver.Patch - Write-Host "Update UmbracoVersion.cs" - $this.ReplaceFileText("$($this.SolutionRoot)\src\Umbraco.Core\Configuration\UmbracoVersion.cs", ` - "(\d+)\.(\d+)\.(\d+)(.(\d+))?", ` - "$release") - $this.ReplaceFileText("$($this.SolutionRoot)\src\Umbraco.Core\Configuration\UmbracoVersion.cs", ` - "CurrentComment => `"(.+)`"", ` - "CurrentComment => `"$($semver.PreRelease)`"") - Write-Host "Update IIS Express port in csproj" $updater = New-Object "Umbraco.Build.ExpressPortUpdater" $csproj = "$($this.SolutionRoot)\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj" @@ -69,7 +61,7 @@ $global:node_nodepath = $this.ClearEnvVar("NODEPATH") $global:node_npmcache = $this.ClearEnvVar("NPM_CONFIG_CACHE") $global:node_npmprefix = $this.ClearEnvVar("NPM_CONFIG_PREFIX") - + # https://github.com/gruntjs/grunt-contrib-connect/issues/235 $this.SetEnvVar("NODE_NO_HTTP2", "1") }) @@ -81,7 +73,7 @@ $this.SetEnvVar("NODEPATH", $node_nodepath) $this.SetEnvVar("NPM_CONFIG_CACHE", $node_npmcache) $this.SetEnvVar("NPM_CONFIG_PREFIX", $node_npmprefix) - + $ignore = $this.ClearEnvVar("NODE_NO_HTTP2") }) @@ -434,7 +426,7 @@ Write-Host "Prepare Azure Gallery" $this.CopyFile("$($this.SolutionRoot)\build\Azure\azuregalleryrelease.ps1", $this.BuildOutput) }) - + $ubuild.DefineMethod("Build", { $error.Clear() diff --git a/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial b/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial index 69f51a101f..7aac413bfd 100644 --- a/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial +++ b/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial @@ -7,7 +7,7 @@ Back to top - Copyright © 2016 Umbraco
Generated by DocFX
+ Copyright © 2016-present Umbraco
Generated by DocFX
diff --git a/src/Umbraco.Core/Collections/ConcurrentHashSet.cs b/src/Umbraco.Core/Collections/ConcurrentHashSet.cs index 4cad6e9f15..54367ed588 100644 --- a/src/Umbraco.Core/Collections/ConcurrentHashSet.cs +++ b/src/Umbraco.Core/Collections/ConcurrentHashSet.cs @@ -70,10 +70,7 @@ namespace Umbraco.Core.Collections /// The number of elements contained in the . /// /// 2 - public int Count - { - get { return GetThreadSafeClone().Count; } - } + public int Count => GetThreadSafeClone().Count; /// /// Gets a value indicating whether the is read-only. @@ -81,10 +78,7 @@ namespace Umbraco.Core.Collections /// /// true if the is read-only; otherwise, false. /// - public bool IsReadOnly - { - get { return false; } - } + public bool IsReadOnly => false; /// /// Adds an item to the . diff --git a/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs b/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs index 82912163b6..80de6d1c35 100644 --- a/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs +++ b/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Composing.CompositionRoots container.Register(factory => factory.GetInstance().Templates); container.Register(factory => factory.GetInstance().RequestHandler); container.Register(factory => UmbracoConfig.For.GlobalSettings()); + container.Register(factory => UmbracoConfig.For.DashboardSettings()); // fixme - other sections we need to add? } diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index a42b84e0c5..9604f599cf 100644 --- a/src/Umbraco.Core/Composing/TypeFinder.cs +++ b/src/Umbraco.Core/Composing/TypeFinder.cs @@ -210,53 +210,55 @@ namespace Umbraco.Core.Composing /// NOTE the comma vs period... comma delimits the name in an Assembly FullName property so if it ends with comma then its an exact name match /// NOTE this means that "foo." will NOT exclude "foo.dll" but only "foo.*.dll" /// - internal static readonly string[] KnownAssemblyExclusionFilter = new[] - { - "mscorlib,", - "System.", - "Antlr3.", - "Autofac.", - "Autofac,", - "Castle.", - "ClientDependency.", - "DataAnnotationsExtensions.", - "DataAnnotationsExtensions,", - "Dynamic,", - "HtmlDiff,", - "Iesi.Collections,", - "Microsoft.", - "Newtonsoft.", - "NHibernate.", - "NHibernate,", - "NuGet.", - "RouteDebugger,", - "SqlCE4Umbraco,", - "umbraco.datalayer,", - "umbraco.interfaces,", - //"umbraco.providers,", - //"Umbraco.Web.UI,", - "umbraco.webservices", - "Lucene.", - "Examine,", - "Examine.", - "ServiceStack.", - "MySql.", - "HtmlAgilityPack.", - "TidyNet.", - "ICSharpCode.", - "CookComputing.", - "AutoMapper,", - "AutoMapper.", - "AzureDirectory,", - "itextsharp,", - "UrlRewritingNet.", - "HtmlAgilityPack,", - "MiniProfiler,", - "Moq,", - "nunit.framework,", - "TidyNet,", - "WebDriver," - }; + internal static readonly string[] KnownAssemblyExclusionFilter = { + "Antlr3.", + "AutoMapper,", + "AutoMapper.", + "Autofac,", // DI + "Autofac.", + "AzureDirectory,", + "Castle.", // DI, tests + "ClientDependency.", + "CookComputing.", + "CSharpTest.", // BTree for NuCache + "DataAnnotationsExtensions,", + "DataAnnotationsExtensions.", + "Dynamic,", + "Examine,", + "Examine.", + "HtmlAgilityPack,", + "HtmlAgilityPack.", + "HtmlDiff,", + "ICSharpCode.", + "Iesi.Collections,", // used by NHibernate + "LightInject.", // DI + "LightInject,", + "Lucene.", + "Markdown,", + "Microsoft.", + "MiniProfiler,", + "Moq,", + "MySql.", + "NHibernate,", + "NHibernate.", + "Newtonsoft.", + "NPoco,", + "NuGet.", + "RouteDebugger,", + "Semver.", + "Serilog.", + "Serilog,", + "ServiceStack.", + "SqlCE4Umbraco,", + "Superpower,", // used by Serilog + "System.", + "TidyNet,", + "TidyNet.", + "WebDriver,", + "itextsharp,", + "mscorlib,", + "nunit.framework,", + }; /// /// Finds any classes derived from the type T that contain the attribute TAttribute diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index 304638e017..f79c288e91 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -520,6 +520,8 @@ namespace Umbraco.Core.Composing // if not caching, or not IDiscoverable, directly get types if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) { + _logger.Logger.Debug("Running a full, non-cached, scan for type {TypeName} (slow).", typeof(T).FullName); + return GetTypesInternal( typeof (T), null, () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), @@ -559,6 +561,8 @@ namespace Umbraco.Core.Composing // if not caching, or not IDiscoverable, directly get types if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) { + _logger.Logger.Debug("Running a full, non-cached, scan for type {TypeName} / attribute {AttributeName} (slow).", typeof(T).FullName, typeof(TAttribute).FullName); + return GetTypesInternal( typeof (T), typeof (TAttribute), () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), @@ -595,6 +599,11 @@ namespace Umbraco.Core.Composing // do not cache anything from specific assemblies cache &= specificAssemblies == null; + if (cache == false) + { + _logger.Logger.Debug("Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", typeof(TAttribute).FullName); + } + return GetTypesInternal( typeof (object), typeof (TAttribute), () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs index 1642f23fc5..01538c8e0b 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs @@ -7,26 +7,22 @@ namespace Umbraco.Core.Configuration.Dashboard internal class AccessElement : RawXmlConfigurationElement, IAccess { public AccessElement() - { - - } + { } public AccessElement(XElement rawXml) - :base(rawXml) - { - } + : base(rawXml) + { } - public IEnumerable Rules + public IEnumerable Rules { get { - var result = new List(); - if (RawXml != null) - { - result.AddRange(RawXml.Elements("deny").Select(x => new AccessItem {Action = AccessType.Deny, Value = x.Value })); - result.AddRange(RawXml.Elements("grant").Select(x => new AccessItem { Action = AccessType.Grant, Value = x.Value })); - result.AddRange(RawXml.Elements("grantBySection").Select(x => new AccessItem { Action = AccessType.GrantBySection, Value = x.Value })); - } + var result = new List(); + if (RawXml == null) return result; + + result.AddRange(RawXml.Elements("deny").Select(x => new AccessRule {Type = AccessRuleType.Deny, Value = x.Value })); + result.AddRange(RawXml.Elements("grant").Select(x => new AccessRule { Type = AccessRuleType.Grant, Value = x.Value })); + result.AddRange(RawXml.Elements("grantBySection").Select(x => new AccessRule { Type = AccessRuleType.GrantBySection, Value = x.Value })); return result; } } diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs deleted file mode 100644 index 37cf491536..0000000000 --- a/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Umbraco.Core.Configuration.Dashboard -{ - internal class AccessItem : IAccessItem - { - /// - /// This can be grant, deny or grantBySection - /// - public AccessType Action { get; set; } - - /// - /// The value of the action - /// - public string Value { get; set; } - } -} diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs new file mode 100644 index 0000000000..fe6840ff64 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Core.Configuration.Dashboard +{ + /// + /// Implements . + /// + internal class AccessRule : IAccessRule + { + /// + public AccessRuleType Type { get; set; } + + /// + public string Value { get; set; } + } +} diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs new file mode 100644 index 0000000000..cb9ce983fe --- /dev/null +++ b/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Core.Configuration.Dashboard +{ + /// + /// Defines dashboard access rules type. + /// + public enum AccessRuleType + { + /// + /// Unknown (default value). + /// + Unknown = 0, + + /// + /// Grant access to the dashboard if user belongs to the specified user group. + /// + Grant, + + /// + /// Deny access to the dashboard if user belongs to the specified user group. + /// + Deny, + + /// + /// Grant access to the dashboard if user has access to the specified section. + /// + GrantBySection + } +} diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs deleted file mode 100644 index d72cac15d0..0000000000 --- a/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Umbraco.Core.Configuration.Dashboard -{ - public enum AccessType - { - Grant, - Deny, - GrantBySection - } -} diff --git a/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs b/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs index 0434eea47e..20dac7460e 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs @@ -1,5 +1,4 @@ -using System; -using System.Configuration; +using System.Configuration; using System.Linq; using System.Xml.Linq; @@ -8,33 +7,12 @@ namespace Umbraco.Core.Configuration.Dashboard internal class ControlElement : RawXmlConfigurationElement, IDashboardControl { - public bool ShowOnce - { - get - { - return RawXml.Attribute("showOnce") == null - ? false - : bool.Parse(RawXml.Attribute("showOnce").Value); - } - } - - public bool AddPanel - { - get - { - return RawXml.Attribute("addPanel") == null - ? true - : bool.Parse(RawXml.Attribute("addPanel").Value); - } - } - public string PanelCaption { get { - return RawXml.Attribute("panelCaption") == null - ? "" - : RawXml.Attribute("panelCaption").Value; + var panelCaption = RawXml.Attribute("panelCaption"); + return panelCaption == null ? "" : panelCaption.Value; } } @@ -43,11 +21,7 @@ namespace Umbraco.Core.Configuration.Dashboard get { var access = RawXml.Element("access"); - if (access == null) - { - return new AccessElement(); - } - return new AccessElement(access); + return access == null ? new AccessElement() : new AccessElement(access); } } @@ -65,10 +39,6 @@ namespace Umbraco.Core.Configuration.Dashboard } } - - IAccess IDashboardControl.AccessRights - { - get { return Access; } - } + IAccess IDashboardControl.AccessRights => Access; } } diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs index b7d8540a79..8ac1b18cca 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs @@ -4,6 +4,6 @@ namespace Umbraco.Core.Configuration.Dashboard { public interface IAccess { - IEnumerable Rules { get; } + IEnumerable Rules { get; } } } diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs deleted file mode 100644 index 8b18d50bb3..0000000000 --- a/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Umbraco.Core.Configuration.Dashboard -{ - public interface IAccessItem - { - /// - /// This can be grant, deny or grantBySection - /// - AccessType Action { get; set; } - - /// - /// The value of the action - /// - string Value { get; set; } - } -} diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs new file mode 100644 index 0000000000..8b51b1b73a --- /dev/null +++ b/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Configuration.Dashboard +{ + /// + /// Represents an access rule. + /// + public interface IAccessRule + { + /// + /// Gets or sets the rule type. + /// + AccessRuleType Type { get; set; } + + /// + /// Gets or sets the value for the rule. + /// + string Value { get; set; } + } +} diff --git a/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs b/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs index 7dab542258..cdf05af1ec 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs @@ -2,10 +2,6 @@ { public interface IDashboardControl { - bool ShowOnce { get; } - - bool AddPanel { get; } - string PanelCaption { get; } string ControlPath { get; } diff --git a/src/Umbraco.Core/Configuration/IGlobalSettings.cs b/src/Umbraco.Core/Configuration/IGlobalSettings.cs index cf9478d30a..a043f608f4 100644 --- a/src/Umbraco.Core/Configuration/IGlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/IGlobalSettings.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Core.Configuration +using System; + +namespace Umbraco.Core.Configuration { /// /// Contains general settings information for the entire Umbraco instance based on information from web.config appsettings @@ -24,6 +26,8 @@ /// /// Defaults to ~/App_Data/umbraco.config /// + //fixme - remove + [Obsolete("This should not be used, need to remove the content xml cache")] string ContentXmlFile { get; } /// diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 46ad221837..73df566a0f 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -10,37 +10,61 @@ namespace Umbraco.Core.Configuration /// public static class UmbracoVersion { - // BEWARE! - // This class is parsed and updated by the build scripts. - // Do NOT modify it unless you understand what you are doing. + static UmbracoVersion() + { + var umbracoCoreAssembly = typeof(UmbracoVersion).Assembly; + + // gets the value indicated by the AssemblyVersion attribute + AssemblyVersion = umbracoCoreAssembly.GetName().Version; + + // gets the value indicated by the AssemblyFileVersion attribute + AssemblyFileVersion = System.Version.Parse(umbracoCoreAssembly.GetCustomAttribute().Version); + + // gets the value indicated by the AssemblyInformationalVersion attribute + // this is the true semantic version of the Umbraco Cms + SemanticVersion = SemVersion.Parse(umbracoCoreAssembly.GetCustomAttribute().InformationalVersion); + + // gets the non-semantic version + Current = SemanticVersion.GetVersion(3); + } /// - /// Gets the version of the executing code. + /// Gets the non-semantic version of the Umbraco code. /// - public static Version Current { get; } = new Version("8.0.0"); + // TODO rename to Version + public static Version Current { get; } /// - /// Gets the version comment of the executing code (eg "beta"). + /// Gets the semantic version comments of the Umbraco code. /// - public static string CurrentComment => "alpha.52"; + public static string Comment => SemanticVersion.Prerelease; /// - /// Gets the assembly version of Umbraco.Code.dll. + /// Gets the assembly version of the Umbraco code. /// - /// Get it by looking at a class in that dll, due to medium trust issues, - /// see http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx, - /// however fixme we don't support medium trust anymore? - public static string AssemblyVersion => new AssemblyName(typeof(UmbracoVersion).Assembly.FullName).Version.ToString(); + /// + /// The assembly version is the value of the . + /// Is is the one that the CLR checks for compatibility. Therefore, it changes only on + /// hard-breaking changes (for instance, on new major versions). + /// + public static Version AssemblyVersion {get; } /// - /// Gets the semantic version of the executing code. + /// Gets the assembly file version of the Umbraco code. /// - public static SemVersion SemanticVersion { get; } = new SemVersion( - Current.Major, - Current.Minor, - Current.Build, - CurrentComment.IsNullOrWhiteSpace() ? null : CurrentComment, - Current.Revision > 0 ? Current.Revision.ToInvariantString() : null); + /// + /// The assembly version is the value of the . + /// + public static Version AssemblyFileVersion { get; } + + /// + /// Gets the semantic version of the Umbraco code. + /// + /// + /// The semantic version is the value of the . + /// It is the full version of Umbraco, including comments. + /// + public static SemVersion SemanticVersion { get; } /// /// Gets the "local" version of the site. @@ -51,7 +75,7 @@ namespace Umbraco.Core.Configuration /// and changes during an upgrade. The executing code version changes when new code is /// deployed. The site/files version changes during an upgrade. /// - public static SemVersion Local + public static SemVersion LocalVersion { get { diff --git a/src/Umbraco.Core/Constants-Indexes.cs b/src/Umbraco.Core/Constants-Indexes.cs new file mode 100644 index 0000000000..c73a170b62 --- /dev/null +++ b/src/Umbraco.Core/Constants-Indexes.cs @@ -0,0 +1,19 @@ +using System; +using System.ComponentModel; + +namespace Umbraco.Core +{ + public static partial class Constants + { + public static class UmbracoIndexes + { + public const string InternalIndexName = InternalIndexPath + "Index"; + public const string ExternalIndexName = ExternalIndexPath + "Index"; + public const string MembersIndexName = MembersIndexPath + "Index"; + + public const string InternalIndexPath = "Internal"; + public const string ExternalIndexPath = "External"; + public const string MembersIndexPath = "Members"; + } + } +} diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index 910717304c..b15e371e87 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -23,6 +23,49 @@ namespace Umbraco.Core #region IContent + /// + /// Gets the current status of the Content + /// + public static ContentStatus GetStatus(this IContent content, string culture = null) + { + if (content.Trashed) + return ContentStatus.Trashed; + + if (!content.ContentType.VariesByCulture()) + culture = string.Empty; + else if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullException($"{nameof(culture)} cannot be null or empty"); + + var expires = content.ContentSchedule.GetSchedule(culture, ContentScheduleAction.Expire); + if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date)) + return ContentStatus.Expired; + + var release = content.ContentSchedule.GetSchedule(culture, ContentScheduleAction.Release); + if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now)) + return ContentStatus.AwaitingRelease; + + if (content.Published) + return ContentStatus.Published; + + return ContentStatus.Unpublished; + } + + /// + /// Gets the cultures that have been flagged for unpublishing. + /// + /// Gets cultures for which content.UnpublishCulture() has been invoked. + internal static IReadOnlyList GetCulturesUnpublishing(this IContent content) + { + if (!content.Published || !content.ContentType.VariesByCulture() || !content.IsPropertyDirty("PublishCultureInfos")) + return Array.Empty(); + + var culturesChanging = content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key); + return culturesChanging + .Where(x => !content.IsCulturePublished(x) && // is not published anymore + content.WasCulturePublished(x)) // but was published before + .ToList(); + } + /// /// Returns true if this entity was just published as part of a recent save operation (i.e. it wasn't previously published) /// diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index c455fadad7..4fa013c095 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -1,11 +1,6 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; -using Umbraco.Core.Logging; namespace Umbraco.Core { @@ -14,6 +9,18 @@ namespace Umbraco.Core /// public static class EnumerableExtensions { + /// + /// Wraps this object instance into an IEnumerable{T} consisting of a single item. + /// + /// Type of the object. + /// The instance that will be wrapped. + /// An IEnumerable{T} consisting of a single item. + public static IEnumerable Yield(this T item) + { + // see EnumeratorBenchmarks - this is faster, and allocates less, than returning an array + yield return item; + } + public static IEnumerable> InGroupsOf(this IEnumerable source, int groupSize) { if (source == null) diff --git a/src/Umbraco.Core/IO/FileSystems.cs b/src/Umbraco.Core/IO/FileSystems.cs index 5d7088b0e1..62ce25dff0 100644 --- a/src/Umbraco.Core/IO/FileSystems.cs +++ b/src/Umbraco.Core/IO/FileSystems.cs @@ -35,11 +35,7 @@ namespace Umbraco.Core.IO private object _wkfsObject; private MediaFileSystem _mediaFileSystem; - - //fixme - is this needed to be a managed file system? seems irrelevant since it won't ever be moved and is only used in one place in code - private IFileSystem _javascriptLibraryFileSystem; - #region Constructor // DI wants a public ctor @@ -129,16 +125,6 @@ namespace Umbraco.Core.IO } } - //fixme - is this needed to be a managed file system? seems irrelevant since it won't ever be moved and is only used in one place in code - internal IFileSystem JavaScriptLibraryFileSystem - { - get - { - if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); - return _javascriptLibraryFileSystem; - } - } - private void EnsureWellKnownFileSystems() { LazyInitializer.EnsureInitialized(ref _wkfsObject, ref _wkfsInitialized, ref _wkfsLock, CreateWellKnownFileSystems); @@ -154,7 +140,6 @@ namespace Umbraco.Core.IO var scriptsFileSystem = new PhysicalFileSystem(SystemDirectories.Scripts); var masterPagesFileSystem = new PhysicalFileSystem(SystemDirectories.Masterpages); var mvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews); - var javaScriptLibraryFileSystem = new PhysicalFileSystem(SystemDirectories.JavaScriptLibrary); _macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "Views/MacroPartials", () => IsScoped()); _partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "Views/Partials", () => IsScoped()); @@ -162,7 +147,6 @@ namespace Umbraco.Core.IO _scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", () => IsScoped()); _masterPagesFileSystem = new ShadowWrapper(masterPagesFileSystem, "masterpages", () => IsScoped()); _mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "Views", () => IsScoped()); - _javascriptLibraryFileSystem = new ShadowWrapper(javaScriptLibraryFileSystem, "Lib", () => IsScoped()); // filesystems obtained from GetFileSystemProvider are already wrapped and do not need to be wrapped again _mediaFileSystem = GetFileSystemProvider(); diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index 46c5bf851c..183d48e3d9 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -8,24 +8,20 @@ namespace Umbraco.Core.IO { public static string Bin => "~/bin"; - public static string Config => IOHelper.ReturnPath("umbracoConfigDirectory", "~/config"); + public static string Config => "~/config"; - public static string Css => IOHelper.ReturnPath("umbracoCssDirectory", "~/css"); + public static string Data => "~/App_Data"; - public static string Data => IOHelper.ReturnPath("umbracoStorageDirectory", "~/App_Data"); + public static string Install => "~/install"; - public static string Install => IOHelper.ReturnPath("umbracoInstallPath", "~/install"); + //fixme: remove this + [Obsolete("Master pages are obsolete and code should be removed")] + public static string Masterpages => "~/masterpages"; - //TODO: Consider removing this - public static string Masterpages => IOHelper.ReturnPath("umbracoMasterPagesPath", "~/masterpages"); - - //NOTE: this is not configurable and shouldn't need to be public static string AppCode => "~/App_Code"; - //NOTE: this is not configurable and shouldn't need to be public static string AppPlugins => "~/App_Plugins"; - //NOTE: this is not configurable and shouldn't need to be public static string MvcViews => "~/Views"; public static string PartialViews => MvcViews + "/Partials/"; @@ -36,21 +32,20 @@ namespace Umbraco.Core.IO public static string Scripts => IOHelper.ReturnPath("umbracoScriptsPath", "~/scripts"); - public static string StyleSheets => IOHelper.ReturnPath("umbracoStylesheetsPath", "~/css"); + public static string Css => IOHelper.ReturnPath("umbracoCssPath", "~/css"); public static string Umbraco => IOHelper.ReturnPath("umbracoPath", "~/umbraco"); - //TODO: Consider removing this - public static string UserControls => IOHelper.ReturnPath("umbracoUsercontrolsPath", "~/usercontrols"); + //fixme: remove this + [Obsolete("Usercontrols are obsolete and code should be removed")] + public static string UserControls => "~/usercontrols"; + [Obsolete("Only used by legacy load balancing which is obsolete and should be removed")] public static string WebServices => IOHelper.ReturnPath("umbracoWebservicesPath", Umbraco.EnsureEndsWith("/") + "webservices"); - //by default the packages folder should exist in the data folder - public static string Packages => IOHelper.ReturnPath("umbracoPackagesPath", Data + IOHelper.DirSepChar + "packages"); + public static string Packages => Data + IOHelper.DirSepChar + "packages"; - public static string Preview => IOHelper.ReturnPath("umbracoPreviewPath", Data + IOHelper.DirSepChar + "preview"); - - public static string JavaScriptLibrary => IOHelper.ReturnPath("umbracoJavaScriptLibraryPath", Umbraco + IOHelper.DirSepChar + "lib"); + public static string Preview => Data + IOHelper.DirSepChar + "preview"; private static string _root; diff --git a/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs b/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs deleted file mode 100644 index 87f104d90e..0000000000 --- a/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Newtonsoft.Json.Linq; -using Umbraco.Core.Models.ContentEditing; -using Umbraco.Core.Serialization; - -namespace Umbraco.Core.Manifest -{ - /// - /// Implements a json read converter for . - /// - internal class ContentAppDefinitionConverter : JsonReadConverter - { - protected override IContentAppDefinition Create(Type objectType, string path, JObject jObject) - => new ManifestContentAppDefinition(); - } -} diff --git a/src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs b/src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs new file mode 100644 index 0000000000..c627728a32 --- /dev/null +++ b/src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs @@ -0,0 +1,45 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Configuration.Dashboard; +using Umbraco.Core.Serialization; + +namespace Umbraco.Core.Manifest +{ + /// + /// Implements a json read converter for . + /// + internal class DashboardAccessRuleConverter : JsonReadConverter + { + /// + protected override IAccessRule Create(Type objectType, string path, JObject jObject) + { + return new AccessRule(); + } + + /// + protected override void Deserialize(JObject jobject, IAccessRule target, JsonSerializer serializer) + { + // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) + + if (!(target is AccessRule accessRule)) + throw new Exception("panic."); + + GetRule(accessRule, jobject, "grant", AccessRuleType.Grant); + GetRule(accessRule, jobject, "deny", AccessRuleType.Deny); + GetRule(accessRule, jobject, "grantBySection", AccessRuleType.GrantBySection); + + if (accessRule.Type == AccessRuleType.Unknown) throw new InvalidOperationException("Rule is not defined."); + } + + private void GetRule(AccessRule rule, JObject jobject, string name, AccessRuleType type) + { + var token = jobject[name]; + if (token == null) return; + if (rule.Type != AccessRuleType.Unknown) throw new InvalidOperationException("Multiple definition of a rule."); + if (token.Type != JTokenType.String) throw new InvalidOperationException("Rule value is not a string."); + rule.Type = type; + rule.Value = token.Value(); + } + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs index d5f6c2b8c4..0667f11aab 100644 --- a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs +++ b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs @@ -31,11 +31,9 @@ namespace Umbraco.Core.Manifest /// Represents a content app definition, parsed from a manifest. /// [DataContract(Name = "appdef", Namespace = "")] - public class ManifestContentAppDefinition : IContentAppDefinition + public class ManifestContentAppDefinition { private string _view; - private ContentApp _app; - private ShowRule[] _showRules; /// /// Gets or sets the name of the content app. @@ -83,132 +81,5 @@ namespace Umbraco.Core.Manifest [DataMember(Name = "show")] public string[] Show { get; set; } = Array.Empty(); - /// - public ContentApp GetContentAppFor(object o, IEnumerable userGroups) - { - string partA, partB; - - switch (o) - { - case IContent content: - partA = "content"; - partB = content.ContentType.Alias; - break; - - case IMedia media: - partA = "media"; - partB = media.ContentType.Alias; - break; - - default: - return null; - } - - var rules = _showRules ?? (_showRules = ShowRule.Parse(Show).ToArray()); - var userGroupsList = userGroups.ToList(); - - var okRole = false; - var hasRole = false; - var okType = false; - var hasType = false; - - foreach (var rule in rules) - { - if (rule.PartA.InvariantEquals("role")) - { - // if roles have been ok-ed already, skip the rule - if (okRole) - continue; - - // remember we have role rules - hasRole = true; - - foreach (var group in userGroupsList) - { - // if the entry does not apply, skip - if (!rule.Matches("role", group.Alias)) - continue; - - // if the entry applies, - // if it's an exclude entry, exit, do not display the content app - if (!rule.Show) - return null; - - // else ok to display, remember roles are ok, break from userGroupsList - okRole = rule.Show; - break; - } - } - else // it is a type rule - { - // if type has been ok-ed already, skip the rule - if (okType) - continue; - - // remember we have type rules - hasType = true; - - // if the entry does not apply, skip it - if (!rule.Matches(partA, partB)) - continue; - - // if the entry applies, - // if it's an exclude entry, exit, do not display the content app - if (!rule.Show) - return null; - - // else ok to display, remember type rules are ok - okType = true; - } - } - - // if roles rules are specified but not ok, - // or if type roles are specified but not ok, - // cannot display the content app - if ((hasRole && !okRole) || (hasType && !okType)) - return null; - - // else - // content app can be displayed - return _app ?? (_app = new ContentApp - { - Alias = Alias, - Name = Name, - Icon = Icon, - View = View, - Weight = Weight - }); - } - - private class ShowRule - { - private static readonly Regex ShowRegex = new Regex("^([+-])?([a-z]+)/([a-z0-9_]+|\\*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public bool Show { get; private set; } - public string PartA { get; private set; } - public string PartB { get; private set; } - - public bool Matches(string partA, string partB) - { - return (PartA == "*" || PartA.InvariantEquals(partA)) && (PartB == "*" || PartB.InvariantEquals(partB)); - } - - public static IEnumerable Parse(string[] rules) - { - foreach (var rule in rules) - { - var match = ShowRegex.Match(rule); - if (!match.Success) - throw new FormatException($"Illegal 'show' entry \"{rule}\" in manifest."); - - yield return new ShowRule - { - Show = match.Groups[1].Value != "-", - PartA = match.Groups[2].Value, - PartB = match.Groups[3].Value - }; - } - } - } } } diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs new file mode 100644 index 0000000000..1c50a4b895 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Core.Manifest +{ + // contentApps: [ + // { + // name: 'App Name', // required + // alias: 'appAlias', // required + // weight: 0, // optional, default is 0, use values between -99 and +99 + // icon: 'icon.app', // required + // view: 'path/view.htm', // required + // show: [ // optional, default is always show + // '-content/foo', // hide for content type 'foo' + // '+content/*', // show for all other content types + // '+media/*', // show for all media types + // '+role/admin' // show for admin users. Role based permissions will override others. + // ] + // }, + // ... + // ] + + /// + /// Represents a content app factory, for content apps parsed from the manifest. + /// + public class ManifestContentAppFactory : IContentAppFactory + { + private readonly ManifestContentAppDefinition _definition; + + public ManifestContentAppFactory(ManifestContentAppDefinition definition) + { + _definition = definition; + } + + private ContentApp _app; + private ShowRule[] _showRules; + + /// + public ContentApp GetContentAppFor(object o,IEnumerable userGroups) + { + string partA, partB; + + switch (o) + { + case IContent content: + partA = "content"; + partB = content.ContentType.Alias; + break; + + case IMedia media: + partA = "media"; + partB = media.ContentType.Alias; + break; + + default: + return null; + } + + var rules = _showRules ?? (_showRules = ShowRule.Parse(_definition.Show).ToArray()); + var userGroupsList = userGroups.ToList(); + + var okRole = false; + var hasRole = false; + var okType = false; + var hasType = false; + + foreach (var rule in rules) + { + if (rule.PartA.InvariantEquals("role")) + { + // if roles have been ok-ed already, skip the rule + if (okRole) + continue; + + // remember we have role rules + hasRole = true; + + foreach (var group in userGroupsList) + { + // if the entry does not apply, skip + if (!rule.Matches("role", group.Alias)) + continue; + + // if the entry applies, + // if it's an exclude entry, exit, do not display the content app + if (!rule.Show) + return null; + + // else ok to display, remember roles are ok, break from userGroupsList + okRole = rule.Show; + break; + } + } + else // it is a type rule + { + // if type has been ok-ed already, skip the rule + if (okType) + continue; + + // remember we have type rules + hasType = true; + + // if the entry does not apply, skip it + if (!rule.Matches(partA, partB)) + continue; + + // if the entry applies, + // if it's an exclude entry, exit, do not display the content app + if (!rule.Show) + return null; + + // else ok to display, remember type rules are ok + okType = true; + } + } + + // if roles rules are specified but not ok, + // or if type roles are specified but not ok, + // cannot display the content app + if ((hasRole && !okRole) || (hasType && !okType)) + return null; + + // else + // content app can be displayed + return _app ?? (_app = new ContentApp + { + Alias = _definition.Alias, + Name = _definition.Name, + Icon = _definition.Icon, + View = _definition.View, + Weight = _definition.Weight + }); + } + + private class ShowRule + { + private static readonly Regex ShowRegex = new Regex("^([+-])?([a-z]+)/([a-z0-9_]+|\\*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public bool Show { get; private set; } + public string PartA { get; private set; } + public string PartB { get; private set; } + + public bool Matches(string partA, string partB) + { + return (PartA == "*" || PartA.InvariantEquals(partA)) && (PartB == "*" || PartB.InvariantEquals(partB)); + } + + public static IEnumerable Parse(string[] rules) + { + foreach (var rule in rules) + { + var match = ShowRegex.Match(rule); + if (!match.Success) + throw new FormatException($"Illegal 'show' entry \"{rule}\" in manifest."); + + yield return new ShowRule + { + Show = match.Groups[1].Value != "-", + PartA = match.Groups[2].Value, + PartB = match.Groups[3].Value + }; + } + } + } + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs b/src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs new file mode 100644 index 0000000000..83f047b264 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel; +using Newtonsoft.Json; +using Umbraco.Core.Configuration.Dashboard; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Manifest +{ + public class ManifestDashboardDefinition + { + private string _view; + + [JsonProperty("name", Required = Required.Always)] + public string Name { get; set; } + + [JsonProperty("alias", Required = Required.Always)] + public string Alias { get; set; } + + [JsonProperty("weight", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(100)] + public int Weight { get; set; } + + [JsonProperty("view", Required = Required.Always)] + public string View + { + get => _view; + set => _view = IOHelper.ResolveVirtualUrl(value); + } + + [JsonProperty("sections")] + public string[] Sections { get; set; } = Array.Empty(); + + [JsonProperty("access")] + public IAccessRule[] AccessRules { get; set; } = Array.Empty(); + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs index 125dee5c05..59753df66a 100644 --- a/src/Umbraco.Core/Manifest/ManifestParser.cs +++ b/src/Umbraco.Core/Manifest/ManifestParser.cs @@ -1,177 +1,179 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Newtonsoft.Json; -using Umbraco.Core.Cache; -using Umbraco.Core.Exceptions; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Models.ContentEditing; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Core.Manifest -{ - /// - /// Parses the Main.js file and replaces all tokens accordingly. - /// - public class ManifestParser - { - private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); - - private readonly IRuntimeCacheProvider _cache; - private readonly ILogger _logger; - private readonly ManifestValueValidatorCollection _validators; - - private string _path; - - /// - /// Initializes a new instance of the class. - /// - public ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, ILogger logger) - : this(cache, validators, "~/App_Plugins", logger) - { } - - /// - /// Initializes a new instance of the class. - /// - private ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, string path, ILogger logger) - { - _cache = cache ?? throw new ArgumentNullException(nameof(cache)); - _validators = validators ?? throw new ArgumentNullException(nameof(validators)); - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path)); - Path = path; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public string Path - { - get => _path; - set => _path = value.StartsWith("~/") ? IOHelper.MapPath(value) : value; - } - - /// - /// Gets all manifests, merged into a single manifest object. - /// - /// - public PackageManifest Manifest - => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => - { - var manifests = GetManifests(); - return MergeManifests(manifests); - }, new TimeSpan(0, 4, 0)); - - /// - /// Gets all manifests. - /// - private IEnumerable GetManifests() - { - var manifests = new List(); - - foreach (var path in GetManifestFiles()) - { - try - { - var text = File.ReadAllText(path); - text = TrimPreamble(text); - if (string.IsNullOrWhiteSpace(text)) - continue; - var manifest = ParseManifest(text); - manifests.Add(manifest); - } - catch (Exception e) - { - _logger.Error(e, "Failed to parse manifest at '{Path}', ignoring.", path); - } - } - - return manifests; - } - - /// - /// Merges all manifests into one. - /// - private static PackageManifest MergeManifests(IEnumerable manifests) - { - var scripts = new HashSet(); - var stylesheets = new HashSet(); - var propertyEditors = new List(); - var parameterEditors = new List(); - var gridEditors = new List(); - var contentApps = new List(); - - foreach (var manifest in manifests) - { - if (manifest.Scripts != null) foreach (var script in manifest.Scripts) scripts.Add(script); - if (manifest.Stylesheets != null) foreach (var stylesheet in manifest.Stylesheets) stylesheets.Add(stylesheet); - if (manifest.PropertyEditors != null) propertyEditors.AddRange(manifest.PropertyEditors); - if (manifest.ParameterEditors != null) parameterEditors.AddRange(manifest.ParameterEditors); - if (manifest.GridEditors != null) gridEditors.AddRange(manifest.GridEditors); - if (manifest.ContentApps != null) contentApps.AddRange(manifest.ContentApps); - } - - return new PackageManifest - { - Scripts = scripts.ToArray(), - Stylesheets = stylesheets.ToArray(), - PropertyEditors = propertyEditors.ToArray(), - ParameterEditors = parameterEditors.ToArray(), - GridEditors = gridEditors.ToArray(), - ContentApps = contentApps.ToArray() - }; - } - - // gets all manifest files (recursively) - private IEnumerable GetManifestFiles() - { - if (Directory.Exists(_path) == false) - return new string[0]; - return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories); - } - - - private static string TrimPreamble(string text) - { - // strangely StartsWith(preamble) would always return true - if (text.Substring(0, 1) == Utf8Preamble) - text = text.Remove(0, Utf8Preamble.Length); - - return text; - } - - /// - /// Parses a manifest. - /// - internal PackageManifest ParseManifest(string text) - { - if (string.IsNullOrWhiteSpace(text)) - throw new ArgumentNullOrEmptyException(nameof(text)); - - var manifest = JsonConvert.DeserializeObject(text, - new DataEditorConverter(_logger), - new ValueValidatorConverter(_validators), - new ContentAppDefinitionConverter()); - - // scripts and stylesheets are raw string, must process here - for (var i = 0; i < manifest.Scripts.Length; i++) - manifest.Scripts[i] = IOHelper.ResolveVirtualUrl(manifest.Scripts[i]); - for (var i = 0; i < manifest.Stylesheets.Length; i++) - manifest.Stylesheets[i] = IOHelper.ResolveVirtualUrl(manifest.Stylesheets[i]); - - // add property editors that are also parameter editors, to the parameter editors list - // (the manifest format is kinda legacy) - var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList(); - if (ppEditors.Count > 0) - manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray(); - - return manifest; - } - - // purely for tests - internal IEnumerable ParseGridEditors(string text) - { - return JsonConvert.DeserializeObject>(text); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Manifest +{ + /// + /// Parses the Main.js file and replaces all tokens accordingly. + /// + public class ManifestParser + { + private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); + + private readonly IRuntimeCacheProvider _cache; + private readonly ILogger _logger; + private readonly ManifestValueValidatorCollection _validators; + + private string _path; + + /// + /// Initializes a new instance of the class. + /// + public ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, ILogger logger) + : this(cache, validators, "~/App_Plugins", logger) + { } + + /// + /// Initializes a new instance of the class. + /// + private ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, string path, ILogger logger) + { + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + _validators = validators ?? throw new ArgumentNullException(nameof(validators)); + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path)); + Path = path; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public string Path + { + get => _path; + set => _path = value.StartsWith("~/") ? IOHelper.MapPath(value) : value; + } + + /// + /// Gets all manifests, merged into a single manifest object. + /// + /// + public PackageManifest Manifest + => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => + { + var manifests = GetManifests(); + return MergeManifests(manifests); + }, new TimeSpan(0, 4, 0)); + + /// + /// Gets all manifests. + /// + private IEnumerable GetManifests() + { + var manifests = new List(); + + foreach (var path in GetManifestFiles()) + { + try + { + var text = File.ReadAllText(path); + text = TrimPreamble(text); + if (string.IsNullOrWhiteSpace(text)) + continue; + var manifest = ParseManifest(text); + manifests.Add(manifest); + } + catch (Exception e) + { + _logger.Error(e, "Failed to parse manifest at '{Path}', ignoring.", path); + } + } + + return manifests; + } + + /// + /// Merges all manifests into one. + /// + private static PackageManifest MergeManifests(IEnumerable manifests) + { + var scripts = new HashSet(); + var stylesheets = new HashSet(); + var propertyEditors = new List(); + var parameterEditors = new List(); + var gridEditors = new List(); + var contentApps = new List(); + var dashboards = new List(); + + foreach (var manifest in manifests) + { + if (manifest.Scripts != null) foreach (var script in manifest.Scripts) scripts.Add(script); + if (manifest.Stylesheets != null) foreach (var stylesheet in manifest.Stylesheets) stylesheets.Add(stylesheet); + if (manifest.PropertyEditors != null) propertyEditors.AddRange(manifest.PropertyEditors); + if (manifest.ParameterEditors != null) parameterEditors.AddRange(manifest.ParameterEditors); + if (manifest.GridEditors != null) gridEditors.AddRange(manifest.GridEditors); + if (manifest.ContentApps != null) contentApps.AddRange(manifest.ContentApps); + if (manifest.Dashboards != null) dashboards.AddRange(manifest.Dashboards); + } + + return new PackageManifest + { + Scripts = scripts.ToArray(), + Stylesheets = stylesheets.ToArray(), + PropertyEditors = propertyEditors.ToArray(), + ParameterEditors = parameterEditors.ToArray(), + GridEditors = gridEditors.ToArray(), + ContentApps = contentApps.ToArray(), + Dashboards = dashboards.ToArray() + }; + } + + // gets all manifest files (recursively) + private IEnumerable GetManifestFiles() + { + if (Directory.Exists(_path) == false) + return new string[0]; + return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories); + } + + private static string TrimPreamble(string text) + { + // strangely StartsWith(preamble) would always return true + if (text.Substring(0, 1) == Utf8Preamble) + text = text.Remove(0, Utf8Preamble.Length); + + return text; + } + + /// + /// Parses a manifest. + /// + internal PackageManifest ParseManifest(string text) + { + if (string.IsNullOrWhiteSpace(text)) + throw new ArgumentNullOrEmptyException(nameof(text)); + + var manifest = JsonConvert.DeserializeObject(text, + new DataEditorConverter(_logger), + new ValueValidatorConverter(_validators), + new DashboardAccessRuleConverter()); + + // scripts and stylesheets are raw string, must process here + for (var i = 0; i < manifest.Scripts.Length; i++) + manifest.Scripts[i] = IOHelper.ResolveVirtualUrl(manifest.Scripts[i]); + for (var i = 0; i < manifest.Stylesheets.Length; i++) + manifest.Stylesheets[i] = IOHelper.ResolveVirtualUrl(manifest.Stylesheets[i]); + + // add property editors that are also parameter editors, to the parameter editors list + // (the manifest format is kinda legacy) + var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList(); + if (ppEditors.Count > 0) + manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray(); + + return manifest; + } + + // purely for tests + internal IEnumerable ParseGridEditors(string text) + { + return JsonConvert.DeserializeObject>(text); + } + } +} diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 32dae46a9a..cd806ac847 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -1,31 +1,34 @@ -using System; -using Newtonsoft.Json; -using Umbraco.Core.Models.ContentEditing; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Core.Manifest -{ - /// - /// Represents the content of a package manifest. - /// - public class PackageManifest - { - [JsonProperty("javascript")] - public string[] Scripts { get; set; } = Array.Empty(); - - [JsonProperty("css")] - public string[] Stylesheets { get; set; }= Array.Empty(); - - [JsonProperty("propertyEditors")] - public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); - - [JsonProperty("parameterEditors")] - public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); - - [JsonProperty("gridEditors")] - public GridEditor[] GridEditors { get; set; } = Array.Empty(); - - [JsonProperty("contentApps")] - public IContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); - } -} +using System; +using Newtonsoft.Json; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Manifest +{ + /// + /// Represents the content of a package manifest. + /// + public class PackageManifest + { + [JsonProperty("javascript")] + public string[] Scripts { get; set; } = Array.Empty(); + + [JsonProperty("css")] + public string[] Stylesheets { get; set; }= Array.Empty(); + + [JsonProperty("propertyEditors")] + public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); + + [JsonProperty("parameterEditors")] + public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); + + [JsonProperty("gridEditors")] + public GridEditor[] GridEditors { get; set; } = Array.Empty(); + + [JsonProperty("contentApps")] + public ManifestContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); + + [JsonProperty("dashboards")] + public ManifestDashboardDefinition[] Dashboards { get; set; } = Array.Empty(); + } +} diff --git a/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs index 8d95d9c14c..eee9826a85 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Migrations.Expressions.Alter.Expressions; using Umbraco.Core.Migrations.Expressions.Common.Expressions; using Umbraco.Core.Migrations.Expressions.Create.Expressions; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Migrations.Expressions.Alter.Table @@ -154,8 +155,8 @@ namespace Umbraco.Core.Migrations.Expressions.Alter.Table var index = new CreateIndexExpression(_context, new IndexDefinition { Name = indexName, - TableName = Expression.TableName, - IsUnique = true + TableName = Expression.TableName, + IndexType = IndexTypes.UniqueNonClustered }); index.Index.Columns.Add(new IndexColumnDefinition diff --git a/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs index 28de5aef14..656aedcea0 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs @@ -1,6 +1,7 @@ using System.Data; using NPoco; using Umbraco.Core.Migrations.Expressions.Common.Expressions; +using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Migrations.Expressions.Create.Column @@ -112,8 +113,8 @@ namespace Umbraco.Core.Migrations.Expressions.Create.Column var index = new CreateIndexExpression(_context, new IndexDefinition { Name = indexName, - TableName = Expression.TableName, - IsUnique = true + TableName = Expression.TableName, + IndexType = IndexTypes.UniqueNonClustered }); index.Index.Columns.Add(new IndexColumnDefinition diff --git a/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs index ad8ac7f22d..1f2cb93f95 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs @@ -56,41 +56,29 @@ namespace Umbraco.Core.Migrations.Expressions.Create.Index /// ICreateIndexOnColumnBuilder ICreateIndexColumnOptionsBuilder.Unique() - { - Expression.Index.IsUnique = true; - //if it is Unique then it must be unique nonclustered and set the other flags - Expression.Index.IndexType = IndexTypes.UniqueNonClustered; - Expression.Index.IsClustered = false; + { + Expression.Index.IndexType = IndexTypes.UniqueNonClustered; return this; } /// public ICreateIndexOnColumnBuilder NonClustered() { - Expression.Index.IndexType = IndexTypes.NonClustered; - Expression.Index.IsClustered = false; - Expression.Index.IndexType = IndexTypes.NonClustered; - Expression.Index.IsUnique = false; + Expression.Index.IndexType = IndexTypes.NonClustered; return this; } /// public ICreateIndexOnColumnBuilder Clustered() - { - Expression.Index.IndexType = IndexTypes.Clustered; - Expression.Index.IsClustered = true; - //if it is clustered then we have to change the index type set the other flags - Expression.Index.IndexType = IndexTypes.Clustered; - Expression.Index.IsClustered = true; - Expression.Index.IsUnique = false; - return this; + { + Expression.Index.IndexType = IndexTypes.Clustered; + return this; } /// ICreateIndexOnColumnBuilder ICreateIndexOptionsBuilder.Unique() { - Expression.Index.IndexType = IndexTypes.UniqueNonClustered; - Expression.Index.IsUnique = true; + Expression.Index.IndexType = IndexTypes.UniqueNonClustered; return this; } } diff --git a/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs index 10836fd228..f765a58169 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs @@ -3,6 +3,7 @@ using NPoco; using Umbraco.Core.Migrations.Expressions.Common.Expressions; using Umbraco.Core.Migrations.Expressions.Create.Expressions; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Migrations.Expressions.Create.Table @@ -176,8 +177,8 @@ namespace Umbraco.Core.Migrations.Expressions.Create.Table { Name = indexName, SchemaName = Expression.SchemaName, - TableName = Expression.TableName, - IsUnique = true + TableName = Expression.TableName, + IndexType = IndexTypes.UniqueNonClustered }); index.Index.Columns.Add(new IndexColumnDefinition diff --git a/src/Umbraco.Core/Migrations/IMigration.cs b/src/Umbraco.Core/Migrations/IMigration.cs index 53b7874b3a..c929234f77 100644 --- a/src/Umbraco.Core/Migrations/IMigration.cs +++ b/src/Umbraco.Core/Migrations/IMigration.cs @@ -7,6 +7,9 @@ namespace Umbraco.Core.Migrations /// public interface IMigration : IDiscoverable { + /// + /// Executes the migration. + /// void Migrate(); } } diff --git a/src/Umbraco.Core/Migrations/IMigrationContext.cs b/src/Umbraco.Core/Migrations/IMigrationContext.cs index 4db1b07b63..80ba78b6de 100644 --- a/src/Umbraco.Core/Migrations/IMigrationContext.cs +++ b/src/Umbraco.Core/Migrations/IMigrationContext.cs @@ -24,8 +24,13 @@ namespace Umbraco.Core.Migrations ISqlContext SqlContext { get; } /// - /// Gets the expression index. + /// Gets or sets the expression index. /// int Index { get; set; } + + /// + /// Gets or sets a value indicating whether an expression is being built. + /// + bool BuildingExpression { get; set; } } } diff --git a/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs b/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs new file mode 100644 index 0000000000..91d1838d6f --- /dev/null +++ b/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs @@ -0,0 +1,28 @@ +using System; + +namespace Umbraco.Core.Migrations +{ + /// + /// Represents errors that occurs when a migration exception is not executed. + /// + /// + /// Migration expression such as Alter.Table(...).Do() *must* end with Do() else they are + /// not executed. When a non-executed expression is detected, an IncompleteMigrationExpressionException + /// is thrown. + /// + public class IncompleteMigrationExpressionException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public IncompleteMigrationExpressionException() + { } + + /// + /// Initializes a new instance of the class with a message. + /// + public IncompleteMigrationExpressionException(string message) + : base(message) + { } + } +} diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index 30f7ba8834..64be8161f2 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -81,7 +81,8 @@ namespace Umbraco.Core.Migrations.Install typeof (ConsentDto), typeof (AuditEntryDto), typeof (ContentVersionCultureVariationDto), - typeof (DocumentCultureVariationDto) + typeof (DocumentCultureVariationDto), + typeof (ContentScheduleDto) }; /// diff --git a/src/Umbraco.Core/Migrations/MigrationBase.cs b/src/Umbraco.Core/Migrations/MigrationBase.cs index 9fbee0ed92..58edcae80a 100644 --- a/src/Umbraco.Core/Migrations/MigrationBase.cs +++ b/src/Umbraco.Core/Migrations/MigrationBase.cs @@ -62,42 +62,65 @@ namespace Umbraco.Core.Migrations /// protected Sql Sql(string sql, params object[] args) => Context.SqlContext.Sql(sql, args); - /// + /// + /// Executes the migration. + /// public abstract void Migrate(); + /// + void IMigration.Migrate() + { + Migrate(); + + // ensure there is no building expression + // ie we did not forget to .Do() an expression + if (Context.BuildingExpression) + throw new IncompleteMigrationExpressionException("The migration has run, but leaves an expression that has not run."); + } + + // ensures we are not already building, + // ie we did not forget to .Do() an expression + private T BeginBuild(T builder) + { + if (Context.BuildingExpression) + throw new IncompleteMigrationExpressionException("Cannot create a new expression: the previous expression has not run."); + Context.BuildingExpression = true; + return builder; + } + /// /// Builds an Alter expression. /// - public IAlterBuilder Alter => new AlterBuilder(Context); + public IAlterBuilder Alter => BeginBuild(new AlterBuilder(Context)); /// /// Builds a Create expression. /// - public ICreateBuilder Create => new CreateBuilder(Context); + public ICreateBuilder Create => BeginBuild(new CreateBuilder(Context)); /// /// Builds a Delete expression. /// - public IDeleteBuilder Delete => new DeleteBuilder(Context); + public IDeleteBuilder Delete => BeginBuild(new DeleteBuilder(Context)); /// /// Builds an Execute expression. /// - public IExecuteBuilder Execute => new ExecuteBuilder(Context); + public IExecuteBuilder Execute => BeginBuild(new ExecuteBuilder(Context)); /// /// Builds an Insert expression. /// - public IInsertBuilder Insert => new InsertBuilder(Context); + public IInsertBuilder Insert => BeginBuild(new InsertBuilder(Context)); /// /// Builds a Rename expression. /// - public IRenameBuilder Rename => new RenameBuilder(Context); + public IRenameBuilder Rename => BeginBuild(new RenameBuilder(Context)); /// /// Builds an Update expression. /// - public IUpdateBuilder Update => new UpdateBuilder(Context); + public IUpdateBuilder Update => BeginBuild(new UpdateBuilder(Context)); } } diff --git a/src/Umbraco.Core/Migrations/MigrationContext.cs b/src/Umbraco.Core/Migrations/MigrationContext.cs index d0802c813d..da454fab03 100644 --- a/src/Umbraco.Core/Migrations/MigrationContext.cs +++ b/src/Umbraco.Core/Migrations/MigrationContext.cs @@ -4,20 +4,33 @@ using Umbraco.Core.Persistence; namespace Umbraco.Core.Migrations { + /// + /// Represents a migration context. + /// internal class MigrationContext : IMigrationContext { + /// + /// Initializes a new instance of the class. + /// public MigrationContext(IUmbracoDatabase database, ILogger logger) { Database = database ?? throw new ArgumentNullException(nameof(database)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// public ILogger Logger { get; } + /// public IUmbracoDatabase Database { get; } + /// public ISqlContext SqlContext => Database.SqlContext; + /// public int Index { get; set; } + + /// + public bool BuildingExpression { get; set; } } } diff --git a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs index 4c5a1f1aa7..6ac92a07aa 100644 --- a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs +++ b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs @@ -50,6 +50,7 @@ namespace Umbraco.Core.Migrations if (_executed) throw new InvalidOperationException("This expression has already been executed."); _executed = true; + Context.BuildingExpression = false; var sql = GetSql(); diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index fb0f18700c..ec49544976 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -119,7 +119,7 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0 Chain("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0 //Chain("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove - + Chain("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // and provide a path out of andy's .CopyChain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // to next @@ -141,6 +141,9 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); Chain("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); Chain("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); + Chain("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); + Chain("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); + Chain("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); //FINAL diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs index d8f2d37067..c8d65961f4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs @@ -1,47 +1,51 @@ -using System; -using System.Linq; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 { /// - /// Set the default storageType for the tags datatype to "CSV" to ensure backwards compatibilty since the default is going to be JSON in new versions - /// + /// Set the default storageType for the tags datatype to "CSV" to ensure backwards compatibility since the default is going to be JSON in new versions. + /// public class SetDefaultTagsStorageType : MigrationBase { - public SetDefaultTagsStorageType(IMigrationContext context) : base(context) - { - } + public SetDefaultTagsStorageType(IMigrationContext context) + : base(context) + { } + + // dummy editor for deserialization + private class TagConfigurationEditor : ConfigurationEditor + { } public override void Migrate() { - if (Context?.Database == null) return; + // get all Umbraco.Tags datatypes + var dataTypeDtos = Database.Fetch(Context.SqlContext.Sql() + .Select() + .From() + .Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.Tags)); - // We need to get all datatypes with an alias of "umbraco.tags" so we can loop over them and set the missing values if needed - var datatypes = Context.Database.Fetch(); - var tagsDataTypes = datatypes.Where(x => string.Equals(x.EditorAlias, Constants.PropertyEditors.Aliases.Tags, StringComparison.InvariantCultureIgnoreCase)); + // get a dummy editor for deserialization + var editor = new TagConfigurationEditor(); - foreach (var datatype in tagsDataTypes) + foreach (var dataTypeDto in dataTypeDtos) { - var dataTypePreValues = JsonConvert.DeserializeObject(datatype.Configuration); + // need to check storageType on raw dictionary, as TagConfiguration would have a default value + var dictionary = JsonConvert.DeserializeObject(dataTypeDto.Configuration); - // We need to check if the node has a "storageType" set - if (!dataTypePreValues.ContainsKey("storageType")) + // if missing, use TagConfiguration to properly update the configuration + // due to ... reasons ... the key can start with a lower or upper 'S' + if (!dictionary.ContainsKey("storageType") && !dictionary.ContainsKey("StorageType")) { - dataTypePreValues["storageType"] = "Csv"; + var configuration = (TagConfiguration)editor.FromDatabase(dataTypeDto.Configuration); + configuration.StorageType = TagsStorageType.Csv; + dataTypeDto.Configuration = ConfigurationEditor.ToDatabase(configuration); + Database.Update(dataTypeDto); } - - Update.Table(Constants.DatabaseSchema.Tables.DataType) - .Set(new { config = JsonConvert.SerializeObject(dataTypePreValues) }) - .Where(new { nodeId = datatype.NodeId }) - .Do(); } } - - } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs index e5cda85168..ed2990aff7 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs @@ -1,92 +1,105 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; +using Umbraco.Core.Cache; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Sync; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { public class DropDownPropertyEditorsMigration : MigrationBase { - public DropDownPropertyEditorsMigration(IMigrationContext context) : base(context) + private readonly CacheRefresherCollection _cacheRefreshers; + private readonly IServerMessenger _serverMessenger; + + public DropDownPropertyEditorsMigration(IMigrationContext context, CacheRefresherCollection cacheRefreshers, IServerMessenger serverMessenger) + : base(context) { + _cacheRefreshers = cacheRefreshers; + _serverMessenger = serverMessenger; } + // dummy editor for deserialization + private class ValueListConfigurationEditor : ConfigurationEditor + { } + public override void Migrate() { //need to convert the old drop down data types to use the new one - var oldDropDowns = Database.Fetch(Sql() + var dataTypes = Database.Fetch(Sql() .Select() .From() .Where(x => x.EditorAlias.Contains(".DropDown"))); - foreach (var dd in oldDropDowns) + + foreach (var dataType in dataTypes) { - //nothing to change if there is no config - if (dd.Configuration.IsNullOrWhiteSpace()) continue; - ValueListConfiguration config; - try - { - config = JsonConvert.DeserializeObject(dd.Configuration); - } - catch (Exception ex) - { - Logger.Error( - ex, "Invalid drop down configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", - dd.Configuration); - dd.Configuration = null; - Database.Update(dd); - continue; - } - var propDataSql = Sql().Select().From() - .InnerJoin().On(x => x.Id, x => x.PropertyTypeId) - .InnerJoin().On(x => x.NodeId, x => x.DataTypeId) - .Where(x => x.DataTypeId == dd.NodeId); - - var propDatas = Database.Query(propDataSql); - var toUpdate = new List(); - foreach (var propData in propDatas) + if (!dataType.Configuration.IsNullOrWhiteSpace()) { - if (UpdatePropertyDataDto(propData, config)) + // parse configuration, and update everything accordingly + try { - //update later, we are iterating all values right now so no SQL can be run inside of this iteration (i.e. Query) - toUpdate.Add(propData); + config = (ValueListConfiguration) new ValueListConfigurationEditor().FromDatabase(dataType.Configuration); } - } + catch (Exception ex) + { + Logger.Error( + ex, "Invalid drop down configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", + dataType.Configuration); - //run the property data updates - foreach (var propData in toUpdate) + // reset + config = new ValueListConfiguration(); + } + + // get property data dtos + var propertyDataDtos = Database.Fetch(Sql() + .Select() + .From() + .InnerJoin().On((pt, pd) => pt.Id == pd.PropertyTypeId) + .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) + .Where(x => x.DataTypeId == dataType.NodeId)); + + // update dtos + var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config)); + + // persist changes + foreach (var propertyDataDto in updatedDtos) + Database.Update(propertyDataDto); + } + else { - Database.Update(propData); + // default configuration + config = new ValueListConfiguration(); } var requiresCacheRebuild = false; - switch (dd.EditorAlias) + switch (dataType.EditorAlias) { case string ea when ea.InvariantEquals("Umbraco.DropDown"): - UpdateDataType(dd, config, false); + UpdateDataType(dataType, config, false); break; case string ea when ea.InvariantEquals("Umbraco.DropdownlistPublishingKeys"): - UpdateDataType(dd, config, false); + UpdateDataType(dataType, config, false); requiresCacheRebuild = true; break; case string ea when ea.InvariantEquals("Umbraco.DropDownMultiple"): - UpdateDataType(dd, config, true); + UpdateDataType(dataType, config, true); break; case string ea when ea.InvariantEquals("Umbraco.DropdownlistMultiplePublishKeys"): - UpdateDataType(dd, config, true); + UpdateDataType(dataType, config, true); requiresCacheRebuild = true; break; } if (requiresCacheRebuild) { - //TODO: How to force rebuild the cache? + var dataTypeCacheRefresher = _cacheRefreshers[Guid.Parse("35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2")]; + _serverMessenger.PerformRefreshAll(dataTypeCacheRefresher); } } } @@ -94,13 +107,15 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 private void UpdateDataType(DataTypeDto dataType, ValueListConfiguration config, bool isMultiple) { dataType.EditorAlias = Constants.PropertyEditors.Aliases.DropDownListFlexible; - var flexConfig = new - { - multiple = isMultiple, - items = config.Items - }; dataType.DbType = ValueStorageType.Nvarchar.ToString(); - dataType.Configuration = JsonConvert.SerializeObject(flexConfig); + + var flexConfig = new DropDownFlexibleConfiguration + { + Items = config.Items, + Multiple = isMultiple + }; + dataType.Configuration = ConfigurationEditor.ToDatabase(flexConfig); + Database.Update(dataType); } @@ -137,7 +152,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 Logger.Warn( "Could not find associated data type configuration for stored Id {DataTypeId}", id); canConvert = false; - } + } } if (canConvert) { @@ -146,7 +161,6 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 propData.IntegerValue = null; return true; } - } return false; @@ -167,6 +181,5 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 return null; } - } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs index f706671022..e8fd4f409e 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs @@ -1,5 +1,6 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { + public class DropTaskTables : MigrationBase { public DropTaskTables(IMigrationContext context) @@ -8,8 +9,10 @@ public override void Migrate() { - Delete.Table("cmsTaskType"); - Delete.Table("cmsTask"); + if (TableExists("cmsTaskType")) + Delete.Table("cmsTaskType"); + if (TableExists("cmsTask")) + Delete.Table("cmsTask"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs new file mode 100644 index 0000000000..f1b25403d4 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class DropTemplateDesignColumn : MigrationBase + { + public DropTemplateDesignColumn(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + if(ColumnExists("cmsTemplate", "design")) + Delete.Column("design").FromTable("cmsTemplate").Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs new file mode 100644 index 0000000000..cd4de179bd --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs @@ -0,0 +1,46 @@ +using NPoco; +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class TablesForScheduledPublishing : MigrationBase + { + public TablesForScheduledPublishing(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + //Get anything currently scheduled + var scheduleSql = new Sql() + .Select("nodeId", "releaseDate", "expireDate") + .From("umbracoDocument") + .Where("releaseDate IS NOT NULL OR expireDate IS NOT NULL"); + var schedules = Database.Dictionary (scheduleSql); + + //drop old cols + Delete.Column("releaseDate").FromTable("umbracoDocument").Do(); + Delete.Column("expireDate").FromTable("umbracoDocument").Do(); + //add new table + Create.Table().Do(); + + //migrate the schedule + foreach(var s in schedules) + { + var date = s.Value.releaseDate; + var action = ContentScheduleAction.Release.ToString(); + if (!date.HasValue) + { + date = s.Value.expireDate; + action = ContentScheduleAction.Expire.ToString(); + } + + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new { nodeId = s.Key, date = date.Value, action = action }) + .Do(); + } + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs index 89cb7e74ac..5dc5e0b6fe 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs @@ -18,7 +18,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 // kill unused parentId column Delete.ForeignKey("FK_cmsTags_cmsTags").OnTable(Constants.DatabaseSchema.Tables.Tag).Do(); - Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag); + Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do(); } } + + // fixes TagsMigration that... originally failed to properly drop the ParentId column } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs new file mode 100644 index 0000000000..4ee95c9f58 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class TagsMigrationFix : MigrationBase + { + public TagsMigrationFix(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + // kill unused parentId column, if it still exists + if (ColumnExists(Constants.DatabaseSchema.Tables.Tag, "ParentId")) + Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index b048998691..3f6e387dec 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -17,10 +17,9 @@ namespace Umbraco.Core.Models { private IContentType _contentType; private ITemplate _template; + private ContentScheduleCollection _schedule; private bool _published; private PublishedState _publishedState; - private DateTime? _releaseDate; - private DateTime? _expireDate; private ContentCultureInfosCollection _publishInfos; private ContentCultureInfosCollection _publishInfosOrig; private HashSet _editedCultures; @@ -86,11 +85,43 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo TemplateSelector = ExpressionHelper.GetPropertyInfo(x => x.Template); public readonly PropertyInfo PublishedSelector = ExpressionHelper.GetPropertyInfo(x => x.Published); - public readonly PropertyInfo ReleaseDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ReleaseDate); - public readonly PropertyInfo ExpireDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ExpireDate); + public readonly PropertyInfo ContentScheduleSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentSchedule); public readonly PropertyInfo PublishCultureInfosSelector = ExpressionHelper.GetPropertyInfo>(x => x.PublishCultureInfos); } + /// + [DoNotClone] + public ContentScheduleCollection ContentSchedule + { + get + { + if (_schedule == null) + { + _schedule = new ContentScheduleCollection(); + _schedule.CollectionChanged += ScheduleCollectionChanged; + } + return _schedule; + } + set + { + if(_schedule != null) + _schedule.CollectionChanged -= ScheduleCollectionChanged; + SetPropertyValueAndDetectChanges(value, ref _schedule, Ps.Value.ContentScheduleSelector); + if (_schedule != null) + _schedule.CollectionChanged += ScheduleCollectionChanged; + } + } + + /// + /// Collection changed event handler to ensure the schedule field is set to dirty when the schedule changes + /// + /// + /// + private void ScheduleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(Ps.Value.ContentScheduleSelector); + } + /// /// Gets or sets the template used by the Content. /// This is used to override the default one from the ContentType. @@ -100,35 +131,12 @@ namespace Umbraco.Core.Models /// the Default template from the ContentType will be returned. /// [DataMember] - public virtual ITemplate Template + public ITemplate Template { get => _template ?? _contentType.DefaultTemplate; set => SetPropertyValueAndDetectChanges(value, ref _template, Ps.Value.TemplateSelector); } - /// - /// Gets the current status of the Content - /// - [IgnoreDataMember] - public ContentStatus Status - { - get - { - if(Trashed) - return ContentStatus.Trashed; - - if(ExpireDate.HasValue && ExpireDate.Value > DateTime.MinValue && DateTime.Now > ExpireDate.Value) - return ContentStatus.Expired; - - if(ReleaseDate.HasValue && ReleaseDate.Value > DateTime.MinValue && ReleaseDate.Value > DateTime.Now) - return ContentStatus.AwaitingRelease; - - if(Published) - return ContentStatus.Published; - - return ContentStatus.Unpublished; - } - } /// /// Gets or sets a value indicating whether this content item is published or not. @@ -169,26 +177,6 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public bool Edited { get; internal set; } - /// - /// The date this Content should be released and thus be published - /// - [DataMember] - public DateTime? ReleaseDate - { - get => _releaseDate; - set => SetPropertyValueAndDetectChanges(value, ref _releaseDate, Ps.Value.ReleaseDateSelector); - } - - /// - /// The date this Content should expire and thus be unpublished - /// - [DataMember] - public DateTime? ExpireDate - { - get => _expireDate; - set => SetPropertyValueAndDetectChanges(value, ref _expireDate, Ps.Value.ExpireDateSelector); - } - /// /// Gets the ContentType used by this content object /// @@ -229,7 +217,7 @@ namespace Umbraco.Core.Models public bool WasCulturePublished(string culture) // just check _publishInfosOrig - a copy of _publishInfos // a non-available culture could not become published anyways - => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture); + => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture); // adjust dates to sync between version, cultures etc // used by the repo when persisting @@ -310,6 +298,9 @@ namespace Umbraco.Core.Models if (_publishInfos == null) return; _publishInfos.Remove(culture); if (_publishInfos.Count == 0) _publishInfos = null; + + // set the culture to be dirty - it's been modified + TouchCultureInfo(culture); } // sets a publish edited @@ -351,7 +342,7 @@ namespace Umbraco.Core.Models public bool Blueprint { get; internal set; } /// - public virtual bool PublishCulture(string culture = "*") + public bool PublishCulture(string culture = "*") { culture = culture.NullOrWhiteSpaceAsNull(); @@ -404,7 +395,7 @@ namespace Umbraco.Core.Models } /// - public virtual void UnpublishCulture(string culture = "*") + public void UnpublishCulture(string culture = "*") { culture = culture.NullOrWhiteSpaceAsNull(); @@ -520,7 +511,14 @@ namespace Umbraco.Core.Models clonedContent._publishInfos = (ContentCultureInfosCollection) _publishInfos.DeepClone(); //manually deep clone clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged; //re-assign correct event handler } - + + //if properties exist then deal with event bindings + if (clonedContent._schedule != null) + { + clonedContent._schedule.CollectionChanged -= ScheduleCollectionChanged; //clear this event handler if any + clonedContent._schedule = (ContentScheduleCollection)_schedule.DeepClone(); //manually deep clone + clonedContent._schedule.CollectionChanged += clonedContent.ScheduleCollectionChanged; //re-assign correct event handler + } } } } diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 7e70238d2f..b0c786d4b0 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -159,7 +159,7 @@ namespace Umbraco.Core.Models /// [DataMember] public virtual IReadOnlyDictionary CultureInfos => _cultureInfos ?? NoInfos; - + /// public string GetCultureName(string culture) { @@ -222,6 +222,12 @@ namespace Umbraco.Core.Models _cultureInfos = null; } + protected void TouchCultureInfo(string culture) + { + if (_cultureInfos == null || !_cultureInfos.TryGetValue(culture, out var infos)) return; + _cultureInfos.AddOrUpdate(culture, infos.Name, DateTime.Now); + } + // internal for repository internal void SetCultureInfo(string culture, string name, DateTime date) { @@ -235,7 +241,7 @@ namespace Umbraco.Core.Models { _cultureInfos = new ContentCultureInfosCollection(); _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; - } + } _cultureInfos.AddOrUpdate(culture, name, date); } @@ -368,7 +374,7 @@ namespace Umbraco.Core.Models #endregion #region Validation - + /// public virtual Property[] ValidateProperties(string culture = "*") { @@ -496,7 +502,6 @@ namespace Umbraco.Core.Models clonedContent._properties = (PropertyCollection) _properties.DeepClone(); //manually deep clone clonedContent._properties.CollectionChanged += clonedContent.PropertiesChanged; //re-assign correct event handler } - } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs b/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs similarity index 89% rename from src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs rename to src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs index 2d30fc6ba9..6b8d90d418 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs @@ -4,9 +4,9 @@ using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Models.ContentEditing { /// - /// Represents a content app definition. + /// Represents a content app factory. /// - public interface IContentAppDefinition + public interface IContentAppFactory { /// /// Gets the content app for an object. diff --git a/src/Umbraco.Core/Models/ContentSchedule.cs b/src/Umbraco.Core/Models/ContentSchedule.cs new file mode 100644 index 0000000000..cac4a0fd1c --- /dev/null +++ b/src/Umbraco.Core/Models/ContentSchedule.cs @@ -0,0 +1,77 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a scheduled action for a document. + /// + [Serializable] + [DataContract(IsReference = true)] + public class ContentSchedule : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public ContentSchedule(string culture, DateTime date, ContentScheduleAction action) + { + Id = Guid.Empty; // will be assigned by document repository + Culture = culture; + Date = date; + Action = action; + } + + /// + /// Initializes a new instance of the class. + /// + public ContentSchedule(Guid id, string culture, DateTime date, ContentScheduleAction action) + { + Id = id; + Culture = culture; + Date = date; + Action = action; + } + + /// + /// Gets the unique identifier of the document targeted by the scheduled action. + /// + [DataMember] + public Guid Id { get; internal set; } + + /// + /// Gets the culture of the scheduled action. + /// + /// + /// string.Empty represents the invariant culture. + /// + [DataMember] + public string Culture { get; } + + /// + /// Gets the date of the scheduled action. + /// + [DataMember] + public DateTime Date { get; } + + /// + /// Gets the action to take. + /// + [DataMember] + public ContentScheduleAction Action { get; } + + public override bool Equals(object obj) + => obj is ContentSchedule other && Equals(other); + + public bool Equals(ContentSchedule other) + { + // don't compare Ids, two ContentSchedule are equal if they are for the same change + // for the same culture, on the same date - and the collection deals w/duplicates + return Culture.InvariantEquals(other.Culture) && Date == other.Date && Action == other.Action; + } + + public object DeepClone() + { + return new ContentSchedule(Id, Culture, Date, Action); + } + } +} diff --git a/src/Umbraco.Core/Models/ContentScheduleAction.cs b/src/Umbraco.Core/Models/ContentScheduleAction.cs new file mode 100644 index 0000000000..0816f17731 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentScheduleAction.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Models +{ + /// + /// Defines scheduled actions for documents. + /// + public enum ContentScheduleAction + { + /// + /// Release the document. + /// + Release, + + /// + /// Expire the document. + /// + Expire + } +} diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs new file mode 100644 index 0000000000..4c06f8927d --- /dev/null +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -0,0 +1,223 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Umbraco.Core.Models +{ + public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneable, IEquatable + { + //underlying storage for the collection backed by a sorted list so that the schedule is always in order of date and that duplicate dates per culture are not allowed + private readonly Dictionary> _schedule + = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + + public event NotifyCollectionChangedEventHandler CollectionChanged; + + private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + { + CollectionChanged?.Invoke(this, args); + } + + /// + /// Add an existing schedule + /// + /// + public void Add(ContentSchedule schedule) + { + if (!_schedule.TryGetValue(schedule.Culture, out var changes)) + { + changes = new SortedList(); + _schedule[schedule.Culture] = changes; + } + + //TODO: Below will throw if there are duplicate dates added, validate/return bool? + changes.Add(schedule.Date, schedule); + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, schedule)); + } + + /// + /// Adds a new schedule for invariant content + /// + /// + /// + public bool Add(DateTime? releaseDate, DateTime? expireDate) + { + return Add(string.Empty, releaseDate, expireDate); + } + + /// + /// Adds a new schedule for a culture + /// + /// + /// + /// + /// true if successfully added, false if validation fails + public bool Add(string culture, DateTime? releaseDate, DateTime? expireDate) + { + if (culture == null) throw new ArgumentNullException(nameof(culture)); + if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) + return false; + + if (!releaseDate.HasValue && !expireDate.HasValue) return false; + + //TODO: Do we allow passing in a release or expiry date that is before now? + + if (!_schedule.TryGetValue(culture, out var changes)) + { + changes = new SortedList(); + _schedule[culture] = changes; + } + + //TODO: Below will throw if there are duplicate dates added, should validate/return bool? + // but the bool won't indicate which date was in error, maybe have 2 diff methods to schedule start/end? + + if (releaseDate.HasValue) + { + var entry = new ContentSchedule(culture, releaseDate.Value, ContentScheduleAction.Release); + changes.Add(releaseDate.Value, entry); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); + } + + if (expireDate.HasValue) + { + var entry = new ContentSchedule(culture, expireDate.Value, ContentScheduleAction.Expire); + changes.Add(expireDate.Value, entry); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); + } + + return true; + } + + /// + /// Remove a scheduled change + /// + /// + public void Remove(ContentSchedule change) + { + if (_schedule.TryGetValue(change.Culture, out var s)) + { + var removed = s.Remove(change.Date); + if (removed) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, change)); + if (s.Count == 0) + _schedule.Remove(change.Culture); + } + + } + } + + /// + /// Clear all of the scheduled change type for invariant content + /// + /// + /// If specified, will clear all entries with dates less than or equal to the value + public void Clear(ContentScheduleAction action, DateTime? changeDate = null) + { + Clear(string.Empty, action, changeDate); + } + + /// + /// Clear all of the scheduled change type for the culture + /// + /// + /// + /// If specified, will clear all entries with dates less than or equal to the value + public void Clear(string culture, ContentScheduleAction action, DateTime? date = null) + { + if (!_schedule.TryGetValue(culture, out var schedules)) + return; + + var removes = schedules.Where(x => x.Value.Action == action && (!date.HasValue || x.Value.Date <= date.Value)).ToList(); + + foreach (var remove in removes) + { + var removed = schedules.Remove(remove.Value.Date); + if (!removed) + continue; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, remove.Value)); + } + + if (schedules.Count == 0) + _schedule.Remove(culture); + } + + /// + /// Returns all pending schedules based on the date and type provided + /// + /// + /// + /// + public IReadOnlyList GetPending(ContentScheduleAction action, DateTime date) + { + return _schedule.Values.SelectMany(x => x.Values).Where(x => x.Date <= date).ToList(); + } + + /// + /// Gets the schedule for invariant content + /// + /// + public IEnumerable GetSchedule(ContentScheduleAction? action = null) + { + return GetSchedule(string.Empty, action); + } + + /// + /// Gets the schedule for a culture + /// + /// + /// + /// + public IEnumerable GetSchedule(string culture, ContentScheduleAction? action = null) + { + if (_schedule.TryGetValue(culture, out var changes)) + return action == null ? changes.Values : changes.Values.Where(x => x.Action == action.Value); + return Enumerable.Empty(); + } + + /// + /// Returns all schedules registered + /// + /// + public IReadOnlyList FullSchedule => _schedule.SelectMany(x => x.Value.Values).ToList(); + + public object DeepClone() + { + var clone = new ContentScheduleCollection(); + foreach(var cultureSched in _schedule) + { + var list = new SortedList(); + foreach (var schedEntry in cultureSched.Value) + list.Add(schedEntry.Key, (ContentSchedule)schedEntry.Value.DeepClone()); + clone._schedule[cultureSched.Key] = list; + } + return clone; + } + + public override bool Equals(object obj) + => obj is ContentScheduleCollection other && Equals(other); + + public bool Equals(ContentScheduleCollection other) + { + if (other == null) return false; + + var thisSched = _schedule; + var thatSched = other._schedule; + + if (thisSched.Count != thatSched.Count) + return false; + + foreach (var (culture, thisList) in thisSched) + { + // if culture is missing, or actions differ, false + if (!thatSched.TryGetValue(culture, out var thatList) || !thatList.SequenceEqual(thisList)) + return false; + } + + return true; + } + } +} diff --git a/src/Umbraco.Core/Models/ContentStatus.cs b/src/Umbraco.Core/Models/ContentStatus.cs index 4caf214399..1d35844874 100644 --- a/src/Umbraco.Core/Models/ContentStatus.cs +++ b/src/Umbraco.Core/Models/ContentStatus.cs @@ -4,20 +4,42 @@ using System.Runtime.Serialization; namespace Umbraco.Core.Models { /// - /// Enum for the various statuses a Content object can have + /// Describes the states of a document, with regard to (schedule) publishing. /// [Serializable] [DataContract] public enum ContentStatus { + // typical flow: + // Unpublished (add release date)-> AwaitingRelease (release)-> Published (expire)-> Expired + + /// + /// The document is not trashed, and not published. + /// [EnumMember] Unpublished, + + /// + /// The document is published. + /// [EnumMember] Published, + + /// + /// The document is not trashed, not published, after being unpublished by a scheduled action. + /// [EnumMember] Expired, + + /// + /// The document is trashed. + /// [EnumMember] Trashed, + + /// + /// The document is not trashed, not published, and pending publication by a scheduled action. + /// [EnumMember] AwaitingRelease } diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index caa63d7526..88b1179f6d 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -92,8 +92,8 @@ namespace Umbraco.Core.Models public readonly PropertyInfo AllowedAsRootSelector = ExpressionHelper.GetPropertyInfo(x => x.AllowedAsRoot); public readonly PropertyInfo IsContainerSelector = ExpressionHelper.GetPropertyInfo(x => x.IsContainer); public readonly PropertyInfo AllowedContentTypesSelector = ExpressionHelper.GetPropertyInfo>(x => x.AllowedContentTypes); - public readonly PropertyInfo PropertyGroupCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyGroups); - public readonly PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo>(x => x.PropertyTypes); + public readonly PropertyInfo PropertyGroupsSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyGroups); + public readonly PropertyInfo PropertyTypesSelector = ExpressionHelper.GetPropertyInfo>(x => x.PropertyTypes); public readonly PropertyInfo HasPropertyTypeBeenRemovedSelector = ExpressionHelper.GetPropertyInfo(x => x.HasPropertyTypeBeenRemoved); public readonly PropertyInfo VaryBy = ExpressionHelper.GetPropertyInfo(x => x.Variations); @@ -106,12 +106,12 @@ namespace Umbraco.Core.Models protected void PropertyGroupsChanged(object sender, NotifyCollectionChangedEventArgs e) { - OnPropertyChanged(Ps.Value.PropertyGroupCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyGroupsSelector); } protected void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e) { - OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyTypesSelector); } /// @@ -263,6 +263,8 @@ namespace Umbraco.Core.Models get => _noGroupPropertyTypes; set { + if (_noGroupPropertyTypes != null) + _noGroupPropertyTypes.CollectionChanged -= PropertyTypesChanged; _noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing, value); _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); @@ -376,7 +378,7 @@ namespace Umbraco.Core.Models if (!HasPropertyTypeBeenRemoved) { HasPropertyTypeBeenRemoved = true; - OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyTypesSelector); } break; } @@ -388,7 +390,7 @@ namespace Umbraco.Core.Models if (!HasPropertyTypeBeenRemoved) { HasPropertyTypeBeenRemoved = true; - OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyTypesSelector); } } } @@ -412,7 +414,7 @@ namespace Umbraco.Core.Models // actually remove the group PropertyGroups.RemoveItem(propertyGroupName); - OnPropertyChanged(Ps.Value.PropertyGroupCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyGroupsSelector); } /// diff --git a/src/Umbraco.Core/Models/GridValue.cs b/src/Umbraco.Core/Models/GridValue.cs index 717fcd2f88..237385f3f4 100644 --- a/src/Umbraco.Core/Models/GridValue.cs +++ b/src/Umbraco.Core/Models/GridValue.cs @@ -5,6 +5,8 @@ using Newtonsoft.Json.Linq; namespace Umbraco.Core.Models { + //TODO: Make a property value converter for this! + /// /// A model representing the value saved for the grid /// @@ -19,7 +21,7 @@ namespace Umbraco.Core.Models public class GridSection { [JsonProperty("grid")] - public string Grid { get; set; } + public string Grid { get; set; } //fixme: what is this? [JsonProperty("rows")] public IEnumerable Rows { get; set; } @@ -46,7 +48,7 @@ namespace Umbraco.Core.Models public class GridArea { [JsonProperty("grid")] - public string Grid { get; set; } + public string Grid { get; set; } //fixme: what is this? [JsonProperty("controls")] public IEnumerable Controls { get; set; } diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 0c0d9449e0..a414a03d2f 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; namespace Umbraco.Core.Models { + /// /// Represents a document. /// @@ -11,6 +12,11 @@ namespace Umbraco.Core.Models /// public interface IContent : IContentBase { + /// + /// Gets or sets the content schedule + /// + ContentScheduleCollection ContentSchedule { get; set; } + /// /// Gets or sets the template used to render the content. /// @@ -60,26 +66,11 @@ namespace Umbraco.Core.Models /// DateTime? PublishDate { get; } - /// - /// Gets or sets the date and time the content item should be published. - /// - DateTime? ReleaseDate { get; set; } - - /// - /// Gets or sets the date and time the content should be unpublished. - /// - DateTime? ExpireDate { get; set; } - /// /// Gets the content type of this content. /// IContentType ContentType { get; } - /// - /// Gets the current status of the content. - /// - ContentStatus Status { get; } - /// /// Gets a value indicating whether a culture is published. /// @@ -89,6 +80,7 @@ namespace Umbraco.Core.Models /// whenever values for this culture are unpublished. /// A culture becomes published as soon as PublishCulture has been invoked, /// even though the document might now have been saved yet (and can have no identity). + /// Does not support the '*' wildcard (returns false). /// bool IsCulturePublished(string culture); @@ -112,6 +104,7 @@ namespace Umbraco.Core.Models /// A culture is edited when it is available, and not published or published but /// with changes. /// A culture can be edited even though the document might now have been saved yet (and can have no identity). + /// Does not support the '*' wildcard (returns false). /// bool IsCultureEdited(string culture); @@ -172,7 +165,7 @@ namespace Umbraco.Core.Models /// /// A value indicating whether the culture can be published. /// - /// Fails if properties don't pass variant validtion rules. + /// Fails if properties don't pass variant validation rules. /// Publishing must be finalized via the content service SavePublishing method. /// bool PublishCulture(string culture = "*"); diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index cef8086207..fb3714cfc0 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -76,6 +76,7 @@ namespace Umbraco.Core.Models /// Returns false for the invariant culture, in order to be consistent /// with , even though the invariant culture is /// always available. + /// Does not support the '*' wildcard (returns false). /// bool IsCultureAvailable(string culture); diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index 940648c4b9..e190c8ad3b 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -1,7 +1,10 @@ using System; using System.Globalization; +using System.Linq; using System.Reflection; using System.Runtime.Serialization; +using System.Threading; +using Umbraco.Core.Configuration; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models @@ -48,7 +51,57 @@ namespace Umbraco.Core.Models [DataMember] public string CultureName { - get => _cultureName ?? CultureInfo.GetCultureInfo(IsoCode).DisplayName; + // CultureInfo.DisplayName is the name in the installed .NET language + // .NativeName is the name in culture info's language + // .EnglishName is the name in English + // + // there is no easy way to get the name in a specified culture (which would need to be installed on the server) + // this works: + // var rm = new ResourceManager("mscorlib", typeof(int).Assembly); + // var name = rm.GetString("Globalization.ci_" + culture.Name, displayCulture); + // but can we rely on it? + // + // and... DisplayName is captured and cached in culture infos returned by GetCultureInfo(), using + // the value for the current thread culture at the moment it is first retrieved - whereas creating + // a new CultureInfo() creates a new instance, which _then_ can get DisplayName again in a different + // culture + // + // I assume that, on a site, all language names should be in the SAME language, in DB, + // and that would be the umbracoDefaultUILanguage (app setting) - BUT if by accident + // ANY culture has been retrieved with another current thread culture - it's now corrupt + // + // so, the logic below ensures that the name always end up being the correct name + // see also LanguageController.GetAllCultures which is doing the same + // + // all this, including the ugly settings injection, because se store language names in db, + // otherwise it would be ok to simply return new CultureInfo(IsoCode).DisplayName to get the name + // in whatever culture is current - we should not do it, see task #3623 + // + // but then, some tests that compare audit strings (for culture names) would need to be fixed + + get + { + if (_cultureName != null) return _cultureName; + + // capture + var threadUiCulture = Thread.CurrentThread.CurrentUICulture; + + try + { + var globalSettings = (IGlobalSettings) Composing.Current.Container.GetInstance(typeof(IGlobalSettings)); + var defaultUiCulture = CultureInfo.GetCultureInfo(globalSettings.DefaultUILanguage); + Thread.CurrentThread.CurrentUICulture = defaultUiCulture; + + // get name - new-ing an instance to get proper display name + return new CultureInfo(IsoCode).DisplayName; + } + finally + { + // restore + Thread.CurrentThread.CurrentUICulture = threadUiCulture; + } + } + set => SetPropertyValueAndDetectChanges(value, ref _cultureName, Ps.Value.CultureNameSelector); } diff --git a/src/Umbraco.Core/Models/PagedResult.cs b/src/Umbraco.Core/Models/PagedResult.cs index 653712d9f8..ef4d4efdfd 100644 --- a/src/Umbraco.Core/Models/PagedResult.cs +++ b/src/Umbraco.Core/Models/PagedResult.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Models if (pageSize > 0) { - TotalPages = (long)Math.Ceiling(totalItems / (Decimal)pageSize); + TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize); } else { diff --git a/src/Umbraco.Core/Models/PropertyGroup.cs b/src/Umbraco.Core/Models/PropertyGroup.cs index 1d0b949932..595e8d1d6a 100644 --- a/src/Umbraco.Core/Models/PropertyGroup.cs +++ b/src/Umbraco.Core/Models/PropertyGroup.cs @@ -35,12 +35,12 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); public readonly PropertyInfo SortOrderSelector = ExpressionHelper.GetPropertyInfo(x => x.SortOrder); - public readonly PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyTypes); + public readonly PropertyInfo PropertyTypes = ExpressionHelper.GetPropertyInfo(x => x.PropertyTypes); } private void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e) { - OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyTypes); } /// @@ -76,6 +76,8 @@ namespace Umbraco.Core.Models get => _propertyTypes; set { + if (_propertyTypes != null) + _propertyTypes.CollectionChanged -= PropertyTypesChanged; _propertyTypes = value; // since we're adding this collection to this group, @@ -83,6 +85,7 @@ namespace Umbraco.Core.Models foreach (var propertyType in _propertyTypes) propertyType.PropertyGroupId = new Lazy(() => Id); + OnPropertyChanged(Ps.Value.PropertyTypes); _propertyTypes.CollectionChanged += PropertyTypesChanged; } } diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index e93dc56e35..df2d9f9ddc 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -21,6 +21,10 @@ namespace Umbraco.Core.Models public PublicAccessEntry(IContent protectedNode, IContent loginNode, IContent noAccessNode, IEnumerable ruleCollection) { + if (protectedNode == null) throw new ArgumentNullException(nameof(protectedNode)); + if (loginNode == null) throw new ArgumentNullException(nameof(loginNode)); + if (noAccessNode == null) throw new ArgumentNullException(nameof(noAccessNode)); + LoginNodeId = loginNode.Id; NoAccessNodeId = noAccessNode.Id; _protectedNodeId = protectedNode.Id; diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 82e4935616..6733eea500 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; +using System.Security.Cryptography; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Composing; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; +using Umbraco.Core.Security; namespace Umbraco.Core.Models { @@ -54,8 +56,11 @@ namespace Umbraco.Core.Models /// internal static string[] GetUserAvatarUrls(this IUser user, ICacheProvider staticCache) { - //check if the user has explicitly removed all avatars including a gravatar, this will be possible and the value will be "none" - if (user.Avatar == "none") + // If FIPS is required, never check the Gravatar service as it only supports MD5 hashing. + // Unfortunately, if the FIPS setting is enabled on Windows, using MD5 will throw an exception + // and the website will not run. + // Also, check if the user has explicitly removed all avatars including a gravatar, this will be possible and the value will be "none" + if (user.Avatar == "none" || CryptoConfig.AllowOnlyFipsAlgorithms) { return new string[0]; } @@ -146,68 +151,46 @@ namespace Umbraco.Core.Models internal static bool HasContentRootAccess(this IUser user, IEntityService entityService) { - return HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissionsHelper.HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } internal static bool HasContentBinAccess(this IUser user, IEntityService entityService) { - return HasPathAccess(Constants.System.RecycleBinContent.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinContent.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService) { - return HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissionsHelper.HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService) { - return HasPathAccess(Constants.System.RecycleBinMedia.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinMedia.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } internal static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService) { - return HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + if (content == null) throw new ArgumentNullException(nameof(content)); + return ContentPermissionsHelper.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } internal static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService) { - return HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + if (media == null) throw new ArgumentNullException(nameof(media)); + return ContentPermissionsHelper.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } - internal static bool HasPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId) + internal static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) { - switch (recycleBinId) - { - case Constants.System.RecycleBinMedia: - return HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), recycleBinId); - case Constants.System.RecycleBinContent: - return HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), recycleBinId); - default: - throw new NotSupportedException("Path access is only determined on content or media"); - } + if (entity == null) throw new ArgumentNullException(nameof(entity)); + return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } - internal static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId) + internal static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - - // check for no access - if (startNodeIds.Length == 0) - return false; - - // check for root access - if (startNodeIds.Contains(Constants.System.Root)) - return true; - - var formattedPath = string.Concat(",", path, ","); - - // only users with root access have access to the recycle bin, - // if the above check didn't pass then access is denied - if (formattedPath.Contains(string.Concat(",", recycleBinId, ","))) - return false; - - // check for a start node in the path - return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x, ","))); + if (entity == null) throw new ArgumentNullException(nameof(entity)); + return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } internal static bool IsInBranchOfStartNode(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId, out bool hasPathAccess) @@ -215,58 +198,14 @@ namespace Umbraco.Core.Models switch (recycleBinId) { case Constants.System.RecycleBinMedia: - return IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess); + return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess); case Constants.System.RecycleBinContent: - return IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess); + return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess); default: throw new NotSupportedException("Path access is only determined on content or media"); } } - internal static bool IsInBranchOfStartNode(string path, int[] startNodeIds, string[] startNodePaths, out bool hasPathAccess) - { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - - hasPathAccess = false; - - // check for no access - if (startNodeIds.Length == 0) - return false; - - // check for root access - if (startNodeIds.Contains(Constants.System.Root)) - { - hasPathAccess = true; - return true; - } - - //is it self? - var self = startNodePaths.Any(x => x == path); - if (self) - { - hasPathAccess = true; - return true; - } - - //is it ancestor? - var ancestor = startNodePaths.Any(x => x.StartsWith(path)); - if (ancestor) - { - //hasPathAccess = false; - return true; - } - - //is it descendant? - var descendant = startNodePaths.Any(x => path.StartsWith(x)); - if (descendant) - { - hasPathAccess = true; - return true; - } - - return false; - } - /// /// Determines whether this user has access to view sensitive data /// diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 44e5968a9f..318a826b25 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -724,8 +724,8 @@ namespace Umbraco.Core { return typeConverter; } - - TypeConverter converter = TypeDescriptor.GetConverter(target); + + var converter = TypeDescriptor.GetConverter(target); if (converter.CanConvertFrom(source)) { return DestinationTypeConverterCache[key] = converter; diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 523d09c1f7..9eb4c3f90f 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -31,6 +31,7 @@ namespace Umbraco.Core public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation"; public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; public const string MediaVersion = TableNamePrefix + "MediaVersion"; + public const string ContentSchedule = TableNamePrefix + "ContentSchedule"; public const string PropertyType = /*TableNamePrefix*/ "cms" + "PropertyType"; public const string PropertyTypeGroup = /*TableNamePrefix*/ "cms" + "PropertyTypeGroup"; diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 97f995e99d..5a0e44a281 100644 --- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -159,9 +159,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions Name = indexName, IndexType = attribute.IndexType, ColumnName = columnName, - TableName = tableName, - IsClustered = attribute.IndexType == IndexTypes.Clustered, - IsUnique = attribute.IndexType == IndexTypes.UniqueNonClustered + TableName = tableName, }; if (string.IsNullOrEmpty(attribute.ForColumns) == false) diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs index d4f2a27ae6..582f9a40f7 100644 --- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs +++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.DatabaseModelDefinitions @@ -14,8 +15,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions public virtual string SchemaName { get; set; } public virtual string TableName { get; set; } public virtual string ColumnName { get; set; } - public virtual bool IsUnique { get; set; } - public bool IsClustered { get; set; } + public virtual ICollection Columns { get; set; } public IndexTypes IndexType { get; set; } } diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs new file mode 100644 index 0000000000..492a3d7cbd --- /dev/null +++ b/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs @@ -0,0 +1,33 @@ +using System; +using NPoco; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Persistence.Dtos +{ + [TableName(TableName)] + [PrimaryKey("id", AutoIncrement = false)] + [ExplicitColumns] + internal class ContentScheduleDto + { + public const string TableName = Constants.DatabaseSchema.Tables.ContentSchedule; + + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = false)] + public Guid Id { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [NullSetting(NullSetting = NullSettings.Null)] // can be invariant + public int? LanguageId { get; set; } + + [Column("date")] + public DateTime Date { get; set; } + + [Column("action")] + public string Action { get; set; } + } +} diff --git a/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs b/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs index fd3df69b8a..abe13a0e23 100644 --- a/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs @@ -1,9 +1,9 @@ -using System; -using NPoco; +using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos { + [TableName(TableName)] [PrimaryKey("nodeId", AutoIncrement = false)] [ExplicitColumns] @@ -23,14 +23,6 @@ namespace Umbraco.Core.Persistence.Dtos [Column("edited")] public bool Edited { get; set; } - [Column("releaseDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? ReleaseDate { get; set; } - - [Column("expireDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? ExpiresDate { get; set; } - //[Column("publishDate")] //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.VersionDate for the published version //public DateTime? PublishDate { get; set; } diff --git a/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs b/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs index 2f0d149ee7..a73425db8d 100644 --- a/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs @@ -22,10 +22,6 @@ namespace Umbraco.Core.Persistence.Dtos [NullSetting(NullSetting = NullSettings.Null)] public string Alias { get; set; } - [Column("design")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string Design { get; set; } - [ResultColumn] [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] public NodeDto NodeDto { get; set; } diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index ec364c7c6a..c8467f47e2 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Repositories; namespace Umbraco.Core.Persistence.Factories { @@ -45,8 +48,6 @@ namespace Umbraco.Core.Persistence.Factories content.Published = dto.Published; content.Edited = dto.Edited; - content.ExpireDate = dto.ExpiresDate; - content.ReleaseDate = dto.ReleaseDate; // fixme - shall we get published infos or not? //if (dto.Published) @@ -142,7 +143,7 @@ namespace Umbraco.Core.Persistence.Factories content.CreateDate = nodeDto.CreateDate; content.UpdateDate = contentVersionDto.VersionDate; - content.ProviderUserKey = content.Key; // fixme explain + content.ProviderUserKey = content.Key; // The `ProviderUserKey` is a membership provider thing // reset dirty initial properties (U4-1946) content.ResetDirtyProperties(false); @@ -155,7 +156,7 @@ namespace Umbraco.Core.Persistence.Factories } /// - /// Buils a dto from an IContent item. + /// Builds a dto from an IContent item. /// public static DocumentDto BuildDto(IContent entity, Guid objectType) { @@ -165,9 +166,6 @@ namespace Umbraco.Core.Persistence.Factories { NodeId = entity.Id, Published = entity.Published, - ReleaseDate = entity.ReleaseDate, - ExpiresDate = entity.ExpireDate, - ContentDto = contentDto, DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto) }; @@ -175,6 +173,19 @@ namespace Umbraco.Core.Persistence.Factories return dto; } + public static IEnumerable<(ContentSchedule Model, ContentScheduleDto Dto)> BuildScheduleDto(IContent entity, ILanguageRepository languageRepository) + { + return entity.ContentSchedule.FullSchedule.Select(x => + (x, new ContentScheduleDto + { + Action = x.Action.ToString(), + Date = x.Date, + NodeId = entity.Id, + LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false), + Id = x.Id + })); + } + /// /// Buils a dto from an IMedia item. /// @@ -302,6 +313,9 @@ namespace Umbraco.Core.Persistence.Factories // more dark magic ;-( internal static bool TryMatch(string text, out string path) { + //fixme: In v8 we should allow exposing this via the property editor in a much nicer way so that the property editor + // can tell us directly what any URL is for a given property if it contains an asset + path = null; if (string.IsNullOrWhiteSpace(text)) return false; diff --git a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs index fa6513775f..01b0e59a1d 100644 --- a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs @@ -53,7 +53,7 @@ namespace Umbraco.Core.Persistence.Factories EditorAlias = entity.EditorAlias, NodeId = entity.Id, DbType = entity.DatabaseType.ToString(), - Configuration = entity.Configuration == null ? null : JsonConvert.SerializeObject(entity.Configuration, ConfigurationEditor.ConfigurationJsonSettings), + Configuration = ConfigurationEditor.ToDatabase(entity.Configuration), NodeDto = BuildNodeDto(entity) }; diff --git a/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs b/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs index b2b5be872e..7682ea47db 100644 --- a/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs @@ -45,7 +45,6 @@ namespace Umbraco.Core.Persistence.Factories var dto = new TemplateDto { Alias = entity.Alias, - Design = entity.Content ?? string.Empty, NodeDto = BuildNodeDto(entity, nodeObjectTypeId) }; diff --git a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs index d2e0e7245c..2cc3a5b140 100644 --- a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs @@ -37,8 +37,6 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.ContentTypeId, dto => dto.ContentTypeId); CacheMap(src => src.UpdateDate, dto => dto.VersionDate); - CacheMap(src => src.ExpireDate, dto => dto.ExpiresDate); - CacheMap(src => src.ReleaseDate, dto => dto.ReleaseDate); CacheMap(src => src.Published, dto => dto.Published); //CacheMap(src => src.Name, dto => dto.Alias); diff --git a/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs b/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs index f402081e08..ca5faab134 100644 --- a/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs @@ -24,7 +24,6 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.MasterTemplateId, dto => dto.ParentId); CacheMap(src => src.Key, dto => dto.UniqueId); CacheMap(src => src.Alias, dto => dto.Alias); - CacheMap(src => src.Content, dto => dto.Design); } } } diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index 76116a8d03..d313d27bbc 100644 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -32,7 +32,7 @@ namespace Umbraco.Core.Persistence.Querying /// /// Gets or sets the SQL syntax provider for the current database. /// - protected ISqlSyntaxProvider SqlSyntax { get; private set; } + protected ISqlSyntaxProvider SqlSyntax { get; } /// /// Gets the list of SQL parameters. @@ -56,6 +56,8 @@ namespace Umbraco.Core.Persistence.Querying /// Also populates the SQL parameters. public virtual string Visit(Expression expression) { + if (expression == null) return string.Empty; + // if the expression is a CachedExpression, // visit the inner expression if not already visited var cachedExpression = expression as CachedExpression; @@ -65,8 +67,6 @@ namespace Umbraco.Core.Persistence.Querying expression = cachedExpression.InnerExpression; } - if (expression == null) return string.Empty; - string result; switch (expression.NodeType) @@ -135,35 +135,28 @@ namespace Umbraco.Core.Persistence.Querying // if the expression is a CachedExpression, // and is not already compiled, assign the result - if (cachedExpression != null) - { - if (cachedExpression.Visited == false) - cachedExpression.VisitResult = result; - result = cachedExpression.VisitResult; - } - - return result; + if (cachedExpression == null) + return result; + if (!cachedExpression.Visited) + cachedExpression.VisitResult = result; + return cachedExpression.VisitResult; } protected abstract string VisitMemberAccess(MemberExpression m); protected virtual string VisitLambda(LambdaExpression lambda) { - if (lambda.Body.NodeType == ExpressionType.MemberAccess) - { - var m = lambda.Body as MemberExpression; - - if (m != null && m.Expression != null) + if (lambda.Body.NodeType == ExpressionType.MemberAccess && + lambda.Body is MemberExpression memberExpression && memberExpression.Expression != null) { //This deals with members that are boolean (i.e. x => IsTrashed ) - var r = VisitMemberAccess(m); + var result = VisitMemberAccess(memberExpression); SqlParameters.Add(true); - return Visited ? string.Empty : string.Format("{0} = @{1}", r, SqlParameters.Count - 1); + return Visited ? string.Empty : $"{result} = @{SqlParameters.Count - 1}"; } - } return Visit(lambda.Body); } @@ -248,21 +241,10 @@ namespace Umbraco.Core.Persistence.Querying { case "MOD": case "COALESCE": - //don't execute if compiled - if (Visited == false) - { - return string.Format("{0}({1},{2})", operand, left, right); - } - //already compiled, return - return string.Empty; + return Visited ? string.Empty : $"{operand}({left},{right})"; + default: - //don't execute if compiled - if (Visited == false) - { - return string.Concat("(", left, " ", operand, " ", right, ")"); - } - //already compiled, return - return string.Empty; + return Visited ? string.Empty : $"({left} {operand} {right})"; } } @@ -284,10 +266,10 @@ namespace Umbraco.Core.Persistence.Querying return list; } - protected virtual string VisitNew(NewExpression nex) + protected virtual string VisitNew(NewExpression newExpression) { // TODO : check ! - var member = Expression.Convert(nex, typeof(object)); + var member = Expression.Convert(newExpression, typeof(object)); var lambda = Expression.Lambda>(member); try { @@ -295,20 +277,16 @@ namespace Umbraco.Core.Persistence.Querying var o = getter(); SqlParameters.Add(o); + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; } catch (InvalidOperationException) { - if (Visited) return string.Empty; + if (Visited) + return string.Empty; - var exprs = VisitExpressionList(nex.Arguments); - var r = new StringBuilder(); - foreach (var e in exprs) - { - if (r.Length > 0) r.Append(","); - r.Append(e); - } - return r.ToString(); + var exprs = VisitExpressionList(newExpression.Arguments); + return string.Join(",", exprs); } } @@ -323,6 +301,7 @@ namespace Umbraco.Core.Persistence.Querying return "null"; SqlParameters.Add(c.Value); + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; } @@ -375,27 +354,11 @@ namespace Umbraco.Core.Persistence.Querying protected virtual string VisitNewArray(NewArrayExpression na) { var exprs = VisitExpressionList(na.Expressions); - - //don't execute if compiled - if (Visited == false) - { - var r = new StringBuilder(); - foreach (var e in exprs) - { - r.Append(r.Length > 0 ? "," + e : e); - } - - return r.ToString(); - } - //already compiled, return - return string.Empty; + return Visited ? string.Empty : string.Join(",", exprs); } protected virtual List VisitNewArrayFromExpressionList(NewArrayExpression na) - { - var exprs = VisitExpressionList(na.Expressions); - return exprs; - } + => VisitExpressionList(na.Expressions); protected virtual string BindOperant(ExpressionType e) { @@ -436,50 +399,61 @@ namespace Umbraco.Core.Persistence.Querying protected virtual string VisitMethodCall(MethodCallExpression m) { - //Here's what happens with a MethodCallExpression: - // If a method is called that contains a single argument, - // then m.Object is the object on the left hand side of the method call, example: - // x.Path.StartsWith(content.Path) - // m.Object = x.Path - // and m.Arguments.Length == 1, therefor m.Arguments[0] == content.Path - // If a method is called that contains multiple arguments, then m.Object == null and the - // m.Arguments collection contains the left hand side of the method call, example: - // x.Path.SqlStartsWith(content.Path, TextColumnType.NVarchar) - // m.Object == null - // m.Arguments.Length == 3, therefor, m.Arguments[0] == x.Path, m.Arguments[1] == content.Path, m.Arguments[2] == TextColumnType.NVarchar - // So, we need to cater for these scenarios. + // m.Object is the expression that represent the instance for instance method class, or null for static method calls + // m.Arguments is the collection of expressions that represent arguments of the called method + // m.MethodInfo is the method info for the method to be called - var objectForMethod = m.Object ?? m.Arguments[0]; - var visitedObjectForMethod = Visit(objectForMethod); + // assume that static methods are extension methods (probably not ok) + // and then, the method object is its first argument - get "safe" object + var methodObject = m.Object ?? m.Arguments[0]; + var visitedMethodObject = Visit(methodObject); + // and then, "safe" arguments are what would come after the first arg var methodArgs = m.Object == null - ? m.Arguments.Skip(1).ToArray() - : m.Arguments.ToArray(); + ? new ReadOnlyCollection(m.Arguments.Skip(1).ToList()) + : m.Arguments; switch (m.Method.Name) { case "ToString": - SqlParameters.Add(objectForMethod.ToString()); - //don't execute if compiled - if (Visited == false) - return string.Format("@{0}", SqlParameters.Count - 1); - //already compiled, return - return string.Empty; + SqlParameters.Add(methodObject.ToString()); + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; + case "ToUpper": - //don't execute if compiled - if (Visited == false) - return string.Format("upper({0})", visitedObjectForMethod); - //already compiled, return - return string.Empty; + return Visited ? string.Empty : $"upper({visitedMethodObject})"; + case "ToLower": - //don't execute if compiled - if (Visited == false) - return string.Format("lower({0})", visitedObjectForMethod); - //already compiled, return - return string.Empty; + return Visited ? string.Empty : $"lower({visitedMethodObject})"; + + case "Contains": + // for 'Contains', it can either be the string.Contains(string) method, or a collection Contains + // method, which would then need to become a SQL IN clause - but beware that string is + // an enumerable of char, and string.Contains(char) is an extension method - but NOT an SQL IN + + var isCollectionContains = + ( + m.Object == null && // static (extension?) method + m.Arguments.Count == 2 && // with two args + m.Arguments[0].Type != typeof(string) && // but not for string + TypeHelper.IsTypeAssignableFrom(m.Arguments[0].Type) && // first arg being an enumerable + m.Arguments[1].NodeType == ExpressionType.MemberAccess // second arg being a member access + ) || + ( + m.Object != null && // instance method + TypeHelper.IsTypeAssignableFrom(m.Object.Type) && // of an enumerable + m.Object.Type != typeof(string) && // but not for string + m.Arguments.Count == 1 && // with 1 arg + m.Arguments[0].NodeType == ExpressionType.MemberAccess // arg being a member access + ); + + if (isCollectionContains) + goto case "SqlIn"; + else + goto case "Contains**String"; + case "SqlWildcard": case "StartsWith": case "EndsWith": - case "Contains": + case "Contains**String": // see "Contains" above case "Equals": case "SqlStartsWith": case "SqlEndsWith": @@ -490,18 +464,6 @@ namespace Umbraco.Core.Persistence.Querying case "InvariantContains": case "InvariantEquals": - //special case, if it is 'Contains' and the argumet that Contains is being called on is - //Enumerable and the methodArgs is the actual member access, then it's an SQL IN claus - if (m.Object == null - && m.Arguments[0].Type != typeof(string) - && m.Arguments.Count == 2 - && methodArgs.Length == 1 - && methodArgs[0].NodeType == ExpressionType.MemberAccess - && TypeHelper.IsTypeAssignableFrom(m.Arguments[0].Type)) - { - goto case "SqlIn"; - } - string compareValue; if (methodArgs[0].NodeType != ExpressionType.Constant) @@ -526,7 +488,7 @@ namespace Umbraco.Core.Persistence.Querying //then check if the col type argument has been passed to the current method (this will be the case for methods like // SqlContains and other Sql methods) - if (methodArgs.Length > 1) + if (methodArgs.Count > 1) { var colTypeArg = methodArgs.FirstOrDefault(x => x is ConstantExpression && x.Type == typeof(TextColumnType)); if (colTypeArg != null) @@ -535,7 +497,7 @@ namespace Umbraco.Core.Persistence.Querying } } - return HandleStringComparison(visitedObjectForMethod, compareValue, m.Method.Name, colType); + return HandleStringComparison(visitedMethodObject, compareValue, m.Method.Name, colType); case "Replace": string searchValue; @@ -581,14 +543,10 @@ namespace Umbraco.Core.Persistence.Querying } SqlParameters.Add(RemoveQuote(searchValue)); - SqlParameters.Add(RemoveQuote(replaceValue)); //don't execute if compiled - if (Visited == false) - return string.Format("replace({0}, @{1}, @{2})", visitedObjectForMethod, SqlParameters.Count - 2, SqlParameters.Count - 1); - //already compiled, return - return string.Empty; + return Visited ? string.Empty : $"replace({visitedMethodObject}, @{SqlParameters.Count - 2}, @{SqlParameters.Count - 1})"; //case "Substring": // var startIndex = Int32.Parse(args[0].ToString()) + 1; @@ -624,30 +582,33 @@ namespace Umbraco.Core.Persistence.Querying case "SqlIn": - if (m.Object == null && methodArgs.Length == 1 && methodArgs[0].NodeType == ExpressionType.MemberAccess) + if (methodArgs.Count != 1 || methodArgs[0].NodeType != ExpressionType.MemberAccess) + throw new NotSupportedException("SqlIn must contain the member being accessed."); + + var memberAccess = VisitMemberAccess((MemberExpression) methodArgs[0]); + + var inMember = Expression.Convert(methodObject, typeof(object)); + var inLambda = Expression.Lambda>(inMember); + var inGetter = inLambda.Compile(); + + var inArgs = (IEnumerable) inGetter(); + + var inBuilder = new StringBuilder(); + var inFirst = true; + + inBuilder.Append(memberAccess); + inBuilder.Append(" IN ("); + + foreach (var e in inArgs) { - var memberAccess = VisitMemberAccess((MemberExpression) methodArgs[0]); - - var member = Expression.Convert(m.Arguments[0], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - - var inArgs = (IEnumerable)getter(); - - var sIn = new StringBuilder(); - foreach (var e in inArgs) - { - SqlParameters.Add(e); - - sIn.AppendFormat("{0}{1}", - sIn.Length > 0 ? "," : "", - string.Format("@{0}", SqlParameters.Count - 1)); - } - - return string.Format("{0} IN ({1})", memberAccess, sIn); + SqlParameters.Add(e); + if (inFirst) inFirst = false; else inBuilder.Append(","); + inBuilder.Append("@"); + inBuilder.Append(SqlParameters.Count - 1); } - throw new NotSupportedException("SqlIn must contain the member being accessed"); + inBuilder.Append(")"); + return inBuilder.ToString(); //case "Desc": // return string.Format("{0} DESC", r); @@ -706,19 +667,13 @@ namespace Umbraco.Core.Persistence.Querying } public virtual string GetQuotedTableName(string tableName) - { - return Visited ? tableName : string.Format("\"{0}\"", tableName); - } + => GetQuotedName(tableName); public virtual string GetQuotedColumnName(string columnName) - { - return Visited ? columnName : string.Format("\"{0}\"", columnName); - } + => GetQuotedName(columnName); public virtual string GetQuotedName(string name) - { - return Visited ? name : string.Format("\"{0}\"", name); - } + => Visited ? name : "\"" + name + "\""; protected string HandleStringComparison(string col, string val, string verb, TextColumnType columnType) { @@ -726,115 +681,38 @@ namespace Umbraco.Core.Persistence.Querying { case "SqlWildcard": SqlParameters.Add(RemoveQuote(val)); - //don't execute if compiled - if (Visited == false) - return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - //already compiled, return - return string.Empty; + return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + case "Equals": - SqlParameters.Add(RemoveQuote(val)); - //don't execute if compiled - if (Visited == false) - return SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); - //already compiled, return - return string.Empty; - case "StartsWith": - SqlParameters.Add(string.Format("{0}{1}", - RemoveQuote(val), - SqlSyntax.GetWildcardPlaceholder())); - //don't execute if compiled - if (Visited == false) - return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - //already compiled, return - return string.Empty; - case "EndsWith": - SqlParameters.Add(string.Format("{0}{1}", - SqlSyntax.GetWildcardPlaceholder(), - RemoveQuote(val))); - //don't execute if compiled - if (Visited == false) - return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - //already compiled, return - return string.Empty; - case "Contains": - SqlParameters.Add(string.Format("{0}{1}{0}", - SqlSyntax.GetWildcardPlaceholder(), - RemoveQuote(val))); - //don't execute if compiled - if (Visited == false) - return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - //already compiled, return - return string.Empty; case "InvariantEquals": case "SqlEquals": - //recurse - return HandleStringComparison(col, val, "Equals", columnType); + SqlParameters.Add(RemoveQuote(val)); + return Visited ? string.Empty : SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); + + case "StartsWith": case "InvariantStartsWith": case "SqlStartsWith": - //recurse - return HandleStringComparison(col, val, "StartsWith", columnType); + SqlParameters.Add(RemoveQuote(val) + SqlSyntax.GetWildcardPlaceholder()); + return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "EndsWith": case "InvariantEndsWith": case "SqlEndsWith": - //recurse - return HandleStringComparison(col, val, "EndsWith", columnType); + SqlParameters.Add(SqlSyntax.GetWildcardPlaceholder() + RemoveQuote(val)); + return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "Contains": case "InvariantContains": case "SqlContains": - //recurse - return HandleStringComparison(col, val, "Contains", columnType); + var wildcardPlaceholder = SqlSyntax.GetWildcardPlaceholder(); + SqlParameters.Add(wildcardPlaceholder + RemoveQuote(val) + wildcardPlaceholder); + return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + default: - throw new ArgumentOutOfRangeException("verb"); + throw new ArgumentOutOfRangeException(nameof(verb)); } } - //public virtual string GetQuotedValue(object value, Type fieldType, Func escapeCallback = null, Func shouldQuoteCallback = null) - //{ - // if (value == null) return "NULL"; - - // if (escapeCallback == null) - // { - // escapeCallback = EscapeParam; - // } - // if (shouldQuoteCallback == null) - // { - // shouldQuoteCallback = ShouldQuoteValue; - // } - - // if (!fieldType.UnderlyingSystemType.IsValueType && fieldType != typeof(string)) - // { - // //if (TypeSerializer.CanCreateFromString(fieldType)) - // //{ - // // return "'" + escapeCallback(TypeSerializer.SerializeToString(value)) + "'"; - // //} - - // throw new NotSupportedException( - // string.Format("Property of type: {0} is not supported", fieldType.FullName)); - // } - - // if (fieldType == typeof(int)) - // return ((int)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(float)) - // return ((float)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(double)) - // return ((double)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(decimal)) - // return ((decimal)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(DateTime)) - // { - // return "'" + escapeCallback(((DateTime)value).ToIsoString()) + "'"; - // } - - // if (fieldType == typeof(bool)) - // return ((bool)value) ? Convert.ToString(1, CultureInfo.InvariantCulture) : Convert.ToString(0, CultureInfo.InvariantCulture); - - // return shouldQuoteCallback(fieldType) - // ? "'" + escapeCallback(value) + "'" - // : value.ToString(); - //} - public virtual string EscapeParam(object paramValue, ISqlSyntaxProvider sqlSyntax) { return paramValue == null @@ -842,34 +720,14 @@ namespace Umbraco.Core.Persistence.Querying : sqlSyntax.EscapeString(paramValue.ToString()); } - public virtual bool ShouldQuoteValue(Type fieldType) - { - return true; - } - protected virtual string RemoveQuote(string exp) { - if ((exp.StartsWith("\"") || exp.StartsWith("`") || exp.StartsWith("'")) - && - (exp.EndsWith("\"") || exp.EndsWith("`") || exp.EndsWith("'"))) - { - exp = exp.Remove(0, 1); - exp = exp.Remove(exp.Length - 1, 1); - } - return exp; + if (exp.IsNullOrWhiteSpace()) return exp; + + var c = exp[0]; + return (c == '"' || c == '`' || c == '\'') && exp[exp.Length - 1] == c + ? exp.Substring(1, exp.Length - 2) + : exp; } - - //protected virtual string RemoveQuoteFromAlias(string expression) - //{ - - // if ((expression.StartsWith("\"") || expression.StartsWith("`") || expression.StartsWith("'")) - // && - // (expression.EndsWith("\"") || expression.EndsWith("`") || expression.EndsWith("'"))) - // { - // expression = expression.Remove(0, 1); - // expression = expression.Remove(expression.Length - 1, 1); - // } - // return expression; - //} } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs index c52601d629..0148a882fd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs @@ -1,5 +1,5 @@ namespace Umbraco.Core.Persistence.Repositories { - interface IDocumentBlueprintRepository : IDocumentRepository + public interface IDocumentBlueprintRepository : IDocumentRepository { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index a3fb50149d..fc5382499f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -7,6 +7,29 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IDocumentRepository : IContentRepository, IReadRepository { + /// + /// Clears the publishing schedule for all entries having an a date before (lower than, or equal to) a specified date. + /// + void ClearSchedule(DateTime date); + + /// + /// Gets objects having an expiration date before (lower than, or equal to) a specified date. + /// + /// + /// The content returned from this method may be culture variant, in which case the resulting should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForExpiration(DateTime date); + + /// + /// Gets objects having a release date before (lower than, or equal to) a specified date. + /// + /// + /// The content returned from this method may be culture variant, in which case the resulting should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForRelease(DateTime date); + /// /// Get the count of published items /// diff --git a/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs index b86898f97a..fbcbf13651 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// This can be optimized and bypass all deep cloning. /// - int? GetIdByIsoCode(string isoCode); + int? GetIdByIsoCode(string isoCode, bool throwOnNotFound = true); /// /// Gets a language ISO code from its identifier. @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// This can be optimized and bypass all deep cloning. /// - string GetIsoCodeById(int? id); + string GetIsoCodeById(int? id, bool throwOnNotFound = true); /// /// Gets the default language ISO code. diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 36b7ab07f1..58f58c3d84 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -265,7 +265,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // create prepared sql // ensure it's single-line as NPoco PagingHelper has issues with multi-lines psql = Sql(psql.SQL.ToSingleLine(), psql.Arguments); - + // replace the magic culture parameter (see DocumentRepository.GetBaseQuery()) if (!ordering.Culture.IsNullOrWhiteSpace()) { @@ -359,7 +359,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // fixme - what if it is NOT a document but a ... media or whatever? // previously, we inserted the join+select *here* so we were sure to have it, // but now that's not the case anymore! - return "variantName"; + return "variantName"; } // previously, we'd accept anything and just sanitize it - not anymore diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 3184c69dfe..44215b7f7e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -190,8 +190,8 @@ AND umbracoNode.nodeObjectType = @objectType", SortOrder = allowedContentType.SortOrder }); } - - + + //Insert Tabs foreach (var propertyGroup in entity.PropertyGroups) { @@ -328,14 +328,14 @@ AND umbracoNode.id <> @id", // We check if the entity's own PropertyTypes has been modified and then also check // any of the property groups PropertyTypes has been modified. // This specifically tells us if any property type collections have changed. - if (entity.IsPropertyDirty("PropertyTypes") || entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) + if (entity.IsPropertyDirty("NoGroupPropertyTypes") || entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) { var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { entity.Id }); - var dbPropertyTypeAlias = dbPropertyTypes.Select(x => x.Id); + var dbPropertyTypeIds = dbPropertyTypes.Select(x => x.Id); var entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); - var items = dbPropertyTypeAlias.Except(entityPropertyTypes); - foreach (var item in items) - DeletePropertyType(entity.Id, item); + var propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes); + foreach (var propertyTypeId in propertyTypeToDeleteIds) + DeletePropertyType(entity.Id, propertyTypeId); } // Delete tabs ... by excepting entries from db with entries from collections. @@ -620,7 +620,7 @@ AND umbracoNode.id <> @id", var sqlDelete = Sql() .Delete() .WhereIn((System.Linq.Expressions.Expression>)(x => x.ContentKey), sqlSelect); - + Database.Execute(sqlDelete); } @@ -690,7 +690,7 @@ AND umbracoNode.id <> @id", //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 + //clear out the versionCultureVariation table var sqlSelect = Sql().Select(x => x.Id) .From() .InnerJoin().On(x => x.Id, x => x.VersionId) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index b3a7c31e54..35496aaba7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -83,13 +83,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); - sql // fixme why? - .OrderBy(x => x.Level) - .OrderBy(x => x.SortOrder); + AddGetByQueryOrderBy(sql); return MapDtosToContent(Database.Fetch(sql)); } + private void AddGetByQueryOrderBy(Sql sql) + { + sql // fixme why - this should be Path + .OrderBy(x => x.Level) + .OrderBy(x => x.SortOrder); + } + protected override Sql GetBaseQuery(QueryType queryType) { return GetBaseQuery(queryType, true); @@ -121,7 +126,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 => r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto, "pcv"))) - // select the variant name, coalesce to the invariant name, as "variantName" + // select the variant name, coalesce to the invariant name, as "variantName" .AndSelect(VariantNameSqlExpression + " AS variantName"); break; } @@ -143,11 +148,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .On((left, right) => left.Id == right.Id && right.Published, "pcv", "pdv"), "pcv") .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv") + //fixme - should we be joining this when the query type is not single/many? // left join on optional culture variation //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code .LeftJoin(nested => - nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccv", "lang"), "ccv") - .On((version, ccv) => version.Id == ccv.VersionId, aliasRight: "ccv"); + nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccv", "lang"), "ccv") + .On((version, ccv) => version.Id == ccv.VersionId, aliasRight: "ccv"); sql .Where(x => x.NodeObjectType == NodeObjectTypeId); @@ -182,6 +188,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var list = new List { + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentSchedule + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.RedirectUrl + " WHERE contentKey IN (SELECT uniqueId FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", @@ -260,7 +267,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // however, it's not just so we have access to AddingEntity // there are tons of things at the end of the methods, that can only work with a true Content // and basically, the repository requires a Content, not an IContent - var content = (Content) entity; + var content = (Content)entity; content.AddingEntity(); var publishing = content.PublishedState == PublishedState.Publishing; @@ -319,7 +326,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = !publishing; - contentVersionDto.Text = content.Name; Database.Insert(contentVersionDto); content.VersionId = contentVersionDto.Id; @@ -357,13 +363,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // persist the document dto // at that point, when publishing, the entity still has its old Published value - // so we need to explicitely update the dto to persist the correct value + // so we need to explicitly update the dto to persist the correct value if (content.PublishedState == PublishedState.Publishing) dto.Published = true; dto.NodeId = nodeDto.NodeId; content.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Insert(dto); + //insert the schedule + PersistContentSchedule(content, false); + // persist the variations if (content.ContentType.VariesByCulture()) { @@ -434,10 +443,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // however, it's not just so we have access to AddingEntity // there are tons of things at the end of the methods, that can only work with a true Content // and basically, the repository requires a Content, not an IContent - var content = (Content) entity; + var content = (Content)entity; // check if we need to make any database changes at all - if ((content.PublishedState == PublishedState.Published || content.PublishedState == PublishedState.Unpublished) && !content.IsEntityDirty() && !content.IsAnyUserPropertyDirty()) + if ((content.PublishedState == PublishedState.Published || content.PublishedState == PublishedState.Unpublished) + && !content.IsEntityDirty() && !content.IsAnyUserPropertyDirty()) return; // no change to save, do nothing, don't even update dates // whatever we do, we must check that we are saving the current version @@ -491,7 +501,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { documentVersionDto.Published = true; // now published contentVersionDto.Current = false; // no more current - contentVersionDto.Text = content.Name; } Database.Update(contentVersionDto); Database.Update(documentVersionDto); @@ -574,7 +583,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // update the document dto // at that point, when un/publishing, the entity still has its old Published value - // so we need to explicitely update the dto to persist the correct value + // so we need to explicitly update the dto to persist the correct value if (content.PublishedState == PublishedState.Publishing) dto.Published = true; else if (content.PublishedState == PublishedState.Unpublishing) @@ -582,8 +591,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Update(dto); + //update the schedule + if (content.IsPropertyDirty("ContentSchedule")) + PersistContentSchedule(content, true); + // if entity is publishing, update tags, else leave tags there - // means that implicitely unpublished, or trashed, entities *still* have tags in db + // means that implicitly unpublished, or trashed, entities *still* have tags in db if (content.PublishedState == PublishedState.Publishing) SetEntityTags(entity, _tagRepository); @@ -613,8 +626,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ClearEntityTags(entity, _tagRepository); } - // note re. tags: explicitely unpublished entities have cleared tags, - // but masked or trashed entitites *still* have tags in the db fixme so what? + // note re. tags: explicitly unpublished entities have cleared tags, + // but masked or trashed entities *still* have tags in the db fixme so what? entity.ResetDirtyProperties(); @@ -631,6 +644,35 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //} } + private void PersistContentSchedule(IContent content, bool update) + { + var schedules = ContentBaseFactory.BuildScheduleDto(content, LanguageRepository).ToList(); + + //remove any that no longer exist + if (update) + { + var ids = schedules.Where(x => x.Model.Id != Guid.Empty).Select(x => x.Model.Id).Distinct(); + Database.Execute(Sql() + .Delete() + .Where(x => x.NodeId == content.Id) + .WhereNotIn(x => x.Id, ids)); + } + + //add/update the rest + foreach (var schedule in schedules) + { + if (schedule.Model.Id == Guid.Empty) + { + schedule.Model.Id = schedule.Dto.Id = Guid.NewGuid(); + Database.Insert(schedule.Dto); + } + else + { + Database.Update(schedule.Dto); + } + } + } + protected override void PersistDeletedItem(IContent entity) { // raise event first else potential FK issues @@ -734,7 +776,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var filterClause in filter.GetWhereClauses()) { var clauseSql = filterClause.Item1; - var clauseArgs = filterClause.Item2; + var clauseArgs = filterClause.Item2; // replace the name field // we cannot reference an aliased field in a WHERE clause, so have to repeat the expression here @@ -869,6 +911,49 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #endregion + #region Schedule + + /// + public void ClearSchedule(DateTime date) + { + var sql = Sql().Delete().Where(x => x.Date <= date); + Database.Execute(sql); + } + + /// + public IEnumerable GetContentForRelease(DateTime date) + { + var action = ContentScheduleAction.Release.ToString(); + + var sql = GetBaseQuery(QueryType.Many) + .WhereIn(x => x.NodeId, Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.Action == action && x.Date <= date)); + + AddGetByQueryOrderBy(sql); + + return MapDtosToContent(Database.Fetch(sql)); + } + + /// + public IEnumerable GetContentForExpiration(DateTime date) + { + var action = ContentScheduleAction.Expire.ToString(); + + var sql = GetBaseQuery(QueryType.Many) + .WhereIn(x => x.NodeId, Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.Action == action && x.Date <= date)); + + AddGetByQueryOrderBy(sql); + + return MapDtosToContent(Database.Fetch(sql)); + } + + #endregion + protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) { // note: 'updater' is the user who created the latest draft version, @@ -939,7 +1024,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id) { - content[i] = (Content) cached; + content[i] = (Content)cached; continue; } } @@ -987,6 +1072,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // load all properties for all documents from database in 1 query - indexed by version id var properties = GetPropertyCollections(temps); + var schedule = GetContentSchedule(temps.Select(x => x.Content.Id).ToArray()); // assign templates and properties foreach (var temp in temps) @@ -997,13 +1083,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (temp.Template2Id.HasValue && templates.TryGetValue(temp.Template2Id.Value, out template)) temp.Content.PublishTemplate = template; - if (properties.ContainsKey(temp.VersionId)) - temp.Content.Properties = properties[temp.VersionId]; - else - throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); + if (properties.ContainsKey(temp.VersionId)) + temp.Content.Properties = properties[temp.VersionId]; + else + throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); - // reset dirty initial properties (U4-1946) - temp.Content.ResetDirtyProperties(false); + //load in the schedule + if (schedule.TryGetValue(temp.Content.Id, out var s)) + temp.Content.ContentSchedule = s; } } @@ -1018,6 +1105,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement SetVariations(temp.Content, contentVariations, documentVariations); } + foreach(var c in content) + c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946) + return content; } @@ -1026,33 +1116,72 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var contentType = _contentTypeRepository.Get(dto.ContentDto.ContentTypeId); var content = ContentBaseFactory.BuildEntity(dto, contentType); - // get template - if (dto.DocumentVersionDto.TemplateId.HasValue && dto.DocumentVersionDto.TemplateId.Value > 0) - content.Template = _templateRepository.Get(dto.DocumentVersionDto.TemplateId.Value); - - // get properties - indexed by version id - var versionId = dto.DocumentVersionDto.Id; - - // fixme - shall we get published properties or not? - //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; - var publishedVersionId = dto.PublishedVersionDto != null ? dto.PublishedVersionDto.Id : 0; - - var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); - var ltemp = new List> { temp }; - var properties = GetPropertyCollections(ltemp); - content.Properties = properties[dto.DocumentVersionDto.Id]; - - // set variations, if varying - if (contentType.VariesByCulture()) + try { - var contentVariations = GetContentVariations(ltemp); - var documentVariations = GetDocumentVariations(ltemp); - SetVariations(content, contentVariations, documentVariations); + content.DisableChangeTracking(); + + // get template + if (dto.DocumentVersionDto.TemplateId.HasValue && dto.DocumentVersionDto.TemplateId.Value > 0) + content.Template = _templateRepository.Get(dto.DocumentVersionDto.TemplateId.Value); + + // get properties - indexed by version id + var versionId = dto.DocumentVersionDto.Id; + + // fixme - shall we get published properties or not? + //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; + var publishedVersionId = dto.PublishedVersionDto != null ? dto.PublishedVersionDto.Id : 0; + + var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); + var ltemp = new List> { temp }; + var properties = GetPropertyCollections(ltemp); + content.Properties = properties[dto.DocumentVersionDto.Id]; + + // set variations, if varying + if (contentType.VariesByCulture()) + { + var contentVariations = GetContentVariations(ltemp); + var documentVariations = GetDocumentVariations(ltemp); + SetVariations(content, contentVariations, documentVariations); + } + + //load in the schedule + var schedule = GetContentSchedule(dto.NodeId); + if (schedule.TryGetValue(dto.NodeId, out var s)) + content.ContentSchedule = s; + + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; + } + finally + { + content.EnableChangeTracking(); + } + } + + private IDictionary GetContentSchedule(params int[] contentIds) + { + var result = new Dictionary(); + + var scheduleDtos = Database.FetchByGroups(contentIds, 2000, batch => Sql() + .Select() + .From() + .WhereIn(x => x.NodeId, batch)); + + foreach (var scheduleDto in scheduleDtos) + { + if (!result.TryGetValue(scheduleDto.NodeId, out var col)) + col = result[scheduleDto.NodeId] = new ContentScheduleCollection(); + + col.Add(new ContentSchedule(scheduleDto.Id, + LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? string.Empty, + scheduleDto.Date, + scheduleDto.Action == ContentScheduleAction.Release.ToString() + ? ContentScheduleAction.Release + : ContentScheduleAction.Expire)); } - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; + return result; } private void SetVariations(Content content, IDictionary> contentVariations, IDictionary> documentVariations) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index fb8c2732e6..8d6f67e9db 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -66,6 +66,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent) BuildVariants(entities.Cast()); + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media if (isMedia) BuildProperties(entities, dtos); @@ -110,14 +111,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return GetEntity(sql, isContent, isMedia); } - public virtual IEntitySlim Get(int id) + public IEntitySlim Get(int id) { var sql = GetBaseWhere(false, false, false, id); var dto = Database.FirstOrDefault(sql); return dto == null ? null : BuildEntity(false, false, dto); } - public virtual IEntitySlim Get(int id, Guid objectTypeId) + public IEntitySlim Get(int id, Guid objectTypeId) { var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectTypeId == Constants.ObjectTypes.Media; @@ -126,21 +127,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return GetEntity(sql, isContent, isMedia); } - public virtual IEnumerable GetAll(Guid objectType, params int[] ids) + public IEnumerable GetAll(Guid objectType, params int[] ids) { return ids.Length > 0 ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) : PerformGetAll(objectType); } - public virtual IEnumerable GetAll(Guid objectType, params Guid[] keys) + public IEnumerable GetAll(Guid objectType, params Guid[] keys) { return keys.Length > 0 ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) : PerformGetAll(objectType); } - private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia) + private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool loadMediaProperties) { //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) @@ -157,7 +158,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entities = dtos.Select(x => BuildEntity(false, isMedia, x)).ToArray(); - if (isMedia) + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media + if (isMedia && loadMediaProperties) BuildProperties(entities, dtos); return entities; @@ -169,17 +171,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var isMedia = objectType == Constants.ObjectTypes.Media; var sql = GetFullSqlForEntityType(isContent, isMedia, objectType, filter); - return GetEntities(sql, isContent, isMedia); + return GetEntities(sql, isContent, isMedia, true); } - public virtual IEnumerable GetAllPaths(Guid objectType, params int[] ids) + public IEnumerable GetAllPaths(Guid objectType, params int[] ids) { return ids.Any() ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) : PerformGetAllPaths(objectType); } - public virtual IEnumerable GetAllPaths(Guid objectType, params Guid[] keys) + public IEnumerable GetAllPaths(Guid objectType, params Guid[] keys) { return keys.Any() ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) @@ -193,7 +195,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return Database.Fetch(sql); } - public virtual IEnumerable GetByQuery(IQuery query) + public IEnumerable GetByQuery(IQuery query) { var sqlClause = GetBase(false, false, null); var translator = new SqlTranslator(sqlClause, query); @@ -203,7 +205,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return dtos.Select(x => BuildEntity(false, false, x)).ToList(); } - public virtual IEnumerable GetByQuery(IQuery query, Guid objectType) + public IEnumerable GetByQuery(IQuery query, Guid objectType) { var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectType == Constants.ObjectTypes.Media; @@ -214,7 +216,22 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql = translator.Translate(); sql = AddGroupBy(isContent, isMedia, sql); - return GetEntities(sql, isContent, isMedia); + return GetEntities(sql, isContent, isMedia, true); + } + + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media + internal IEnumerable GetMediaByQueryWithoutPropertyData(IQuery query) + { + var isContent = false; + var isMedia = true; + + var sql = GetBaseWhere(isContent, isMedia, false, null, Constants.ObjectTypes.Media); + + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + sql = AddGroupBy(isContent, isMedia, sql); + + return GetEntities(sql, isContent, isMedia, false); } public UmbracoObjectTypes GetObjectType(int id) @@ -241,6 +258,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return Database.ExecuteScalar(sql) > 0; } + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media private void BuildProperties(EntitySlim entity, BaseDto dto) { var pdtos = Database.Fetch(GetPropertyData(dto.VersionId)); @@ -248,6 +266,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement BuildProperty(entity, pdto); } + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media private void BuildProperties(EntitySlim[] entities, List dtos) { var versionIds = dtos.Select(x => x.VersionId).Distinct().ToList(); @@ -263,6 +282,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } } + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media private void BuildProperty(EntitySlim entity, PropertyDataDto pdto) { // explain ?! diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index e316d1d04b..09fb664ffe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -236,9 +236,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // fast way of getting an id for an isoCode - avoiding cloning // _codeIdMap is rebuilt whenever PerformGetAll runs - public int? GetIdByIsoCode(string isoCode) => GetIdByIsoCode(isoCode, throwOnNotFound: true); - - private int? GetIdByIsoCode(string isoCode, bool throwOnNotFound) + public int? GetIdByIsoCode(string isoCode, bool throwOnNotFound = true) { if (isoCode == null) return null; @@ -254,14 +252,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } if (throwOnNotFound) throw new ArgumentException($"Code {isoCode} does not correspond to an existing language.", nameof(isoCode)); - return 0; - } + return null; + } + // fast way of getting an isoCode for an id - avoiding cloning // _idCodeMap is rebuilt whenever PerformGetAll runs - public string GetIsoCodeById(int? id) => GetIsoCodeById(id, throwOnNotFound: true); - - private string GetIsoCodeById(int? id, bool throwOnNotFound) + public string GetIsoCodeById(int? id, bool throwOnNotFound = true) { if (id == null) return null; @@ -277,6 +274,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } if (throwOnNotFound) throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id)); + return null; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs index 64489bb059..84c76dbb53 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs @@ -211,12 +211,20 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //.Where(x => Equals(x, default(TId)) == false) .ToArray(); - if (ids.Length > 2000) + // can't query more than 2000 ids at a time... but if someone is really querying 2000+ entities, + // the additional overhead of fetching them in groups is minimal compared to the lookup time of each group + const int maxParams = 2000; + if (ids.Length <= maxParams) { - throw new InvalidOperationException("Cannot perform a query with more than 2000 parameters"); + return CachePolicy.GetAll(ids, PerformGetAll); } - return CachePolicy.GetAll(ids, PerformGetAll); + var entities = new List(); + foreach (var groupOfIds in ids.InGroupsOf(maxParams)) + { + entities.AddRange(CachePolicy.GetAll(groupOfIds.ToArray(), PerformGetAll)); + } + return entities; } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs index 415aa2bcb1..83876db599 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs @@ -181,7 +181,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement template.Path = nodeDto.Path; //now do the file work - SaveFile(template, dto); + SaveFile(template); template.ResetDirtyProperties(); @@ -236,7 +236,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); //now do the file work - SaveFile((Template) entity, dto, originalAlias); + SaveFile((Template) entity, originalAlias); entity.ResetDirtyProperties(); @@ -245,7 +245,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement template.GetFileContent = file => GetFileContent((Template) file, false); } - private void SaveFile(Template template, TemplateDto dto, string originalAlias = null) + private void SaveFile(Template template, string originalAlias = null) { string content; @@ -275,10 +275,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // once content has been set, "template on disk" are not "on disk" anymore template.Content = content; SetVirtualPath(template); - - if (dto.Design == content) return; - dto.Design = content; - Database.Update(dto); // though... we don't care about the db value really??!! } protected override void PersistDeletedItem(ITemplate entity) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index 23c7c055f8..b14c7659a3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -129,7 +129,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public IProfile GetProfile(string username) { - var dto = GetDtoWith(sql => sql.Where(x => x.UserName == username), false); + var dto = GetDtoWith(sql => sql.Where(x => x.Login == username), false); return dto == null ? null : new UserProfile(dto.Id, dto.UserName); } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index 4ff0545281..d69786fbfc 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -334,14 +334,9 @@ ORDER BY TABLE_NAME, INDEX_NAME", switch (systemMethod) { case SystemMethods.NewGuid: - return null; // NOT SUPPORTED! - //return "NEWID()"; + return null; // NOT SUPPORTED! case SystemMethods.CurrentDateTime: return "CURRENT_TIMESTAMP"; - //case SystemMethods.NewSequentialId: - // return "NEWSEQUENTIALID()"; - //case SystemMethods.CurrentUTCDateTime: - // return "GETUTCDATE()"; } return null; diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs index e311c3bbfc..bb58a8ef72 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs @@ -53,6 +53,12 @@ namespace Umbraco.Core.PropertyEditors throw new InvalidCastException($"Cannot cast configuration of type {obj.GetType().Name} to {typeof(TConfiguration).Name}."); } + /// + /// Converts a configuration object into a serialized database value. + /// + public static string ToDatabase(object configuration) + => configuration == null ? null : JsonConvert.SerializeObject(configuration, ConfigurationJsonSettings); + /// [JsonProperty("defaultConfig")] public virtual IDictionary DefaultConfiguration => new Dictionary(); diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index 2d0b34a849..f3fc4f669b 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -153,6 +153,9 @@ namespace Umbraco.Core.PropertyEditors set => _defaultConfiguration = value; } + /// + public virtual IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory(); + /// /// Creates a value editor instance. /// diff --git a/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs new file mode 100644 index 0000000000..413f31d79e --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Provides a default implementation for , returning a single field to index containing the property value. + /// + public class DefaultPropertyIndexValueFactory : IPropertyIndexValueFactory + { + /// + public IEnumerable>> GetIndexValues(Property property, string culture, string segment, bool published) + { + yield return new KeyValuePair>( + property.Alias, + property.GetValue(culture, segment, published).Yield()); + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/DropDownFlexibleConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs similarity index 79% rename from src/Umbraco.Web/PropertyEditors/DropDownFlexibleConfiguration.cs rename to src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs index 4d4d47ad5b..d1c2d23c4f 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownFlexibleConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs @@ -1,6 +1,4 @@ -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors +namespace Umbraco.Core.PropertyEditors { internal class DropDownFlexibleConfiguration : ValueListConfiguration { diff --git a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs index 003fe9a80e..875e2c0e8f 100644 --- a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs @@ -47,7 +47,7 @@ namespace Umbraco.Core.PropertyEditors /// Converts the serialized database value into the actual configuration object. /// /// Converting the configuration object to the serialized database value is - /// achieved by simply serializing the configuration. + /// achieved by simply serializing the configuration. See . object FromDatabase(string configurationJson); /// diff --git a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs index 8137101826..f109620ad9 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs @@ -65,5 +65,10 @@ namespace Umbraco.Core.PropertyEditors /// Is expected to throw if the editor does not support being configured, e.g. for most parameter editors. /// IConfigurationEditor GetConfigurationEditor(); + + /// + /// Gets the index value factory for the editor. + /// + IPropertyIndexValueFactory PropertyIndexValueFactory { get; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs new file mode 100644 index 0000000000..fd4e272f08 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Represents a property index value factory. + /// + public interface IPropertyIndexValueFactory + { + /// + /// Gets the index values for a property. + /// + /// + /// Returns key-value pairs, where keys are indexed field names. By default, that would be the property alias, + /// and there would be only one pair, but some implementations (see for instance the grid one) may return more than + /// one pair, with different indexed field names. + /// And then, values are an enumerable of objects, because each indexed field can in turn have multiple + /// values. By default, there would be only one object: the property value. But some implementations may return + /// more than one value for a given field. + /// + IEnumerable>> GetIndexValues(Property property, string culture, string segment, bool published); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs index 54b69cea45..2ce6e2ec04 100644 --- a/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.PropertyEditors /// public class ImageCropperConfiguration { - [ConfigurationField("crops", "Crop sizes", "views/propertyeditors/imagecropper/imagecropper.prevalues.html")] + [ConfigurationField("crops", "Define crops", "views/propertyeditors/imagecropper/imagecropper.prevalues.html")] public Crop[] Crops { get; set; } public class Crop diff --git a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs index b2bf99bdc6..55ca199121 100644 --- a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs @@ -8,9 +8,6 @@ [ConfigurationField("enableRange", "Enable range", "boolean")] public bool EnableRange { get; set; } - [ConfigurationField("orientation", "Orientation", "views/propertyeditors/slider/orientation.prevalues.html")] - public string Orientation { get; set; } - [ConfigurationField("initVal1", "Initial value", "number")] public int InitialValue { get; set; } @@ -25,38 +22,5 @@ [ConfigurationField("step", "Step increments", "number")] public int StepIncrements { get; set; } - - [ConfigurationField("precision", "Precision", "number", Description = "The number of digits shown after the decimal. Defaults to the number of digits after the decimal of step value.")] - public int Precision { get; set; } - - [ConfigurationField("handle", "Handle", "views/propertyeditors/slider/handle.prevalues.html", Description = "Handle shape. Default is 'round\'")] - public string Handle { get; set; } - - [ConfigurationField("tooltip", "Tooltip", "views/propertyeditors/slider/tooltip.prevalues.html", Description = "Whether to show the tooltip on drag, hide the tooltip, or always show the tooltip. Accepts: 'show', 'hide', or 'always'")] - public string Tooltip { get; set; } - - [ConfigurationField("tooltipSplit", "Tooltip split", "boolean", Description = "If false show one tootip if true show two tooltips one for each handler")] - public bool TooltipSplit { get; set; } // fixme bool? - - [ConfigurationField("tooltipFormat", "Tooltip format", "textstring", Description = "The value wanted to be displayed in the tooltip. Use {0} and {1} for current values - {1} is only for range slider and if not using tooltip split.")] - public string TooltipFormat { get; set; } - - [ConfigurationField("tooltipPosition", "Tooltip position", "textstring", Description = "Position of tooltip, relative to slider. Accepts 'top'/'bottom' for horizontal sliders and 'left'/'right' for vertically orientated sliders. Default positions are 'top' for horizontal and 'right' for vertical slider.")] - public string TooltipPosition { get; set; } - - [ConfigurationField("reversed", "Reversed", "boolean", Description = "Whether or not the slider should be reversed")] - public bool Reversed { get; set; } // fixme bool? - - [ConfigurationField("ticks", "Ticks", "textstring", Description = "Comma-separated values. Used to define the values of ticks. Tick marks are indicators to denote special values in the range. This option overwrites min and max options.")] - public string Ticks { get; set; } - - [ConfigurationField("ticksPositions", "Ticks positions", "textstring", Description = "Comma-separated values. Defines the positions of the tick values in percentages. The first value should always be 0, the last value should always be 100 percent.")] - public string TicksPositions { get; set; } - - [ConfigurationField("ticksLabels", "Ticks labels", "textstring", Description = "Comma-separated values. Defines the labels below the tick marks. Accepts HTML input.")] - public string TicksLabels { get; set; } - - [ConfigurationField("ticksSnapBounds", "Ticks snap bounds", "number", Description = "Used to define the snap bounds of a tick. Snaps to the tick if value is within these bounds.")] - public int TicksSnapBounds { get; set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs index fcf6cef868..9b857c2dff 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Models; @@ -31,7 +30,9 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) { - // if Json storage type deserialzie and return as string array + if (source == null) return Array.Empty(); + + // if Json storage type deserialize and return as string array if (JsonStorageType(propertyType.DataType.Id)) { var jArray = JsonConvert.DeserializeObject(source.ToString()); @@ -39,11 +40,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters } // Otherwise assume CSV storage type and return as string array - var sourceString = source?.ToString() ?? string.Empty; - var csvTags = sourceString - .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) - .ToArray(); - return csvTags; + return source.ToString().Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); } public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) @@ -62,10 +59,9 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters /// private bool JsonStorageType(int dataTypeId) { - // fixme - // GetPreValuesCollectionByDataTypeId is cached at repository level; - // still, the collection is deep-cloned so this is kinda expensive, - // better to cache here + trigger refresh in DataTypeCacheRefresher + // GetDataType(id) is cached at repository level; still, there is some + // deep-cloning involved (expensive) - better cache here + trigger + // refresh in DataTypeCacheRefresher return Storages.GetOrAdd(dataTypeId, id => { diff --git a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs deleted file mode 100644 index 73cc752508..0000000000 --- a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Linq; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Core.Services.Implement; - -namespace Umbraco.Core.Publishing -{ - /// - /// Used to perform scheduled publishing/unpublishing - /// - internal class ScheduledPublisher - { - private readonly IContentService _contentService; - private readonly ILogger _logger; - private readonly IUserService _userService; - - public ScheduledPublisher(IContentService contentService, ILogger logger, IUserService userService) - { - _contentService = contentService; - _logger = logger; - _userService = userService; - } - - /// - /// Processes scheduled operations - /// - /// - /// Returns the number of items successfully completed - /// - public int CheckPendingAndProcess() - { - // fixme isn't this done in ContentService already? - var counter = 0; - var contentForRelease = _contentService.GetContentForRelease().ToArray(); - if (contentForRelease.Length > 0) - _logger.Debug("There's {ContentItemsForRelease} item(s) of content to be published", contentForRelease.Length); - foreach (var d in contentForRelease) - { - try - { - d.ReleaseDate = null; - d.PublishCulture(); // fixme variants? - var result = _contentService.SaveAndPublish(d, userId: _userService.GetProfileById(d.WriterId).Id); - _logger.Debug("Result of publish attempt: {PublishResult}", result.Result); - if (result.Success == false) - { - _logger.Error(null, "Error publishing node {NodeId}", d.Id); - } - else - { - counter++; - } - } - catch (Exception ex) - { - _logger.Error(ex, "Error publishing node {NodeId}", d.Id); - throw; - } - } - - var contentForExpiration = _contentService.GetContentForExpiration().ToArray(); - if (contentForExpiration.Length > 0) - _logger.Debug("There's {ContentItemsForExpiration} item(s) of content to be unpublished", contentForExpiration.Length); - foreach (var d in contentForExpiration) - { - try - { - d.ExpireDate = null; - var result = _contentService.Unpublish(d, userId: _userService.GetProfileById(d.WriterId).Id); - if (result.Success) - { - counter++; - } - } - catch (Exception ex) - { - _logger.Error(ex, "Error unpublishing node {NodeId}", d.Id); - throw; - } - } - - return counter; - } - } -} diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs old mode 100644 new mode 100755 index 04eaba3f9a..cf2712974d --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Configuration; +using System.Reflection; using System.Threading; using System.Web; using LightInject; @@ -12,6 +13,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Logging.Serilog; using Umbraco.Core.Migrations.Upgrade; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -29,26 +31,29 @@ namespace Umbraco.Core.Runtime /// should be possible to use this runtime in console apps. public class CoreRuntime : IRuntime { - private readonly UmbracoApplicationBase _app; private BootLoader _bootLoader; private RuntimeState _state; /// /// Initializes a new instance of the class. /// - /// The Umbraco HttpApplication. - public CoreRuntime(UmbracoApplicationBase umbracoApplication) - { - _app = umbracoApplication ?? throw new ArgumentNullException(nameof(umbracoApplication)); - } + public CoreRuntime() + { } /// public virtual void Boot(ServiceContainer container) { - // some components may want to initialize with the UmbracoApplicationBase - // well, they should not - we should not do this - // TODO remove this eventually. - container.RegisterInstance(_app); + container.ConfigureUmbracoCore(); // also sets Current.Container + + // register the essential stuff, + // ie the global application logger + // (profiler etc depend on boot manager) + var logger = GetLogger(); + container.RegisterInstance(logger); + // now it is ok to use Current.Logger + + ConfigureUnhandledException(logger); + ConfigureAssemblyResolve(logger); Compose(container); @@ -115,6 +120,46 @@ namespace Umbraco.Core.Runtime //sa.Scope?.Dispose(); } + /// + /// Gets a logger. + /// + protected virtual ILogger GetLogger() + { + return SerilogLogger.CreateWithDefaultConfiguration(); + } + + protected virtual void ConfigureUnhandledException(ILogger logger) + { + //take care of unhandled exceptions - there is nothing we can do to + // prevent the launch process to go down but at least we can try + // and log the exception + AppDomain.CurrentDomain.UnhandledException += (_, args) => + { + var exception = (Exception)args.ExceptionObject; + var isTerminating = args.IsTerminating; // always true? + + var msg = "Unhandled exception in AppDomain"; + if (isTerminating) msg += " (terminating)"; + msg += "."; + logger.Error(exception, msg); + }; + } + + protected virtual void ConfigureAssemblyResolve(ILogger logger) + { + // When an assembly can't be resolved. In here we can do magic with the assembly name and try loading another. + // This is used for loading a signed assembly of AutoMapper (v. 3.1+) without having to recompile old code. + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => + { + // ensure the assembly is indeed AutoMapper and that the PublicKeyToken is null before trying to Load again + // do NOT just replace this with 'return Assembly', as it will cause an infinite loop -> stackoverflow + if (args.Name.StartsWith("AutoMapper") && args.Name.EndsWith("PublicKeyToken=null")) + return Assembly.Load(args.Name.Replace(", PublicKeyToken=null", ", PublicKeyToken=be96cd2c38ef1005")); + return null; + }; + } + + private void AquireMainDom(IServiceFactory container) { using (var timer = ProfilingLogger.DebugDuration("Acquiring MainDom.", "Aquired.")) @@ -246,7 +291,7 @@ namespace Umbraco.Core.Runtime private void SetRuntimeStateLevel(IUmbracoDatabaseFactory databaseFactory, ILogger logger) { - var localVersion = UmbracoVersion.Local; // the local, files, version + var localVersion = UmbracoVersion.LocalVersion; // the local, files, version var codeVersion = _state.SemanticVersion; // the executing code version var connect = false; diff --git a/src/Umbraco.Core/RuntimeState.cs b/src/Umbraco.Core/RuntimeState.cs index 0177619a68..4f6f56531b 100644 --- a/src/Umbraco.Core/RuntimeState.cs +++ b/src/Umbraco.Core/RuntimeState.cs @@ -55,7 +55,7 @@ namespace Umbraco.Core /// /// Gets the version comment of the executing code. /// - public string VersionComment => UmbracoVersion.CurrentComment; + public string VersionComment => UmbracoVersion.Comment; /// /// Gets the semantic version of the executing code. diff --git a/src/Umbraco.Core/Security/ContentPermissionsHelper.cs b/src/Umbraco.Core/Security/ContentPermissionsHelper.cs new file mode 100644 index 0000000000..1a329fcdcb --- /dev/null +++ b/src/Umbraco.Core/Security/ContentPermissionsHelper.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Security +{ + internal class ContentPermissionsHelper + { + public enum ContentAccess + { + Granted, + Denied, + NotFound + } + + public static ContentAccess CheckPermissions( + IContent content, + IUser user, + IUserService userService, + IEntityService entityService, + params char[] permissionsToCheck) + { + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + if (content == null) return ContentAccess.NotFound; + + var hasPathAccess = user.HasPathAccess(content, entityService); + + if (hasPathAccess == false) + return ContentAccess.Denied; + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + return ContentAccess.Granted; + + //get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(content.Path, user, userService, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + public static ContentAccess CheckPermissions( + IUmbracoEntity entity, + IUser user, + IUserService userService, + IEntityService entityService, + params char[] permissionsToCheck) + { + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + if (entity == null) return ContentAccess.NotFound; + + var hasPathAccess = user.HasContentPathAccess(entity, entityService); + + if (hasPathAccess == false) + return ContentAccess.Denied; + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + return ContentAccess.Granted; + + //get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(entity.Path, user, userService, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + /// + /// Checks if the user has access to the specified node and permissions set + /// + /// + /// + /// + /// + /// The item resolved if one was found for the id + /// + /// + public static ContentAccess CheckPermissions( + int nodeId, + IUser user, + IUserService userService, + IEntityService entityService, + out IUmbracoEntity entity, + params char[] permissionsToCheck) + { + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + bool? hasPathAccess = null; + entity = null; + + if (nodeId == Constants.System.Root) + hasPathAccess = user.HasContentRootAccess(entityService); + else if (nodeId == Constants.System.RecycleBinContent) + hasPathAccess = user.HasContentBinAccess(entityService); + + if (hasPathAccess.HasValue) + return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; + + entity = entityService.Get(nodeId, UmbracoObjectTypes.Document); + if (entity == null) return ContentAccess.NotFound; + hasPathAccess = user.HasContentPathAccess(entity, entityService); + + if (hasPathAccess == false) + return ContentAccess.Denied; + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + return ContentAccess.Granted; + + //get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(entity.Path, user, userService, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + /// + /// Checks if the user has access to the specified node and permissions set + /// + /// + /// + /// + /// + /// + /// The item resolved if one was found for the id + /// + /// + public static ContentAccess CheckPermissions( + int nodeId, + IUser user, + IUserService userService, + IContentService contentService, + IEntityService entityService, + out IContent contentItem, + params char[] permissionsToCheck) + { + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (contentService == null) throw new ArgumentNullException("contentService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + bool? hasPathAccess = null; + contentItem = null; + + if (nodeId == Constants.System.Root) + hasPathAccess = user.HasContentRootAccess(entityService); + else if (nodeId == Constants.System.RecycleBinContent) + hasPathAccess = user.HasContentBinAccess(entityService); + + if (hasPathAccess.HasValue) + return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; + + contentItem = contentService.GetById(nodeId); + if (contentItem == null) return ContentAccess.NotFound; + hasPathAccess = user.HasPathAccess(contentItem, entityService); + + if (hasPathAccess == false) + return ContentAccess.Denied; + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + return ContentAccess.Granted; + + //get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(contentItem.Path, user, userService, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + private static bool CheckPermissionsPath(string path, IUser user, IUserService userService, params char[] permissionsToCheck) + { + //get the implicit/inherited permissions for the user for this path, + //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + var permission = userService.GetPermissionsForPath(user, path); + + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null + || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + allowed = false; + } + } + return allowed; + } + + public static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId) + { + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + + // check for no access + if (startNodeIds.Length == 0) + return false; + + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) + return true; + + var formattedPath = string.Concat(",", path, ","); + + // only users with root access have access to the recycle bin, + // if the above check didn't pass then access is denied + if (formattedPath.Contains(string.Concat(",", recycleBinId, ","))) + return false; + + // check for a start node in the path + return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x, ","))); + } + + internal static bool IsInBranchOfStartNode(string path, int[] startNodeIds, string[] startNodePaths, out bool hasPathAccess) + { + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + + hasPathAccess = false; + + // check for no access + if (startNodeIds.Length == 0) + return false; + + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) + { + hasPathAccess = true; + return true; + } + + //is it self? + var self = startNodePaths.Any(x => x == path); + if (self) + { + hasPathAccess = true; + return true; + } + + //is it ancestor? + var ancestor = startNodePaths.Any(x => x.StartsWith(path)); + if (ancestor) + { + //hasPathAccess = false; + return true; + } + + //is it descendant? + var descendant = startNodePaths.Any(x => path.StartsWith(x)); + if (descendant) + { + hasPathAccess = true; + return true; + } + + return false; + } + } +} diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 275957e453..7915bbe24b 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -132,14 +132,24 @@ namespace Umbraco.Core.Services IEnumerable GetRootContent(); /// - /// Gets documents with an expiration date greater then today. + /// Gets documents having an expiration date before (lower than, or equal to) a specified date. /// - IEnumerable GetContentForExpiration(); + /// An Enumerable list of objects + /// + /// The content returned from this method may be culture variant, in which case the resulting should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForExpiration(DateTime date); /// - /// Gets documents with a release date greater then today. + /// Gets documents having a release date before (lower than, or equal to) a specified date. /// - IEnumerable GetContentForRelease(); + /// An Enumerable list of objects + /// + /// The content returned from this method may be culture variant, in which case the resulting should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForRelease(DateTime date); /// /// Gets documents in the recycle bin. @@ -166,10 +176,8 @@ namespace Umbraco.Core.Services /// The page number. /// The page size. /// Total number of documents. - /// A field to order by. - /// The ordering direction. - /// A flag indicating whether the ordering field is a system field. /// Query filter. + /// Ordering infos. IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, IQuery filter = null, Ordering ordering = null); @@ -355,31 +363,57 @@ namespace Umbraco.Core.Services /// /// Saves and publishes a document branch. /// + /// The root document. + /// A value indicating whether to force-publish documents that are not already published. + /// A culture, or "*" for all cultures. + /// The identifier of the user performing the operation. /// /// Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more - /// that one culture, see the other overload of this method. + /// than one culture, see the other overloads of this method. /// The parameter determines which documents are published. When false, /// only those documents that are already published, are republished. When true, all documents are - /// published. + /// published. The root of the branch is always published, regardless of . /// IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = 0); /// /// Saves and publishes a document branch. /// + /// The root document. + /// A value indicating whether to force-publish documents that are not already published. + /// The cultures to publish. + /// The identifier of the user performing the operation. /// /// The parameter determines which documents are published. When false, /// only those documents that are already published, are republished. When true, all documents are - /// published. + /// published. The root of the branch is always published, regardless of . + /// + IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = 0); + + /// + /// Saves and publishes a document branch. + /// + /// The root document. + /// A value indicating whether to force-publish documents that are not already published. + /// A function determining cultures to publish. + /// A function publishing cultures. + /// The identifier of the user performing the operation. + /// + /// The parameter determines which documents are published. When false, + /// only those documents that are already published, are republished. When true, all documents are + /// published. The root of the branch is always published, regardless of . /// The parameter is a function which determines whether a document has - /// values to publish (else there is no need to publish it). If one wants to publish only a selection of + /// changes to publish (else there is no need to publish it). If one wants to publish only a selection of /// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other /// cultures may trigger an unwanted republish. /// The parameter is a function to execute to publish cultures, on /// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating /// whether the cultures could be published. /// - IEnumerable SaveAndPublishBranch(IContent content, bool force, Func editing, Func publishCultures, int userId = 0); + IEnumerable SaveAndPublishBranch(IContent content, bool force, + Func> shouldPublish, + Func, bool> publishCultures, + int userId = 0); /// /// Unpublishes a document. @@ -391,7 +425,7 @@ namespace Umbraco.Core.Services /// If the content type is variant, then culture can be either '*' or an actual culture, but neither null nor /// empty. If the content type is invariant, then culture can be either '*' or null or empty. /// - UnpublishResult Unpublish(IContent content, string culture = "*", int userId = 0); + PublishResult Unpublish(IContent content, string culture = "*", int userId = 0); /// /// Gets a value indicating whether a document is path-publishable. @@ -413,7 +447,7 @@ namespace Umbraco.Core.Services /// /// Publishes and unpublishes scheduled documents. /// - IEnumerable PerformScheduledPublish(); + IEnumerable PerformScheduledPublish(DateTime date); #endregion diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 9cc559ccd5..ce46b197a0 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Services { - /// + /// /// Defines the Media Service, which is an easy access to operations involving /// public interface IMediaService : IContentServiceBase @@ -151,8 +151,9 @@ namespace Umbraco.Core.Services /// The to move /// Id of the Media's new Parent /// Id of the User moving the Media - void Move(IMedia media, int parentId, int userId = 0); - + /// True if moving succeeded, otherwise False + Attempt Move(IMedia media, int parentId, int userId = 0); + /// /// Deletes an object by moving it to the Recycle Bin /// diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 2b585cebad..cd15732564 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; @@ -20,7 +19,7 @@ namespace Umbraco.Core.Services.Implement /// /// Implements the content service. /// - internal class ContentService : RepositoryService, IContentService + public class ContentService : RepositoryService, IContentService { private readonly IDocumentRepository _documentRepository; private readonly IEntityRepository _entityRepository; @@ -409,7 +408,7 @@ namespace Umbraco.Core.Services.Implement public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords , IQuery filter = null, Ordering ordering = null) { - if(pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); if (ordering == null) @@ -674,31 +673,23 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a collection of objects, which has an expiration date less than or equal to today. - /// - /// An Enumerable list of objects - public IEnumerable GetContentForExpiration() + /// + public IEnumerable GetContentForExpiration(DateTime date) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.Published && x.ExpireDate <= DateTime.Now); - return _documentRepository.Get(query); + return _documentRepository.GetContentForExpiration(date); } } - /// - /// Gets a collection of objects, which has a release date less than or equal to today. - /// - /// An Enumerable list of objects - public IEnumerable GetContentForRelease() + /// + public IEnumerable GetContentForRelease(DateTime date) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); - return _documentRepository.Get(query); + return _documentRepository.GetContentForRelease(date); } } @@ -810,7 +801,7 @@ namespace Umbraco.Core.Services.Implement .Where(x => culturesChanging.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs); - } + } else Audit(AuditType.Save, userId, content.Id); @@ -892,13 +883,13 @@ namespace Umbraco.Core.Services.Implement // publish the invariant values var publishInvariant = content.PublishCulture(null); if (!publishInvariant) - return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, content); + return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content); } // publish the culture(s) var publishCulture = content.PublishCulture(culture); if (!publishCulture) - return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, content); + return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content); // finally, "save publishing" // what happens next depends on whether the content can be published or not @@ -906,7 +897,7 @@ namespace Umbraco.Core.Services.Implement } /// - public UnpublishResult Unpublish(IContent content, string culture = "*", int userId = 0) + public PublishResult Unpublish(IContent content, string culture = "*", int userId = 0) { var evtMsgs = EventMessagesFactory.Get(); @@ -931,342 +922,490 @@ namespace Umbraco.Core.Services.Implement // if the content is not published, nothing to do if (!content.Published) - return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); + return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); // all cultures = unpublish whole if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null)) { - ((Content) content).PublishedState = PublishedState.Unpublishing; + ((Content)content).PublishedState = PublishedState.Unpublishing; } else { // if the culture we want to unpublish was already unpublished, nothing to do if (!content.WasCulturePublished(culture)) - return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); + return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); // unpublish the culture content.UnpublishCulture(culture); } // finally, "save publishing" - // what happens next depends on whether the content can be published or not - using (var scope = ScopeProvider.CreateScope()) - { - var saved = SavePublishing(content, userId); - if (saved.Success) - { - UnpublishResultType result; - if (culture == "*" || culture == null) - { - Audit(AuditType.Unpublish, userId, content.Id); - result = UnpublishResultType.Success; - } - else - { - //unpublishing a specific culture - Audit(AuditType.UnpublishVariant, userId, content.Id, $"Culture \"{culture}\" unpublished", culture); - if (!content.Published) - { - //log that the whole content item has been unpublished due to mandatory culture unpublished - Audit(AuditType.Unpublish, userId, content.Id, $"Unpublished (culture \"{culture}\" is mandatory)"); - } - - result = content.Published ? UnpublishResultType.SuccessCulture : UnpublishResultType.SuccessMandatoryCulture; - } - scope.Complete(); - return new UnpublishResult(result, evtMsgs, content); - } - - // failed - map result - var r = saved.Result == PublishResultType.FailedCancelledByEvent - ? UnpublishResultType.FailedCancelledByEvent - : UnpublishResultType.Failed; - return new UnpublishResult(r, evtMsgs, content); - } + return SavePublishing(content, userId); } /// public PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true) + { + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + var result = SavePublishingInternal(scope, content, userId, raiseEvents); + scope.Complete(); + return result; + } + } + + private PublishResult SavePublishingInternal(IScope scope, IContent content, int userId = 0, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) { var evtMsgs = EventMessagesFactory.Get(); PublishResult publishResult = null; - UnpublishResult unpublishResult = null; + PublishResult unpublishResult = null; // nothing set = republish it all if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing) - ((Content) content).PublishedState = PublishedState.Publishing; + ((Content)content).PublishedState = PublishedState.Publishing; // state here is either Publishing or Unpublishing + // (even though, Publishing to unpublish a culture may end up unpublishing everything) var publishing = content.PublishedState == PublishedState.Publishing; var unpublishing = content.PublishedState == PublishedState.Unpublishing; - IEnumerable culturesChanging = null; + var variesByCulture = content.ContentType.VariesByCulture(); - using (var scope = ScopeProvider.CreateScope()) + //track cultures that are being published, changed, unpublished + IReadOnlyList culturesPublishing = null; + IReadOnlyList culturesUnpublishing = null; + IReadOnlyList culturesChanging = variesByCulture + ? content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key).ToList() + : null; + + var isNew = !content.HasIdentity; + var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch; + var previouslyPublished = content.HasIdentity && content.Published; + + // always save + var saveEventArgs = new SaveEventArgs(content, evtMsgs); + if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); + + if (publishing) { - // is the content going to end up published, or unpublished? - if (publishing && content.ContentType.VariesByCulture()) - { - var publishedCultures = content.PublishedCultures.ToList(); - var cannotBePublished = publishedCultures.Count == 0; // no published cultures = cannot be published - if (!cannotBePublished) - { - var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); - cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); // missing mandatory culture = cannot be published - } + culturesUnpublishing = content.GetCulturesUnpublishing(); + culturesPublishing = variesByCulture + ? content.PublishCultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key).ToList() + : null; - if (cannotBePublished) + // ensure that the document can be published, and publish handling events, business rules, etc + publishResult = StrategyCanPublish(scope, content, userId, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs); + if (publishResult.Success) + { + // note: StrategyPublish flips the PublishedState to Publishing! + publishResult = StrategyPublish(scope, content, userId, culturesPublishing, culturesUnpublishing, evtMsgs); + } + else + { + // in a branch, just give up + if (branchOne && !branchRoot) + return publishResult; + + //check for mandatory culture missing, and then unpublish document as a whole + if (publishResult.Result == PublishResultType.FailedPublishMandatoryCultureMissing) { publishing = false; unpublishing = content.Published; // if not published yet, nothing to do // we may end up in a state where we won't publish nor unpublish - // keep going, though, as we want to save anways - } - else - { - culturesChanging = content.PublishCultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key).ToList(); + // keep going, though, as we want to save anyways } + + //fixme - casting + // reset published state from temp values (publishing, unpublishing) to original value + // (published, unpublished) in order to save the document, unchanged + ((Content)content).Published = content.Published; } + } - var isNew = !content.HasIdentity; - var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch; - var previouslyPublished = content.HasIdentity && content.Published; + if (unpublishing) // won't happen in a branch + { + var newest = GetById(content.Id); // ensure we have the newest version - in scope + if (content.VersionId != newest.VersionId) + return new PublishResult(PublishResultType.FailedPublishConcurrencyViolation, evtMsgs, content); - scope.WriteLock(Constants.Locks.ContentTree); - - // always save - var saveEventArgs = new SaveEventArgs(content, evtMsgs); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + if (content.Published) { - scope.Complete(); - return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); - } - - if (publishing) - { - // ensure that the document can be published, and publish + // ensure that the document can be unpublished, and unpublish // handling events, business rules, etc - // note: StrategyPublish flips the PublishedState to Publishing! - publishResult = StrategyCanPublish(scope, content, userId, /*checkPath:*/ true, evtMsgs); - if (publishResult.Success) - publishResult = StrategyPublish(scope, content, /*canPublish:*/ true, userId, evtMsgs); - if (!publishResult.Success) - ((Content) content).Published = content.Published; // reset published state = save unchanged - } - - if (unpublishing) - { - var newest = GetById(content.Id); // ensure we have the newest version - in scope - if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version - content = newest; - - if (content.Published) - { - // ensure that the document can be unpublished, and unpublish - // handling events, business rules, etc - // note: StrategyUnpublish flips the PublishedState to Unpublishing! - // note: This unpublishes the entire document (not different variants) - unpublishResult = StrategyCanUnpublish(scope, content, userId, evtMsgs); - if (unpublishResult.Success) - unpublishResult = StrategyUnpublish(scope, content, true, userId, evtMsgs); - if (!unpublishResult.Success) - ((Content) content).Published = content.Published; // reset published state = save unchanged - } + // note: StrategyUnpublish flips the PublishedState to Unpublishing! + // note: This unpublishes the entire document (not different variants) + unpublishResult = StrategyCanUnpublish(scope, content, userId, evtMsgs); + if (unpublishResult.Success) + unpublishResult = StrategyUnpublish(scope, content, userId, evtMsgs); else { - // already unpublished - optimistic concurrency collision, really, - // and I am not sure at all what we should do, better die fast, else - // we may end up corrupting the db - throw new InvalidOperationException("Concurrency collision."); + //fixme - casting + // reset published state from temp values (publishing, unpublishing) to original value + // (published, unpublished) in order to save the document, unchanged + ((Content)content).Published = content.Published; } } - - // save, always - if (content.HasIdentity == false) - content.CreatorId = userId; - content.WriterId = userId; - - // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing - _documentRepository.Save(content); - - // raise the Saved event, always - if (raiseEvents) + else { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + // already unpublished - optimistic concurrency collision, really, + // and I am not sure at all what we should do, better die fast, else + // we may end up corrupting the db + throw new InvalidOperationException("Concurrency collision."); } + } - if (unpublishing) // we have tried to unpublish + // save, always + if (content.HasIdentity == false) + content.CreatorId = userId; + content.WriterId = userId; + + // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing + _documentRepository.Save(content); + + // raise the Saved event, always + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + + if (unpublishing) // we have tried to unpublish - won't happen in a branch + { + if (unpublishResult.Success) // and succeeded, trigger events { - if (unpublishResult.Success) // and succeeded, trigger events + // events and audit + scope.Events.Dispatch(Unpublished, this, new PublishEventArgs(content, false, false), "Unpublished"); + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + + if (culturesUnpublishing != null) { - // events and audit - scope.Events.Dispatch(Unpublished, this, new PublishEventArgs(content, false, false), "Unpublished"); - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + //If we are here, it means we tried unpublishing a culture but it was mandatory so now everything is unpublished + var langs = string.Join(", ", _languageRepository.GetMany() + .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) + .Select(x => x.CultureName)); + Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); + //log that the whole content item has been unpublished due to mandatory culture unpublished + Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)"); + } + else Audit(AuditType.Unpublish, userId, content.Id); - scope.Complete(); - return new PublishResult(PublishResultType.Success, evtMsgs, content); - } - // or, failed - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - scope.Complete(); // compete the save - return new PublishResult(PublishResultType.FailedToUnpublish, evtMsgs, content); // bah + return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); } - if (publishing) // we have tried to publish - { - if (publishResult.Success) // and succeeded, trigger events - { - if (isNew == false && previouslyPublished == false) - changeType = TreeChangeTypes.RefreshBranch; // whole branch + // or, failed + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + return new PublishResult(PublishResultType.FailedUnpublish, evtMsgs, content); // bah + } - // invalidate the node/branch + if (publishing) // we have tried to publish + { + if (publishResult.Success) // and succeeded, trigger events + { + if (isNew == false && previouslyPublished == false) + changeType = TreeChangeTypes.RefreshBranch; // whole branch + + // invalidate the node/branch + if (!branchOne) // for branches, handled by SaveAndPublishBranch + { scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); scope.Events.Dispatch(Published, this, new PublishEventArgs(content, false, false), "Published"); - - // if was not published and now is... descendants that were 'published' (but - // had an unpublished ancestor) are 're-published' ie not explicitely published - // but back as 'published' nevertheless - if (isNew == false && previouslyPublished == false && HasChildren(content.Id)) - { - var descendants = GetPublishedDescendantsLocked(content).ToArray(); - scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); - } - - if (culturesChanging != null) - { - var langs = string.Join(", ", _languageRepository.GetMany() - .Where(x => culturesChanging.InvariantContains(x.IsoCode)) - .Select(x => x.CultureName)); - Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", langs); - } - else - Audit(AuditType.Publish, userId, content.Id); - - scope.Complete(); - return publishResult; } - // or, failed - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - scope.Complete(); // compete the save + // if was not published and now is... descendants that were 'published' (but + // had an unpublished ancestor) are 're-published' ie not explicitely published + // but back as 'published' nevertheless + if (!branchOne && isNew == false && previouslyPublished == false && HasChildren(content.Id)) + { + var descendants = GetPublishedDescendantsLocked(content).ToArray(); + scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); + } + + switch (publishResult.Result) + { + case PublishResultType.SuccessPublish: + Audit(AuditType.Publish, userId, content.Id); + break; + case PublishResultType.SuccessPublishCulture: + if (culturesPublishing != null) + { + var langs = string.Join(", ", _languageRepository.GetMany() + .Where(x => culturesPublishing.InvariantContains(x.IsoCode)) + .Select(x => x.CultureName)); + Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", langs); + } + break; + case PublishResultType.SuccessUnpublishCulture: + if (culturesUnpublishing != null) + { + var langs = string.Join(", ", _languageRepository.GetMany() + .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) + .Select(x => x.CultureName)); + Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); + } + break; + } + return publishResult; } - - // both publishing and unpublishing are false - // this means that we wanted to publish, in a variant scenario, a document that - // was not published yet, and we could not, due to cultures issues - // - // raise event (we saved), report - - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - scope.Complete(); // compete the save - return new PublishResult(PublishResultType.FailedByCulture, evtMsgs, content); } + + // should not happen + if (branchOne && !branchRoot) + throw new Exception("panic"); + + //if publishing didn't happen or if it has failed, we still need to log which cultures were saved + if (!branchOne && (publishResult == null || !publishResult.Success)) + { + if (culturesChanging != null) + { + var langs = string.Join(", ", _languageRepository.GetMany() + .Where(x => culturesChanging.InvariantContains(x.IsoCode)) + .Select(x => x.CultureName)); + Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs); + } + else + { + Audit(AuditType.Save, userId, content.Id); + } + } + + // or, failed + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + return publishResult; } /// - public IEnumerable PerformScheduledPublish() + public IEnumerable PerformScheduledPublish(DateTime date) + => PerformScheduledPublishInternal(date).ToList(); + + // beware! this method yields results, so the returned IEnumerable *must* be + // enumerated for anything to happen - dangerous, so private + exposed via + // the public method above, which forces ToList(). + private IEnumerable PerformScheduledPublishInternal(DateTime date) { + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.ContentTree); - foreach (var d in GetContentForRelease()) + foreach (var d in _documentRepository.GetContentForRelease(date)) { PublishResult result; - try + if (d.ContentType.VariesByCulture()) { - d.ReleaseDate = null; - d.PublishCulture(); // fixme variants? - result = SaveAndPublish(d, userId: d.WriterId); + //find which cultures have pending schedules + var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Release, date) + .Select(x => x.Culture) + .Distinct() + .ToList(); + + var publishing = true; + foreach (var culture in pendingCultures) + { + //Clear this schedule for this culture + d.ContentSchedule.Clear(culture, ContentScheduleAction.Release, date); + + if (d.Trashed) continue; // won't publish + + publishing &= d.PublishCulture(culture); //set the culture to be published + if (!publishing) break; // no point continuing + } + + if (d.Trashed) + result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d); + else if (!publishing) + result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); + else + result = SavePublishing(d, d.WriterId); + if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + + yield return result; } - catch (Exception e) + else { - Logger.Error(e, "Failed to publish document id={DocumentId}, an exception was thrown.", d.Id); - throw; + //Clear this schedule + d.ContentSchedule.Clear(ContentScheduleAction.Release, date); + + result = d.Trashed + ? new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d) + : SaveAndPublish(d, userId: d.WriterId); + + if (result.Success == false) + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + + yield return result; } - yield return result; } - foreach (var d in GetContentForExpiration()) + + foreach (var d in _documentRepository.GetContentForExpiration(date)) { - try + PublishResult result; + if (d.ContentType.VariesByCulture()) { - d.ExpireDate = null; - var result = Unpublish(d, userId: d.WriterId); + //find which cultures have pending schedules + var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date) + .Select(x => x.Culture) + .Distinct() + .ToList(); + + foreach (var c in pendingCultures) + { + //Clear this schedule for this culture + d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date); + //set the culture to be published + d.UnpublishCulture(c); + } + + if (pendingCultures.Count > 0) + { + result = SavePublishing(d, d.WriterId); + if (result.Success == false) + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + yield return result; + } + } + else + { + //Clear this schedule + d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); + result = Unpublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + yield return result; } - catch (Exception e) - { - Logger.Error(e, "Failed to unpublish document id={DocumentId}, an exception was thrown.", d.Id); - throw; - } + + } + _documentRepository.ClearSchedule(date); + scope.Complete(); } } + private bool SaveAndPublishBranch_PublishCultures(IContent c, HashSet culturesToPublish) + { + // variant content type - publish specified cultures + // invariant content type - publish only the invariant culture + return c.ContentType.VariesByCulture() + ? culturesToPublish.All(c.PublishCulture) + : c.PublishCulture(); + } + + private HashSet SaveAndPublishBranch_ShouldPublish3(ref HashSet cultures, string c, bool published, bool edited, bool isRoot, bool force) + { + // if published, republish + if (published) + { + if (cultures == null) cultures = new HashSet(); // empty means 'already published' + if (edited) cultures.Add(c); // means 'republish this culture' + return cultures; + } + + // if not published, publish if force/root else do nothing + if (!force && !isRoot) return cultures; // null means 'nothing to do' + + if (cultures == null) cultures = new HashSet(); + cultures.Add(c); // means 'publish this culture' + return cultures; + } + + /// public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = 0) { // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() // and not to == them, else we would be comparing references, and that is a bad thing - bool IsEditing(IContent c, string l) - => c.PublishName != c.Name || - c.PublishedCultures.Where(x => x.InvariantEquals(l)).Any(x => c.GetCultureName(x) != c.GetPublishName(x)) || - c.Properties.Any(x => x.Values.Where(y => culture == "*" || y.Culture.InvariantEquals(l)).Any(y => !y.EditedValue.Equals(y.PublishedValue))); + // determines whether the document is edited, and thus needs to be published, + // for the specified culture (it may be edited for other cultures and that + // should not trigger a publish). - return SaveAndPublishBranch(content, force, document => IsEditing(document, culture), document => document.PublishCulture(culture), userId); + // determines cultures to be published + // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures + HashSet ShouldPublish(IContent c) + { + var isRoot = c.Id == content.Id; + HashSet culturesToPublish = null; + + if (!c.ContentType.VariesByCulture()) // invariant content type + return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + + if (culture != "*") // variant content type, specific culture + return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force); + + // variant content type, all cultures + if (c.Published) + { + // then some (and maybe all) cultures will be 'already published' (unless forcing), + // others will have to 'republish this culture' + foreach (var x in c.AvailableCultures) + SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + return culturesToPublish; + } + + // if not published, publish if force/root else do nothing + return force || isRoot + ? new HashSet { "*" } // "*" means 'publish all' + : null; // null means 'nothing to do' + } + + return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId); } - // fixme - make this public once we know it works + document - private IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = 0) + /// + public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = 0) { // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() // and not to == them, else we would be comparing references, and that is a bad thing cultures = cultures ?? Array.Empty(); - // determines whether the document is edited, and thus needs to be published, - // for the specified cultures (it may be edited for other cultures and that - // should not trigger a publish). - bool IsEdited(IContent c) + // determines cultures to be published + // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures + HashSet ShouldPublish(IContent c) { - if (cultures.Length == 0) + var isRoot = c.Id == content.Id; + HashSet culturesToPublish = null; + + if (!c.ContentType.VariesByCulture()) // invariant content type + return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + + // variant content type, specific cultures + if (c.Published) { - // nothing = everything - return c.PublishName != c.Name || - c.PublishedCultures.Any(x => c.GetCultureName(x) != c.GetPublishName(x)) || - c.Properties.Any(x => x.Values.Any(y => !y.EditedValue.Equals(y.PublishedValue))); + // then some (and maybe all) cultures will be 'already published' (unless forcing), + // others will have to 'republish this culture' + foreach (var x in cultures) + SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + return culturesToPublish; } - return c.PublishName != c.Name || - c.PublishedCultures.Where(x => cultures.Contains(x, StringComparer.InvariantCultureIgnoreCase)).Any(x => c.GetCultureName(x) != c.GetPublishName(x)) || - c.Properties.Any(x => x.Values.Where(y => cultures.Contains(y.Culture, StringComparer.InvariantCultureIgnoreCase)).Any(y => !y.EditedValue.Equals(y.PublishedValue))); + // if not published, publish if force/root else do nothing + return force || isRoot + ? new HashSet(cultures) // means 'publish specified cultures' + : null; // null means 'nothing to do' } - // publish the specified cultures - bool PublishCultures(IContent c) - { - return cultures.Length == 0 - ? c.PublishCulture() // nothing = everything - : cultures.All(c.PublishCulture); - } - - return SaveAndPublishBranch(content, force, IsEdited, PublishCultures, userId); + return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId); } /// public IEnumerable SaveAndPublishBranch(IContent document, bool force, - Func editing, Func publishCultures, int userId = 0) + Func> shouldPublish, + Func, bool> publishCultures, + int userId = 0) { + if (shouldPublish == null) throw new ArgumentNullException(nameof(shouldPublish)); + if (publishCultures == null) throw new ArgumentNullException(nameof(publishCultures)); + var evtMsgs = EventMessagesFactory.Get(); var results = new List(); var publishedDocuments = new List(); @@ -1278,54 +1417,63 @@ namespace Umbraco.Core.Services.Implement // fixme events?! if (!document.HasIdentity) - throw new InvalidOperationException("Do not branch-publish a new document."); + throw new InvalidOperationException("Cannot not branch-publish a new document."); - var publishedState = ((Content) document).PublishedState; + var publishedState = ((Content)document).PublishedState; if (publishedState == PublishedState.Publishing) - throw new InvalidOperationException("Do not publish values when publishing branches."); + throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch."); // deal with the branch root - if it fails, abort - var result = SaveAndPublishBranchOne(scope, document, editing, publishCultures, true, publishedDocuments, evtMsgs, userId); - results.Add(result); - if (!result.Success) return results; + var result = SaveAndPublishBranchOne(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId); + if (result != null) + { + results.Add(result); + if (!result.Success) return results; + } // deal with descendants // if one fails, abort its branch var exclude = new HashSet(); - const int pageSize = 500; + int count; var page = 0; - var total = long.MaxValue; - while (page * pageSize < total) + const int pageSize = 100; + do { - var descendants = GetPagedDescendants(document.Id, page++, pageSize, out total); - - foreach (var d in descendants) + count = 0; + // important to order by Path ASC so make it explicit in case defaults change + // ReSharper disable once RedundantArgumentDefaultValue + foreach (var d in GetPagedDescendants(document.Id, page, pageSize, out _, ordering: Ordering.By("Path", Direction.Ascending))) { - // if parent is excluded, exclude document and ignore - // if not forcing, and not publishing, exclude document and ignore - if (exclude.Contains(d.ParentId) || !force && !d.Published) + count++; + + // if parent is excluded, exclude child too + if (exclude.Contains(d.ParentId)) { exclude.Add(d.Id); continue; } - // no need to check path here, - // 1. because we know the parent is path-published (we just published it) - // 2. because it would not work as nothing's been written out to the db until the uow completes - result = SaveAndPublishBranchOne(scope, d, editing, publishCultures, false, publishedDocuments, evtMsgs, userId); - results.Add(result); - if (result.Success) continue; + // no need to check path here, parent has to be published here + result = SaveAndPublishBranchOne(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId); + if (result != null) + { + results.Add(result); + if (result.Success) continue; + } - // abort branch + // if we could not publish the document, cut its branch exclude.Add(d.Id); } - } - + page++; + } while (count > 0); + + Audit(AuditType.Publish, userId, document.Id, "Branch published"); + + // trigger events for the entire branch scope.Events.Dispatch(TreeChanged, this, new TreeChange(document, TreeChangeTypes.RefreshBranch).ToEventArgs()); scope.Events.Dispatch(Published, this, new PublishEventArgs(publishedDocuments, false, false), "Published"); - Audit(AuditType.Publish, userId, document.Id, "Branch published"); scope.Complete(); } @@ -1333,36 +1481,30 @@ namespace Umbraco.Core.Services.Implement return results; } + // shouldPublish: a function determining whether the document has changes that need to be published + // note - 'force' is handled by 'editing' + // publishValues: a function publishing values (using the appropriate PublishCulture calls) private PublishResult SaveAndPublishBranchOne(IScope scope, IContent document, - Func editing, Func publishValues, - bool checkPath, - List publishedDocuments, + Func> shouldPublish, + Func, bool> publishCultures, + bool isRoot, + ICollection publishedDocuments, EventMessages evtMsgs, int userId) { - // if already published, and values haven't changed - i.e. not changing anything - // nothing to do - fixme - unless we *want* to bump dates? - if (document.Published && (editing == null || !editing(document))) - return new PublishResult(PublishResultType.SuccessAlready, evtMsgs, document); + var culturesToPublish = shouldPublish(document); + if (culturesToPublish == null) // null = do not include + return null; + if (culturesToPublish.Count == 0) // empty = already published + return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document); // publish & check if values are valid - if (publishValues != null && !publishValues(document)) - return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, document); + if (!publishCultures(document, culturesToPublish)) + return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document); - // check if we can publish - var result = StrategyCanPublish(scope, document, userId, checkPath, evtMsgs); - if (!result.Success) - return result; - - // publish - should be successful - var publishResult = StrategyPublish(scope, document, /*canPublish:*/ true, userId, evtMsgs); - if (!publishResult.Success) - throw new Exception("oops: failed to publish."); - - // save - document.WriterId = userId; - _documentRepository.Save(document); - publishedDocuments.Add(document); - return publishResult; + var result = SavePublishingInternal(scope, document, userId, branchOne: true, branchRoot: isRoot); + if (result.Success) + publishedDocuments.Add(document); + return result; } #endregion @@ -1597,7 +1739,7 @@ namespace Umbraco.Core.Services.Implement { // however, it had been masked when being trashed, so there's no need for // any special event here - just change its state - ((Content) content).PublishedState = PublishedState.Unpublishing; + ((Content)content).PublishedState = PublishedState.Unpublishing; } PerformMoveLocked(content, parentId, parent, userId, moves, trashed); @@ -1652,7 +1794,7 @@ namespace Umbraco.Core.Services.Implement const int pageSize = 500; var page = 0; var total = long.MaxValue; - while(page * pageSize < total) + while (page * pageSize < total) { var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); foreach (var descendant in descendants) @@ -1665,13 +1807,13 @@ namespace Umbraco.Core.Services.Implement PerformMoveContentLocked(descendant, userId, trash); } } - + } private void PerformMoveContentLocked(IContent content, int userId, bool? trash) { //fixme no casting - if (trash.HasValue) ((ContentBase) content).Trashed = trash.Value; + if (trash.HasValue) ((ContentBase)content).Trashed = trash.Value; content.WriterId = userId; _documentRepository.Save(content); } @@ -1775,7 +1917,7 @@ namespace Umbraco.Core.Services.Implement // a copy is not published (but not really unpublishing either) // update the create author and last edit author if (copy.Published) - ((Content) copy).Published = false; + ((Content)copy).Published = false; copy.CreatorId = userId; copy.WriterId = userId; @@ -1802,7 +1944,7 @@ namespace Umbraco.Core.Services.Implement const int pageSize = 500; var page = 0; var total = long.MaxValue; - while(page * pageSize < total) + while (page * pageSize < total) { var descendants = GetPagedDescendants(content.Id, page++, pageSize, out total); foreach (var descendant in descendants) @@ -1868,29 +2010,29 @@ namespace Umbraco.Core.Services.Implement var culturesChanging = content.ContentType.VariesByCulture() ? string.Join(",", content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key)) : null; + //TODO: Currently there's no way to change track which variant properties have changed, we only have change // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed. // in this particular case, determining which cultures have changed works with the above with names since it will // have always changed if it's been saved in the back office but that's not really fail safe. //Save before raising event - // fixme - nesting uow? var saveResult = Save(content, userId); - if (saveResult.Success) - { - sendToPublishEventArgs.CanCancel = false; - scope.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs); - - if (culturesChanging != null) - Audit(AuditType.SendToPublishVariant, userId, content.Id, $"Send To Publish for cultures: {culturesChanging}", culturesChanging); - else - Audit(AuditType.SendToPublish, content.WriterId, content.Id); - } - - // fixme here, on only on success? + // always complete (but maybe return a failed status) scope.Complete(); + if (!saveResult.Success) + return saveResult.Success; + + sendToPublishEventArgs.CanCancel = false; + scope.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs); + + if (culturesChanging != null) + Audit(AuditType.SendToPublishVariant, userId, content.Id, $"Send To Publish for cultures: {culturesChanging}", culturesChanging); + else + Audit(AuditType.SendToPublish, content.WriterId, content.Id); + return saveResult.Success; } } @@ -2212,110 +2354,197 @@ namespace Umbraco.Core.Services.Implement #region Publishing Strategies - // ensures that a document can be published - internal PublishResult StrategyCanPublish(IScope scope, IContent content, int userId, bool checkPath, EventMessages evtMsgs) + /// + /// Ensures that a document can be published + /// + /// + /// + /// + /// + /// + /// + private PublishResult StrategyCanPublish(IScope scope, IContent content, int userId, bool checkPath, IReadOnlyList culturesPublishing, IReadOnlyList culturesUnpublishing, EventMessages evtMsgs) { // raise Publishing event if (scope.Events.DispatchCancelable(Publishing, this, new PublishEventArgs(content, evtMsgs))) { Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled"); - return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); + return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); + } + + var variesByCulture = content.ContentType.VariesByCulture(); + + //First check if mandatory languages fails, if this fails it will mean anything that the published flag on the document will + // be changed to Unpublished and any culture currently published will not be visible. + if (variesByCulture) + { + if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing.Count == 0) // no published cultures = cannot be published + return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + + // missing mandatory culture = cannot be published + var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); + var mandatoryMissing = mandatoryCultures.Any(x => !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); + if (mandatoryMissing) + return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content); + + if (culturesPublishing.Count == 0 && culturesUnpublishing.Count > 0) + return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content); } // ensure that the document has published values // either because it is 'publishing' or because it already has a published version - if (((Content) content).PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0) + //fixme - casting + if (((Content)content).PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0) { Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document does not have published values"); - return new PublishResult(PublishResultType.FailedNoPublishedValues, evtMsgs, content); + return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); } - // ensure that the document status is correct - switch (content.Status) + //loop over each culture publishing - or string.Empty for invariant + foreach (var culture in culturesPublishing ?? (new[] { string.Empty })) { - case ContentStatus.Expired: - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document has expired"); - return new PublishResult(PublishResultType.FailedHasExpired, evtMsgs, content); + // ensure that the document status is correct + // note: culture will be string.Empty for invariant + switch (content.GetStatus(culture)) + { + case ContentStatus.Expired: + if (!variesByCulture) + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document has expired"); + else + Logger.Info("Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", content.Name, content.Id, culture, "document culture has expired"); + return new PublishResult(!variesByCulture ? PublishResultType.FailedPublishHasExpired : PublishResultType.FailedPublishCultureHasExpired, evtMsgs, content); - case ContentStatus.AwaitingRelease: - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is awaiting release"); - return new PublishResult(PublishResultType.FailedAwaitingRelease, evtMsgs, content); + case ContentStatus.AwaitingRelease: + if (!variesByCulture) + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is awaiting release"); + else + Logger.Info("Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", content.Name, content.Id, culture, "document is culture awaiting release"); + return new PublishResult(!variesByCulture ? PublishResultType.FailedPublishAwaitingRelease : PublishResultType.FailedPublishCultureAwaitingRelease, evtMsgs, content); - case ContentStatus.Trashed: - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is trashed"); - return new PublishResult(PublishResultType.FailedIsTrashed, evtMsgs, content); + case ContentStatus.Trashed: + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is trashed"); + return new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, content); + } } - if (!checkPath) return new PublishResult(evtMsgs, content); - - // check if the content can be path-published - // root content can be published - // else check ancestors - we know we are not trashed - var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content)); - if (pathIsOk == false) + if (checkPath) { - Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "parent is not published"); - return new PublishResult(PublishResultType.FailedPathNotPublished, evtMsgs, content); + // check if the content can be path-published + // root content can be published + // else check ancestors - we know we are not trashed + var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content)); + if (!pathIsOk) + { + Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "parent is not published"); + return new PublishResult(PublishResultType.FailedPublishPathNotPublished, evtMsgs, content); + } } + //If we are both publishing and unpublishing cultures, then return a mixed status + if (variesByCulture && culturesPublishing.Count > 0 && culturesUnpublishing.Count > 0) + return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content); + return new PublishResult(evtMsgs, content); } - // publishes a document - internal PublishResult StrategyPublish(IScope scope, IContent content, bool canPublish, int userId, EventMessages evtMsgs) + /// + /// Publishes a document + /// + /// + /// + /// + /// + /// + /// + /// It is assumed that all publishing checks have passed before calling this method like + /// + private PublishResult StrategyPublish(IScope scope, IContent content, int userId, + IReadOnlyList culturesPublishing, IReadOnlyList culturesUnpublishing, + EventMessages evtMsgs) { - // note: when used at top-level, StrategyCanPublish with checkPath=true should have run already - // and alreadyCheckedCanPublish should be true, so not checking again. when used at nested level, - // there is no need to check the path again. so, checkPath=false in StrategyCanPublish below - - var result = canPublish - ? new PublishResult(evtMsgs, content) // already know we can - : StrategyCanPublish(scope, content, userId, /*checkPath:*/ false, evtMsgs); // else check - - if (result.Success == false) - return result; - // change state to publishing - ((Content) content).PublishedState = PublishedState.Publishing; + // fixme - casting + ((Content)content).PublishedState = PublishedState.Publishing; + + //if this is a variant then we need to log which cultures have been published/unpublished and return an appropriate result + if (content.ContentType.VariesByCulture()) + { + if (content.Published && culturesUnpublishing.Count == 0 && culturesPublishing.Count == 0) + return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + + if (culturesUnpublishing.Count > 0) + Logger.Info("Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.", + content.Name, content.Id, string.Join(",", culturesUnpublishing)); + + if (culturesPublishing.Count > 0) + Logger.Info("Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.", + content.Name, content.Id, string.Join(",", culturesPublishing)); + + if (culturesUnpublishing.Count > 0 && culturesPublishing.Count > 0) + return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content); + + if (culturesUnpublishing.Count > 0 && culturesPublishing.Count == 0) + return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content); + + return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content); + } + Logger.Info("Document {ContentName} (id={ContentId}) has been published.", content.Name, content.Id); - return result; + return new PublishResult(evtMsgs, content); } - // ensures that a document can be unpublished - internal UnpublishResult StrategyCanUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs) + /// + /// Ensures that a document can be unpublished + /// + /// + /// + /// + /// + /// + private PublishResult StrategyCanUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs) { // raise Unpublishing event if (scope.Events.DispatchCancelable(Unpublishing, this, new PublishEventArgs(content, evtMsgs))) { Logger.Info("Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id); - return new UnpublishResult(UnpublishResultType.FailedCancelledByEvent, evtMsgs, content); + return new PublishResult(PublishResultType.FailedUnpublishCancelledByEvent, evtMsgs, content); } - return new UnpublishResult(evtMsgs, content); + return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); } - // unpublishes a document - internal UnpublishResult StrategyUnpublish(IScope scope, IContent content, bool canUnpublish, int userId, EventMessages evtMsgs) + /// + /// Unpublishes a document + /// + /// + /// + /// + /// + /// + /// + /// It is assumed that all unpublishing checks have passed before calling this method like + /// + private PublishResult StrategyUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs) { - var attempt = canUnpublish - ? new UnpublishResult(evtMsgs, content) // already know we can - : StrategyCanUnpublish(scope, content, userId, evtMsgs); // else check + var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); if (attempt.Success == false) return attempt; - // if the document has a release date set to before now, - // it should be removed so it doesn't interrupt an unpublish + // if the document has any release dates set to before now, + // they should be removed so they don't interrupt an unpublish // otherwise it would remain released == published - if (content.ReleaseDate.HasValue && content.ReleaseDate.Value <= DateTime.Now) - { - content.ReleaseDate = null; + + var pastReleases = content.ContentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.Now); + foreach (var p in pastReleases) + content.ContentSchedule.Remove(p); + if (pastReleases.Count > 0) Logger.Info("Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", content.Name, content.Id); - } // change state to unpublishing - ((Content) content).PublishedState = PublishedState.Unpublishing; + // fixme - casting + ((Content)content).PublishedState = PublishedState.Unpublishing; Logger.Info("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name, content.Id); return attempt; @@ -2452,7 +2681,7 @@ namespace Umbraco.Core.Services.Implement scope.ReadLock(Constants.Locks.ContentTree); var blueprint = _documentBlueprintRepository.Get(id); if (blueprint != null) - ((Content) blueprint).Blueprint = true; + ((Content)blueprint).Blueprint = true; return blueprint; } } @@ -2464,7 +2693,7 @@ namespace Umbraco.Core.Services.Implement scope.ReadLock(Constants.Locks.ContentTree); var blueprint = _documentBlueprintRepository.Get(id); if (blueprint != null) - ((Content) blueprint).Blueprint = true; + ((Content)blueprint).Blueprint = true; return blueprint; } } @@ -2475,7 +2704,7 @@ namespace Umbraco.Core.Services.Implement if (content.ParentId != -1) content.ParentId = -1; - ((Content) content).Blueprint = true; + ((Content)content).Blueprint = true; using (var scope = ScopeProvider.CreateScope()) { @@ -2539,7 +2768,7 @@ namespace Umbraco.Core.Services.Implement } return _documentBlueprintRepository.Get(query).Select(x => { - ((Content) x).Blueprint = true; + ((Content)x).Blueprint = true; return x; }); } @@ -2558,7 +2787,7 @@ namespace Umbraco.Core.Services.Implement var blueprints = _documentBlueprintRepository.Get(query).Select(x => { - ((Content) x).Blueprint = true; + ((Content)x).Blueprint = true; return x; }).ToArray(); @@ -2584,7 +2813,7 @@ namespace Umbraco.Core.Services.Implement public OperationResult Rollback(int id, int versionId, string culture = "*", int userId = 0) { var evtMsgs = EventMessagesFactory.Get(); - + //Get the current copy of the node var content = GetById(id); @@ -2633,7 +2862,7 @@ namespace Umbraco.Core.Services.Implement Logger.Info("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId); Audit(AuditType.RollBack, userId, id, $"Content '{content.Name}' was rolled back to version '{versionId}'"); } - + scope.Complete(); } diff --git a/src/Umbraco.Core/Services/Implement/EntityService.cs b/src/Umbraco.Core/Services/Implement/EntityService.cs index 385b5eabe0..45c229214a 100644 --- a/src/Umbraco.Core/Services/Implement/EntityService.cs +++ b/src/Umbraco.Core/Services/Implement/EntityService.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; namespace Umbraco.Core.Services.Implement @@ -373,6 +374,22 @@ namespace Umbraco.Core.Services.Implement } } + /// + /// Gets a collection of children by the parent's Id and UmbracoObjectType without adding property data + /// + /// Id of the parent to retrieve children for + /// An enumerable list of objects + internal IEnumerable GetMediaChildrenWithoutPropertyData(int parentId) + { + using (ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.ParentId == parentId); + + //fixme - see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media + return ((EntityRepository)_entityRepository).GetMediaByQueryWithoutPropertyData(query); + } + } + /// public virtual IEnumerable GetDescendants(int id) { diff --git a/src/Umbraco.Core/Services/Implement/KeyValueService.cs b/src/Umbraco.Core/Services/Implement/KeyValueService.cs index e0d78b0258..b30543ed48 100644 --- a/src/Umbraco.Core/Services/Implement/KeyValueService.cs +++ b/src/Umbraco.Core/Services/Implement/KeyValueService.cs @@ -40,7 +40,7 @@ namespace Umbraco.Core.Services.Implement // if already running 8, either following an upgrade or an install, // then everything should be ok (the table should exist, etc) - if (UmbracoVersion.Local.Major >= 8) + if (UmbracoVersion.LocalVersion.Major >= 8) return; // else we are upgrading from 7, we can assume that the locks table @@ -197,7 +197,7 @@ namespace Umbraco.Core.Services.Implement internal static string GetValue(IUmbracoDatabase database, string key) { // not 8 yet = no key/value table, no value - if (UmbracoVersion.Local.Major < 8) + if (UmbracoVersion.LocalVersion.Major < 8) return null; var sql = database.SqlContext.Sql() diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 5f35e35acf..1d04462836 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -888,7 +888,9 @@ namespace Umbraco.Core.Services.Implement var originalPath = media.Path; - if (scope.Events.DispatchCancelable(Trashing, this, new MoveEventArgs(new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia)), nameof(Trashing))) + var moveEventInfo = new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia); + var moveEventArgs = new MoveEventArgs(true, evtMsgs, moveEventInfo); + if (scope.Events.DispatchCancelable(Trashing, this, moveEventArgs, nameof(Trashing))) { scope.Complete(); return OperationResult.Attempt.Cancel(evtMsgs); @@ -899,8 +901,9 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, TreeChangeTypes.RefreshBranch).ToEventArgs()); var moveInfo = moves.Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); - - scope.Events.Dispatch(Trashed, this, new MoveEventArgs(false, evtMsgs, moveInfo), nameof(Trashed)); + moveEventArgs.MoveInfoCollection = moveInfo; + moveEventArgs.CanCancel = false; + scope.Events.Dispatch(Trashed, this, moveEventArgs, nameof(Trashed)); Audit(AuditType.Move, userId, media.Id, "Move Media to recycle bin"); scope.Complete(); @@ -915,13 +918,15 @@ namespace Umbraco.Core.Services.Implement /// The to move /// Id of the Media's new Parent /// Id of the User moving the Media - public void Move(IMedia media, int parentId, int userId = 0) + public Attempt Move(IMedia media, int parentId, int userId = 0) { + var evtMsgs = EventMessagesFactory.Get(); + // if moving to the recycle bin then use the proper method if (parentId == Constants.System.RecycleBinMedia) { MoveToRecycleBin(media, userId); - return; + return OperationResult.Attempt.Succeed(evtMsgs); } var moves = new List>(); @@ -935,11 +940,11 @@ namespace Umbraco.Core.Services.Implement throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback // causes rollback var moveEventInfo = new MoveEventInfo(media, media.Path, parentId); - var moveEventArgs = new MoveEventArgs(moveEventInfo); + var moveEventArgs = new MoveEventArgs(true, evtMsgs, moveEventInfo); if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs, nameof(Moving))) { scope.Complete(); - return; + return OperationResult.Attempt.Cancel(evtMsgs); } // if media was trashed, and since we're not moving to the recycle bin, @@ -964,6 +969,7 @@ namespace Umbraco.Core.Services.Implement Audit(AuditType.Move, userId, media.Id); scope.Complete(); } + return OperationResult.Attempt.Succeed(evtMsgs); } // MUST be called from within WriteLock diff --git a/src/Umbraco.Core/Services/Implement/MemberService.cs b/src/Umbraco.Core/Services/Implement/MemberService.cs index 5a644cfec1..e191493736 100644 --- a/src/Umbraco.Core/Services/Implement/MemberService.cs +++ b/src/Umbraco.Core/Services/Implement/MemberService.cs @@ -1033,7 +1033,7 @@ namespace Umbraco.Core.Services.Implement scope.WriteLock(Constants.Locks.MemberTree); var ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.AssignRoles(ids, roleNames); - scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(ids, roleNames)); + scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(ids, roleNames), nameof(AssignedRoles)); scope.Complete(); } } @@ -1050,7 +1050,7 @@ namespace Umbraco.Core.Services.Implement scope.WriteLock(Constants.Locks.MemberTree); var ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.DissociateRoles(ids, roleNames); - scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(ids, roleNames)); + scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(ids, roleNames), nameof(RemovedRoles)); scope.Complete(); } } @@ -1066,7 +1066,7 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.AssignRoles(memberIds, roleNames); - scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(memberIds, roleNames)); + scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(memberIds, roleNames), nameof(AssignedRoles)); scope.Complete(); } } @@ -1082,7 +1082,7 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.DissociateRoles(memberIds, roleNames); - scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(memberIds, roleNames)); + scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(memberIds, roleNames), nameof(RemovedRoles)); scope.Complete(); } } diff --git a/src/Umbraco.Core/Services/MoveOperationStatusType.cs b/src/Umbraco.Core/Services/MoveOperationStatusType.cs index 95ccce93ca..b4b4c2b42e 100644 --- a/src/Umbraco.Core/Services/MoveOperationStatusType.cs +++ b/src/Umbraco.Core/Services/MoveOperationStatusType.cs @@ -7,7 +7,7 @@ /// /// Anything less than 10 = Success! /// - public enum MoveOperationStatusType + public enum MoveOperationStatusType : byte { /// /// The move was successful. diff --git a/src/Umbraco.Core/Services/PublishResult.cs b/src/Umbraco.Core/Services/PublishResult.cs index 073d7ce1cb..4f1ff776a2 100644 --- a/src/Umbraco.Core/Services/PublishResult.cs +++ b/src/Umbraco.Core/Services/PublishResult.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core.Services /// Initializes a new instance of the class. /// public PublishResult(EventMessages eventMessages, IContent content) - : base(PublishResultType.Success, eventMessages, content) + : base(PublishResultType.SuccessPublish, eventMessages, content) { } /// diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index 15b2f503c7..f79dab91d3 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -1,80 +1,146 @@ namespace Umbraco.Core.Services { - /// - /// A value indicating the result of publishing a content item. + /// A value indicating the result of publishing or unpublishing a document. /// public enum PublishResultType : byte { // all "ResultType" enums must be byte-based, and declare Failed = 128, and declare // every failure codes as >128 - see OperationResult and OperationResultType for details. - /// - /// The publishing was successful. - /// - Success = 0, + #region Success - Publish /// - /// The item was already published. + /// The document was successfully published. /// - SuccessAlready = 1, + SuccessPublish = 0, + + /// + /// The specified document culture was successfully published. + /// + SuccessPublishCulture = 1, + + /// + /// The document was already published. + /// + SuccessPublishAlready = 2, + + #endregion + + #region Success - Unpublish + + /// + /// The document was successfully unpublished. + /// + SuccessUnpublish = 3, + + /// + /// The document was already unpublished. + /// + SuccessUnpublishAlready = 4, + + /// + /// The specified document culture was unpublished, the document item itself remains published. + /// + SuccessUnpublishCulture = 5, + + /// + /// The specified document culture was unpublished, and was a mandatory culture, therefore the document itself was unpublished. + /// + SuccessUnpublishMandatoryCulture = 6, + + #endregion + + #region Success - Mixed + + /// + /// Specified document cultures were successfully published and unpublished (in the same operation). + /// + SuccessMixedCulture = 7, + + #endregion + + #region Failed - Publish /// /// The operation failed. /// /// All values above this value indicate a failure. - Failed = 128, + FailedPublish = 128, /// - /// The content could not be published because it's ancestor path isn't published. + /// The document could not be published because its ancestor path is not published. /// - FailedPathNotPublished = Failed | 1, + FailedPublishPathNotPublished = FailedPublish | 1, /// - /// The content item was scheduled to be un-published and it has expired so we cannot force it to be + /// The document has expired so we cannot force it to be /// published again as part of a bulk publish operation. /// - FailedHasExpired = Failed | 2, + FailedPublishHasExpired = FailedPublish | 2, /// - /// The content item is scheduled to be released in the future and therefore we cannot force it to + /// The document is scheduled to be released in the future and therefore we cannot force it to /// be published during a bulk publish operation. /// - FailedAwaitingRelease = Failed | 3, + FailedPublishAwaitingRelease = FailedPublish | 3, /// - /// The content item could not be published because it is in the trash. + /// A document culture has expired so we cannot force it to be + /// published again as part of a bulk publish operation. /// - FailedIsTrashed = Failed | 4, + FailedPublishCultureHasExpired = FailedPublish | 4, + + /// + /// A document culture is scheduled to be released in the future and therefore we cannot force it to + /// be published during a bulk publish operation. + /// + FailedPublishCultureAwaitingRelease = FailedPublish | 5, + + /// + /// The document could not be published because it is in the trash. + /// + FailedPublishIsTrashed = FailedPublish | 6, /// /// The publish action has been cancelled by an event handler. /// - FailedCancelledByEvent = Failed | 5, + FailedPublishCancelledByEvent = FailedPublish | 7, /// - /// The content item could not be published because it contains invalid data (has not passed validation requirements). + /// The document could not be published because it contains invalid data (has not passed validation requirements). /// - FailedContentInvalid = Failed | 6, + FailedPublishContentInvalid = FailedPublish | 8, /// - /// Cannot republish a document that hasn't been published. + /// The document could not be published because it has no publishing flags or values. /// - FailedNoPublishedValues = Failed | 7, // in ContentService.StrategyCanPublish - fixme weird + FailedPublishNothingToPublish = FailedPublish | 9, // in ContentService.StrategyCanPublish - fixme weird /// - /// Some mandatory cultures are missing, or are not valid. + /// The document could not be published because some mandatory cultures are missing. /// - FailedCannotPublish = Failed | 8, // in ContentController.PublishInternal - fixme // FailedByCulture? + FailedPublishMandatoryCultureMissing = FailedPublish | 10, // in ContentService.SavePublishing /// - /// Publishing changes triggered an unpublishing, due to missing mandatory cultures, and unpublishing failed. + /// The document could not be published because it has been modified by another user. /// - FailedToUnpublish = Failed | 9, // in ContentService.SavePublishing + FailedPublishConcurrencyViolation = FailedPublish | 11, + + #endregion + + #region Failed - Unpublish /// - /// Some mandatory cultures are missing. + /// The document could not be unpublished. /// - FailedByCulture = Failed | 10, // in ContentService.SavePublishing + FailedUnpublish = FailedPublish | 11, // in ContentService.SavePublishing + + /// + /// The unpublish action has been cancelled by an event handler. + /// + FailedUnpublishCancelledByEvent = FailedPublish | 12, + + #endregion } } diff --git a/src/Umbraco.Core/Services/UnpublishResult.cs b/src/Umbraco.Core/Services/UnpublishResult.cs deleted file mode 100644 index 7cd1506e6c..0000000000 --- a/src/Umbraco.Core/Services/UnpublishResult.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Umbraco.Core.Events; -using Umbraco.Core.Models; - -namespace Umbraco.Core.Services -{ - /// - /// Represents the result of unpublishing a document. - /// - public class UnpublishResult : OperationResult - { - /// - /// Creates a successful result - /// - /// - /// - public UnpublishResult(EventMessages eventMessages, IContent entity) : base(UnpublishResultType.Success, eventMessages, entity) - { - } - - public UnpublishResult(UnpublishResultType result, EventMessages eventMessages, IContent entity) : base(result, eventMessages, entity) - { - } - - /// - /// Gets the document. - /// - public IContent Content => Entity; - } -} diff --git a/src/Umbraco.Core/Services/UnpublishResultType.cs b/src/Umbraco.Core/Services/UnpublishResultType.cs deleted file mode 100644 index e61e786a05..0000000000 --- a/src/Umbraco.Core/Services/UnpublishResultType.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Umbraco.Core.Services -{ - /// - /// A value indicating the result of unpublishing a content item. - /// - public enum UnpublishResultType : byte - { - /// - /// The unpublishing was successful. - /// - Success = 0, - - /// - /// The item was already unpublished. - /// - SuccessAlready = 1, - - /// - /// The specified variant was unpublished, the content item itself remains published. - /// - SuccessCulture = 2, - - /// - /// The specified variant was a mandatory culture therefore it was unpublished and the content item itself is unpublished - /// - SuccessMandatoryCulture = 3, - - /// - /// The operation failed. - /// - /// All values above this value indicate a failure. - Failed = 128, - - /// - /// The publish action has been cancelled by an event handler. - /// - FailedCancelledByEvent = Failed | 5, - } -} diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 9c686c4353..03a371204c 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -540,7 +540,7 @@ namespace Umbraco.Core public static string StripHtml(this string text) { const string pattern = @"<(.|\n)*?>"; - return Regex.Replace(text, pattern, String.Empty); + return Regex.Replace(text, pattern, string.Empty, RegexOptions.Compiled); } /// diff --git a/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs b/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs index cac53e1f99..5ad9140811 100644 --- a/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs +++ b/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Web.Services; using Umbraco.Core.IO; @@ -8,6 +9,7 @@ namespace Umbraco.Core.Sync /// The client Soap service for making distrubuted cache calls between servers /// [WebServiceBinding(Name = "CacheRefresherSoap", Namespace = "http://umbraco.org/webservices/")] + [Obsolete("Legacy load balancing is obsolete and should be removed")] internal class ServerSyncWebServiceClient : System.Web.Services.Protocols.SoapHttpClientProtocol { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj old mode 100644 new mode 100755 index 49fa197850..f57c14a03e --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -89,7 +89,7 @@ 1.0.0 - 2.1.2 + 2.2.2 4.0.0 @@ -193,8 +193,8 @@ - - + + @@ -202,7 +202,7 @@ - + @@ -312,6 +312,7 @@ + @@ -334,8 +335,11 @@ - + + + + @@ -360,14 +364,17 @@ + + + @@ -375,7 +382,10 @@ - + + + + @@ -411,6 +421,7 @@ + @@ -432,6 +443,8 @@ + + @@ -439,6 +452,7 @@ + @@ -600,9 +614,6 @@ - - - @@ -1263,7 +1274,6 @@ - @@ -1283,6 +1293,7 @@ + @@ -1405,8 +1416,6 @@ - - @@ -1453,7 +1462,6 @@ - diff --git a/src/Umbraco.Examine/BaseValueSetBuilder.cs b/src/Umbraco.Examine/BaseValueSetBuilder.cs new file mode 100644 index 0000000000..22d379d148 --- /dev/null +++ b/src/Umbraco.Examine/BaseValueSetBuilder.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using Examine; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Examine +{ + + /// + public abstract class BaseValueSetBuilder : IValueSetBuilder + where TContent : IContentBase + { + protected bool PublishedValuesOnly { get; } + private readonly PropertyEditorCollection _propertyEditors; + + protected BaseValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) + { + PublishedValuesOnly = publishedValuesOnly; + _propertyEditors = propertyEditors ?? throw new System.ArgumentNullException(nameof(propertyEditors)); + } + + /// + public abstract IEnumerable GetValueSets(params TContent[] content); + + protected void AddPropertyValue(Property property, string culture, string segment, IDictionary> values) + { + var editor = _propertyEditors[property.PropertyType.PropertyEditorAlias]; + if (editor == null) return; + + var indexVals = editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly); + foreach (var keyVal in indexVals) + { + if (keyVal.Key.IsNullOrWhiteSpace()) continue; + + var cultureSuffix = culture == null ? string.Empty : "_" + culture; + + foreach (var val in keyVal.Value) + { + switch (val) + { + //only add the value if its not null or empty (we'll check for string explicitly here too) + case null: + continue; + case string strVal: + { + if (strVal.IsNullOrWhiteSpace()) return; + var key = $"{keyVal.Key}{cultureSuffix}"; + if (values.TryGetValue(key, out var v)) + values[key] = new List(v) { val }.ToArray(); + else + values.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); + } + break; + default: + { + var key = $"{keyVal.Key}{cultureSuffix}"; + if (values.TryGetValue(key, out var v)) + values[key] = new List(v) { val }.ToArray(); + else + values.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); + } + + break; + } + } + } + } + } + +} diff --git a/src/Umbraco.Examine/Config/IndexFieldCollection.cs b/src/Umbraco.Examine/Config/IndexFieldCollection.cs new file mode 100644 index 0000000000..063c157dbe --- /dev/null +++ b/src/Umbraco.Examine/Config/IndexFieldCollection.cs @@ -0,0 +1,47 @@ +using System.Configuration; + +namespace Umbraco.Examine.Config +{ + public sealed class IndexFieldCollection : ConfigurationElementCollection + { + #region Overridden methods to define collection + protected override ConfigurationElement CreateNewElement() + { + return new ConfigIndexField(); + } + protected override object GetElementKey(ConfigurationElement element) + { + ConfigIndexField field = (ConfigIndexField)element; + return field.Name; + } + + public override bool IsReadOnly() + { + return false; + } + #endregion + + /// + /// Adds an index field to the collection + /// + /// + public void Add(ConfigIndexField field) + { + BaseAdd(field, true); + } + + /// + /// Default property for accessing an IndexField definition + /// + /// Field Name + /// + public new ConfigIndexField this[string name] + { + get + { + return (ConfigIndexField)this.BaseGet(name); + } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Examine/Config/IndexSet.cs b/src/Umbraco.Examine/Config/IndexSet.cs index 33b67412f2..65b1cd5aec 100644 --- a/src/Umbraco.Examine/Config/IndexSet.cs +++ b/src/Umbraco.Examine/Config/IndexSet.cs @@ -11,48 +11,6 @@ namespace Umbraco.Examine.Config [ConfigurationProperty("SetName", IsRequired = true, IsKey = true)] public string SetName => (string)this["SetName"]; - private string _indexPath = ""; - - /// - /// The folder path of where the lucene index is stored - /// - /// The index path. - /// - /// This can be set at runtime but will not be persisted to the configuration file - /// - [ConfigurationProperty("IndexPath", IsRequired = true, IsKey = false)] - public string IndexPath - { - get - { - if (string.IsNullOrEmpty(_indexPath)) - _indexPath = (string)this["IndexPath"]; - - return _indexPath; - } - set => _indexPath = value; - } - - /// - /// Returns the DirectoryInfo object for the index path. - /// - /// The index directory. - public DirectoryInfo IndexDirectory - { - get - { - //TODO: Get this out of the index set. We need to use the Indexer's DataService to lookup the folder so it can be unit tested. Probably need DataServices on the searcher then too - - //we need to de-couple the context - if (HttpContext.Current != null) - return new DirectoryInfo(HttpContext.Current.Server.MapPath(this.IndexPath)); - else if (HostingEnvironment.ApplicationID != null) - return new DirectoryInfo(HostingEnvironment.MapPath(this.IndexPath)); - else - return new DirectoryInfo(this.IndexPath); - } - } - /// /// When this property is set, the indexing will only index documents that are descendants of this node. /// diff --git a/src/Umbraco.Examine/Config/IndexSetCollection.cs b/src/Umbraco.Examine/Config/IndexSetCollection.cs index 5ba2382563..bfce8e4cce 100644 --- a/src/Umbraco.Examine/Config/IndexSetCollection.cs +++ b/src/Umbraco.Examine/Config/IndexSetCollection.cs @@ -3,50 +3,6 @@ namespace Umbraco.Examine.Config { - public sealed class IndexFieldCollection : ConfigurationElementCollection - { - #region Overridden methods to define collection - protected override ConfigurationElement CreateNewElement() - { - return new ConfigIndexField(); - } - protected override object GetElementKey(ConfigurationElement element) - { - ConfigIndexField field = (ConfigIndexField)element; - return field.Name; - } - - public override bool IsReadOnly() - { - return false; - } - #endregion - - /// - /// Adds an index field to the collection - /// - /// - public void Add(ConfigIndexField field) - { - BaseAdd(field, true); - } - - /// - /// Default property for accessing an IndexField definition - /// - /// Field Name - /// - public new ConfigIndexField this[string name] - { - get - { - return (ConfigIndexField)this.BaseGet(name); - } - } - - } - - public sealed class IndexSetCollection : ConfigurationElementCollection { #region Overridden methods to define collection diff --git a/src/Umbraco.Examine/ContentIndexPopulator.cs b/src/Umbraco.Examine/ContentIndexPopulator.cs new file mode 100644 index 0000000000..d1255b4d46 --- /dev/null +++ b/src/Umbraco.Examine/ContentIndexPopulator.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Examine; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; + +namespace Umbraco.Examine +{ + + /// + /// Performs the data lookups required to rebuild a content index + /// + public class ContentIndexPopulator : IndexPopulator + { + private readonly IContentService _contentService; + private readonly IValueSetBuilder _contentValueSetBuilder; + + /// + /// This is a static query, it's parameters don't change so store statically + /// + private static IQuery _publishedQuery; + + private readonly bool _publishedValuesOnly; + private readonly int? _parentId; + + /// + /// Default constructor to lookup all content data + /// + /// + /// + /// + public ContentIndexPopulator(IContentService contentService, ISqlContext sqlContext, IContentValueSetBuilder contentValueSetBuilder) + : this(false, null, contentService, sqlContext, contentValueSetBuilder) + { + } + + /// + /// Optional constructor allowing specifying custom query parameters + /// + /// + /// + /// + /// + /// + public ContentIndexPopulator(bool publishedValuesOnly, int? parentId, IContentService contentService, ISqlContext sqlContext, IValueSetBuilder contentValueSetBuilder) + { + 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) + _publishedQuery = sqlContext.Query().Where(x => x.Published); + _publishedValuesOnly = publishedValuesOnly; + _parentId = parentId; + + RegisterIndex(Constants.UmbracoIndexes.InternalIndexName); + RegisterIndex(Constants.UmbracoIndexes.ExternalIndexName); + } + + protected override void PopulateIndexes(IEnumerable indexes) + { + const int pageSize = 10000; + var pageIndex = 0; + + var contentParentId = -1; + if (_parentId.HasValue && _parentId.Value > 0) + { + contentParentId = _parentId.Value; + } + IContent[] content; + + do + { + if (!_publishedValuesOnly) + { + content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _).ToArray(); + } + else + { + //add the published filter + //note: We will filter for published variants in the validator + content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _, + _publishedQuery, Ordering.By("Path", Direction.Ascending)).ToArray(); + } + + if (content.Length > 0) + { + // ReSharper disable once PossibleMultipleEnumeration + foreach (var index in indexes) + index.IndexItems(_contentValueSetBuilder.GetValueSets(content)); + } + + pageIndex++; + } while (content.Length == pageSize); + } + } +} diff --git a/src/Umbraco.Examine/ContentValueSetBuilder.cs b/src/Umbraco.Examine/ContentValueSetBuilder.cs new file mode 100644 index 0000000000..07e5e4565b --- /dev/null +++ b/src/Umbraco.Examine/ContentValueSetBuilder.cs @@ -0,0 +1,106 @@ +using Examine; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; + +namespace Umbraco.Examine +{ + /// + /// Builds s for items + /// + public class ContentValueSetBuilder : BaseValueSetBuilder, IContentValueSetBuilder, IPublishedContentValueSetBuilder + { + private readonly IEnumerable _urlSegmentProviders; + private readonly IUserService _userService; + + public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, + IEnumerable urlSegmentProviders, + IUserService userService, + bool publishedValuesOnly) + : base(propertyEditors, publishedValuesOnly) + { + _urlSegmentProviders = urlSegmentProviders; + _userService = userService; + } + + /// + public override IEnumerable GetValueSets(params IContent[] content) + { + //TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways + // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since + // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` + // references and then each array is an array of `FieldValue[]` and values are assigned accordingly. Not sure if it will make a difference or not. + + foreach (var c in content) + { + var isVariant = c.ContentType.VariesByCulture(); + + var urlValue = c.GetUrlSegment(_urlSegmentProviders); //Always add invariant urlName + var values = new Dictionary> + { + {"icon", c.ContentType.Icon.Yield()}, + {UmbracoExamineIndex.PublishedFieldName, new object[] {c.Published ? 1 : 0}}, //Always add invariant published value + {"id", new object[] {c.Id}}, + {"key", new object[] {c.Key}}, + {"parentID", new object[] {c.Level > 1 ? c.ParentId : -1}}, + {"level", new object[] {c.Level}}, + {"creatorID", new object[] {c.CreatorId}}, + {"sortOrder", new object[] {c.SortOrder}}, + {"createDate", new object[] {c.CreateDate}}, //Always add invariant createDate + {"updateDate", new object[] {c.UpdateDate}}, //Always add invariant updateDate + {"nodeName", PublishedValuesOnly //Always add invariant nodeName + ? c.PublishName.Yield() + : c.Name.Yield()}, + {"urlName", urlValue.Yield()}, //Always add invariant urlName + {"path", c.Path.Yield()}, + {"nodeType", new object[] {c.ContentType.Id}}, + {"creatorName", (c.GetCreatorProfile(_userService)?.Name ?? "??").Yield() }, + {"writerName",(c.GetWriterProfile(_userService)?.Name ?? "??").Yield() }, + {"writerID", new object[] {c.WriterId}}, + {"template", new object[] {c.Template?.Id ?? 0}}, + {UmbracoContentIndex.VariesByCultureFieldName, new object[] {0}}, + }; + + if (isVariant) + { + values[UmbracoContentIndex.VariesByCultureFieldName] = new object[] { 1 }; + + foreach (var culture in c.AvailableCultures) + { + var variantUrl = c.GetUrlSegment(_urlSegmentProviders, culture); + var lowerCulture = culture.ToLowerInvariant(); + values[$"urlName_{lowerCulture}"] = variantUrl.Yield(); + values[$"nodeName_{lowerCulture}"] = PublishedValuesOnly + ? c.GetPublishName(culture).Yield() + : c.GetCultureName(culture).Yield(); + values[$"{UmbracoExamineIndex.PublishedFieldName}_{lowerCulture}"] = (c.IsCulturePublished(culture) ? 1 : 0).Yield(); + values[$"updateDate_{lowerCulture}"] = PublishedValuesOnly + ? c.GetPublishDate(culture).Yield() + : c.GetUpdateDate(culture).Yield(); + } + } + + foreach (var property in c.Properties) + { + if (!property.PropertyType.VariesByCulture()) + { + AddPropertyValue(property, null, null, values); + } + else + { + foreach (var culture in c.AvailableCultures) + AddPropertyValue(property, culture.ToLowerInvariant(), null, values); + } + } + + var vs = new ValueSet(c.Id.ToInvariantString(), IndexTypes.Content, c.ContentType.Alias, values); + + yield return vs; + } + } + } + +} diff --git a/src/Umbraco.Examine/ContentValueSetValidator.cs b/src/Umbraco.Examine/ContentValueSetValidator.cs new file mode 100644 index 0000000000..d4f6ceb15f --- /dev/null +++ b/src/Umbraco.Examine/ContentValueSetValidator.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Examine; +using Examine.LuceneEngine.Providers; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Examine +{ + /// + /// Used to validate a ValueSet for content/media - based on permissions, parent id, etc.... + /// + public class ContentValueSetValidator : ValueSetValidator, IContentValueSetValidator + { + private readonly IPublicAccessService _publicAccessService; + + private const string PathKey = "path"; + private static readonly IEnumerable ValidCategories = new[] {IndexTypes.Content, IndexTypes.Media}; + protected override IEnumerable ValidIndexCategories => ValidCategories; + + public bool PublishedValuesOnly { get; } + public bool SupportProtectedContent { get; } + public int? ParentId { get; } + + public bool ValidatePath(string path, string category) + { + //check if this document is a descendent of the parent + if (ParentId.HasValue && ParentId.Value > 0) + { + // we cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there + // because we need to remove anything that doesn't pass by parent Id in the cases that umbraco data is moved to an illegal parent. + if (!path.Contains(string.Concat(",", ParentId.Value, ","))) + return false; + } + + return true; + } + + public bool ValidateRecycleBin(string path, string category) + { + var recycleBinId = category == IndexTypes.Content ? Constants.System.RecycleBinContent : Constants.System.RecycleBinMedia; + + //check for recycle bin + if (PublishedValuesOnly) + { + if (path.Contains(string.Concat(",", recycleBinId, ","))) + return false; + } + return true; + } + + public bool ValidateProtectedContent(string path, string category) + { + if (category == IndexTypes.Content + && !SupportProtectedContent + //if the service is null we can't look this up so we'll return false + && (_publicAccessService == null || _publicAccessService.IsProtected(path))) + { + return false; + } + + return true; + } + + public ContentValueSetValidator(bool publishedValuesOnly, int? parentId = null, + IEnumerable includeItemTypes = null, IEnumerable excludeItemTypes = null) + : this(publishedValuesOnly, true, null, parentId, includeItemTypes, excludeItemTypes) + { + } + + public ContentValueSetValidator(bool publishedValuesOnly, bool supportProtectedContent, + IPublicAccessService publicAccessService, int? parentId = null, + IEnumerable includeItemTypes = null, IEnumerable excludeItemTypes = null) + : base(includeItemTypes, excludeItemTypes, null, null) + { + PublishedValuesOnly = publishedValuesOnly; + SupportProtectedContent = supportProtectedContent; + ParentId = parentId; + _publicAccessService = publicAccessService; + } + + public override ValueSetValidationResult Validate(ValueSet valueSet) + { + var baseValidate = base.Validate(valueSet); + if (baseValidate == ValueSetValidationResult.Failed) + return ValueSetValidationResult.Failed; + + var isFiltered = baseValidate == ValueSetValidationResult.Filtered; + + //check for published content + if (valueSet.Category == IndexTypes.Content && PublishedValuesOnly) + { + if (!valueSet.Values.TryGetValue(UmbracoExamineIndex.PublishedFieldName, out var published)) + return ValueSetValidationResult.Failed; + + if (!published[0].Equals(1)) + return ValueSetValidationResult.Failed; + + //deal with variants, if there are unpublished variants than we need to remove them from the value set + if (valueSet.Values.TryGetValue(UmbracoContentIndex.VariesByCultureFieldName, out var variesByCulture) + && variesByCulture.Count > 0 && variesByCulture[0].Equals(1)) + { + //so this valueset is for a content that varies by culture, now check for non-published cultures and remove those values + foreach(var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineIndex.PublishedFieldName}_")).ToList()) + { + if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals(1)) + { + //this culture is not published, so remove all of these culture values + var cultureSuffix = publishField.Key.Substring(publishField.Key.LastIndexOf('_')); + foreach (var cultureField in valueSet.Values.Where(x => x.Key.InvariantEndsWith(cultureSuffix)).ToList()) + { + valueSet.Values.Remove(cultureField.Key); + isFiltered = true; + } + } + } + } + } + + //must have a 'path' + if (!valueSet.Values.TryGetValue(PathKey, out var pathValues)) return ValueSetValidationResult.Failed; + if (pathValues.Count == 0) return ValueSetValidationResult.Failed; + if (pathValues[0] == null) return ValueSetValidationResult.Failed; + if (pathValues[0].ToString().IsNullOrWhiteSpace()) return ValueSetValidationResult.Failed; + var path = pathValues[0].ToString(); + + // We need to validate the path of the content based on ParentId, protected content and recycle bin rules. + // We cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there + // because we need to remove anything that doesn't pass by protected content in the cases that umbraco data is moved to an illegal parent. + if (!ValidatePath(path, valueSet.Category) + || !ValidateRecycleBin(path, valueSet.Category) + || !ValidateProtectedContent(path, valueSet.Category)) + return ValueSetValidationResult.Filtered; + + return isFiltered ? ValueSetValidationResult.Filtered: ValueSetValidationResult.Valid; + } + } +} diff --git a/src/Umbraco.Examine/ExamineExtensions.cs b/src/Umbraco.Examine/ExamineExtensions.cs new file mode 100644 index 0000000000..3681979267 --- /dev/null +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -0,0 +1,72 @@ +using System; +using Examine.LuceneEngine.Providers; +using Lucene.Net.Index; +using Lucene.Net.Search; +using Lucene.Net.Store; + +namespace Umbraco.Examine +{ + /// + /// Extension methods for the LuceneIndex + /// + internal static class ExamineExtensions + { + /// + /// Checks if the index can be read/opened + /// + /// + /// The exception returned if there was an error + /// + public static bool IsHealthy(this LuceneIndex indexer, out Exception ex) + { + try + { + using (indexer.GetIndexWriter().GetReader()) + { + ex = null; + return true; + } + } + catch (Exception e) + { + ex = e; + return false; + } + } + + /// + /// Return the number of indexed documents in Lucene + /// + /// + /// + public static int GetIndexDocumentCount(this LuceneIndex indexer) + { + if (!((indexer.GetSearcher() as LuceneSearcher)?.GetLuceneSearcher() is IndexSearcher searcher)) + return 0; + + using (searcher) + using (var reader = searcher.IndexReader) + { + return reader.NumDocs(); + } + } + + /// + /// Return the total number of fields in the index + /// + /// + /// + public static int GetIndexFieldCount(this LuceneIndex indexer) + { + if (!((indexer.GetSearcher() as LuceneSearcher)?.GetLuceneSearcher() is IndexSearcher searcher)) + return 0; + + using (searcher) + using (var reader = searcher.IndexReader) + { + return reader.GetFieldNames(IndexReader.FieldOption.ALL).Count; + } + } + + } +} diff --git a/src/Umbraco.Examine/IContentValueSetBuilder.cs b/src/Umbraco.Examine/IContentValueSetBuilder.cs new file mode 100644 index 0000000000..fed706d592 --- /dev/null +++ b/src/Umbraco.Examine/IContentValueSetBuilder.cs @@ -0,0 +1,13 @@ +using Examine; +using Umbraco.Core.Models; + +namespace Umbraco.Examine +{ + /// + /// + /// Marker interface for a builder for supporting unpublished content + /// + public interface IContentValueSetBuilder : IValueSetBuilder + { + } +} diff --git a/src/Umbraco.Examine/IContentValueSetValidator.cs b/src/Umbraco.Examine/IContentValueSetValidator.cs new file mode 100644 index 0000000000..fa85a0d32b --- /dev/null +++ b/src/Umbraco.Examine/IContentValueSetValidator.cs @@ -0,0 +1,31 @@ +using Examine; + +namespace Umbraco.Examine +{ + /// + /// An extended for content indexes + /// + public interface IContentValueSetValidator : IValueSetValidator + { + /// + /// When set to true the index will only retain published values + /// + /// + /// Any non-published values will not be put or kept in the index: + /// * Deleted, Trashed, non-published Content items + /// * non-published Variants + /// + bool PublishedValuesOnly { get; } + + /// + /// If true, protected content will be indexed otherwise it will not be put or kept in the index + /// + bool SupportProtectedContent { get; } + + int? ParentId { get; } + + bool ValidatePath(string path, string category); + bool ValidateRecycleBin(string path, string category); + bool ValidateProtectedContent(string path, string category); + } +} diff --git a/src/Umbraco.Examine/IIndexDiagnostics.cs b/src/Umbraco.Examine/IIndexDiagnostics.cs new file mode 100644 index 0000000000..04ca4a6ab9 --- /dev/null +++ b/src/Umbraco.Examine/IIndexDiagnostics.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using Examine; +using Umbraco.Core; + +namespace Umbraco.Examine +{ + + + /// + /// Exposes diagnostic information about an index + /// + public interface IIndexDiagnostics + { + /// + /// The number of documents in the index + /// + int DocumentCount { get; } + + /// + /// The number of fields in the index + /// + int FieldCount { get; } + + /// + /// If the index can be open/read + /// + /// + /// A successful attempt if it is healthy, else a failed attempt with a message if unhealthy + /// + Attempt IsHealthy(); + + /// + /// A key/value collection of diagnostic properties for the index + /// + /// + /// Used to display in the UI + /// + IReadOnlyDictionary Metadata { get; } + } +} diff --git a/src/Umbraco.Examine/IIndexPopulator.cs b/src/Umbraco.Examine/IIndexPopulator.cs new file mode 100644 index 0000000000..aeb3514578 --- /dev/null +++ b/src/Umbraco.Examine/IIndexPopulator.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Examine; + +namespace Umbraco.Examine +{ + + + public interface IIndexPopulator + { + bool IsRegistered(string indexName); + void RegisterIndex(string indexName); + + /// + /// Populate indexers + /// + /// + void Populate(params IIndex[] indexes); + } + +} diff --git a/src/Umbraco.Examine/IPublishedContentValueSetBuilder.cs b/src/Umbraco.Examine/IPublishedContentValueSetBuilder.cs new file mode 100644 index 0000000000..c337a7a1e6 --- /dev/null +++ b/src/Umbraco.Examine/IPublishedContentValueSetBuilder.cs @@ -0,0 +1,12 @@ +using Examine; +using Umbraco.Core.Models; + +namespace Umbraco.Examine +{ + /// + /// Marker interface for a builder for only published content + /// + public interface IPublishedContentValueSetBuilder : IValueSetBuilder + { + } +} \ No newline at end of file diff --git a/src/Umbraco.Examine/IUmbracoIndexer.cs b/src/Umbraco.Examine/IUmbracoIndexer.cs new file mode 100644 index 0000000000..00f772b1e2 --- /dev/null +++ b/src/Umbraco.Examine/IUmbracoIndexer.cs @@ -0,0 +1,25 @@ +using Examine; + +namespace Umbraco.Examine +{ + /// + /// A Marker interface for defining an Umbraco indexer + /// + public interface IUmbracoIndexer : IIndex + { + /// + /// When set to true Umbraco will keep the index in sync with Umbraco data automatically + /// + bool EnableDefaultEventHandler { get; } + + /// + /// When set to true the index will only retain published values + /// + /// + /// Any non-published values will not be put or kept in the index: + /// * Deleted, Trashed, non-published Content items + /// * non-published Variants + /// + bool PublishedValuesOnly { get; } + } +} diff --git a/src/Umbraco.Examine/IValueSetBuilder.cs b/src/Umbraco.Examine/IValueSetBuilder.cs new file mode 100644 index 0000000000..89aa907926 --- /dev/null +++ b/src/Umbraco.Examine/IValueSetBuilder.cs @@ -0,0 +1,22 @@ +using Examine; +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Examine +{ + /// + /// Creates a collection of to be indexed based on a collection of + /// + /// + public interface IValueSetBuilder + where TContent : IContentBase + { + /// + /// Creates a collection of to be indexed based on a collection of + /// + /// + /// + IEnumerable GetValueSets(params TContent[] content); + } + +} diff --git a/src/Umbraco.Examine/IndexPopulator.cs b/src/Umbraco.Examine/IndexPopulator.cs new file mode 100644 index 0000000000..5f701312ea --- /dev/null +++ b/src/Umbraco.Examine/IndexPopulator.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using Examine; +using Umbraco.Core.Collections; + +namespace Umbraco.Examine +{ + public abstract class IndexPopulator : IIndexPopulator + { + private readonly ConcurrentHashSet _registeredIndexes = new ConcurrentHashSet(); + + public bool IsRegistered(string indexName) + { + return _registeredIndexes.Contains(indexName); + } + + /// + /// Registers an index for this populator + /// + /// + public void RegisterIndex(string indexName) + { + _registeredIndexes.Add(indexName); + } + + public void Populate(params IIndex[] indexes) + { + PopulateIndexes(indexes.Where(x => IsRegistered(x.Name))); + } + + protected abstract void PopulateIndexes(IEnumerable indexes); + } +} diff --git a/src/Umbraco.Examine/IndexRebuilder.cs b/src/Umbraco.Examine/IndexRebuilder.cs new file mode 100644 index 0000000000..39b29c5a2b --- /dev/null +++ b/src/Umbraco.Examine/IndexRebuilder.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Examine; + +namespace Umbraco.Examine +{ + /// + /// Utility to rebuild all indexes ensuring minimal data queries + /// + public class IndexRebuilder + { + private readonly IEnumerable _populators; + public IExamineManager ExamineManager { get; } + + public IndexRebuilder(IExamineManager examineManager, IEnumerable populators) + { + _populators = populators; + ExamineManager = examineManager; + } + + public bool CanRebuild(string indexName) + { + return _populators.Any(x => x.IsRegistered(indexName)); + } + + public void RebuildIndex(string indexName) + { + if (!ExamineManager.TryGetIndex(indexName, out var index)) + throw new InvalidOperationException($"No index found with name {indexName}"); + index.CreateIndex(); // clear the index + foreach (var populator in _populators) + { + populator.Populate(index); + } + } + + public void RebuildIndexes(bool onlyEmptyIndexes) + { + var indexes = (onlyEmptyIndexes + ? ExamineManager.Indexes.Where(x => !x.IndexExists()) + : ExamineManager.Indexes).ToArray(); + + foreach (var index in indexes) + { + index.CreateIndex(); // clear the index + } + + //run the populators in parallel against all indexes + Parallel.ForEach(_populators, populator => populator.Populate(indexes)); + } + + } +} diff --git a/src/Umbraco.Examine/IndexTypes.cs b/src/Umbraco.Examine/IndexTypes.cs index fab72eca25..3fa00e234c 100644 --- a/src/Umbraco.Examine/IndexTypes.cs +++ b/src/Umbraco.Examine/IndexTypes.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Lucene.Net.Store; - -namespace Umbraco.Examine +namespace Umbraco.Examine { /// /// The index types stored in the Lucene Index diff --git a/src/Umbraco.Examine/MediaIndexPopulator.cs b/src/Umbraco.Examine/MediaIndexPopulator.cs new file mode 100644 index 0000000000..39a0b3f7c8 --- /dev/null +++ b/src/Umbraco.Examine/MediaIndexPopulator.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Linq; +using Examine; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Examine +{ + /// + /// Performs the data lookups required to rebuild a media index + /// + public class MediaIndexPopulator : IndexPopulator + { + private readonly int? _parentId; + private readonly IMediaService _mediaService; + private readonly IValueSetBuilder _mediaValueSetBuilder; + + /// + /// Default constructor to lookup all content data + /// + /// + /// + public MediaIndexPopulator(IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + : this(null, mediaService, mediaValueSetBuilder) + { + } + + /// + /// Optional constructor allowing specifying custom query parameters + /// + /// + /// + /// + public MediaIndexPopulator(int? parentId, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + { + _parentId = parentId; + _mediaService = mediaService; + _mediaValueSetBuilder = mediaValueSetBuilder; + + RegisterIndex(Constants.UmbracoIndexes.InternalIndexName); + RegisterIndex(Constants.UmbracoIndexes.ExternalIndexName); + } + + protected override void PopulateIndexes(IEnumerable indexes) + { + const int pageSize = 10000; + var pageIndex = 0; + + var mediaParentId = -1; + + if (_parentId.HasValue && _parentId.Value > 0) + { + mediaParentId = _parentId.Value; + } + + IMedia[] media; + + do + { + media = _mediaService.GetPagedDescendants(mediaParentId, pageIndex, pageSize, out var total).ToArray(); + + if (media.Length > 0) + { + // ReSharper disable once PossibleMultipleEnumeration + foreach (var index in indexes) + index.IndexItems(_mediaValueSetBuilder.GetValueSets(media)); + } + + pageIndex++; + } while (media.Length == pageSize); + } + + } +} diff --git a/src/Umbraco.Examine/MediaValueSetBuilder.cs b/src/Umbraco.Examine/MediaValueSetBuilder.cs new file mode 100644 index 0000000000..8df51570c1 --- /dev/null +++ b/src/Umbraco.Examine/MediaValueSetBuilder.cs @@ -0,0 +1,61 @@ +using Examine; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; + +namespace Umbraco.Examine +{ + public class MediaValueSetBuilder : BaseValueSetBuilder + { + private readonly IEnumerable _urlSegmentProviders; + private readonly IUserService _userService; + + public MediaValueSetBuilder(PropertyEditorCollection propertyEditors, + IEnumerable urlSegmentProviders, + IUserService userService) + : base(propertyEditors, false) + { + _urlSegmentProviders = urlSegmentProviders; + _userService = userService; + } + + /// + public override IEnumerable GetValueSets(params IMedia[] media) + { + foreach (var m in media) + { + var urlValue = m.GetUrlSegment(_urlSegmentProviders); + var values = new Dictionary> + { + {"icon", m.ContentType.Icon.Yield()}, + {"id", new object[] {m.Id}}, + {"key", new object[] {m.Key}}, + {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, + {"level", new object[] {m.Level}}, + {"creatorID", new object[] {m.CreatorId}}, + {"sortOrder", new object[] {m.SortOrder}}, + {"createDate", new object[] {m.CreateDate}}, + {"updateDate", new object[] {m.UpdateDate}}, + {"nodeName", m.Name.Yield()}, + {"urlName", urlValue.Yield()}, + {"path", m.Path.Yield()}, + {"nodeType", new object[] {m.ContentType.Id}}, + {"creatorName", m.GetCreatorProfile(_userService).Name.Yield()} + }; + + foreach (var property in m.Properties) + { + AddPropertyValue(property, null, null, values); + } + + var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Media, m.ContentType.Alias, values); + + yield return vs; + } + } + } + +} diff --git a/src/Umbraco.Examine/MemberIndexPopulator.cs b/src/Umbraco.Examine/MemberIndexPopulator.cs new file mode 100644 index 0000000000..cf04861372 --- /dev/null +++ b/src/Umbraco.Examine/MemberIndexPopulator.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using Examine; +using Lucene.Net.Util; +using Umbraco.Core.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Examine +{ + public class MemberIndexPopulator : IndexPopulator + { + private readonly IMemberService _memberService; + private readonly IValueSetBuilder _valueSetBuilder; + + public MemberIndexPopulator(IMemberService memberService, IValueSetBuilder valueSetBuilder) + { + _memberService = memberService; + _valueSetBuilder = valueSetBuilder; + + RegisterIndex(Core.Constants.UmbracoIndexes.MembersIndexName); + } + protected override void PopulateIndexes(IEnumerable indexes) + { + const int pageSize = 1000; + var pageIndex = 0; + + IMember[] members; + + //no node types specified, do all members + do + { + members = _memberService.GetAll(pageIndex, pageSize, out _).ToArray(); + + if (members.Length > 0) + { + // ReSharper disable once PossibleMultipleEnumeration + foreach (var index in indexes) + index.IndexItems(_valueSetBuilder.GetValueSets(members)); + } + + pageIndex++; + } while (members.Length == pageSize); + } + } +} diff --git a/src/Umbraco.Examine/MemberValueSetBuilder.cs b/src/Umbraco.Examine/MemberValueSetBuilder.cs new file mode 100644 index 0000000000..9864aba18d --- /dev/null +++ b/src/Umbraco.Examine/MemberValueSetBuilder.cs @@ -0,0 +1,52 @@ +using Examine; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Examine +{ + + public class MemberValueSetBuilder : BaseValueSetBuilder + { + public MemberValueSetBuilder(PropertyEditorCollection propertyEditors) + : base(propertyEditors, false) + { + } + + /// + public override IEnumerable GetValueSets(params IMember[] members) + { + foreach (var m in members) + { + var values = new Dictionary> + { + {"icon", m.ContentType.Icon.Yield()}, + {"id", new object[] {m.Id}}, + {"key", new object[] {m.Key}}, + {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, + {"level", new object[] {m.Level}}, + {"creatorID", new object[] {m.CreatorId}}, + {"sortOrder", new object[] {m.SortOrder}}, + {"createDate", new object[] {m.CreateDate}}, + {"updateDate", new object[] {m.UpdateDate}}, + {"nodeName", m.Name.Yield()}, + {"path", m.Path.Yield()}, + {"nodeType", new object[] {m.ContentType.Id}}, + {"loginName", m.Username.Yield()}, + {"email", m.Email.Yield()}, + }; + + foreach (var property in m.Properties) + { + AddPropertyValue(property, null, null, values); + } + + var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Member, m.ContentType.Alias, values); + + yield return vs; + } + } + } + +} diff --git a/src/Umbraco.Examine/MemberValueSetValidator.cs b/src/Umbraco.Examine/MemberValueSetValidator.cs new file mode 100644 index 0000000000..71de6001ce --- /dev/null +++ b/src/Umbraco.Examine/MemberValueSetValidator.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using Examine; + +namespace Umbraco.Examine +{ + public class MemberValueSetValidator : ValueSetValidator + { + public MemberValueSetValidator() : base(null, null, DefaultMemberIndexFields, null) + { + } + + public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes) + : base(includeItemTypes, excludeItemTypes, DefaultMemberIndexFields, null) + { + } + + public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes, IEnumerable includeFields, IEnumerable excludeFields) + : base(includeItemTypes, excludeItemTypes, includeFields, excludeFields) + { + } + + /// + /// By default these are the member fields we index + /// + public static readonly string[] DefaultMemberIndexFields = { "id", "nodeName", "updateDate", "loginName", "email" }; + + private static readonly IEnumerable ValidCategories = new[] { IndexTypes.Member }; + protected override IEnumerable ValidIndexCategories => ValidCategories; + + } +} diff --git a/src/Umbraco.Examine/PublishedContentIndexPopulator.cs b/src/Umbraco.Examine/PublishedContentIndexPopulator.cs new file mode 100644 index 0000000000..143e2db630 --- /dev/null +++ b/src/Umbraco.Examine/PublishedContentIndexPopulator.cs @@ -0,0 +1,22 @@ +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Persistence; + +namespace Umbraco.Examine +{ + /// + /// Performs the data lookups required to rebuild a content index containing only published content + /// + /// + /// The published (external) index will still rebuild just fine using the default which is what + /// is used when rebuilding all indexes, but this will be used when the single index is rebuilt and will go a little bit faster + /// since the data query is more specific. + /// + public class PublishedContentIndexPopulator : ContentIndexPopulator + { + public PublishedContentIndexPopulator(IContentService contentService, ISqlContext sqlContext, IPublishedContentValueSetBuilder contentValueSetBuilder) : + base(true, null, contentService, sqlContext, contentValueSetBuilder) + { + } + } +} diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 678ae124d2..0aedf6e754 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -48,30 +48,51 @@ - + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - + Properties\SolutionInfo.cs + diff --git a/src/Umbraco.Examine/UmbracoContentIndex.cs b/src/Umbraco.Examine/UmbracoContentIndex.cs new file mode 100644 index 0000000000..910941ea7b --- /dev/null +++ b/src/Umbraco.Examine/UmbracoContentIndex.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using Examine; +using Umbraco.Core; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Examine.LuceneEngine.Indexing; +using Examine.LuceneEngine.Providers; +using Lucene.Net.Analysis; +using Lucene.Net.Store; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Examine.Config; +using Examine.LuceneEngine; + +namespace Umbraco.Examine +{ + /// + /// An indexer for Umbraco content and media + /// + public class UmbracoContentIndex : UmbracoExamineIndex + { + public const string VariesByCultureFieldName = SpecialFieldPrefix + "VariesByCulture"; + protected ILocalizationService LanguageService { get; } + + #region Constructors + + /// + /// Constructor for configuration providers + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public UmbracoContentIndex() + { + LanguageService = Current.Services.LocalizationService; + + //note: The validator for this config based indexer is set in the Initialize method + } + + /// + /// Create an index at runtime + /// + /// + /// + /// + /// + /// + /// + /// + /// + public UmbracoContentIndex( + string name, + IEnumerable fieldDefinitions, + Directory luceneDirectory, + Analyzer defaultAnalyzer, + ProfilingLogger profilingLogger, + ILocalizationService languageService, + IContentValueSetValidator validator, + IReadOnlyDictionary> indexValueTypes = null) + : base(name, fieldDefinitions, luceneDirectory, defaultAnalyzer, profilingLogger, validator, indexValueTypes) + { + if (validator == null) throw new ArgumentNullException(nameof(validator)); + LanguageService = languageService ?? throw new ArgumentNullException(nameof(languageService)); + + if (validator is IContentValueSetValidator contentValueSetValidator) + PublishedValuesOnly = contentValueSetValidator.PublishedValuesOnly; + } + + #endregion + + #region Initialize + + /// + /// Set up all properties for the indexer based on configuration information specified. This will ensure that + /// all of the folders required by the indexer are created and exist. This will also create an instruction + /// file declaring the computer name that is part taking in the indexing. This file will then be used to + /// determine the master indexer machine in a load balanced environment (if one exists). + /// + /// The friendly name of the provider. + /// A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider. + /// + /// The name of the provider is null. + /// + /// + /// The name of the provider has a length of zero. + /// + /// + /// An attempt is made to call on a provider after the provider has already been initialized. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override void Initialize(string name, NameValueCollection config) + { + base.Initialize(name, config); + + var supportUnpublished = false; + var supportProtected = false; + + //check if there's a flag specifying to support unpublished content, + //if not, set to false; + if (config["supportUnpublished"] != null) + bool.TryParse(config["supportUnpublished"], out supportUnpublished); + + //check if there's a flag specifying to support protected content, + //if not, set to false; + if (config["supportProtected"] != null) + bool.TryParse(config["supportProtected"], out supportProtected); + + + //now we need to build up the indexer options so we can create our validator + int? parentId = null; + if (IndexSetName.IsNullOrWhiteSpace() == false) + { + var indexSet = IndexSets.Instance.Sets[IndexSetName]; + parentId = indexSet.IndexParentId; + } + + ValueSetValidator = new ContentValueSetValidator( + supportUnpublished, supportProtected, + //Using a singleton here, we can't inject this when using config based providers and we don't use this + //anywhere else in this class + Current.Services.PublicAccessService, + parentId, + ConfigIndexCriteria.IncludeItemTypes, ConfigIndexCriteria.ExcludeItemTypes); + + PublishedValuesOnly = supportUnpublished; + } + + #endregion + + /// + /// Special check for invalid paths + /// + /// + /// + protected override void PerformIndexItems(IEnumerable values, Action onComplete) + { + //We don't want to re-enumerate this list, but we need to split it into 2x enumerables: invalid and valid items. + // The Invalid items will be deleted, these are items that have invalid paths (i.e. moved to the recycle bin, etc...) + // Then we'll index the Value group all together. + // We return 0 or 1 here so we can order the results and do the invalid first and then the valid. + var invalidOrValid = values.GroupBy(v => + { + if (!v.Values.TryGetValue("path", out var paths) || paths.Count <= 0 || paths[0] == null) + return 0; + + //we know this is an IContentValueSetValidator + var validator = (IContentValueSetValidator)ValueSetValidator; + var path = paths[0].ToString(); + + return (!validator.ValidatePath(path, v.Category) + || !validator.ValidateRecycleBin(path, v.Category) + || !validator.ValidateProtectedContent(path, v.Category)) + ? 0 + : 1; + }); + + var hasDeletes = false; + var hasUpdates = false; + foreach (var group in invalidOrValid.OrderBy(x => x.Key)) + { + if (group.Key == 0) + { + hasDeletes = true; + //these are the invalid items so we'll delete them + //since the path is not valid we need to delete this item in case it exists in the index already and has now + //been moved to an invalid parent. + foreach (var i in group) + base.PerformDeleteFromIndex(i.Id, args => { /*noop*/ }); + } + else + { + hasUpdates = true; + //these are the valid ones, so just index them all at once + base.PerformIndexItems(group, onComplete); + } + } + + if (hasDeletes && !hasUpdates || !hasDeletes && !hasUpdates) + { + //we need to manually call the completed method + onComplete(new IndexOperationEventArgs(this, 0)); + } + } + + /// + /// + /// Deletes a node from the index. + /// + /// + /// When a content node is deleted, we also need to delete it's children from the index so we need to perform a + /// custom Lucene search to find all decendents and create Delete item queues for them too. + /// + /// ID of the node to delete + /// + protected override void PerformDeleteFromIndex(string nodeId, Action onComplete) + { + //find all descendants based on path + var descendantPath = $@"\-1\,*{nodeId}\,*"; + var rawQuery = $"{IndexPathFieldName}:{descendantPath}"; + var searcher = GetSearcher(); + var c = searcher.CreateCriteria(); + var filtered = c.RawQuery(rawQuery); + var results = searcher.Search(filtered); + + ProfilingLogger.Logger.Debug(GetType(), "DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); + + //need to queue a delete item for each one found + foreach (var r in results) + { + QueueIndexOperation(new IndexOperation(new ValueSet(r.Id), IndexOperationType.Delete)); + } + + base.PerformDeleteFromIndex(nodeId, onComplete); + } + + /// + /// Overridden to ensure that the variant system fields have the right value types + /// + /// + /// + /// + protected override FieldValueTypeCollection CreateFieldValueTypes(IReadOnlyDictionary> indexValueTypesFactory = null) + { + //fixme: languages are dynamic so although this will work on startup it wont work when languages are edited + foreach(var lang in LanguageService.GetAllLanguages()) + { + foreach (var field in UmbracoIndexFieldDefinitions) + { + var def = new FieldDefinition($"{field.Name}_{lang.IsoCode.ToLowerInvariant()}", field.Type); + FieldDefinitionCollection.TryAdd(def.Name, def); + } + } + + return base.CreateFieldValueTypes(indexValueTypesFactory); + } + + } +} diff --git a/src/Umbraco.Examine/UmbracoContentIndexer.cs b/src/Umbraco.Examine/UmbracoContentIndexer.cs deleted file mode 100644 index fab9f226a4..0000000000 --- a/src/Umbraco.Examine/UmbracoContentIndexer.cs +++ /dev/null @@ -1,420 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Linq; -using Examine; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Examine.LuceneEngine.Indexing; -using Examine.LuceneEngine.Providers; -using Lucene.Net.Analysis; -using Lucene.Net.Store; -using Umbraco.Core.Composing; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Scoping; -using Umbraco.Examine.Config; -using IContentService = Umbraco.Core.Services.IContentService; -using IMediaService = Umbraco.Core.Services.IMediaService; - - -namespace Umbraco.Examine -{ - /// - /// An indexer for Umbraco content and media - /// - public class UmbracoContentIndexer : UmbracoExamineIndexer - { - protected IContentService ContentService { get; } - protected IMediaService MediaService { get; } - protected IUserService UserService { get; } - - private readonly IEnumerable _urlSegmentProviders; - private int? _parentId; - - #region Constructors - - /// - /// Constructor for configuration providers - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public UmbracoContentIndexer() - { - ContentService = Current.Services.ContentService; - MediaService = Current.Services.MediaService; - UserService = Current.Services.UserService; - - _urlSegmentProviders = Current.UrlSegmentProviders; - - InitializeQueries(Current.SqlContext); - } - - /// - /// Create an index at runtime - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public UmbracoContentIndexer( - string name, - IEnumerable fieldDefinitions, - Directory luceneDirectory, - Analyzer defaultAnalyzer, - ProfilingLogger profilingLogger, - IContentService contentService, - IMediaService mediaService, - IUserService userService, - ISqlContext sqlContext, - IEnumerable urlSegmentProviders, - IValueSetValidator validator, - UmbracoContentIndexerOptions options, - IReadOnlyDictionary> indexValueTypes = null) - : base(name, fieldDefinitions, luceneDirectory, defaultAnalyzer, profilingLogger, validator, indexValueTypes) - { - if (validator == null) throw new ArgumentNullException(nameof(validator)); - if (options == null) throw new ArgumentNullException(nameof(options)); - - SupportProtectedContent = options.SupportProtectedContent; - SupportUnpublishedContent = options.SupportUnpublishedContent; - ParentId = options.ParentId; - - ContentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); - MediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); - UserService = userService ?? throw new ArgumentNullException(nameof(userService)); - _urlSegmentProviders = urlSegmentProviders ?? throw new ArgumentNullException(nameof(urlSegmentProviders)); - - InitializeQueries(sqlContext); - } - - private void InitializeQueries(ISqlContext sqlContext) - { - if (sqlContext == null) throw new ArgumentNullException(nameof(sqlContext)); - if (_publishedQuery == null) - _publishedQuery = sqlContext.Query().Where(x => x.Published); - } - - #endregion - - #region Initialize - - /// - /// Set up all properties for the indexer based on configuration information specified. This will ensure that - /// all of the folders required by the indexer are created and exist. This will also create an instruction - /// file declaring the computer name that is part taking in the indexing. This file will then be used to - /// determine the master indexer machine in a load balanced environment (if one exists). - /// - /// The friendly name of the provider. - /// A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider. - /// - /// The name of the provider is null. - /// - /// - /// The name of the provider has a length of zero. - /// - /// - /// An attempt is made to call on a provider after the provider has already been initialized. - /// - - public override void Initialize(string name, NameValueCollection config) - { - //check if there's a flag specifying to support unpublished content, - //if not, set to false; - if (config["supportUnpublished"] != null && bool.TryParse(config["supportUnpublished"], out var supportUnpublished)) - SupportUnpublishedContent = supportUnpublished; - else - SupportUnpublishedContent = false; - - - //check if there's a flag specifying to support protected content, - //if not, set to false; - if (config["supportProtected"] != null && bool.TryParse(config["supportProtected"], out var supportProtected)) - SupportProtectedContent = supportProtected; - else - SupportProtectedContent = false; - - base.Initialize(name, config); - - //now we need to build up the indexer options so we can create our validator - int? parentId = null; - if (IndexSetName.IsNullOrWhiteSpace() == false) - { - var indexSet = IndexSets.Instance.Sets[IndexSetName]; - parentId = indexSet.IndexParentId; - } - ValueSetValidator = new UmbracoContentValueSetValidator( - new UmbracoContentIndexerOptions(SupportUnpublishedContent, SupportProtectedContent, parentId), - //Using a singleton here, we can't inject this when using config based providers and we don't use this - //anywhere else in this class - Current.Services.PublicAccessService); - } - - #endregion - - #region Properties - - /// - /// By default this is false, if set to true then the indexer will include indexing content that is flagged as publicly protected. - /// This property is ignored if SupportUnpublishedContent is set to true. - /// - public bool SupportProtectedContent { get; protected set; } - - /// - /// Determines if the manager will call the indexing methods when content is saved or deleted as - /// opposed to cache being updated. - /// - public bool SupportUnpublishedContent { get; protected set; } - - /// - /// If set this will filter the content items allowed to be indexed - /// - public int? ParentId - { - get => _parentId ?? ConfigIndexCriteria?.ParentNodeId; - protected set => _parentId = value; - } - - protected override IEnumerable SupportedTypes => new[] {IndexTypes.Content, IndexTypes.Media}; - - #endregion - - #region Public methods - - /// - /// Deletes a node from the index. - /// - /// - /// When a content node is deleted, we also need to delete it's children from the index so we need to perform a - /// custom Lucene search to find all decendents and create Delete item queues for them too. - /// - /// ID of the node to delete - public override void DeleteFromIndex(string nodeId) - { - //find all descendants based on path - var descendantPath = $@"\-1\,*{nodeId}\,*"; - var rawQuery = $"{IndexPathFieldName}:{descendantPath}"; - var searcher = GetSearcher(); - var c = searcher.CreateCriteria(); - var filtered = c.RawQuery(rawQuery); - var results = searcher.Search(filtered); - - ProfilingLogger.Logger.Debug(GetType(), "DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); - - //need to queue a delete item for each one found - foreach (var r in results) - { - QueueIndexOperation(new IndexOperation(IndexItem.ForId(r.Id), IndexOperationType.Delete)); - } - - base.DeleteFromIndex(nodeId); - } - #endregion - - #region Protected - - /// - /// This is a static query, it's parameters don't change so store statically - /// - private static IQuery _publishedQuery; - - protected override void PerformIndexAll(string type) - { - const int pageSize = 10000; - var pageIndex = 0; - - switch (type) - { - case IndexTypes.Content: - var contentParentId = -1; - if (ParentId.HasValue && ParentId.Value > 0) - { - contentParentId = ParentId.Value; - } - IContent[] content; - - do - { - long total; - - IEnumerable descendants; - if (SupportUnpublishedContent) - { - descendants = ContentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out total); - } - else - { - //add the published filter - descendants = ContentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out total, - _publishedQuery, Ordering.By("Path", Direction.Ascending)); - } - - //if specific types are declared we need to post filter them - //TODO: Update the service layer to join the cmsContentType table so we can query by content type too - if (ConfigIndexCriteria != null && ConfigIndexCriteria.IncludeItemTypes.Any()) - { - content = descendants.Where(x => ConfigIndexCriteria.IncludeItemTypes.Contains(x.ContentType.Alias)).ToArray(); - } - else - { - content = descendants.ToArray(); - } - - IndexItems(GetValueSets(_urlSegmentProviders, UserService, content)); - - pageIndex++; - } while (content.Length == pageSize); - - break; - case IndexTypes.Media: - var mediaParentId = -1; - - if (ParentId.HasValue && ParentId.Value > 0) - { - mediaParentId = ParentId.Value; - } - - // merge note: 7.5 changes this to use mediaService.GetPagedXmlEntries but we cannot merge the - // change here as mediaService does not provide access to Xml in v8 - and actually Examine should - // not assume that Umbraco provides Xml at all. - - IMedia[] media; - - do - { - var descendants = MediaService.GetPagedDescendants(mediaParentId, pageIndex, pageSize, out _); - - //if specific types are declared we need to post filter them - //TODO: Update the service layer to join the cmsContentType table so we can query by content type too - if (ConfigIndexCriteria != null && ConfigIndexCriteria.IncludeItemTypes.Any()) - { - media = descendants.Where(x => ConfigIndexCriteria.IncludeItemTypes.Contains(x.ContentType.Alias)).ToArray(); - } - else - { - media = descendants.ToArray(); - } - - IndexItems(GetValueSets(_urlSegmentProviders, UserService, media)); - - pageIndex++; - } while (media.Length == pageSize); - - break; - } - } - - public static IEnumerable GetValueSets(IEnumerable urlSegmentProviders, IUserService userService, params IContent[] content) - { - foreach (var c in content) - { - var urlValue = c.GetUrlSegment(urlSegmentProviders); // for now, index with invariant culture - var values = new Dictionary - { - {"icon", new object[] {c.ContentType.Icon}}, - {PublishedFieldName, new object[] {c.Published ? 1 : 0}}, - {"id", new object[] {c.Id}}, - {"key", new object[] {c.Key}}, - {"parentID", new object[] {c.Level > 1 ? c.ParentId : -1}}, - {"level", new object[] {c.Level}}, - {"creatorID", new object[] {c.CreatorId}}, - {"sortOrder", new object[] {c.SortOrder}}, - {"createDate", new object[] {c.CreateDate}}, - {"updateDate", new object[] {c.UpdateDate}}, - {"nodeName", new object[] {c.Name}}, - {"urlName", new object[] {urlValue}}, - {"path", new object[] {c.Path}}, - {"nodeType", new object[] {c.ContentType.Id}}, - {"creatorName", new object[] {c.GetCreatorProfile(userService)?.Name ?? "??"}}, - {"writerName", new object[] {c.GetWriterProfile(userService)?.Name ?? "??"}}, - {"writerID", new object[] {c.WriterId}}, - {"template", new object[] {c.Template?.Id ?? 0}} - }; - - foreach (var property in c.Properties) - { - //only add the value if its not null or empty (we'll check for string explicitly here too) - //fixme support variants with language id - var val = property.GetValue("", ""); // for now, index the invariant values - switch (val) - { - case null: - continue; - case string strVal when strVal.IsNullOrWhiteSpace() == false: - values.Add(property.Alias, new[] { val }); - break; - default: - values.Add(property.Alias, new[] { val }); - break; - } - } - - var vs = new ValueSet(c.Id.ToInvariantString(), IndexTypes.Content, c.ContentType.Alias, values); - - yield return vs; - } - } - - public static IEnumerable GetValueSets(IEnumerable urlSegmentProviders, IUserService userService, params IMedia[] media) - { - foreach (var m in media) - { - var urlValue = m.GetUrlSegment(urlSegmentProviders); - var values = new Dictionary - { - {"icon", new object[] {m.ContentType.Icon}}, - {"id", new object[] {m.Id}}, - {"key", new object[] {m.Key}}, - {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, - {"level", new object[] {m.Level}}, - {"creatorID", new object[] {m.CreatorId}}, - {"sortOrder", new object[] {m.SortOrder}}, - {"createDate", new object[] {m.CreateDate}}, - {"updateDate", new object[] {m.UpdateDate}}, - {"nodeName", new object[] {m.Name}}, - {"urlName", new object[] {urlValue}}, - {"path", new object[] {m.Path}}, - {"nodeType", new object[] {m.ContentType.Id}}, - {"creatorName", new object[] {m.GetCreatorProfile(userService).Name}} - }; - - foreach (var property in m.Properties) - { - //only add the value if its not null or empty (we'll check for string explicitly here too) - var val = property.GetValue(); - switch (val) - { - case null: - continue; - case string strVal when strVal.IsNullOrWhiteSpace() == false: - values.Add(property.Alias, new[] { val }); - break; - default: - values.Add(property.Alias, new[] { val }); - break; - } - } - - var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Media, m.ContentType.Alias, values); - - yield return vs; - } - } - - - #endregion - } -} diff --git a/src/Umbraco.Examine/UmbracoContentIndexerOptions.cs b/src/Umbraco.Examine/UmbracoContentIndexerOptions.cs deleted file mode 100644 index 47b8a76c0f..0000000000 --- a/src/Umbraco.Examine/UmbracoContentIndexerOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace Umbraco.Examine -{ - - /// - /// Options used to configure the umbraco content indexer - /// - public class UmbracoContentIndexerOptions - { - public bool SupportUnpublishedContent { get; private set; } - public bool SupportProtectedContent { get; private set; } - //TODO: We should make this a GUID! But to do that we sort of need to store the 'Path' as a comma list of GUIDs instead of int - public int? ParentId { get; private set; } - - public UmbracoContentIndexerOptions(bool supportUnpublishedContent, bool supportProtectedContent, int? parentId) - { - SupportUnpublishedContent = supportUnpublishedContent; - SupportProtectedContent = supportProtectedContent; - ParentId = parentId; - } - } -} diff --git a/src/Umbraco.Examine/UmbracoContentValueSetValidator.cs b/src/Umbraco.Examine/UmbracoContentValueSetValidator.cs deleted file mode 100644 index 01056be6b9..0000000000 --- a/src/Umbraco.Examine/UmbracoContentValueSetValidator.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Linq; -using Examine; -using Examine.LuceneEngine.Providers; -using Umbraco.Core; -using Umbraco.Core.Services; - -namespace Umbraco.Examine -{ - /// - /// Used to validate a ValueSet for content - based on permissions, parent id, etc.... - /// - public class UmbracoContentValueSetValidator : IValueSetValidator - { - private readonly UmbracoContentIndexerOptions _options; - private readonly IPublicAccessService _publicAccessService; - private const string PathKey = "path"; - public UmbracoContentValueSetValidator(UmbracoContentIndexerOptions options, IPublicAccessService publicAccessService) - { - _options = options; - _publicAccessService = publicAccessService; - } - - public bool Validate(ValueSet valueSet) - { - //check for published content - if (valueSet.Category == IndexTypes.Content - && valueSet.Values.ContainsKey(UmbracoExamineIndexer.PublishedFieldName)) - { - var published = valueSet.Values[UmbracoExamineIndexer.PublishedFieldName] != null && valueSet.Values[UmbracoExamineIndexer.PublishedFieldName][0].Equals(1); - //we don't support unpublished and the item is not published return false - if (_options.SupportUnpublishedContent == false && published == false) - { - return false; - } - } - - //must have a 'path' - if (valueSet.Values.ContainsKey(PathKey) == false) return false; - var path = valueSet.Values[PathKey]?[0].ToString() ?? string.Empty; - - // Test for access if we're only indexing published content - // return nothing if we're not supporting protected content and it is protected, and we're not supporting unpublished content - if (valueSet.Category == IndexTypes.Content - && _options.SupportUnpublishedContent == false - && _options.SupportProtectedContent == false - && _publicAccessService.IsProtected(path)) - { - return false; - } - - //check if this document is a descendent of the parent - if (_options.ParentId.HasValue && _options.ParentId.Value > 0) - { - if (path.IsNullOrWhiteSpace()) return false; - if (path.Contains(string.Concat(",", _options.ParentId.Value, ",")) == false) - return false; - } - - //check for recycle bin - if (_options.SupportUnpublishedContent == false) - { - if (path.IsNullOrWhiteSpace()) return false; - var recycleBinId = valueSet.Category == IndexTypes.Content ? Constants.System.RecycleBinContent : Constants.System.RecycleBinMedia; - if (path.Contains(string.Concat(",", recycleBinId, ","))) - return false; - } - - return true; - } - } -} diff --git a/src/Umbraco.Examine/UmbracoExamineExtensions.cs b/src/Umbraco.Examine/UmbracoExamineExtensions.cs index ab8819b276..8be5a6c1e3 100644 --- a/src/Umbraco.Examine/UmbracoExamineExtensions.cs +++ b/src/Umbraco.Examine/UmbracoExamineExtensions.cs @@ -76,17 +76,6 @@ namespace Umbraco.Examine return fieldQuery; } - /// - /// Used to replace any available tokens in the index path before the lucene directory is assigned to the path - /// - /// - internal static void ReplaceTokensInIndexPath(this IndexSet indexSet) - { - if (indexSet == null) return; - indexSet.IndexPath = indexSet.IndexPath - .Replace("{machinename}", NetworkHelper.FileSafeMachineName) - .Replace("{appdomainappid}", (HttpRuntime.AppDomainAppId ?? string.Empty).ReplaceNonAlphanumericChars("")) - .EnsureEndsWith('/'); - } + } -} \ No newline at end of file +} diff --git a/src/Umbraco.Examine/UmbracoExamineIndexer.cs b/src/Umbraco.Examine/UmbracoExamineIndex.cs similarity index 59% rename from src/Umbraco.Examine/UmbracoExamineIndexer.cs rename to src/Umbraco.Examine/UmbracoExamineIndex.cs index a4c1fb4336..15f1a75955 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndexer.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndex.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.IO; using System.Linq; using Examine.LuceneEngine.Providers; using Lucene.Net.Analysis; @@ -11,19 +10,20 @@ using Umbraco.Core; using Examine; using Examine.LuceneEngine; using Examine.LuceneEngine.Indexing; +using Lucene.Net.Store; using Umbraco.Core.Composing; using Umbraco.Core.Logging; -using Umbraco.Core.Xml; using Umbraco.Examine.Config; using Directory = Lucene.Net.Store.Directory; namespace Umbraco.Examine { + /// /// An abstract provider containing the basic functionality to be able to query against /// Umbraco data. /// - public abstract class UmbracoExamineIndexer : LuceneIndexer + public abstract class UmbracoExamineIndex : LuceneIndex, IUmbracoIndexer, IIndexDiagnostics { // note // wrapping all operations that end up calling base.SafelyProcessQueueItems in a safe call @@ -34,44 +34,38 @@ namespace Umbraco.Examine /// /// Used to store the path of a content object /// - public const string IndexPathFieldName = "__Path"; - public const string NodeKeyFieldName = "__Key"; - public const string IconFieldName = "__Icon"; - public const string PublishedFieldName = "__Published"; + public const string IndexPathFieldName = SpecialFieldPrefix + "Path"; + public const string NodeKeyFieldName = SpecialFieldPrefix + "Key"; + public const string IconFieldName = SpecialFieldPrefix + "Icon"; + public const string PublishedFieldName = SpecialFieldPrefix + "Published"; + /// /// The prefix added to a field when it is duplicated in order to store the original raw value. /// - public const string RawFieldPrefix = "__Raw_"; + public const string RawFieldPrefix = SpecialFieldPrefix + "Raw_"; /// /// Constructor for config provider based indexes /// [EditorBrowsable(EditorBrowsableState.Never)] - protected UmbracoExamineIndexer() + protected UmbracoExamineIndex() : base() { ProfilingLogger = Current.ProfilingLogger; _configBased = true; - - //This is using the config so we'll validate based on that - ValueSetValidator = new ValueSetValidatorDelegate(set => - { - - //check if this document is of a correct type of node type alias - if (ConfigIndexCriteria.IncludeItemTypes.Any()) - if (!ConfigIndexCriteria.IncludeItemTypes.Contains(set.ItemType)) - return false; - - //if this node type is part of our exclusion list, do not validate - if (ConfigIndexCriteria.ExcludeItemTypes.Any()) - if (ConfigIndexCriteria.ExcludeItemTypes.Contains(set.ItemType)) - return false; - - return true; - }); } - protected UmbracoExamineIndexer( + /// + /// Create a new + /// + /// + /// + /// + /// + /// + /// + /// + protected UmbracoExamineIndex( string name, IEnumerable fieldDefinitions, Directory luceneDirectory, @@ -82,6 +76,12 @@ namespace Umbraco.Examine : base(name, fieldDefinitions, luceneDirectory, defaultAnalyzer, validator, indexValueTypes) { ProfilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); + + //try to set the value of `LuceneIndexFolder` for diagnostic reasons + if (luceneDirectory is FSDirectory fsDir) + LuceneIndexFolder = fsDir.Directory; + + _diagnostics = new UmbracoExamineIndexDiagnostics(this, ProfilingLogger.Logger); } private readonly bool _configBased = false; @@ -91,7 +91,7 @@ namespace Umbraco.Examine /// Alot of standard umbraco fields shouldn't be tokenized or even indexed, just stored into lucene /// for retreival after searching. /// - internal static readonly FieldDefinition[] UmbracoIndexFields = + public static readonly FieldDefinition[] UmbracoIndexFieldDefinitions = { new FieldDefinition("parentID", FieldDefinitionTypes.Integer), new FieldDefinition("level", FieldDefinitionTypes.Integer), @@ -110,6 +110,10 @@ namespace Umbraco.Examine new FieldDefinition("urlName", FieldDefinitionTypes.InvariantCultureIgnoreCase), new FieldDefinition("path", FieldDefinitionTypes.Raw), + new FieldDefinition("email", FieldDefinitionTypes.EmailAddress), + + new FieldDefinition(PublishedFieldName, FieldDefinitionTypes.Raw), + new FieldDefinition(NodeKeyFieldName, FieldDefinitionTypes.Raw), new FieldDefinition(IndexPathFieldName, FieldDefinitionTypes.Raw), new FieldDefinition(IconFieldName, FieldDefinitionTypes.Raw) }; @@ -119,17 +123,21 @@ namespace Umbraco.Examine /// /// Overridden to ensure that the umbraco system field definitions are in place /// - /// /// /// - protected override FieldValueTypeCollection CreateFieldValueTypes(Directory x, IReadOnlyDictionary> indexValueTypesFactory = null) + protected override FieldValueTypeCollection CreateFieldValueTypes(IReadOnlyDictionary> indexValueTypesFactory = null) { - foreach (var field in UmbracoIndexFields) + //if config based then ensure the value types else it's assumed these were passed in via ctor + if (_configBased) { - FieldDefinitionCollection.TryAdd(field.Name, field); + foreach (var field in UmbracoIndexFieldDefinitions) + { + FieldDefinitionCollection.TryAdd(field.Name, field); + } } + - return base.CreateFieldValueTypes(x, indexValueTypesFactory); + return base.CreateFieldValueTypes(indexValueTypesFactory); } /// @@ -137,10 +145,7 @@ namespace Umbraco.Examine /// public bool EnableDefaultEventHandler { get; set; } = true; - /// - /// the supported indexable types - /// - protected abstract IEnumerable SupportedTypes { get; } + public bool PublishedValuesOnly { get; protected set; } = false; protected ConfigIndexCriteria ConfigIndexCriteria { get; private set; } @@ -170,38 +175,35 @@ namespace Umbraco.Examine } //Need to check if the index set or IndexerData is specified... - if (config["indexSet"] == null && (FieldDefinitionCollection.Count == 0)) + if (config["indexSet"] == null && FieldDefinitionCollection.Count == 0) { //if we don't have either, then we'll try to set the index set by naming conventions var found = false; - if (name.EndsWith("Indexer")) + + var possibleSuffixes = new[] {"Index", "Indexer"}; + foreach (var suffix in possibleSuffixes) { - var setNameByConvension = name.Remove(name.LastIndexOf("Indexer")) + "IndexSet"; + if (!name.EndsWith(suffix)) continue; + + var setNameByConvension = name.Remove(name.LastIndexOf(suffix, StringComparison.Ordinal)) + "IndexSet"; //check if we can assign the index set by naming convention var set = IndexSets.Instance.Sets.Cast().SingleOrDefault(x => x.SetName == setNameByConvension); - if (set != null) + if (set == null) continue; + + //we've found an index set by naming conventions :) + IndexSetName = set.SetName; + + var indexSet = IndexSets.Instance.Sets[IndexSetName]; + + //get the index criteria and ensure folder + ConfigIndexCriteria = CreateFieldDefinitionsFromConfig(indexSet); + foreach (var fieldDefinition in ConfigIndexCriteria.StandardFields.Union(ConfigIndexCriteria.UserFields)) { - //we've found an index set by naming conventions :) - IndexSetName = set.SetName; - - var indexSet = IndexSets.Instance.Sets[IndexSetName]; - - //if tokens are declared in the path, then use them (i.e. {machinename} ) - indexSet.ReplaceTokensInIndexPath(); - - //get the index criteria and ensure folder - ConfigIndexCriteria = CreateFieldDefinitionsFromConfig(indexSet); - foreach (var fieldDefinition in ConfigIndexCriteria.StandardFields.Union(ConfigIndexCriteria.UserFields)) - { - FieldDefinitionCollection.TryAdd(fieldDefinition.Name, fieldDefinition); - } - - //now set the index folder - LuceneIndexFolder = new DirectoryInfo(Path.Combine(IndexSets.Instance.Sets[IndexSetName].IndexDirectory.FullName, "Index")); - - found = true; + FieldDefinitionCollection.TryAdd(fieldDefinition.Name, fieldDefinition); } + found = true; + break; } if (!found) @@ -222,86 +224,33 @@ namespace Umbraco.Examine var indexSet = IndexSets.Instance.Sets[IndexSetName]; - //if tokens are declared in the path, then use them (i.e. {machinename} ) - indexSet.ReplaceTokensInIndexPath(); - //get the index criteria and ensure folder ConfigIndexCriteria = CreateFieldDefinitionsFromConfig(indexSet); foreach (var fieldDefinition in ConfigIndexCriteria.StandardFields.Union(ConfigIndexCriteria.UserFields)) { FieldDefinitionCollection.TryAdd(fieldDefinition.Name, fieldDefinition); } - - //now set the index folder - LuceneIndexFolder = new DirectoryInfo(Path.Combine(IndexSets.Instance.Sets[IndexSetName].IndexDirectory.FullName, "Index")); } } base.Initialize(name, config); } - + #endregion - /// /// override to check if we can actually initialize. /// /// /// This check is required since the base examine lib will try to rebuild on startup /// - public override void RebuildIndex() - { - if (CanInitialize()) - { - ProfilingLogger.Logger.Debug(GetType(), "Rebuilding index"); - using (new SafeCallContext()) - { - base.RebuildIndex(); - } - } - } - - /// - /// override to check if we can actually initialize. - /// - /// - /// This check is required since the base examine lib will try to rebuild on startup - /// - public override void IndexAll(string type) + protected override void PerformDeleteFromIndex(string nodeId, Action onComplete) { if (CanInitialize()) { using (new SafeCallContext()) { - base.IndexAll(type); - } - } - } - - public override void IndexItems(IEnumerable nodes) - { - if (CanInitialize()) - { - using (new SafeCallContext()) - { - base.IndexItems(nodes); - } - } - } - - /// - /// override to check if we can actually initialize. - /// - /// - /// This check is required since the base examine lib will try to rebuild on startup - /// - public override void DeleteFromIndex(string nodeId) - { - if (CanInitialize()) - { - using (new SafeCallContext()) - { - base.DeleteFromIndex(nodeId); + base.PerformDeleteFromIndex(nodeId, onComplete); } } } @@ -317,17 +266,6 @@ namespace Umbraco.Examine return _configBased == false || Current.RuntimeState.Level == RuntimeLevel.Run; } - /// - /// Reindexes all supported types - /// - protected override void PerformIndexRebuild() - { - foreach (var t in SupportedTypes) - { - IndexAll(t); - } - } - /// /// overridden for logging /// @@ -339,17 +277,20 @@ namespace Umbraco.Examine } /// - /// This ensures that the special __Raw_ fields are indexed + /// This ensures that the special __Raw_ fields are indexed correctly /// /// protected override void OnDocumentWriting(DocumentWritingEventArgs docArgs) { var d = docArgs.Document; - foreach (var f in docArgs.ValueSet.Values.Where(x => x.Key.StartsWith(RawFieldPrefix))) + foreach (var f in docArgs.ValueSet.Values.Where(x => x.Key.StartsWith(RawFieldPrefix)).ToList()) { if (f.Value.Count > 0) { + //remove the original value so we can store it the correct way + d.RemoveField(f.Key); + d.Add(new Field( f.Key, f.Value[0].ToString(), @@ -359,26 +300,21 @@ namespace Umbraco.Examine } } - ProfilingLogger.Logger.Debug(GetType(), - "Write lucene doc id:{DocumentId}, category:{DocumentCategory}, type:{DocumentItemType}", - docArgs.ValueSet.Id, - docArgs.ValueSet.Category, - docArgs.ValueSet.ItemType); - - base.OnDocumentWriting(docArgs); } /// /// Overridden for logging. /// - protected override void AddDocument(Document doc, IndexItem item, IndexWriter writer) + protected override void AddDocument(Document doc, ValueSet valueSet, IndexWriter writer) { ProfilingLogger.Logger.Debug(GetType(), - "AddDocument {DocumentId} with type {DocumentItemType}", - item.ValueSet.Id, - item.ValueSet.ItemType); - base.AddDocument(doc, item, writer); + "Write lucene doc id:{DocumentId}, category:{DocumentCategory}, type:{DocumentItemType}", + valueSet.Id, + valueSet.Category, + valueSet.ItemType); + + base.AddDocument(doc, valueSet, writer); } protected override void OnTransformingIndexValues(IndexingItemEventArgs e) @@ -386,39 +322,16 @@ namespace Umbraco.Examine base.OnTransformingIndexValues(e); //ensure special __Path field - var path = e.IndexItem.ValueSet.GetValue("path"); + var path = e.ValueSet.GetValue("path"); if (path != null) { - e.IndexItem.ValueSet.Set(IndexPathFieldName, path); - } - - //strip html of all users fields if we detect it has HTML in it. - //if that is the case, we'll create a duplicate 'raw' copy of it so that we can return - //the value of the field 'as-is'. - foreach (var value in e.IndexItem.ValueSet.Values.ToList()) //ToList here to make a diff collection else we'll get collection modified errors - { - if (value.Value == null) continue; - - if (value.Value.Count > 0) - { - if (value.Value.First() is string str) - { - if (XmlHelper.CouldItBeXml(str)) - { - //First save the raw value to a raw field, we will change the policy of this field by detecting the prefix later - e.IndexItem.ValueSet.Values[string.Concat(RawFieldPrefix, value.Key)] = new List { str }; - //now replace the original value with the stripped html - //TODO: This should be done with an analzer?! - e.IndexItem.ValueSet.Values[value.Key] = new List { str.StripHtml() }; - } - } - } + e.ValueSet.Set(IndexPathFieldName, path); } //icon - if (e.IndexItem.ValueSet.Values.TryGetValue("icon", out var icon) && e.IndexItem.ValueSet.Values.ContainsKey(IconFieldName) == false) + if (e.ValueSet.Values.TryGetValue("icon", out var icon) && e.ValueSet.Values.ContainsKey(IconFieldName) == false) { - e.IndexItem.ValueSet.Values[IconFieldName] = icon; + e.ValueSet.Values[IconFieldName] = icon; } } @@ -431,5 +344,16 @@ namespace Umbraco.Examine indexSet.ExcludeNodeTypes.ToList().Select(x => x.Name).ToArray(), indexSet.IndexParentId); } + + #region IIndexDiagnostics + + private readonly UmbracoExamineIndexDiagnostics _diagnostics; + + public int DocumentCount => _diagnostics.DocumentCount; + public int FieldCount => _diagnostics.FieldCount; + public Attempt IsHealthy() => _diagnostics.IsHealthy(); + public virtual IReadOnlyDictionary Metadata => _diagnostics.Metadata; + + #endregion } } diff --git a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs new file mode 100644 index 0000000000..227b52e085 --- /dev/null +++ b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using Lucene.Net.Store; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; + +namespace Umbraco.Examine +{ + public class UmbracoExamineIndexDiagnostics : IIndexDiagnostics + { + private readonly UmbracoExamineIndex _index; + private readonly ILogger _logger; + + public UmbracoExamineIndexDiagnostics(UmbracoExamineIndex index, ILogger logger) + { + _index = index; + _logger = logger; + } + + public int DocumentCount + { + get + { + try + { + return _index.GetIndexDocumentCount(); + } + catch (AlreadyClosedException) + { + _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexDocumentCount, the writer is already closed"); + return 0; + } + } + } + + public int FieldCount + { + get + { + try + { + return _index.GetIndexFieldCount(); + } + catch (AlreadyClosedException) + { + _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexFieldCount, the writer is already closed"); + return 0; + } + } + } + + public Attempt IsHealthy() + { + var isHealthy = _index.IsHealthy(out var indexError); + return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError.Message); + } + + public virtual IReadOnlyDictionary Metadata + { + get + { + var d = new Dictionary + { + [nameof(UmbracoExamineIndex.CommitCount)] = _index.CommitCount, + [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = _index.DefaultAnalyzer.GetType().Name, + [nameof(UmbracoExamineIndex.DirectoryFactory)] = _index.DirectoryFactory, + [nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler, + [nameof(UmbracoExamineIndex.LuceneIndexFolder)] = + _index.LuceneIndexFolder == null + ? string.Empty + : _index.LuceneIndexFolder.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'), + [nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly, + //There's too much info here + //[nameof(UmbracoExamineIndexer.FieldDefinitionCollection)] = _index.FieldDefinitionCollection, + }; + + if (_index.ValueSetValidator is ValueSetValidator vsv) + { + d[nameof(ValueSetValidator.IncludeItemTypes)] = vsv.IncludeItemTypes; + d[nameof(ContentValueSetValidator.ExcludeItemTypes)] = vsv.ExcludeItemTypes; + d[nameof(ContentValueSetValidator.IncludeFields)] = vsv.IncludeFields; + d[nameof(ContentValueSetValidator.ExcludeFields)] = vsv.ExcludeFields; + } + + if (_index.ValueSetValidator is ContentValueSetValidator cvsv) + { + d[nameof(ContentValueSetValidator.PublishedValuesOnly)] = cvsv.PublishedValuesOnly; + d[nameof(ContentValueSetValidator.SupportProtectedContent)] = cvsv.SupportProtectedContent; + d[nameof(ContentValueSetValidator.ParentId)] = cvsv.ParentId; + } + + return d.Where(x => x.Value != null).ToDictionary(x => x.Key, x => x.Value); + } + } + } +} diff --git a/src/Umbraco.Examine/UmbracoExamineSearcher.cs b/src/Umbraco.Examine/UmbracoExamineSearcher.cs index 4b5ac50af7..50c6c21e37 100644 --- a/src/Umbraco.Examine/UmbracoExamineSearcher.cs +++ b/src/Umbraco.Examine/UmbracoExamineSearcher.cs @@ -1,156 +1,52 @@ -using System; -using System.ComponentModel; -using System.IO; -using System.Linq; -using Umbraco.Core; -using Examine.LuceneEngine.Providers; -using Lucene.Net.Analysis; -using Lucene.Net.Index; -using Umbraco.Core.Composing; -using Umbraco.Examine.Config; -using Directory = Lucene.Net.Store.Directory; +//using System; +//using System.ComponentModel; +//using System.IO; +//using System.Linq; +//using Umbraco.Core; +//using Examine.LuceneEngine.Providers; +//using Lucene.Net.Analysis; +//using Lucene.Net.Index; +//using Umbraco.Core.Composing; +//using Umbraco.Examine.Config; +//using Directory = Lucene.Net.Store.Directory; +//using Examine.LuceneEngine; +//namespace Umbraco.Examine +//{ +// /// +// /// An Examine searcher which uses Lucene.Net as the +// /// +// public class UmbracoExamineSearcher : LuceneSearcher +// { +// /// +// /// Constructor to allow for creating an indexer at runtime +// /// +// /// +// /// +// /// +// public UmbracoExamineSearcher(string name, Directory luceneDirectory, Analyzer analyzer, FieldValueTypeCollection fieldValueTypeCollection) +// : base(name, luceneDirectory, analyzer, fieldValueTypeCollection) +// { +// } -namespace Umbraco.Examine -{ - /// - /// An Examine searcher which uses Lucene.Net as the - /// - public class UmbracoExamineSearcher : LuceneSearcher - { - private readonly bool _configBased = false; +// /// +// public UmbracoExamineSearcher(string name, IndexWriter writer, Analyzer analyzer, FieldValueTypeCollection fieldValueTypeCollection) +// : base(name, writer, analyzer, fieldValueTypeCollection) +// { +// } - /// - /// Default constructor for config based construction - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public UmbracoExamineSearcher() - { - _configBased = true; - } +// /// +// /// Returns a list of fields to search on, this will also exclude the IndexPathFieldName and node type alias +// /// +// /// +// public override string[] GetAllIndexedFields() +// { +// var fields = base.GetAllIndexedFields(); +// return fields +// .Where(x => x != UmbracoExamineIndexer.IndexPathFieldName) +// .Where(x => x != LuceneIndex.ItemTypeFieldName) +// .ToArray(); +// } - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - public UmbracoExamineSearcher(string name, DirectoryInfo indexPath, Analyzer analyzer) - : base(name, indexPath, analyzer) - { - _configBased = false; - } - - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - public UmbracoExamineSearcher(string name, Directory luceneDirectory, Analyzer analyzer) - : base(name, luceneDirectory, analyzer) - { - _configBased = false; - } - - /// - public UmbracoExamineSearcher(string name, IndexWriter writer, Analyzer analyzer) : base(name, writer, analyzer) - { - _configBased = false; - } - - /// - /// Name of the Lucene.NET index set - /// - public string IndexSetName { get; private set; } - - /// - /// Method used for initializing based on a configuration based searcher - /// - /// - /// - public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); - - //We need to check if we actually can initialize, if not then don't continue - if (CanInitialize() == false) - { - return; - } - - //need to check if the index set is specified, if it's not, we'll see if we can find one by convension - //if the folder is not null and the index set is null, we'll assume that this has been created at runtime. - //NOTE: Don't proceed if the _luceneDirectory is set since we already know where to look. - var luceneDirectory = GetLuceneDirectory(); - if (config["indexSet"] == null && (LuceneIndexFolder == null && luceneDirectory == null)) - { - //if we don't have either, then we'll try to set the index set by naming convensions - var found = false; - if (name.EndsWith("Searcher")) - { - var setNameByConvension = name.Remove(name.LastIndexOf("Searcher")) + "IndexSet"; - //check if we can assign the index set by naming convension - var set = IndexSets.Instance.Sets.Cast().SingleOrDefault(x => x.SetName == setNameByConvension); - - if (set != null) - { - set.ReplaceTokensInIndexPath(); - - //we've found an index set by naming convensions :) - IndexSetName = set.SetName; - found = true; - } - } - - if (!found) - throw new ArgumentNullException("indexSet on LuceneExamineIndexer provider has not been set in configuration"); - - //get the folder to index - LuceneIndexFolder = new DirectoryInfo(Path.Combine(IndexSets.Instance.Sets[IndexSetName].IndexDirectory.FullName, "Index")); - } - else if (config["indexSet"] != null && luceneDirectory == null) - { - if (IndexSets.Instance.Sets[config["indexSet"]] == null) - throw new ArgumentException("The indexSet specified for the LuceneExamineIndexer provider does not exist"); - - IndexSetName = config["indexSet"]; - - var indexSet = IndexSets.Instance.Sets[IndexSetName]; - - indexSet.ReplaceTokensInIndexPath(); - - //get the folder to index - LuceneIndexFolder = new DirectoryInfo(Path.Combine(indexSet.IndexDirectory.FullName, "Index")); - } - - base.Initialize(name, config); - - } - - /// - /// Returns true if the Umbraco application is in a state that we can initialize the examine indexes - /// - - protected bool CanInitialize() - { - // only affects indexers that are config file based, if an index was created via code then - // this has no effect, it is assumed the index would not be created if it could not be initialized - return _configBased == false || Current.RuntimeState.Level == RuntimeLevel.Run; - } - - /// - /// Returns a list of fields to search on, this will also exclude the IndexPathFieldName and node type alias - /// - /// - public override string[] GetAllIndexedFields() - { - var fields = base.GetAllIndexedFields(); - return fields - .Where(x => x != UmbracoExamineIndexer.IndexPathFieldName) - .Where(x => x != LuceneIndexer.ItemTypeFieldName) - .ToArray(); - } - - } -} +// } +//} diff --git a/src/Umbraco.Examine/UmbracoMemberIndex.cs b/src/Umbraco.Examine/UmbracoMemberIndex.cs new file mode 100644 index 0000000000..28b46f72dd --- /dev/null +++ b/src/Umbraco.Examine/UmbracoMemberIndex.cs @@ -0,0 +1,92 @@ +using System; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Services; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using Examine; +using Examine.LuceneEngine; +using Examine.LuceneEngine.Indexing; +using Examine.LuceneEngine.Providers; +using Lucene.Net.Analysis; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Directory = Lucene.Net.Store.Directory; + +namespace Umbraco.Examine +{ + + /// + /// Custom indexer for members + /// + public class UmbracoMemberIndex : UmbracoExamineIndex + { + /// + /// Constructor for config/provider based indexes + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public UmbracoMemberIndex() + { + } + + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + /// + /// + public UmbracoMemberIndex( + string name, + IEnumerable fieldDefinitions, + Directory luceneDirectory, + Analyzer analyzer, + ProfilingLogger profilingLogger, + IValueSetValidator validator = null) : + base(name, fieldDefinitions, luceneDirectory, analyzer, profilingLogger, validator) + { + } + + public override void Initialize(string name, NameValueCollection config) + { + base.Initialize(name, config); + + ValueSetValidator = new MemberValueSetValidator(ConfigIndexCriteria.IncludeItemTypes, ConfigIndexCriteria.ExcludeItemTypes); + } + + /// + /// Overridden to ensure that the umbraco system field definitions are in place + /// + /// + /// + protected override FieldValueTypeCollection CreateFieldValueTypes(IReadOnlyDictionary> indexValueTypesFactory = null) + { + var keyDef = new FieldDefinition("__key", FieldDefinitionTypes.Raw); + FieldDefinitionCollection.TryAdd(keyDef.Name, keyDef); + + return base.CreateFieldValueTypes(indexValueTypesFactory); + } + + /// + /// Ensure some custom values are added to the index + /// + /// + protected override void OnTransformingIndexValues(IndexingItemEventArgs e) + { + base.OnTransformingIndexValues(e); + + if (e.ValueSet.Values.TryGetValue("key", out var key) && e.ValueSet.Values.ContainsKey("__key") == false) + { + //double __ prefix means it will be indexed as culture invariant + e.ValueSet.Values["__key"] = key; + } + + } + + } +} diff --git a/src/Umbraco.Examine/UmbracoMemberIndexer.cs b/src/Umbraco.Examine/UmbracoMemberIndexer.cs deleted file mode 100644 index 82bf3b9cf6..0000000000 --- a/src/Umbraco.Examine/UmbracoMemberIndexer.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Services; -using System.Collections.Generic; -using System.ComponentModel; -using Examine; -using Examine.LuceneEngine; -using Examine.LuceneEngine.Indexing; -using Examine.LuceneEngine.Providers; -using Lucene.Net.Analysis; -using Umbraco.Core.Composing; -using Umbraco.Core.Logging; -using Directory = Lucene.Net.Store.Directory; - -namespace Umbraco.Examine -{ - - /// - /// Custom indexer for members - /// - public class UmbracoMemberIndexer : UmbracoExamineIndexer - { - private readonly IMemberService _memberService; - - /// - /// Constructor for config/provider based indexes - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public UmbracoMemberIndexer() - { - _memberService = Current.Services.MemberService; - } - - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - /// - /// - /// - /// - public UmbracoMemberIndexer( - string name, - IEnumerable fieldDefinitions, - Directory luceneDirectory, - Analyzer analyzer, - ProfilingLogger profilingLogger, - IValueSetValidator validator, - IMemberService memberService) : - base(name, fieldDefinitions, luceneDirectory, analyzer, profilingLogger, validator) - { - _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); - } - - - /// - /// Overridden to ensure that the umbraco system field definitions are in place - /// - /// - /// - /// - protected override FieldValueTypeCollection CreateFieldValueTypes(Directory x, IReadOnlyDictionary> indexValueTypesFactory = null) - { - var keyDef = new FieldDefinition("__key", FieldDefinitionTypes.Raw); - FieldDefinitionCollection.TryAdd(keyDef.Name, keyDef); - - return base.CreateFieldValueTypes(x, indexValueTypesFactory); - } - - /// - protected override IEnumerable SupportedTypes => new[] {IndexTypes.Member}; - - /// - /// Reindex all members - /// - /// - protected override void PerformIndexAll(string type) - { - //This only supports members - if (SupportedTypes.Contains(type) == false) - return; - - const int pageSize = 1000; - var pageIndex = 0; - - IMember[] members; - - if (ConfigIndexCriteria != null && ConfigIndexCriteria.IncludeItemTypes.Any()) - { - //if there are specific node types then just index those - foreach (var nodeType in ConfigIndexCriteria.IncludeItemTypes) - { - do - { - members = _memberService.GetAll(pageIndex, pageSize, out _, "LoginName", Direction.Ascending, true, null, nodeType).ToArray(); - - IndexItems(GetValueSets(members)); - - pageIndex++; - } while (members.Length == pageSize); - } - } - else - { - //no node types specified, do all members - do - { - members = _memberService.GetAll(pageIndex, pageSize, out _).ToArray(); - - IndexItems(GetValueSets(members)); - - pageIndex++; - } while (members.Length == pageSize); - } - } - - public static IEnumerable GetValueSets(params IMember[] members) - { - foreach (var m in members) - { - var values = new Dictionary - { - {"icon", new object[] {m.ContentType.Icon}}, - {"id", new object[] {m.Id}}, - {"key", new object[] {m.Key}}, - {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, - {"level", new object[] {m.Level}}, - {"creatorID", new object[] {m.CreatorId}}, - {"sortOrder", new object[] {m.SortOrder}}, - {"createDate", new object[] {m.CreateDate}}, - {"updateDate", new object[] {m.UpdateDate}}, - {"nodeName", new object[] {m.Name}}, - {"path", new object[] {m.Path}}, - {"nodeType", new object[] {m.ContentType.Id}}, - {"loginName", new object[] {m.Username}}, - {"email", new object[] {m.Email}}, - }; - - foreach (var property in m.Properties) - { - //only add the value if its not null or empty (we'll check for string explicitly here too) - var val = property.GetValue(); - switch (val) - { - case null: - continue; - case string strVal when strVal.IsNullOrWhiteSpace() == false: - values.Add(property.Alias, new[] { val }); - break; - default: - values.Add(property.Alias, new[] { val }); - break; - } - } - - var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Content, m.ContentType.Alias, values); - - yield return vs; - } - } - - /// - /// Ensure some custom values are added to the index - /// - /// - protected override void OnTransformingIndexValues(IndexingItemEventArgs e) - { - base.OnTransformingIndexValues(e); - - if (e.IndexItem.ValueSet.Values.TryGetValue("key", out var key) && e.IndexItem.ValueSet.Values.ContainsKey("__key") == false) - { - //double __ prefix means it will be indexed as culture invariant - e.IndexItem.ValueSet.Values["__key"] = key; - } - - if (e.IndexItem.ValueSet.Values.TryGetValue("email", out var email) && e.IndexItem.ValueSet.Values.ContainsKey("_searchEmail") == false) - { - if (email.Count > 0) - { - //will be indexed as full text (the default anaylyzer) - e.IndexItem.ValueSet.Values["_searchEmail"] = new List { email[0]?.ToString().Replace(".", " ").Replace("@", " ") }; - } - - } - } - - } -} diff --git a/src/Umbraco.Examine/ValueSetValidator.cs b/src/Umbraco.Examine/ValueSetValidator.cs new file mode 100644 index 0000000000..4db251c0f1 --- /dev/null +++ b/src/Umbraco.Examine/ValueSetValidator.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Examine; +using Examine.LuceneEngine.Providers; +using Umbraco.Core; + +namespace Umbraco.Examine +{ + /// + /// Performing basic validation of a value set + /// + public class ValueSetValidator : IValueSetValidator + { + public ValueSetValidator( + IEnumerable includeItemTypes, + IEnumerable excludeItemTypes, + IEnumerable includeFields, + IEnumerable excludeFields) + { + IncludeItemTypes = includeItemTypes; + ExcludeItemTypes = excludeItemTypes; + IncludeFields = includeFields; + ExcludeFields = excludeFields; + ValidIndexCategories = null; + } + + protected virtual IEnumerable ValidIndexCategories { get; } + + /// + /// Optional inclusion list of content types to index + /// + /// + /// All other types will be ignored if they do not match this list + /// + public IEnumerable IncludeItemTypes { get; } + + /// + /// Optional exclusion list of content types to ignore + /// + /// + /// Any content type alias matched in this will not be included in the index + /// + public IEnumerable ExcludeItemTypes { get; } + + /// + /// Optional inclusion list of index fields to index + /// + /// + /// If specified, all other fields in a will be filtered + /// + public IEnumerable IncludeFields { get; } + + /// + /// Optional exclusion list of index fields + /// + /// + /// If specified, all fields matching these field names will be filtered from the + /// + public IEnumerable ExcludeFields { get; } + + public virtual ValueSetValidationResult Validate(ValueSet valueSet) + { + if (ValidIndexCategories != null && !ValidIndexCategories.InvariantContains(valueSet.Category)) + return ValueSetValidationResult.Failed; + + //check if this document is of a correct type of node type alias + if (IncludeItemTypes != null && !IncludeItemTypes.InvariantContains(valueSet.ItemType)) + return ValueSetValidationResult.Failed; + + //if this node type is part of our exclusion list + if (ExcludeItemTypes != null && ExcludeItemTypes.InvariantContains(valueSet.ItemType)) + return ValueSetValidationResult.Failed; + + var isFiltered = false; + + //filter based on the fields provided (if any) + if (IncludeFields != null || ExcludeFields != null) + { + foreach (var key in valueSet.Values.Keys.ToList()) + { + if (IncludeFields != null && !IncludeFields.InvariantContains(key)) + { + valueSet.Values.Remove(key); //remove any value with a key that doesn't match the inclusion list + isFiltered = true; + } + + if (ExcludeFields != null && ExcludeFields.InvariantContains(key)) + { + valueSet.Values.Remove(key); //remove any value with a key that matches the exclusion list + isFiltered = true; + } + + } + } + + return isFiltered ? ValueSetValidationResult.Filtered : ValueSetValidationResult.Valid; + } + } +} diff --git a/src/Umbraco.Tests.Benchmarks/EnumeratorBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/EnumeratorBenchmarks.cs new file mode 100644 index 0000000000..b3df1c14e8 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/EnumeratorBenchmarks.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; + +namespace Umbraco.Tests.Benchmarks +{ + [MemoryDiagnoser] + public class EnumeratorBenchmarks + { + [Benchmark(Baseline = true)] + public void WithArray() + { + foreach (var t in EnumerateOneWithArray(1)) ; + } + + [Benchmark] + public void WithYield() + { + foreach (var t in EnumerateOneWithYield(1)) ; + } + + private IEnumerable EnumerateOneWithArray(T o) => new [] { o }; + + private IEnumerable EnumerateOneWithYield(T o) + { + yield return o; + } + } +} diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index e5bc3e88b0..bb14fb5a77 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -52,6 +52,7 @@ + diff --git a/src/Umbraco.Tests/Composing/TypeFinderTests.cs b/src/Umbraco.Tests/Composing/TypeFinderTests.cs index 955f6f94c8..9b52546dff 100644 --- a/src/Umbraco.Tests/Composing/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeFinderTests.cs @@ -49,7 +49,7 @@ namespace Umbraco.Tests.Composing //typeof(TabPage).Assembly, typeof(System.Web.Mvc.ActionResult).Assembly, typeof(TypeFinder).Assembly, - typeof(global::Umbraco.Examine.UmbracoExamineIndexer).Assembly + typeof(global::Umbraco.Examine.UmbracoExamineIndex).Assembly }; } diff --git a/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config b/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config index 4040412603..4c86355a1b 100644 --- a/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config +++ b/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config @@ -6,10 +6,10 @@ settings - + views/dashboard/settings/settingsdashboardintro.html - + views/dashboard/settings/settingsdashboardvideos.html @@ -23,10 +23,10 @@ developer - + views/dashboard/developer/developerdashboardintro.html - + views/dashboard/developer/developerdashboardvideos.html @@ -37,7 +37,7 @@ media - + views/dashboard/media/mediafolderbrowser.html @@ -45,13 +45,13 @@ admin - + views/dashboard/media/mediadashboardintro.html - + views/dashboard/media/desktopmediauploader.html - + views/dashboard/media/mediadashboardvideos.html @@ -70,25 +70,25 @@ admin - + views/dashboard/default/startupdashboardintro.html - + views/dashboard/default/startupdashboardkits.html editor writer - + views/dashboard/default/startupdashboardvideos.html - dashboard/latestEdits.ascx + dashboard/latestEdits.ascx - + views/dashboard/changepassword.html @@ -100,13 +100,13 @@ member - + views/dashboard/members/membersdashboardintro.html - + members/membersearch.ascx - + views/dashboard/members/membersdashboardvideos.html diff --git a/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs b/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs index 862dfb3dc2..920de683b4 100644 --- a/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs +++ b/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs @@ -56,11 +56,11 @@ namespace Umbraco.Tests.Configurations.DashboardSettings Assert.AreEqual(3, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.Count()); Assert.AreEqual("translator", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Value); - Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Action); + Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Type); Assert.AreEqual("hello", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Value); - Assert.AreEqual(AccessType.Grant, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Action); + Assert.AreEqual(AccessRuleType.Grant, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Type); Assert.AreEqual("world", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Value); - Assert.AreEqual(AccessType.GrantBySection, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Action); + Assert.AreEqual(AccessRuleType.GrantBySection, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Type); } [Test] @@ -94,21 +94,17 @@ namespace Umbraco.Tests.Configurations.DashboardSettings public void Test_Tab_Access() { Assert.AreEqual(1, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.Count()); - Assert.AreEqual(AccessType.Grant, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Action); + Assert.AreEqual(AccessRuleType.Grant, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Type); Assert.AreEqual("admin", SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Value); } [Test] public void Test_Control() { - Assert.AreEqual(true, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).ShowOnce); - Assert.AreEqual(true, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).AddPanel); Assert.AreEqual("hello", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).PanelCaption); Assert.AreEqual("views/dashboard/settings/settingsdashboardintro.html", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).ControlPath); - Assert.AreEqual(false, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).ShowOnce); - Assert.AreEqual(false, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).AddPanel); Assert.AreEqual("", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).PanelCaption); Assert.AreEqual("views/dashboard/settings/settingsdashboardvideos.html", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).ControlPath); @@ -118,9 +114,9 @@ namespace Umbraco.Tests.Configurations.DashboardSettings public void Test_Control_Access() { Assert.AreEqual(2, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.Count()); - Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Action); + Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Type); Assert.AreEqual("editor", SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Value); - Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Action); + Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Type); Assert.AreEqual("writer", SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Value); } } diff --git a/src/Umbraco.Tests/Manifest/ManifestContentAppTests.cs b/src/Umbraco.Tests/Manifest/ManifestContentAppTests.cs index eed0919149..016eb4113a 100644 --- a/src/Umbraco.Tests/Manifest/ManifestContentAppTests.cs +++ b/src/Umbraco.Tests/Manifest/ManifestContentAppTests.cs @@ -67,7 +67,8 @@ namespace Umbraco.Tests.Manifest private void AssertDefinition(object source, bool expected, string[] show, IReadOnlyUserGroup[] groups) { var definition = JsonConvert.DeserializeObject("{" + (show.Length == 0 ? "" : " \"show\": [" + string.Join(",", show.Select(x => "\"" + x + "\"")) + "] ") + "}"); - var app = definition.GetContentAppFor(source, groups); + var factory = new ManifestContentAppFactory(definition); + var app = factory.GetContentAppFor(source, groups); if (expected) Assert.IsNotNull(app); else diff --git a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs index 5145b848ed..19aaf79581 100644 --- a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs +++ b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Cache; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration.Dashboard; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.PropertyEditors; @@ -383,5 +384,53 @@ javascript: ['~/test.js',/*** some note about stuff asd09823-4**09234*/ '~/test2 Assert.AreEqual("icon-bar", app1.Icon); Assert.AreEqual("/App_Plugins/MyPackage/ContentApps/MyApp2.html", app1.View); } + + [Test] + public void CanParseManifest_Dashboards() + { + const string json = @"{'dashboards': [ + { + 'name': 'First One', + 'alias': 'something', + 'view': '~/App_Plugins/MyPackage/Dashboards/one.html', + 'sections': [ 'content' ], + 'access': [ {'grant':'user'}, {'deny':'foo'} ] + + }, + { + 'name': 'Second-One', + 'alias': 'something.else', + 'weight': -1, + 'view': '~/App_Plugins/MyPackage/Dashboards/two.html', + 'sections': [ 'forms' ], + } +]}"; + + var manifest = _parser.ParseManifest(json); + Assert.AreEqual(2, manifest.Dashboards.Length); + + Assert.IsInstanceOf(manifest.Dashboards[0]); + var db0 = manifest.Dashboards[0]; + Assert.AreEqual("something", db0.Alias); + Assert.AreEqual("First One", db0.Name); + Assert.AreEqual(100, db0.Weight); + Assert.AreEqual("/App_Plugins/MyPackage/Dashboards/one.html", db0.View); + Assert.AreEqual(1, db0.Sections.Length); + Assert.AreEqual("content", db0.Sections[0]); + Assert.AreEqual(2, db0.AccessRules.Length); + Assert.AreEqual(AccessRuleType.Grant, db0.AccessRules[0].Type); + Assert.AreEqual("user", db0.AccessRules[0].Value); + Assert.AreEqual(AccessRuleType.Deny, db0.AccessRules[1].Type); + Assert.AreEqual("foo", db0.AccessRules[1].Value); + + Assert.IsInstanceOf(manifest.Dashboards[1]); + var db1 = manifest.Dashboards[1]; + Assert.AreEqual("something.else", db1.Alias); + Assert.AreEqual("Second-One", db1.Name); + Assert.AreEqual(-1, db1.Weight); + Assert.AreEqual("/App_Plugins/MyPackage/Dashboards/two.html", db1.View); + Assert.AreEqual(1, db1.Sections.Length); + Assert.AreEqual("forms", db1.Sections[0]); + } } } diff --git a/src/Umbraco.Tests/Migrations/MigrationTests.cs b/src/Umbraco.Tests/Migrations/MigrationTests.cs index b408d351d7..d06cf2244e 100644 --- a/src/Umbraco.Tests/Migrations/MigrationTests.cs +++ b/src/Umbraco.Tests/Migrations/MigrationTests.cs @@ -1,16 +1,19 @@ using System; using System.Data; +using Moq; +using NUnit.Framework; using Semver; using Umbraco.Core.Events; -using Umbraco.Core.Logging; using Umbraco.Core.Migrations; using Umbraco.Core.Migrations.Upgrade; using Umbraco.Core.Persistence; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using ILogger = Umbraco.Core.Logging.ILogger; namespace Umbraco.Tests.Migrations { + [TestFixture] public class MigrationTests { public class TestUpgrader : Upgrader @@ -67,5 +70,68 @@ namespace Umbraco.Tests.Migrations public IScopeContext Context { get; set; } public ISqlContext SqlContext { get; set; } } + + [Test] + public void RunGoodMigration() + { + var migrationContext = new MigrationContext(Mock.Of(), Mock.Of()); + IMigration migration = new GoodMigration(migrationContext); + migration.Migrate(); + } + + [Test] + public void DetectBadMigration1() + { + var migrationContext = new MigrationContext(Mock.Of(), Mock.Of()); + IMigration migration = new BadMigration1(migrationContext); + Assert.Throws(() => migration.Migrate()); + } + + [Test] + public void DetectBadMigration2() + { + var migrationContext = new MigrationContext(Mock.Of(), Mock.Of()); + IMigration migration = new BadMigration2(migrationContext); + Assert.Throws(() => migration.Migrate()); + } + + public class GoodMigration : MigrationBase + { + public GoodMigration(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Execute.Sql("").Do(); + } + } + + public class BadMigration1 : MigrationBase + { + public BadMigration1(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Alter.Table("foo"); // stop here, don't Do it + } + } + + public class BadMigration2 : MigrationBase + { + public BadMigration2(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Alter.Table("foo"); // stop here, don't Do it + + // and try to start another one + Alter.Table("bar"); + } + } } } diff --git a/src/Umbraco.Tests/Models/Collections/PropertyCollectionTests.cs b/src/Umbraco.Tests/Models/Collections/PropertyCollectionTests.cs index 82aae0ada5..404dc5ba74 100644 --- a/src/Umbraco.Tests/Models/Collections/PropertyCollectionTests.cs +++ b/src/Umbraco.Tests/Models/Collections/PropertyCollectionTests.cs @@ -78,7 +78,7 @@ namespace Umbraco.Tests.Models.Collections [Test] public void PropertyGroups_Collection_FirstOrDefault_Returns_Null() { - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); Assert.That(contentType.PropertyGroups, Is.Not.Null); Assert.That(contentType.PropertyGroups.FirstOrDefault(x => x.Name.InvariantEquals("Content")) == null, Is.False); diff --git a/src/Umbraco.Tests/Models/ContentExtensionsTests.cs b/src/Umbraco.Tests/Models/ContentExtensionsTests.cs index 38a93c0c35..5c95f1ead5 100644 --- a/src/Umbraco.Tests/Models/ContentExtensionsTests.cs +++ b/src/Umbraco.Tests/Models/ContentExtensionsTests.cs @@ -16,7 +16,7 @@ namespace Umbraco.Tests.Models [Test] public void DirtyProperty_Reset_Clears_SavedPublishedState() { - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); content.PublishedState = PublishedState.Publishing; @@ -29,7 +29,7 @@ namespace Umbraco.Tests.Models [Test] public void DirtyProperty_OnlyIfActuallyChanged_Content() { - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); // if you assign a content property with its value it is not dirty @@ -51,7 +51,7 @@ namespace Umbraco.Tests.Models [Test] public void DirtyProperty_OnlyIfActuallyChanged_User() { - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); var prop = content.Properties.First(); @@ -75,7 +75,7 @@ namespace Umbraco.Tests.Models [Test] public void DirtyProperty_UpdateDate() { - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); var prop = content.Properties.First(); @@ -98,7 +98,7 @@ namespace Umbraco.Tests.Models [Test] public void DirtyProperty_WasDirty_ContentProperty() { - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); content.ResetDirtyProperties(false); Assert.IsFalse(content.IsDirty()); @@ -125,7 +125,7 @@ namespace Umbraco.Tests.Models [Test] public void DirtyProperty_WasDirty_ContentSortOrder() { - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); content.ResetDirtyProperties(false); Assert.IsFalse(content.IsDirty()); @@ -152,7 +152,7 @@ namespace Umbraco.Tests.Models [Test] public void DirtyProperty_WasDirty_UserProperty() { - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); var prop = content.Properties.First(); content.ResetDirtyProperties(false); diff --git a/src/Umbraco.Tests/Models/ContentScheduleTests.cs b/src/Umbraco.Tests/Models/ContentScheduleTests.cs new file mode 100644 index 0000000000..5d03da5bae --- /dev/null +++ b/src/Umbraco.Tests/Models/ContentScheduleTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Models; + +namespace Umbraco.Tests.Models +{ + [TestFixture] + public class ContentScheduleTests + { + [Test] + public void Release_Date_Less_Than_Expire_Date() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + Assert.IsFalse(schedule.Add(now, now)); + } + + [Test] + public void Cannot_Add_Duplicate_Dates_Invariant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, null); + Assert.Throws(() => schedule.Add(null, now)); + } + + [Test] + public void Cannot_Add_Duplicate_Dates_Variant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, null); + schedule.Add("en-US", now, null); + Assert.Throws(() => schedule.Add("en-US", null, now)); + Assert.Throws(() => schedule.Add(null, now)); + } + + [Test] + public void Can_Remove_Invariant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, null); + var invariantSched = schedule.GetSchedule(string.Empty); + schedule.Remove(invariantSched.First()); + Assert.AreEqual(0, schedule.FullSchedule.Count()); + } + + [Test] + public void Can_Remove_Variant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, null); + schedule.Add("en-US", now, null); + var invariantSched = schedule.GetSchedule(string.Empty); + schedule.Remove(invariantSched.First()); + Assert.AreEqual(0, schedule.GetSchedule(string.Empty).Count()); + Assert.AreEqual(1, schedule.FullSchedule.Count()); + var variantSched = schedule.GetSchedule("en-US"); + schedule.Remove(variantSched.First()); + Assert.AreEqual(0, schedule.GetSchedule("en-US").Count()); + Assert.AreEqual(0, schedule.FullSchedule.Count()); + } + + [Test] + public void Can_Clear_Start_Invariant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, now.AddDays(1)); + + schedule.Clear(ContentScheduleAction.Release); + + Assert.AreEqual(0, schedule.GetSchedule(ContentScheduleAction.Release).Count()); + Assert.AreEqual(1, schedule.GetSchedule(ContentScheduleAction.Expire).Count()); + Assert.AreEqual(1, schedule.FullSchedule.Count()); + } + + [Test] + public void Can_Clear_End_Variant() + { + var now = DateTime.Now; + var schedule = new ContentScheduleCollection(); + schedule.Add(now, now.AddDays(1)); + schedule.Add("en-US", now, now.AddDays(1)); + + schedule.Clear(ContentScheduleAction.Expire); + + Assert.AreEqual(0, schedule.GetSchedule(ContentScheduleAction.Expire).Count()); + Assert.AreEqual(1, schedule.GetSchedule(ContentScheduleAction.Release).Count()); + Assert.AreEqual(1, schedule.GetSchedule("en-US", ContentScheduleAction.Expire).Count()); + Assert.AreEqual(1, schedule.GetSchedule("en-US", ContentScheduleAction.Release).Count()); + Assert.AreEqual(3, schedule.FullSchedule.Count()); + + schedule.Clear("en-US", ContentScheduleAction.Expire); + + Assert.AreEqual(0, schedule.GetSchedule(ContentScheduleAction.Expire).Count()); + Assert.AreEqual(1, schedule.GetSchedule(ContentScheduleAction.Release).Count()); + Assert.AreEqual(0, schedule.GetSchedule("en-US", ContentScheduleAction.Expire).Count()); + Assert.AreEqual(1, schedule.GetSchedule("en-US", ContentScheduleAction.Release).Count()); + Assert.AreEqual(2, schedule.FullSchedule.Count()); + } + + } +} diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs index 31110d7196..d31fcbf744 100644 --- a/src/Umbraco.Tests/Models/ContentTests.cs +++ b/src/Umbraco.Tests/Models/ContentTests.cs @@ -22,6 +22,7 @@ using Umbraco.Tests.Testing; namespace Umbraco.Tests.Models { + [TestFixture] public class ContentTests : UmbracoTestBase { @@ -127,7 +128,7 @@ namespace Umbraco.Tests.Models [Test] public void All_Dirty_Properties_Get_Reset() { - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); content.ResetDirtyProperties(false); @@ -143,7 +144,7 @@ namespace Umbraco.Tests.Models public void Can_Verify_Mocked_Content() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); // Act @@ -156,7 +157,7 @@ namespace Umbraco.Tests.Models public void Can_Change_Property_Value() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); // Act @@ -172,7 +173,7 @@ namespace Umbraco.Tests.Models public void Can_Set_Property_Value_As_String() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); // Act @@ -188,7 +189,7 @@ namespace Umbraco.Tests.Models public void Can_Clone_Content_With_Reset_Identity() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); content.Id = 10; content.Key = new Guid("29181B97-CB8F-403F-86DE-5FEB497F4800"); @@ -217,7 +218,7 @@ namespace Umbraco.Tests.Models public void Can_Deep_Clone_Perf_Test() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.Id = 99; var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); var i = 200; @@ -228,11 +229,10 @@ namespace Umbraco.Tests.Models content.Id = 10; content.CreateDate = DateTime.Now; content.CreatorId = 22; - content.ExpireDate = DateTime.Now; content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ReleaseDate = DateTime.Now; + content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); //content.ChangePublishedState(PublishedState.Published); content.SortOrder = 5; content.Template = new Template((string) "Test Template", (string) "testTemplate") @@ -269,7 +269,7 @@ namespace Umbraco.Tests.Models public void Can_Deep_Clone() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.Id = 99; contentType.Variations = ContentVariation.Culture; var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); @@ -292,11 +292,10 @@ namespace Umbraco.Tests.Models content.Id = 10; content.CreateDate = DateTime.Now; content.CreatorId = 22; - content.ExpireDate = DateTime.Now; content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ReleaseDate = DateTime.Now; + content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); content.SortOrder = 5; content.Template = new Template((string) "Test Template", (string) "testTemplate") { @@ -333,11 +332,10 @@ namespace Umbraco.Tests.Models Assert.AreEqual(clone.ContentTypeId, content.ContentTypeId); Assert.AreEqual(clone.CreateDate, content.CreateDate); Assert.AreEqual(clone.CreatorId, content.CreatorId); - Assert.AreEqual(clone.ExpireDate, content.ExpireDate); Assert.AreEqual(clone.Key, content.Key); Assert.AreEqual(clone.Level, content.Level); Assert.AreEqual(clone.Path, content.Path); - Assert.AreEqual(clone.ReleaseDate, content.ReleaseDate); + Assert.IsTrue(clone.ContentSchedule.Equals(content.ContentSchedule)); Assert.AreEqual(clone.Published, content.Published); Assert.AreEqual(clone.PublishedState, content.PublishedState); Assert.AreEqual(clone.SortOrder, content.SortOrder); @@ -396,7 +394,7 @@ namespace Umbraco.Tests.Models public void Remember_Dirty_Properties() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.Id = 99; contentType.Variations = ContentVariation.Culture; var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); @@ -413,11 +411,10 @@ namespace Umbraco.Tests.Models content.Id = 10; content.CreateDate = DateTime.Now; content.CreatorId = 22; - content.ExpireDate = DateTime.Now; + content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ReleaseDate = DateTime.Now; content.SortOrder = 5; content.Template = new Template((string)"Test Template", (string)"testTemplate") { @@ -439,11 +436,10 @@ namespace Umbraco.Tests.Models Assert.IsTrue(content.WasPropertyDirty("Id")); Assert.IsTrue(content.WasPropertyDirty("CreateDate")); Assert.IsTrue(content.WasPropertyDirty("CreatorId")); - Assert.IsTrue(content.WasPropertyDirty("ExpireDate")); Assert.IsTrue(content.WasPropertyDirty("Key")); Assert.IsTrue(content.WasPropertyDirty("Level")); Assert.IsTrue(content.WasPropertyDirty("Path")); - Assert.IsTrue(content.WasPropertyDirty("ReleaseDate")); + Assert.IsTrue(content.WasPropertyDirty("ContentSchedule")); Assert.IsTrue(content.WasPropertyDirty("SortOrder")); Assert.IsTrue(content.WasPropertyDirty("Template")); Assert.IsTrue(content.WasPropertyDirty("Trashed")); @@ -479,7 +475,7 @@ namespace Umbraco.Tests.Models var ss = new SerializationService(new JsonNetSerializer()); // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.Id = 99; var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); var i = 200; @@ -490,11 +486,10 @@ namespace Umbraco.Tests.Models content.Id = 10; content.CreateDate = DateTime.Now; content.CreatorId = 22; - content.ExpireDate = DateTime.Now; content.Key = Guid.NewGuid(); content.Level = 3; content.Path = "-1,4,10"; - content.ReleaseDate = DateTime.Now; + content.ContentSchedule.Add(DateTime.Now, DateTime.Now.AddDays(1)); //content.ChangePublishedState(PublishedState.Publishing); content.SortOrder = 5; content.Template = new Template((string) "Test Template", (string) "testTemplate") @@ -535,7 +530,7 @@ namespace Umbraco.Tests.Models public void Can_Change_Property_Value_Through_Anonymous_Object() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); // Act @@ -553,7 +548,7 @@ namespace Umbraco.Tests.Models public void Can_Verify_Dirty_Property_On_Content() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); // Act @@ -569,7 +564,7 @@ namespace Umbraco.Tests.Models public void Can_Add_PropertyGroup_On_ContentType() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); // Act @@ -584,7 +579,7 @@ namespace Umbraco.Tests.Models public void Can_Remove_PropertyGroup_From_ContentType() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.ResetDirtyProperties(); // Act @@ -599,7 +594,7 @@ namespace Umbraco.Tests.Models public void Can_Add_PropertyType_To_Group_On_ContentType() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); // Act @@ -621,7 +616,7 @@ namespace Umbraco.Tests.Models public void Can_Add_New_Property_To_New_PropertyType() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); // Act @@ -643,7 +638,7 @@ namespace Umbraco.Tests.Models public void Can_Add_New_Property_To_New_PropertyType_In_New_PropertyGroup() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); // Act @@ -674,7 +669,7 @@ namespace Umbraco.Tests.Models public void Can_Update_PropertyType_Through_Content_Properties() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); // Act - note that the PropertyType's properties like SortOrder is not updated through the Content object @@ -694,7 +689,7 @@ namespace Umbraco.Tests.Models public void Can_Change_ContentType_On_Content() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var simpleContentType = MockedContentTypes.CreateSimpleContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); @@ -713,7 +708,7 @@ namespace Umbraco.Tests.Models public void Can_Change_ContentType_On_Content_And_Set_Property_Value() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var simpleContentType = MockedContentTypes.CreateSimpleContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); @@ -730,7 +725,7 @@ namespace Umbraco.Tests.Models public void Can_Change_ContentType_On_Content_And_Still_Get_Old_Properties() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var simpleContentType = MockedContentTypes.CreateSimpleContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); @@ -749,7 +744,7 @@ namespace Umbraco.Tests.Models public void Can_Change_ContentType_On_Content_And_Clear_Old_PropertyTypes() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var simpleContentType = MockedContentTypes.CreateSimpleContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); @@ -765,7 +760,7 @@ namespace Umbraco.Tests.Models [Test] public void Can_Verify_Content_Is_Published() { - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); content.ResetDirtyProperties(); @@ -799,7 +794,7 @@ namespace Umbraco.Tests.Models public void Adding_PropertyGroup_To_ContentType_Results_In_Dirty_Entity() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.ResetDirtyProperties(); // Act @@ -816,7 +811,7 @@ namespace Umbraco.Tests.Models public void After_Committing_Changes_Was_Dirty_Is_True() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.ResetDirtyProperties(); //reset // Act @@ -833,7 +828,7 @@ namespace Umbraco.Tests.Models public void After_Committing_Changes_Was_Dirty_Is_True_On_Changed_Property() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.ResetDirtyProperties(); //reset var content = MockedContent.CreateTextpageContent(contentType, "test", -1); content.ResetDirtyProperties(); @@ -864,7 +859,7 @@ namespace Umbraco.Tests.Models public void If_Not_Committed_Was_Dirty_Is_False() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); // Act contentType.Alias = "newAlias"; @@ -878,7 +873,7 @@ namespace Umbraco.Tests.Models public void Detect_That_A_Property_Is_Removed() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); Assert.That(contentType.WasPropertyDirty("HasPropertyTypeBeenRemoved"), Is.False); // Act @@ -892,7 +887,7 @@ namespace Umbraco.Tests.Models public void Adding_PropertyType_To_PropertyGroup_On_ContentType_Results_In_Dirty_Entity() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.ResetDirtyProperties(); // Act @@ -979,7 +974,7 @@ namespace Umbraco.Tests.Models [Test] public void Can_Avoid_Circular_Dependencies_In_Composition() { - var textPage = MockedContentTypes.CreateTextpageContentType(); + var textPage = MockedContentTypes.CreateTextPageContentType(); var parent = MockedContentTypes.CreateSimpleContentType("parent", "Parent", null, true); var meta = MockedContentTypes.CreateMetaContentType(); var mixin1 = MockedContentTypes.CreateSimpleContentType("mixin1", "Mixin1", new PropertyTypeCollection(true, diff --git a/src/Umbraco.Tests/Models/ContentTypeTests.cs b/src/Umbraco.Tests/Models/ContentTypeTests.cs index a0e9a370da..6ef28d8290 100644 --- a/src/Umbraco.Tests/Models/ContentTypeTests.cs +++ b/src/Umbraco.Tests/Models/ContentTypeTests.cs @@ -33,7 +33,7 @@ namespace Umbraco.Tests.Models [Test] public void Can_Deep_Clone_Content_Type_With_Reset_Identities() { - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.Id = 99; var i = 200; @@ -105,7 +105,7 @@ namespace Umbraco.Tests.Models public void Can_Deep_Clone_Content_Type_Perf_Test() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.Id = 99; var i = 200; @@ -155,7 +155,7 @@ namespace Umbraco.Tests.Models public void Can_Deep_Clone_Content_Type() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.Id = 99; var i = 200; @@ -256,7 +256,7 @@ namespace Umbraco.Tests.Models var ss = new SerializationService(new JsonNetSerializer()); // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); contentType.Id = 99; var i = 200; diff --git a/src/Umbraco.Tests/Models/ContentXmlTest.cs b/src/Umbraco.Tests/Models/ContentXmlTest.cs index 9cc41ffa6f..ab318ec1cb 100644 --- a/src/Umbraco.Tests/Models/ContentXmlTest.cs +++ b/src/Umbraco.Tests/Models/ContentXmlTest.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.Models public void Can_Generate_Xml_Representation_Of_Content() { // Arrange - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! ServiceContext.ContentTypeService.Save(contentType); diff --git a/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs b/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs index 0d5c9548fd..76e618ea26 100644 --- a/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs +++ b/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs @@ -427,7 +427,7 @@ namespace Umbraco.Tests.Models.Mapping // setup the mocks to return the data we want to test against... - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); MockedContentTypes.EnsureAllIds(contentType, 8888); //Act diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs b/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs index 570fbfb7e3..db3b02bacf 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/PetaPocoCachesTest.cs @@ -129,9 +129,9 @@ namespace Umbraco.Tests.Persistence.NPocoTests contentService.GetRootContent(); - contentService.GetContentForExpiration(); + contentService.GetContentForExpiration(DateTime.Now); - contentService.GetContentForRelease(); + contentService.GetContentForRelease(DateTime.Now); ((ContentService)contentService).GetPublishedDescendants(new Content("Test", -1, new ContentType(-1)) { diff --git a/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs b/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs index 64e8d41669..ce6447f4b5 100644 --- a/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs +++ b/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs @@ -35,8 +35,8 @@ namespace Umbraco.Tests.Persistence.Querying scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} OFF ", SqlSyntax.GetQuotedTableName("umbracoNode")))); scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} ON ", SqlSyntax.GetQuotedTableName("cmsTemplate")))); - scope.Database.Insert("cmsTemplate", "pk", false, new TemplateDto { NodeId = 55554, Alias = "testTemplate1", Design = "", PrimaryKey = 22221}); - scope.Database.Insert("cmsTemplate", "pk", false, new TemplateDto { NodeId = 55555, Alias = "testTemplate2", Design = "", PrimaryKey = 22222 }); + scope.Database.Insert("cmsTemplate", "pk", false, new TemplateDto { NodeId = 55554, Alias = "testTemplate1", PrimaryKey = 22221}); + scope.Database.Insert("cmsTemplate", "pk", false, new TemplateDto { NodeId = 55555, Alias = "testTemplate2", PrimaryKey = 22222 }); scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} OFF ", SqlSyntax.GetQuotedTableName("cmsTemplate")))); scope.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} ON ", SqlSyntax.GetQuotedTableName("cmsContentType")))); diff --git a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs index f73831e8bc..70d70d4a31 100644 --- a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs +++ b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs @@ -52,9 +52,9 @@ namespace Umbraco.Tests.Persistence.Querying } [Test] - public void Can_Query_With_Content_Type_Aliases() + public void Can_Query_With_Content_Type_Aliases_IEnumerable() { - //Arrange + //Arrange - Contains is IEnumerable.Contains extension method var aliases = new[] { "Test1", "Test2" }; Expression> predicate = content => aliases.Contains(content.ContentType.Alias); var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(SqlContext.SqlSyntax, Mappers); @@ -67,6 +67,22 @@ namespace Umbraco.Tests.Persistence.Querying Assert.AreEqual("Test2", modelToSqlExpressionHelper.GetSqlParameters()[2]); } + [Test] + public void Can_Query_With_Content_Type_Aliases_List() + { + //Arrange - Contains is List.Contains instance method + var aliases = new System.Collections.Generic.List { "Test1", "Test2" }; + Expression> predicate = content => aliases.Contains(content.ContentType.Alias); + var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(SqlContext.SqlSyntax, Mappers); + var result = modelToSqlExpressionHelper.Visit(predicate); + + Debug.Print("Model to Sql ExpressionHelper: \n" + result); + + Assert.AreEqual("[cmsContentType].[alias] IN (@1,@2)", result); + Assert.AreEqual("Test1", modelToSqlExpressionHelper.GetSqlParameters()[1]); + Assert.AreEqual("Test2", modelToSqlExpressionHelper.GetSqlParameters()[2]); + } + [Test] public void CachedExpression_Can_Verify_Path_StartsWith_Predicate_In_Same_Result() { diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 6087279285..6a282169fb 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -964,7 +964,7 @@ namespace Umbraco.Tests.Persistence.Repositories ServiceContext.ContentTypeService.Save(simpleContentType); //Create and Save ContentType "textPage" -> (NodeDto.NodeIdSeed + 1) - ContentType textpageContentType = MockedContentTypes.CreateTextpageContentType(); + ContentType textpageContentType = MockedContentTypes.CreateTextPageContentType(); ServiceContext.ContentTypeService.Save(textpageContentType); } } diff --git a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs index 8e7681d17a..df842dc43c 100644 --- a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs +++ b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs @@ -89,7 +89,7 @@ WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, var sqlSyntax = new SqlServerSyntaxProvider(new Lazy(() => null)); var indexDefinition = CreateIndexDefinition(); - indexDefinition.IsClustered = false; + indexDefinition.IndexType = IndexTypes.Clustered; var actual = sqlSyntax.Format(indexDefinition); Assert.AreEqual("CREATE CLUSTERED INDEX [IX_A] ON [TheTable] ([A])", actual); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs index b9d106ca1b..508a005663 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs @@ -23,6 +23,7 @@ using Umbraco.Tests.Testing; using LightInject; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Tests.PublishedContent { @@ -110,13 +111,16 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void Ensure_Children_Sorted_With_Examine() { - using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, options: new UmbracoContentIndexerOptions(true, false, null))) - { + var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); - indexer.RebuildIndex(); + using (var luceneDir = new RandomIdRamDirectory()) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, + validator: new ContentValueSetValidator(true))) + { + rebuilder.Populate(indexer); var searcher = indexer.GetSearcher(); var ctx = GetUmbracoContext("/test"); @@ -135,16 +139,18 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void Do_Not_Find_In_Recycle_Bin() { + var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, //include unpublished content since this uses the 'internal' indexer, it's up to the media cache to filter - options: new UmbracoContentIndexerOptions(true, false, null))) + validator: new ContentValueSetValidator(true))) using (indexer.ProcessNonAsync()) { - indexer.RebuildIndex(); - + rebuilder.Populate(indexer); var searcher = indexer.GetSearcher(); var ctx = GetUmbracoContext("/test"); @@ -181,14 +187,17 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void Children_With_Examine() { + var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, options: new UmbracoContentIndexerOptions(true, false, null))) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, + validator: new ContentValueSetValidator(true))) using (indexer.ProcessNonAsync()) { - indexer.RebuildIndex(); - + rebuilder.Populate(indexer); var searcher = indexer.GetSearcher(); var ctx = GetUmbracoContext("/test"); @@ -206,14 +215,17 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void Descendants_With_Examine() { + var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, options: new UmbracoContentIndexerOptions(true, false, null))) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, + validator: new ContentValueSetValidator(true))) using (indexer.ProcessNonAsync()) { - indexer.RebuildIndex(); - + rebuilder.Populate(indexer); var searcher = indexer.GetSearcher(); var ctx = GetUmbracoContext("/test"); @@ -231,14 +243,17 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void DescendantsOrSelf_With_Examine() { + var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, options: new UmbracoContentIndexerOptions(true, false, null))) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, + validator: new ContentValueSetValidator(true))) using (indexer.ProcessNonAsync()) { - indexer.RebuildIndex(); - + rebuilder.Populate(indexer); var searcher = indexer.GetSearcher(); var ctx = GetUmbracoContext("/test"); @@ -256,14 +271,18 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void Ancestors_With_Examine() { + var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); + + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, options: new UmbracoContentIndexerOptions(true, false, null))) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, + validator: new ContentValueSetValidator(true))) using (indexer.ProcessNonAsync()) { - indexer.RebuildIndex(); - + rebuilder.Populate(indexer); var ctx = GetUmbracoContext("/test"); var searcher = indexer.GetSearcher(); @@ -278,13 +297,17 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void AncestorsOrSelf_With_Examine() { + var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, options: new UmbracoContentIndexerOptions(true, false, null))) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, + validator: new ContentValueSetValidator(true))) using (indexer.ProcessNonAsync()) { - indexer.RebuildIndex(); + rebuilder.Populate(indexer); var ctx = GetUmbracoContext("/test"); diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs old mode 100644 new mode 100755 index 770dead600..9a8164356a --- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs +++ b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs @@ -13,7 +13,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Runtime; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; -using Umbraco.Examine; +using Umbraco.Web; namespace Umbraco.Tests.Runtimes { @@ -60,9 +60,13 @@ namespace Umbraco.Tests.Runtimes { protected override IRuntime GetRuntime() { - return new TestRuntime(this); + return new TestRuntime(); } + } + // test runtime + public class TestRuntime : CoreRuntime + { // the application's logger is created by the application // through GetLogger, that custom application can override protected override ILogger GetLogger() @@ -71,18 +75,6 @@ namespace Umbraco.Tests.Runtimes return new DebugDiagnosticsLogger(); } - // don't register anything against AppDomain - protected override void ConfigureUnhandledException(ILogger logger) - { } - } - - // test runtime - public class TestRuntime : CoreRuntime - { - public TestRuntime(UmbracoApplicationBase umbracoApplication) - : base(umbracoApplication) - { } - public override void Compose(ServiceContainer container) { base.Compose(container); diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index 4d4825323c..322b79429e 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -65,9 +65,9 @@ namespace Umbraco.Tests.Services // ... NOPE, made even more nice changes, it is now... // 4452ms !!!!!!! - var contentType1 = MockedContentTypes.CreateTextpageContentType("test1", "test1"); - var contentType2 = MockedContentTypes.CreateTextpageContentType("test2", "test2"); - var contentType3 = MockedContentTypes.CreateTextpageContentType("test3", "test3"); + var contentType1 = MockedContentTypes.CreateTextPageContentType("test1", "test1"); + var contentType2 = MockedContentTypes.CreateTextPageContentType("test2", "test2"); + var contentType3 = MockedContentTypes.CreateTextPageContentType("test3", "test3"); ServiceContext.ContentTypeService.Save(new[] { contentType1, contentType2, contentType3 }); contentType1.AllowedContentTypes = new[] { @@ -291,7 +291,7 @@ namespace Umbraco.Tests.Services public void CreateTestData() { //Create and Save ContentType "textpage" -> NodeDto.NodeIdSeed - ContentType contentType = MockedContentTypes.CreateTextpageContentType(); + ContentType contentType = MockedContentTypes.CreateTextPageContentType(); ServiceContext.ContentTypeService.Save(contentType); } } diff --git a/src/Umbraco.Tests/Services/ContentServicePublishBranchTests.cs b/src/Umbraco.Tests/Services/ContentServicePublishBranchTests.cs new file mode 100644 index 0000000000..4afd5e33eb --- /dev/null +++ b/src/Umbraco.Tests/Services/ContentServicePublishBranchTests.cs @@ -0,0 +1,476 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing; + +// ReSharper disable CommentTypo +// ReSharper disable StringLiteralTypo +namespace Umbraco.Tests.Services +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, WithApplication = true)] + public class ContentServicePublishBranchTests : TestWithDatabaseBase + { + [TestCase(1)] // use overload w/ culture: "*" + [TestCase(2)] // use overload w/ cultures: new [] { "*" } + public void Can_Publish_Invariant_Branch(int method) + { + CreateTypes(out var iContentType, out _); + + IContent iRoot = new Content("iroot", -1, iContentType); + iRoot.SetValue("ip", "iroot"); + ServiceContext.ContentService.Save(iRoot); + IContent ii1 = new Content("ii1", iRoot, iContentType); + ii1.SetValue("ip", "vii1"); + ServiceContext.ContentService.Save(ii1); + IContent ii2 = new Content("ii2", iRoot, iContentType); + ii2.SetValue("ip", "vii2"); + ServiceContext.ContentService.Save(ii2); + + // iroot !published !edited + // ii1 !published !edited + // ii2 !published !edited + + // !force = publishes those that are actually published, and have changes + // here: root (root is always published) + + var r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + + // not forcing, ii1 and ii2 not published yet: only root got published + AssertPublishResults(r, x => x.Content.Name, + "iroot"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublish); + + // prepare + + ServiceContext.ContentService.SaveAndPublish(iRoot); + ServiceContext.ContentService.SaveAndPublish(ii1); + + IContent ii11 = new Content("ii11", ii1, iContentType); + ii11.SetValue("ip", "vii11"); + ServiceContext.ContentService.SaveAndPublish(ii11); + IContent ii12 = new Content("ii12", ii1, iContentType); + ii11.SetValue("ip", "vii12"); + ServiceContext.ContentService.Save(ii12); + + ServiceContext.ContentService.SaveAndPublish(ii2); + IContent ii21 = new Content("ii21", ii2, iContentType); + ii21.SetValue("ip", "vii21"); + ServiceContext.ContentService.SaveAndPublish(ii21); + IContent ii22 = new Content("ii22", ii2, iContentType); + ii22.SetValue("ip", "vii22"); + ServiceContext.ContentService.Save(ii22); + ServiceContext.ContentService.Unpublish(ii2); + + // iroot published !edited + // ii1 published !edited + // ii11 published !edited + // ii12 !published !edited + // ii2 !published !edited + // ii21 (published) !edited + // ii22 !published !edited + + // !force = publishes those that are actually published, and have changes + // here: nothing + + r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + + // not forcing, ii12 and ii2, ii21, ii22 not published yet: only root, ii1, ii11 got published + AssertPublishResults(r, x => x.Content.Name, + "iroot", "ii1", "ii11"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready); + + // prepare + + iRoot.SetValue("ip", "changed"); + ServiceContext.ContentService.Save(iRoot); + ii11.SetValue("ip", "changed"); + ServiceContext.ContentService.Save(ii11); + + // iroot published edited *** + // ii1 published !edited + // ii11 published edited *** + // ii12 !published !edited + // ii2 !published !edited + // ii21 (published) !edited + // ii22 !published !edited + + // !force = publishes those that are actually published, and have changes + // here: iroot and ii11 + + // not forcing, ii12 and ii2, ii21, ii22 not published yet: only root, ii1, ii11 got published + r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + AssertPublishResults(r, x => x.Content.Name, + "iroot", "ii1", "ii11"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublish, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublish); + + // force = publishes everything that has changes + // here: ii12, ii2, ii22 - ii21 was published already but masked + + r = SaveAndPublishInvariantBranch(iRoot, true, method).ToArray(); + AssertPublishResults(r, x => x.Content.Name, + "iroot", "ii1", "ii11", "ii12", "ii2", "ii21", "ii22"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublish, + PublishResultType.SuccessPublish, + PublishResultType.SuccessPublishAlready, // was masked + PublishResultType.SuccessPublish); + + ii21 = ServiceContext.ContentService.GetById(ii21.Id); + Assert.IsTrue(ii21.Published); + } + + [Test] + public void Can_Publish_Variant_Branch_When_No_Changes_On_Root_All_Cultures() + { + CreateTypes(out _, out var vContentType); + + //create/publish root + IContent vRoot = new Content("vroot", -1, vContentType, "de"); + vRoot.SetCultureName("vroot.de", "de"); + vRoot.SetCultureName("vroot.ru", "ru"); + vRoot.SetCultureName("vroot.es", "es"); + vRoot.SetValue("ip", "vroot"); + vRoot.SetValue("vp", "vroot.de", "de"); + vRoot.SetValue("vp", "vroot.ru", "ru"); + vRoot.SetValue("vp", "vroot.es", "es"); + ServiceContext.ContentService.SaveAndPublish(vRoot); + + //create/publish child + IContent iv1 = new Content("iv1", vRoot, vContentType, "de"); + iv1.SetCultureName("iv1.de", "de"); + iv1.SetCultureName("iv1.ru", "ru"); + iv1.SetCultureName("iv1.es", "es"); + iv1.SetValue("ip", "iv1"); + iv1.SetValue("vp", "iv1.de", "de"); + iv1.SetValue("vp", "iv1.ru", "ru"); + iv1.SetValue("vp", "iv1.es", "es"); + ServiceContext.ContentService.SaveAndPublish(iv1); + + //update the child + iv1.SetValue("vp", "UPDATED-iv1.de", "de"); + ServiceContext.ContentService.Save(iv1); + + var r = ServiceContext.ContentService.SaveAndPublishBranch(vRoot, false).ToArray(); //no culture specified so "*" is used, so all cultures + Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); + Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); + } + + [Test] + public void Can_Publish_Variant_Branch_When_No_Changes_On_Root_Specific_Culture() + { + CreateTypes(out _, out var vContentType); + + //create/publish root + IContent vRoot = new Content("vroot", -1, vContentType, "de"); + vRoot.SetCultureName("vroot.de", "de"); + vRoot.SetCultureName("vroot.ru", "ru"); + vRoot.SetCultureName("vroot.es", "es"); + vRoot.SetValue("ip", "vroot"); + vRoot.SetValue("vp", "vroot.de", "de"); + vRoot.SetValue("vp", "vroot.ru", "ru"); + vRoot.SetValue("vp", "vroot.es", "es"); + ServiceContext.ContentService.SaveAndPublish(vRoot); + + //create/publish child + IContent iv1 = new Content("iv1", vRoot, vContentType, "de"); + iv1.SetCultureName("iv1.de", "de"); + iv1.SetCultureName("iv1.ru", "ru"); + iv1.SetCultureName("iv1.es", "es"); + iv1.SetValue("ip", "iv1"); + iv1.SetValue("vp", "iv1.de", "de"); + iv1.SetValue("vp", "iv1.ru", "ru"); + iv1.SetValue("vp", "iv1.es", "es"); + ServiceContext.ContentService.SaveAndPublish(iv1); + + //update the child + iv1.SetValue("vp", "UPDATED-iv1.de", "de"); + ServiceContext.ContentService.Save(iv1); + + var r = ServiceContext.ContentService.SaveAndPublishBranch(vRoot, false, "de").ToArray(); + Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); + Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); + } + + [Test] + public void Can_Publish_Variant_Branch() + { + CreateTypes(out _, out var vContentType); + + IContent vRoot = new Content("vroot", -1, vContentType, "de"); + vRoot.SetCultureName("vroot.de", "de"); + vRoot.SetCultureName("vroot.ru", "ru"); + vRoot.SetCultureName("vroot.es", "es"); + vRoot.SetValue("ip", "vroot"); + vRoot.SetValue("vp", "vroot.de", "de"); + vRoot.SetValue("vp", "vroot.ru", "ru"); + vRoot.SetValue("vp", "vroot.es", "es"); + ServiceContext.ContentService.Save(vRoot); + + IContent iv1 = new Content("iv1", vRoot, vContentType, "de"); + iv1.SetCultureName("iv1.de", "de"); + iv1.SetCultureName("iv1.ru", "ru"); + iv1.SetCultureName("iv1.es", "es"); + iv1.SetValue("ip", "iv1"); + iv1.SetValue("vp", "iv1.de", "de"); + iv1.SetValue("vp", "iv1.ru", "ru"); + iv1.SetValue("vp", "iv1.es", "es"); + ServiceContext.ContentService.Save(iv1); + + IContent iv2 = new Content("iv2", vRoot, vContentType, "de"); + iv2.SetCultureName("iv2.de", "de"); + iv2.SetCultureName("iv2.ru", "ru"); + iv2.SetCultureName("iv2.es", "es"); + iv2.SetValue("ip", "iv2"); + iv2.SetValue("vp", "iv2.de", "de"); + iv2.SetValue("vp", "iv2.ru", "ru"); + iv2.SetValue("vp", "iv2.es", "es"); + ServiceContext.ContentService.Save(iv2); + + // vroot !published !edited + // iv1 !published !edited + // iv2 !published !edited + + // !force = publishes those that are actually published, and have changes + // here: nothing + + var r = ServiceContext.ContentService.SaveAndPublishBranch(vRoot, false).ToArray(); // no culture specified = all cultures + + // not forcing, iv1 and iv2 not published yet: only root got published + AssertPublishResults(r, x => x.Content.Name, + "vroot.de"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishCulture); + + // prepare + vRoot.SetValue("ip", "changed"); + vRoot.SetValue("vp", "changed.de", "de"); + vRoot.SetValue("vp", "changed.ru", "ru"); + vRoot.SetValue("vp", "changed.es", "es"); + ServiceContext.ContentService.Save(vRoot); // now root has drafts in all cultures + + iv1.PublishCulture("de"); + iv1.PublishCulture("ru"); + ServiceContext.ContentService.SavePublishing(iv1); // now iv1 de and ru are published + + iv1.SetValue("ip", "changed"); + iv1.SetValue("vp", "changed.de", "de"); + iv1.SetValue("vp", "changed.ru", "ru"); + iv1.SetValue("vp", "changed.es", "es"); + ServiceContext.ContentService.Save(iv1); // now iv1 has drafts in all cultures + + // validate - everything published for root, because no culture was specified = all + Assert.IsTrue(vRoot.Published); + Assert.IsTrue(vRoot.IsCulturePublished("de")); + Assert.IsTrue(vRoot.IsCulturePublished("ru")); + Assert.IsTrue(vRoot.IsCulturePublished("es")); + + // validate - only some cultures published for iv1 + Assert.IsTrue(iv1.Published); + Assert.IsTrue(iv1.IsCulturePublished("de")); + Assert.IsTrue(iv1.IsCulturePublished("ru")); + Assert.IsFalse(iv1.IsCulturePublished("es")); + + r = ServiceContext.ContentService.SaveAndPublishBranch(vRoot, false, "de").ToArray(); + + // not forcing, iv2 not published yet: only root and iv1 got published + AssertPublishResults(r, x => x.Content.Name, + "vroot.de", "iv1.de"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishCulture, + PublishResultType.SuccessPublishCulture); + + // reload - SaveAndPublishBranch has modified other instances + Reload(ref iv1); + Reload(ref iv2); + + // validate - root + Assert.IsTrue(vRoot.Published); + Assert.IsTrue(vRoot.IsCulturePublished("de")); + Assert.IsFalse(vRoot.IsCultureEdited("de")); // no drafts, this was just published + Assert.IsTrue(vRoot.IsCulturePublished("ru")); + Assert.IsTrue(vRoot.IsCultureEdited("ru")); // has draft + Assert.IsTrue(vRoot.IsCulturePublished("es")); + Assert.IsTrue(vRoot.IsCultureEdited("es")); // has draft + + Assert.AreEqual("changed", vRoot.GetValue("ip", published: true)); // publishing de implies publishing invariants + Assert.AreEqual("changed.de", vRoot.GetValue("vp", "de", published: true)); + + // validate - de and ru are published, es has not been published + Assert.IsTrue(iv1.Published); + Assert.IsTrue(iv1.IsCulturePublished("de")); + Assert.IsTrue(iv1.IsCulturePublished("ru")); + Assert.IsFalse(iv1.IsCulturePublished("es")); + Assert.AreEqual("changed", iv1.GetValue("ip", published: true)); + Assert.AreEqual("changed.de", iv1.GetValue("vp", "de", published: true)); + Assert.AreEqual("iv1.ru", iv1.GetValue("vp", "ru", published: true)); + } + + private void Can_Publish_Mixed_Branch(out IContent iRoot, out IContent ii1, out IContent iv11) + { + // invariant root -> variant -> invariant + // variant root -> variant -> invariant + // variant root -> invariant -> variant + + CreateTypes(out var iContentType, out var vContentType); + + // invariant root -> invariant -> variant + iRoot = new Content("iroot", -1, iContentType); + iRoot.SetValue("ip", "iroot"); + ServiceContext.ContentService.SaveAndPublish(iRoot); + ii1 = new Content("ii1", iRoot, iContentType); + ii1.SetValue("ip", "vii1"); + ServiceContext.ContentService.SaveAndPublish(ii1); + ii1.SetValue("ip", "changed"); + ServiceContext.ContentService.Save(ii1); + iv11 = new Content("iv11.de", ii1, vContentType, "de"); + iv11.SetValue("ip", "iv11"); + iv11.SetValue("vp", "iv11.de", "de"); + iv11.SetValue("vp", "iv11.ru", "ru"); + iv11.SetValue("vp", "iv11.es", "es"); + ServiceContext.ContentService.Save(iv11); + + iv11.PublishCulture("de"); + iv11.SetCultureName("iv11.ru", "ru"); + iv11.PublishCulture("ru"); + ServiceContext.ContentService.SavePublishing(iv11); + + Assert.AreEqual("iv11.de", iv11.GetValue("vp", "de", published: true)); + Assert.AreEqual("iv11.ru", iv11.GetValue("vp", "ru", published: true)); + + iv11.SetValue("ip", "changed"); + iv11.SetValue("vp", "changed.de", "de"); + iv11.SetValue("vp", "changed.ru", "ru"); + ServiceContext.ContentService.Save(iv11); + } + + [Test] + public void Can_Publish_Mixed_Branch_1() + { + Can_Publish_Mixed_Branch(out var iRoot, out var ii1, out var iv11); + + var r = ServiceContext.ContentService.SaveAndPublishBranch(iRoot, false, "de").ToArray(); + AssertPublishResults(r, x => x.Content.Name, + "iroot", "ii1", "iv11.de"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublish, + PublishResultType.SuccessPublishCulture); + + // reload - SaveAndPublishBranch has modified other instances + Reload(ref ii1); + Reload(ref iv11); + + // the invariant child has been published + // the variant child has been published for 'de' only + + Assert.AreEqual("changed", ii1.GetValue("ip", published: true)); + Assert.AreEqual("changed", iv11.GetValue("ip", published: true)); + Assert.AreEqual("changed.de", iv11.GetValue("vp", "de", published: true)); + Assert.AreEqual("iv11.ru", iv11.GetValue("vp", "ru", published: true)); + } + + [Test] + public void Can_Publish_MixedBranch_2() + { + Can_Publish_Mixed_Branch(out var iRoot, out var ii1, out var iv11); + + var r = ServiceContext.ContentService.SaveAndPublishBranch(iRoot, false, new[] { "de", "ru" }).ToArray(); + AssertPublishResults(r, x => x.Content.Name, + "iroot", "ii1", "iv11.de"); + AssertPublishResults(r, x => x.Result, + PublishResultType.SuccessPublishAlready, + PublishResultType.SuccessPublish, + PublishResultType.SuccessPublishCulture); + + // reload - SaveAndPublishBranch has modified other instances + Reload(ref ii1); + Reload(ref iv11); + + // the invariant child has been published + // the variant child has been published for 'de' and 'ru' + + Assert.AreEqual("changed", ii1.GetValue("ip", published: true)); + Assert.AreEqual("changed", iv11.GetValue("ip", published: true)); + Assert.AreEqual("changed.de", iv11.GetValue("vp", "de", published: true)); + Assert.AreEqual("changed.ru", iv11.GetValue("vp", "ru", published: true)); + } + + private void AssertPublishResults(PublishResult[] values, Func getter, params T[] expected) + { + if (expected.Length != values.Length) + Console.WriteLine(string.Join(", ", values.Select(x => getter(x).ToString()))); + Assert.AreEqual(expected.Length, values.Length); + + for (var i = 0; i < values.Length; i++) + { + var value = getter(values[i]); + Assert.AreEqual(expected[i], value, $"Expected {expected[i]} at {i} but got {value}."); + } + } + + private void Reload(ref IContent document) + => document = ServiceContext.ContentService.GetById(document.Id); + + private void CreateTypes(out IContentType iContentType, out IContentType vContentType) + { + var langDe = new Language("de") { IsDefault = true }; + ServiceContext.LocalizationService.Save(langDe); + var langRu = new Language("ru"); + ServiceContext.LocalizationService.Save(langRu); + var langEs = new Language("es"); + ServiceContext.LocalizationService.Save(langEs); + + iContentType = new ContentType(-1) + { + Alias = "ict", + Name = "Invariant Content Type", + Variations = ContentVariation.Nothing + }; + iContentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "ip") { Variations = ContentVariation.Nothing }); + ServiceContext.ContentTypeService.Save(iContentType); + + vContentType = new ContentType(-1) + { + Alias = "vct", + Name = "Variant Content Type", + Variations = ContentVariation.Culture + }; + vContentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "ip") { Variations = ContentVariation.Nothing }); + vContentType.AddPropertyType(new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Nvarchar, "vp") { Variations = ContentVariation.Culture }); + ServiceContext.ContentTypeService.Save(vContentType); + } + + private IEnumerable SaveAndPublishInvariantBranch(IContent content, bool force, int method) + { + // ReSharper disable RedundantArgumentDefaultValue + // ReSharper disable ArgumentsStyleOther + switch (method) + { + case 1: + return ServiceContext.ContentService.SaveAndPublishBranch(content, force, culture: "*"); + case 2: + return ServiceContext.ContentService.SaveAndPublishBranch(content, force, cultures: new [] { "*" }); + default: + throw new ArgumentOutOfRangeException(nameof(method)); + } + // ReSharper restore RedundantArgumentDefaultValue + // ReSharper restore ArgumentsStyleOther + } + } +} diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index d5d74ee754..1b12c24d82 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -12,18 +11,15 @@ using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using LightInject; -using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Core.Events; -using Umbraco.Core.Logging; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services.Implement; using Umbraco.Tests.Testing; -using Umbraco.Web.PropertyEditors; using System.Reflection; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -96,7 +92,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; var contentTypeService = ServiceContext.ContentTypeService; - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); contentTypeService.Save(contentType); @@ -122,7 +118,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; var contentTypeService = ServiceContext.ContentTypeService; - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); contentTypeService.Save(contentType); @@ -146,7 +142,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; var contentTypeService = ServiceContext.ContentTypeService; - var contentType = MockedContentTypes.CreateTextpageContentType(); + var contentType = MockedContentTypes.CreateTextPageContentType(); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); contentTypeService.Save(contentType); @@ -174,10 +170,10 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; var contentTypeService = ServiceContext.ContentTypeService; - var ct1 = MockedContentTypes.CreateTextpageContentType("ct1"); + var ct1 = MockedContentTypes.CreateTextPageContentType("ct1"); ServiceContext.FileService.SaveTemplate(ct1.DefaultTemplate); contentTypeService.Save(ct1); - var ct2 = MockedContentTypes.CreateTextpageContentType("ct2"); + var ct2 = MockedContentTypes.CreateTextPageContentType("ct2"); ServiceContext.FileService.SaveTemplate(ct2.DefaultTemplate); contentTypeService.Save(ct2); @@ -215,11 +211,11 @@ namespace Umbraco.Tests.Services var root = ServiceContext.ContentService.GetById(NodeDto.NodeIdSeed + 1); root.PublishCulture(); Assert.IsTrue(contentService.SaveAndPublish(root).Success); - var content = contentService.CreateAndSave("Test", -1, "umbTextpage", 0); + var content = contentService.CreateAndSave("Test", -1, "umbTextpage", Constants.Security.SuperUserId); content.PublishCulture(); Assert.IsTrue(contentService.SaveAndPublish(content).Success); var hierarchy = CreateContentHierarchy().OrderBy(x => x.Level).ToArray(); - contentService.Save(hierarchy, 0); + contentService.Save(hierarchy, Constants.Security.SuperUserId); foreach (var c in hierarchy) { c.PublishCulture(); @@ -254,6 +250,96 @@ namespace Umbraco.Tests.Services Assert.That(hierarchy.All(c => c.Path.StartsWith("-1,-20") == false), Is.True); } + [Test] + public void Perform_Scheduled_Publishing() + { + var langUk = new Language("en-GB") { IsDefault = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langUk); + + var ctInvariant = MockedContentTypes.CreateBasicContentType("invariantPage"); + ServiceContext.ContentTypeService.Save(ctInvariant); + + var ctVariant = MockedContentTypes.CreateBasicContentType("variantPage"); + ctVariant.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(ctVariant); + + var now = DateTime.Now; + + //10x invariant content, half is scheduled to be published in 5 seconds, the other half is scheduled to be unpublished in 5 seconds + var invariant = new List(); + for (var i = 0; i < 10; i++) + { + var c = MockedContent.CreateBasicContent(ctInvariant); + c.Name = "name" + i; + if (i % 2 == 0) + { + c.ContentSchedule.Add(now.AddSeconds(5), null); //release in 5 seconds + var r = ServiceContext.ContentService.Save(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + } + else + { + c.ContentSchedule.Add(null, now.AddSeconds(5)); //expire in 5 seconds + var r = ServiceContext.ContentService.SaveAndPublish(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + } + invariant.Add(c); + } + + //10x variant content, half is scheduled to be published in 5 seconds, the other half is scheduled to be unpublished in 5 seconds + var variant = new List(); + var alternatingCulture = langFr.IsoCode; + for (var i = 0; i < 10; i++) + { + var c = MockedContent.CreateBasicContent(ctVariant); + c.SetCultureName("name-uk" + i, langUk.IsoCode); + c.SetCultureName("name-fr" + i, langFr.IsoCode); + + if (i % 2 == 0) + { + c.ContentSchedule.Add(alternatingCulture, now.AddSeconds(5), null); //release in 5 seconds + var r = ServiceContext.ContentService.Save(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + + alternatingCulture = alternatingCulture == langFr.IsoCode ? langUk.IsoCode : langFr.IsoCode; + } + else + { + c.ContentSchedule.Add(alternatingCulture, null, now.AddSeconds(5)); //expire in 5 seconds + var r = ServiceContext.ContentService.SaveAndPublish(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + } + variant.Add(c); + } + + + var runSched = ServiceContext.ContentService.PerformScheduledPublish( + now.AddMinutes(1)).ToList(); //process anything scheduled before a minute from now + + //this is 21 because the test data installed before this test runs has a scheduled item! + Assert.AreEqual(21, runSched.Count); + Assert.AreEqual(20, runSched.Count(x => x.Success), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessPublish), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessUnpublish), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessPublishCulture), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessUnpublishCulture), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + + //re-run the scheduled publishing, there should be no results + runSched = ServiceContext.ContentService.PerformScheduledPublish( + now.AddMinutes(1)).ToList(); + + Assert.AreEqual(0, runSched.Count); + } + [Test] public void Remove_Scheduled_Publishing_Date() { @@ -261,17 +347,24 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; // Act - var content = contentService.CreateAndSave("Test", -1, "umbTextpage", 0); + var content = contentService.CreateAndSave("Test", -1, "umbTextpage", Constants.Security.SuperUserId); - content.ReleaseDate = DateTime.Now.AddHours(2); - contentService.Save(content, 0); + content.ContentSchedule.Add(null, DateTime.Now.AddHours(2)); + contentService.Save(content, Constants.Security.SuperUserId); + Assert.AreEqual(1, content.ContentSchedule.FullSchedule.Count); content = contentService.GetById(content.Id); - content.ReleaseDate = null; - contentService.Save(content, 0); + var sched = content.ContentSchedule.FullSchedule; + Assert.AreEqual(1, sched.Count); + Assert.AreEqual(1, sched.Count(x => x.Culture == string.Empty)); + content.ContentSchedule.Clear(ContentScheduleAction.Expire); + contentService.Save(content, Constants.Security.SuperUserId); // Assert + content = contentService.GetById(content.Id); + sched = content.ContentSchedule.FullSchedule; + Assert.AreEqual(0, sched.Count); Assert.IsTrue(contentService.SaveAndPublish(content).Success); } @@ -282,7 +375,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; // Act - var content = contentService.CreateAndSave("Test", -1, "umbTextpage", 0); + var content = contentService.CreateAndSave("Test", -1, "umbTextpage", Constants.Security.SuperUserId); for (var i = 0; i < 20; i++) { content.SetValue("bodyText", "hello world " + Guid.NewGuid()); @@ -327,7 +420,7 @@ namespace Umbraco.Tests.Services // Act for (int i = 0; i < 20; i++) { - contentService.CreateAndSave("Test", -1, "umbTextpage", 0); + contentService.CreateAndSave("Test", -1, "umbTextpage", Constants.Security.SuperUserId); } // Assert @@ -346,7 +439,7 @@ namespace Umbraco.Tests.Services // Act for (int i = 0; i < 20; i++) { - contentService.CreateAndSave("Test", -1, "umbBlah", 0); + contentService.CreateAndSave("Test", -1, "umbBlah", Constants.Security.SuperUserId); } // Assert @@ -361,7 +454,7 @@ namespace Umbraco.Tests.Services var contentTypeService = ServiceContext.ContentTypeService; var contentType = MockedContentTypes.CreateSimpleContentType("umbBlah", "test Doc Type"); contentTypeService.Save(contentType); - var parent = contentService.CreateAndSave("Test", -1, "umbBlah", 0); + var parent = contentService.CreateAndSave("Test", -1, "umbBlah", Constants.Security.SuperUserId); // Act for (int i = 0; i < 20; i++) @@ -381,7 +474,7 @@ namespace Umbraco.Tests.Services var contentTypeService = ServiceContext.ContentTypeService; var contentType = MockedContentTypes.CreateSimpleContentType("umbBlah", "test Doc Type"); contentTypeService.Save(contentType); - var parent = contentService.CreateAndSave("Test", -1, "umbBlah", 0); + var parent = contentService.CreateAndSave("Test", -1, "umbBlah", Constants.Security.SuperUserId); // Act IContent current = parent; @@ -920,7 +1013,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; // Act - var content = contentService.Create("Test", -1, "umbTextpage", 0); + var content = contentService.Create("Test", -1, "umbTextpage", Constants.Security.SuperUserId); // Assert Assert.That(content, Is.Not.Null); @@ -934,7 +1027,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; // Act - var content = contentService.Create("Test", -1, "umbTextpage", 0); + var content = contentService.Create("Test", -1, "umbTextpage", Constants.Security.SuperUserId); // Assert Assert.That(content, Is.Not.Null); @@ -1136,12 +1229,12 @@ namespace Umbraco.Tests.Services var root = contentService.GetById(NodeDto.NodeIdSeed + 2); contentService.SaveAndPublish(root); var content = contentService.GetById(NodeDto.NodeIdSeed + 4); - content.ExpireDate = DateTime.Now.AddSeconds(1); + content.ContentSchedule.Add(null, DateTime.Now.AddSeconds(1)); contentService.SaveAndPublish(content); // Act Thread.Sleep(new TimeSpan(0, 0, 0, 2)); - var contents = contentService.GetContentForExpiration().ToList(); + var contents = contentService.GetContentForExpiration(DateTime.Now).ToList(); // Assert Assert.That(contents, Is.Not.Null); @@ -1156,7 +1249,7 @@ namespace Umbraco.Tests.Services var contentService = ServiceContext.ContentService; // Act - var contents = contentService.GetContentForRelease().ToList(); + var contents = contentService.GetContentForRelease(DateTime.Now).ToList(); // Assert Assert.That(DateTime.Now.AddMinutes(-5) <= DateTime.Now); @@ -1200,6 +1293,7 @@ namespace Umbraco.Tests.Services Assert.That(published.Success, Is.True); Assert.That(unpublished.Success, Is.True); Assert.That(content.Published, Is.False); + Assert.AreEqual(PublishResultType.SuccessUnpublish, unpublished.Result); using (var scope = ScopeProvider.CreateScope()) { @@ -1243,6 +1337,7 @@ namespace Umbraco.Tests.Services var unpublished = ServiceContext.ContentService.Unpublish(content, langFr.IsoCode); Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); //this is slightly confusing but this will be false because this method is used for checking the state of the current model, @@ -1293,6 +1388,48 @@ namespace Umbraco.Tests.Services Assert.AreEqual($"Published languages: English (United Kingdom)", lastLog.Comment); } + [Test] + public void Can_Unpublish_Content_Variation_And_Detect_Changed_Cultures() + { + // Arrange + + var langGB = new Language("en-GB") { IsDefault = true, IsMandatory = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langGB); + + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + IContent content = new Content("content", -1, contentType); + content.SetCultureName("content-fr", langFr.IsoCode); + content.SetCultureName("content-gb", langGB.IsoCode); + content.PublishCulture(langGB.IsoCode); + content.PublishCulture(langFr.IsoCode); + var published = ServiceContext.ContentService.SavePublishing(content); + Assert.IsTrue(published.Success); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + content.UnpublishCulture(langFr.IsoCode); //unpublish non-mandatory lang + var unpublished = ServiceContext.ContentService.SavePublishing(content); + //audit log will only show that french was unpublished + var lastLog = ServiceContext.AuditService.GetLogs(content.Id).Last(); + Assert.AreEqual($"Unpublished languages: French (France)", lastLog.Comment); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + content.SetCultureName("content-en", langGB.IsoCode); + content.UnpublishCulture(langGB.IsoCode); //unpublish mandatory lang + unpublished = ServiceContext.ContentService.SavePublishing(content); + //audit log will only show that english was published + var logs = ServiceContext.AuditService.GetLogs(content.Id).ToList(); + Assert.AreEqual($"Unpublished languages: English (United Kingdom)", logs[logs.Count - 2].Comment); + Assert.AreEqual($"Unpublished (mandatory language unpublished)", logs[logs.Count - 1].Comment); + } + [Test] public void Can_Publish_Content_1() { @@ -1302,7 +1439,7 @@ namespace Umbraco.Tests.Services // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); // Assert Assert.That(published.Success, Is.True); @@ -1357,7 +1494,7 @@ namespace Umbraco.Tests.Services content.Name = "foo"; content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); Assert.That(published.Success, Is.True); Assert.That(content.Published, Is.True); @@ -1414,10 +1551,45 @@ namespace Umbraco.Tests.Services // because it did not have a published version at all var contentPublished = contentService.SaveAndPublish(content); Assert.IsFalse(contentPublished.Success); - Assert.AreEqual(PublishResultType.FailedContentInvalid, contentPublished.Result); + Assert.AreEqual(PublishResultType.FailedPublishContentInvalid, contentPublished.Result); Assert.IsFalse(content.Published); } + [Test] + public void Can_Publish_And_Unpublish_Cultures_In_Single_Operation() + { + var langFr = new Language("fr"); + var langDa = new Language("da"); + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langDa); + + var ct = MockedContentTypes.CreateBasicContentType(); + ct.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(ct); + + IContent content = MockedContent.CreateBasicContent(ct); + content.SetCultureName("name-fr", langFr.IsoCode); + content.SetCultureName("name-da", langDa.IsoCode); + + content.PublishCulture(langFr.IsoCode); + var result = ServiceContext.ContentService.SavePublishing(content); + Assert.IsTrue(result.Success); + content = ServiceContext.ContentService.GetById(content.Id); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langDa.IsoCode)); + + content.UnpublishCulture(langFr.IsoCode); + content.PublishCulture(langDa.IsoCode); + + result = ServiceContext.ContentService.SavePublishing(content); + Assert.IsTrue(result.Success); + Assert.AreEqual(PublishResultType.SuccessMixedCulture, result.Result); + + content = ServiceContext.ContentService.GetById(content.Id); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langDa.IsoCode)); + } + // documents: an enumeration of documents, in tree order // map: applies (if needed) PublishValue, returns a value indicating whether to proceed with the branch private IEnumerable MapPublishValues(IEnumerable documents, Func map) @@ -1484,21 +1656,41 @@ namespace Umbraco.Tests.Services // Arrange var contentService = ServiceContext.ContentService; var content = contentService.GetById(NodeDto.NodeIdSeed + 4); //This Content expired 5min ago - content.ExpireDate = DateTime.Now.AddMinutes(-5); + content.ContentSchedule.Add(null, DateTime.Now.AddMinutes(-5)); contentService.Save(content); var parent = contentService.GetById(NodeDto.NodeIdSeed + 2); parent.PublishCulture(); - var parentPublished = contentService.SavePublishing(parent, 0);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' + var parentPublished = contentService.SavePublishing(parent, Constants.Security.SuperUserId);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); // Assert Assert.That(parentPublished.Success, Is.True); Assert.That(published.Success, Is.False); Assert.That(content.Published, Is.False); + Assert.AreEqual(PublishResultType.FailedPublishHasExpired, published.Result); + } + + [Test] + public void Cannot_Publish_Expired_Culture() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + var content = MockedContent.CreateBasicContent(contentType); + content.SetCultureName("Hello", "en-US"); + content.ContentSchedule.Add("en-US", null, DateTime.Now.AddMinutes(-5)); + ServiceContext.ContentService.Save(content); + + var published = ServiceContext.ContentService.SaveAndPublish(content, "en-US", Constants.Security.SuperUserId); + + Assert.IsFalse(published.Success); + Assert.AreEqual(PublishResultType.FailedPublishCultureHasExpired, published.Result); + Assert.That(content.Published, Is.False); } [Test] @@ -1507,21 +1699,41 @@ namespace Umbraco.Tests.Services // Arrange var contentService = ServiceContext.ContentService; var content = contentService.GetById(NodeDto.NodeIdSeed + 3); - content.ReleaseDate = DateTime.Now.AddHours(2); - contentService.Save(content, 0); + content.ContentSchedule.Add(DateTime.Now.AddHours(2), null); + contentService.Save(content, Constants.Security.SuperUserId); var parent = contentService.GetById(NodeDto.NodeIdSeed + 2); parent.PublishCulture(); - var parentPublished = contentService.SavePublishing(parent, 0);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' + var parentPublished = contentService.SavePublishing(parent, Constants.Security.SuperUserId);//Publish root Home node to enable publishing of 'NodeDto.NodeIdSeed + 3' // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); // Assert Assert.That(parentPublished.Success, Is.True); Assert.That(published.Success, Is.False); Assert.That(content.Published, Is.False); + Assert.AreEqual(PublishResultType.FailedPublishAwaitingRelease, published.Result); + } + + [Test] + public void Cannot_Publish_Culture_Awaiting_Release() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + var content = MockedContent.CreateBasicContent(contentType); + content.SetCultureName("Hello", "en-US"); + content.ContentSchedule.Add("en-US", DateTime.Now.AddHours(2), null); + ServiceContext.ContentService.Save(content); + + var published = ServiceContext.ContentService.SaveAndPublish(content, "en-US", Constants.Security.SuperUserId); + + Assert.IsFalse(published.Success); + Assert.AreEqual(PublishResultType.FailedPublishCultureAwaitingRelease, published.Result); + Assert.That(content.Published, Is.False); } [Test] @@ -1529,8 +1741,8 @@ namespace Umbraco.Tests.Services { // Arrange var contentService = ServiceContext.ContentService; - var content = contentService.Create("Subpage with Unpublisehed Parent", NodeDto.NodeIdSeed + 2, "umbTextpage", 0); - contentService.Save(content, 0); + var content = contentService.Create("Subpage with Unpublished Parent", NodeDto.NodeIdSeed + 2, "umbTextpage", Constants.Security.SuperUserId); + contentService.Save(content, Constants.Security.SuperUserId); // Act var published = contentService.SaveAndPublishBranch(content, true); @@ -1549,7 +1761,7 @@ namespace Umbraco.Tests.Services // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); // Assert Assert.That(published.Success, Is.False); @@ -1562,12 +1774,12 @@ namespace Umbraco.Tests.Services { // Arrange var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", - 1, "umbTextpage", 0); + var content = contentService.Create("Home US", - 1, "umbTextpage", Constants.Security.SuperUserId); content.SetValue("author", "Barack Obama"); // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); // Assert Assert.That(content.HasIdentity, Is.True); @@ -1587,19 +1799,19 @@ namespace Umbraco.Tests.Services { // Arrange var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", -1, "umbTextpage", 0); + var content = contentService.Create("Home US", -1, "umbTextpage", Constants.Security.SuperUserId); content.SetValue("author", "Barack Obama"); // Act content.PublishCulture(); - var published = contentService.SavePublishing(content, 0); - var childContent = contentService.Create("Child", content.Id, "umbTextpage", 0); + var published = contentService.SavePublishing(content, Constants.Security.SuperUserId); + var childContent = contentService.Create("Child", content.Id, "umbTextpage", Constants.Security.SuperUserId); // Reset all identity properties childContent.Id = 0; childContent.Path = null; ((Content)childContent).ResetIdentity(); childContent.PublishCulture(); - var childPublished = contentService.SavePublishing(childContent, 0); + var childPublished = contentService.SavePublishing(childContent, Constants.Security.SuperUserId); // Assert Assert.That(content.HasIdentity, Is.True); @@ -1661,11 +1873,11 @@ namespace Umbraco.Tests.Services { // Arrange var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", - 1, "umbTextpage", 0); + var content = contentService.Create("Home US", - 1, "umbTextpage", Constants.Security.SuperUserId); content.SetValue("author", "Barack Obama"); // Act - contentService.Save(content, 0); + contentService.Save(content, Constants.Security.SuperUserId); // Assert Assert.That(content.HasIdentity, Is.True); @@ -1684,7 +1896,7 @@ namespace Umbraco.Tests.Services var list = new List {subpage, subpage2}; // Act - contentService.Save(list, 0); + contentService.Save(list, Constants.Security.SuperUserId); // Assert Assert.That(list.Any(x => !x.HasIdentity), Is.False); @@ -1698,7 +1910,7 @@ namespace Umbraco.Tests.Services var hierarchy = CreateContentHierarchy().ToList(); // Act - contentService.Save(hierarchy, 0); + contentService.Save(hierarchy, Constants.Security.SuperUserId); Assert.That(hierarchy.Any(), Is.True); Assert.That(hierarchy.Any(x => x.HasIdentity == false), Is.False); @@ -1733,7 +1945,7 @@ namespace Umbraco.Tests.Services var content = contentService.GetById(NodeDto.NodeIdSeed + 4); // Act - contentService.Delete(content, 0); + contentService.Delete(content, Constants.Security.SuperUserId); var deleted = contentService.GetById(NodeDto.NodeIdSeed + 4); // Assert @@ -1748,7 +1960,7 @@ namespace Umbraco.Tests.Services var content = contentService.GetById(NodeDto.NodeIdSeed + 3); // Act - contentService.MoveToRecycleBin(content, 0); + contentService.MoveToRecycleBin(content, Constants.Security.SuperUserId); // Assert Assert.That(content.ParentId, Is.EqualTo(-20)); @@ -1762,7 +1974,7 @@ namespace Umbraco.Tests.Services var contentType = ServiceContext.ContentTypeService.Get("umbTextpage"); var subsubpage = MockedContent.CreateSimpleContent(contentType, "Text Page 3", NodeDto.NodeIdSeed + 3); - contentService.Save(subsubpage, 0); + contentService.Save(subsubpage, Constants.Security.SuperUserId); var content = contentService.GetById(NodeDto.NodeIdSeed + 2); const int pageSize = 500; @@ -1778,7 +1990,7 @@ namespace Umbraco.Tests.Services Assert.IsFalse(descendants.Any(x => x.Path.StartsWith("-1,-20,"))); Assert.IsFalse(descendants.Any(x => x.Trashed)); - contentService.MoveToRecycleBin(content, 0); + contentService.MoveToRecycleBin(content, Constants.Security.SuperUserId); descendants.Clear(); page = 0; @@ -1927,7 +2139,7 @@ namespace Umbraco.Tests.Services // * multiple versions var contentType = MockedContentTypes.CreateAllTypesContentType("test", "test"); - ServiceContext.ContentTypeService.Save(contentType, 0); + ServiceContext.ContentTypeService.Save(contentType, Constants.Security.SuperUserId); object obj = new @@ -1937,13 +2149,13 @@ namespace Umbraco.Tests.Services var content1 = MockedContent.CreateBasicContent(contentType); content1.PropertyValues(obj); content1.ResetDirtyProperties(false); - ServiceContext.ContentService.Save(content1, 0); + ServiceContext.ContentService.Save(content1, Constants.Security.SuperUserId); content1.PublishCulture(); Assert.IsTrue(ServiceContext.ContentService.SavePublishing(content1, 0).Success); var content2 = MockedContent.CreateBasicContent(contentType); content2.PropertyValues(obj); content2.ResetDirtyProperties(false); - ServiceContext.ContentService.Save(content2, 0); + ServiceContext.ContentService.Save(content2, Constants.Security.SuperUserId); content2.PublishCulture(); Assert.IsTrue(ServiceContext.ContentService.SavePublishing(content2, 0).Success); @@ -2012,7 +2224,7 @@ namespace Umbraco.Tests.Services var temp = contentService.GetById(NodeDto.NodeIdSeed + 4); // Act - var copy = contentService.Copy(temp, temp.ParentId, false, 0); + var copy = contentService.Copy(temp, temp.ParentId, false, Constants.Security.SuperUserId); var content = contentService.GetById(NodeDto.NodeIdSeed + 4); // Assert @@ -2036,7 +2248,7 @@ namespace Umbraco.Tests.Services Assert.AreEqual(2, contentService.CountChildren(temp.Id)); // Act - var copy = contentService.Copy(temp, temp.ParentId, false, true, 0); + var copy = contentService.Copy(temp, temp.ParentId, false, true, Constants.Security.SuperUserId); var content = contentService.GetById(NodeDto.NodeIdSeed + 2); // Assert @@ -2062,7 +2274,7 @@ namespace Umbraco.Tests.Services Assert.AreEqual(2, contentService.CountChildren(temp.Id)); // Act - var copy = contentService.Copy(temp, temp.ParentId, false, false, 0); + var copy = contentService.Copy(temp, temp.ParentId, false, false, Constants.Security.SuperUserId); var content = contentService.GetById(NodeDto.NodeIdSeed + 2); // Assert @@ -2531,7 +2743,7 @@ namespace Umbraco.Tests.Services var version = content.VersionId; // Act - contentService.DeleteVersion(NodeDto.NodeIdSeed + 5, version, true, 0); + contentService.DeleteVersion(NodeDto.NodeIdSeed + 5, version, true, Constants.Security.SuperUserId); var sut = contentService.GetById(NodeDto.NodeIdSeed + 5); // Assert @@ -2542,7 +2754,7 @@ namespace Umbraco.Tests.Services public void Ensure_Content_Xml_Created() { var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", -1, "umbTextpage", 0); + var content = contentService.Create("Home US", -1, "umbTextpage", Constants.Security.SuperUserId); content.SetValue("author", "Barack Obama"); contentService.Save(content); @@ -2564,7 +2776,7 @@ namespace Umbraco.Tests.Services public void Ensure_Preview_Xml_Created() { var contentService = ServiceContext.ContentService; - var content = contentService.Create("Home US", -1, "umbTextpage", 0); + var content = contentService.Create("Home US", -1, "umbTextpage", Constants.Security.SuperUserId); content.SetValue("author", "Barack Obama"); contentService.Save(content); diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index f186ae8e83..bc0854bdb7 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -611,7 +611,7 @@ namespace Umbraco.Tests.Services [Test] public void Deleting_PropertyType_Removes_The_Property_From_Content() { - IContentType contentType1 = MockedContentTypes.CreateTextpageContentType("test1", "Test1"); + IContentType contentType1 = MockedContentTypes.CreateTextPageContentType("test1", "Test1"); ServiceContext.FileService.SaveTemplate(contentType1.DefaultTemplate); ServiceContext.ContentTypeService.Save(contentType1); IContent contentItem = MockedContent.CreateTextpageContent(contentType1, "Testing", -1); @@ -633,11 +633,11 @@ namespace Umbraco.Tests.Services [Test] public void Rebuild_Content_Xml_On_Alias_Change() { - var contentType1 = MockedContentTypes.CreateTextpageContentType("test1", "Test1"); + var contentType1 = MockedContentTypes.CreateTextPageContentType("test1", "Test1"); ServiceContext.FileService.SaveTemplate(contentType1.DefaultTemplate); ServiceContext.ContentTypeService.Save(contentType1); - var contentType2 = MockedContentTypes.CreateTextpageContentType("test2", "Test2"); + var contentType2 = MockedContentTypes.CreateTextPageContentType("test2", "Test2"); ServiceContext.FileService.SaveTemplate(contentType2.DefaultTemplate); ServiceContext.ContentTypeService.Save(contentType2); @@ -701,7 +701,7 @@ namespace Umbraco.Tests.Services [Test] public void Rebuild_Content_Xml_On_Property_Removal() { - var contentType1 = MockedContentTypes.CreateTextpageContentType("test1", "Test1"); + var contentType1 = MockedContentTypes.CreateTextPageContentType("test1", "Test1"); ServiceContext.FileService.SaveTemplate(contentType1.DefaultTemplate); ServiceContext.ContentTypeService.Save(contentType1); var contentItems1 = MockedContent.CreateTextpageContent(contentType1, -1, 10).ToArray(); diff --git a/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs b/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs index b821e5a516..2b313afc5c 100644 --- a/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs +++ b/src/Umbraco.Tests/Services/TestWithSomeContentBase.cs @@ -34,7 +34,7 @@ namespace Umbraco.Tests.Services //Create and Save Content "Text Page 1" based on "umbTextpage" -> 1062 Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); - subpage.ReleaseDate = DateTime.Now.AddMinutes(-5); + subpage.ContentSchedule.Add(DateTime.Now.AddMinutes(-5), null); ServiceContext.ContentService.Save(subpage, 0); //Create and Save Content "Text Page 1" based on "umbTextpage" -> 1063 diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index fe0e46941a..2cf64f04d1 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -163,18 +163,13 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting var membershipHelper = new MembershipHelper(umbCtx, Mock.Of(), Mock.Of()); - var mockedTypedContent = Mock.Of(); - var umbHelper = new UmbracoHelper(umbCtx, Mock.Of(), - mockedTypedContent, Mock.Of(), - Mock.Of(), Mock.Of(), Mock.Of(), membershipHelper, - serviceContext, - CacheHelper.NoCache); + serviceContext); return CreateController(controllerType, request, umbHelper); } diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs index 42beea7df3..14b967b1c9 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs @@ -26,7 +26,7 @@ namespace Umbraco.Tests.TestHelpers.Entities return contentType; } - public static ContentType CreateTextpageContentType(string alias = "textPage", string name = "Text Page") + public static ContentType CreateTextPageContentType(string alias = "textPage", string name = "Text Page") { var contentType = new ContentType(-1) { diff --git a/src/Umbraco.Tests/TestHelpers/Stubs/TestExamineManager.cs b/src/Umbraco.Tests/TestHelpers/Stubs/TestExamineManager.cs index 14c6a08c8b..f8d48c5703 100644 --- a/src/Umbraco.Tests/TestHelpers/Stubs/TestExamineManager.cs +++ b/src/Umbraco.Tests/TestHelpers/Stubs/TestExamineManager.cs @@ -6,64 +6,38 @@ namespace Umbraco.Tests.TestHelpers.Stubs { internal class TestExamineManager : IExamineManager { - private readonly ConcurrentDictionary _indexers = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _indexers = new ConcurrentDictionary(); private readonly ConcurrentDictionary _searchers = new ConcurrentDictionary(); - public void AddIndexer(string name, IIndexer indexer) + public void AddIndex(IIndex indexer) { - _indexers.TryAdd(name, indexer); + _indexers.TryAdd(indexer.Name, indexer); } - public void AddSearcher(string name, ISearcher searcher) + public void AddSearcher(ISearcher searcher) { - _searchers.TryAdd(name, searcher); + _searchers.TryAdd(searcher.Name, searcher); } - - public void DeleteFromIndexes(string nodeId) - { - //noop - } - - public void DeleteFromIndexes(string nodeId, IEnumerable providers) - { - //noop - } - + public void Dispose() { //noop } - public IIndexer GetIndexer(string indexerName) + public bool TryGetIndex(string indexName, out IIndex index) { - return _indexers.TryGetValue(indexerName, out var indexer) ? indexer : null; + return _indexers.TryGetValue(indexName, out index); } - public ISearcher GetRegisteredSearcher(string searcherName) + public bool TryGetSearcher(string searcherName, out ISearcher searcher) { - return _searchers.TryGetValue(searcherName, out var indexer) ? indexer : null; + return _searchers.TryGetValue(searcherName, out searcher); } - public void IndexAll(string indexCategory) - { - //noop - } + public IEnumerable Indexes => _indexers.Values; - public void IndexItems(ValueSet[] nodes) - { - //noop - } + public IEnumerable RegisteredSearchers => _searchers.Values; - public void IndexItems(ValueSet[] nodes, IEnumerable providers) - { - //noop - } - - public void RebuildIndexes() - { - //noop - } - - public IReadOnlyDictionary IndexProviders => _indexers; + public IReadOnlyDictionary IndexProviders => _indexers; } } diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index e79e504a69..51855f7e19 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -60,14 +60,11 @@ namespace Umbraco.Tests.Testing.TestingTests // ReSharper disable once UnusedVariable var helper = new UmbracoHelper(umbracoContext, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of(), Mock.Of(), Mock.Of(), new MembershipHelper(umbracoContext, Mock.Of(), Mock.Of()), - new ServiceContext(), - CacheHelper.CreateDisabledCacheHelper()); + new ServiceContext()); Assert.Pass(); } diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index d80802b1cf..1eb222cd6a 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -213,7 +213,7 @@ namespace Umbraco.Tests.Testing Container.RegisterSingleton(); // register empty content apps collection - Container.RegisterCollectionBuilder(); + Container.RegisterCollectionBuilder(); } protected virtual void ComposeCacheHelper() diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 8d3da825f5..513701bf6a 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -77,7 +77,7 @@ - + 1.8.9 @@ -124,6 +124,7 @@ + @@ -131,6 +132,7 @@ + @@ -206,6 +208,7 @@ + @@ -463,7 +466,6 @@ - True True diff --git a/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs b/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs index e2279ee833..47fa32cbaa 100644 --- a/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs @@ -1,13 +1,16 @@ using System; using System.Linq; using Examine; +using LightInject; using Lucene.Net.Store; using NUnit.Framework; using Umbraco.Tests.Testing; using Umbraco.Examine; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Tests.UmbracoExamine { + [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class EventsTest : ExamineBaseTest @@ -16,13 +19,13 @@ namespace Umbraco.Tests.UmbracoExamine public void Events_Ignoring_Node() { using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, //make parent id 999 so all are ignored - options: new UmbracoContentIndexerOptions(false, false, 999))) + validator: new ContentValueSetValidator(false, 999))) using (indexer.ProcessNonAsync()) { var searcher = indexer.GetSearcher(); - + var contentService = new ExamineDemoDataContentService(); //get a node from the data repo var node = contentService.GetPublishedContentByXPath("//*[string-length(@id)>0 and number(@id)>0]") @@ -31,9 +34,9 @@ namespace Umbraco.Tests.UmbracoExamine .First(); var valueSet = node.ConvertToValueSet(IndexTypes.Content); - indexer.IndexItems(new[] {valueSet}); + indexer.IndexItems(new[] { valueSet }); - var found = searcher.Search(searcher.CreateCriteria().Id((string) node.Attribute("id")).Compile()); + var found = searcher.Search(searcher.CreateCriteria().Id((string)node.Attribute("id")).Compile()); Assert.AreEqual(0, found.TotalItemCount); } diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 8b57e10849..cbd335a6c4 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; @@ -28,177 +29,158 @@ namespace Umbraco.Tests.UmbracoExamine /// internal static class IndexInitializer { - public static UmbracoContentIndexer GetUmbracoIndexer( + public static ContentValueSetBuilder GetContentValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) + { + var contentValueSetBuilder = new ContentValueSetBuilder(propertyEditors, new[] { new DefaultUrlSegmentProvider() }, GetMockUserService(), publishedValuesOnly); + return contentValueSetBuilder; + } + + public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, ISqlContext sqlContext, bool publishedValuesOnly) + { + var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, publishedValuesOnly); + var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, sqlContext, contentValueSetBuilder); + return contentIndexDataSource; + } + + public static MediaIndexPopulator GetMediaIndexRebuilder(PropertyEditorCollection propertyEditors, IMediaService mediaService) + { + var mediaValueSetBuilder = new MediaValueSetBuilder(propertyEditors, new[] { new DefaultUrlSegmentProvider() }, GetMockUserService()); + var mediaIndexDataSource = new MediaIndexPopulator(null, mediaService, mediaValueSetBuilder); + return mediaIndexDataSource; + } + + public static IContentService GetMockContentService() + { + long longTotalRecs; + var demoData = new ExamineDemoDataContentService(); + + var allRecs = demoData.GetLatestContentByXPath("//*[@isDoc]") + .Root + .Elements() + .Select(x => Mock.Of( + m => + m.Id == (int)x.Attribute("id") && + m.ParentId == (int)x.Attribute("parentID") && + m.Level == (int)x.Attribute("level") && + m.CreatorId == 0 && + m.SortOrder == (int)x.Attribute("sortOrder") && + m.CreateDate == (DateTime)x.Attribute("createDate") && + m.UpdateDate == (DateTime)x.Attribute("updateDate") && + m.Name == (string)x.Attribute("nodeName") && + m.GetCultureName(It.IsAny()) == (string)x.Attribute("nodeName") && + m.Path == (string)x.Attribute("path") && + m.Properties == new PropertyCollection() && + m.ContentType == Mock.Of(mt => + mt.Icon == "test" && + mt.Alias == x.Name.LocalName && + mt.Id == (int)x.Attribute("nodeType")))) + .ToArray(); + + + return Mock.Of( + x => x.GetPagedDescendants( + It.IsAny(), It.IsAny(), It.IsAny(), out longTotalRecs, It.IsAny>(), It.IsAny()) + == allRecs); + } + + public static IUserService GetMockUserService() + { + return Mock.Of(x => x.GetProfileById(It.IsAny()) == Mock.Of(p => p.Id == 0 && p.Name == "admin")); + } + + public static IMediaService GetMockMediaService() + { + long totalRecs; + + var demoData = new ExamineDemoDataMediaService(); + + var allRecs = demoData.GetLatestMediaByXpath("//node") + .Root + .Elements() + .Select(x => Mock.Of( + m => + m.Id == (int)x.Attribute("id") && + m.ParentId == (int)x.Attribute("parentID") && + m.Level == (int)x.Attribute("level") && + m.CreatorId == 0 && + m.SortOrder == (int)x.Attribute("sortOrder") && + m.CreateDate == (DateTime)x.Attribute("createDate") && + m.UpdateDate == (DateTime)x.Attribute("updateDate") && + m.Name == (string)x.Attribute("nodeName") && + m.GetCultureName(It.IsAny()) == (string)x.Attribute("nodeName") && + m.Path == (string)x.Attribute("path") && + m.Properties == new PropertyCollection() && + m.ContentType == Mock.Of(mt => + mt.Alias == (string)x.Attribute("nodeTypeAlias") && + mt.Id == (int)x.Attribute("nodeType")))) + .ToArray(); + + // MOCK! + var mediaServiceMock = new Mock(); + + mediaServiceMock + .Setup(x => x.GetPagedDescendants( + It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny>(), It.IsAny()) + ).Returns(() => allRecs); + + //mediaServiceMock.Setup(service => service.GetPagedXmlEntries(It.IsAny(), It.IsAny(), It.IsAny(), out longTotalRecs)) + // .Returns(() => allRecs.Select(x => x.ToXml())); + + return mediaServiceMock.Object; + } + + public static ILocalizationService GetMockLocalizationService() + { + return Mock.Of(x => x.GetAllLanguages() == Array.Empty()); + } + + public static IMediaTypeService GetMockMediaTypeService() + { + var mediaTypeServiceMock = new Mock(); + mediaTypeServiceMock.Setup(x => x.GetAll()) + .Returns(new List + { + new MediaType(-1) {Alias = "Folder", Name = "Folder", Id = 1031, Icon = "icon-folder"}, + new MediaType(-1) {Alias = "Image", Name = "Image", Id = 1032, Icon = "icon-picture"} + }); + return mediaTypeServiceMock.Object; + } + + public static UmbracoContentIndex GetUmbracoIndexer( ProfilingLogger profilingLogger, Directory luceneDir, - ISqlContext sqlContext, Analyzer analyzer = null, - IContentService contentService = null, - IMediaService mediaService = null, - IMemberService memberService = null, - IUserService userService = null, - IContentTypeService contentTypeService = null, - IMediaTypeService mediaTypeService = null, - UmbracoContentIndexerOptions options = null) + ILocalizationService languageService = null, + IContentValueSetValidator validator = null) { - if (contentService == null) - { - long longTotalRecs; - var demoData = new ExamineDemoDataContentService(); - - var allRecs = demoData.GetLatestContentByXPath("//*[@isDoc]") - .Root - .Elements() - .Select(x => Mock.Of( - m => - m.Id == (int)x.Attribute("id") && - m.ParentId == (int)x.Attribute("parentID") && - m.Level == (int)x.Attribute("level") && - m.CreatorId == 0 && - m.SortOrder == (int)x.Attribute("sortOrder") && - m.CreateDate == (DateTime)x.Attribute("createDate") && - m.UpdateDate == (DateTime)x.Attribute("updateDate") && - m.Name == (string)x.Attribute("nodeName") && - m.GetCultureName(It.IsAny()) == (string)x.Attribute("nodeName") && - m.Path == (string)x.Attribute("path") && - m.Properties == new PropertyCollection() && - m.ContentType == Mock.Of(mt => - mt.Icon == "test" && - mt.Alias == x.Name.LocalName && - mt.Id == (int)x.Attribute("nodeType")))) - .ToArray(); - - - contentService = Mock.Of( - x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out longTotalRecs, It.IsAny>(), It.IsAny()) - == - allRecs); - } - if (userService == null) - { - userService = Mock.Of(x => x.GetProfileById(It.IsAny()) == Mock.Of(p => p.Id == 0 && p.Name == "admin")); - } - - if (mediaService == null) - { - long totalRecs; - - var demoData = new ExamineDemoDataMediaService(); - - var allRecs = demoData.GetLatestMediaByXpath("//node") - .Root - .Elements() - .Select(x => Mock.Of( - m => - m.Id == (int) x.Attribute("id") && - m.ParentId == (int) x.Attribute("parentID") && - m.Level == (int) x.Attribute("level") && - m.CreatorId == 0 && - m.SortOrder == (int) x.Attribute("sortOrder") && - m.CreateDate == (DateTime) x.Attribute("createDate") && - m.UpdateDate == (DateTime) x.Attribute("updateDate") && - m.Name == (string) x.Attribute("nodeName") && - m.GetCultureName(It.IsAny()) == (string)x.Attribute("nodeName") && - m.Path == (string) x.Attribute("path") && - m.Properties == new PropertyCollection() && - m.ContentType == Mock.Of(mt => - mt.Alias == (string) x.Attribute("nodeTypeAlias") && - mt.Id == (int) x.Attribute("nodeType")))) - .ToArray(); - - // MOCK! - var mediaServiceMock = new Mock(); - - mediaServiceMock - .Setup(x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny>(), It.IsAny()) - ).Returns(() => allRecs); - - //mediaServiceMock.Setup(service => service.GetPagedXmlEntries(It.IsAny(), It.IsAny(), It.IsAny(), out longTotalRecs)) - // .Returns(() => allRecs.Select(x => x.ToXml())); - - mediaService = mediaServiceMock.Object; - - } + if (languageService == null) + languageService = GetMockLocalizationService(); if (analyzer == null) - { analyzer = new StandardAnalyzer(Version.LUCENE_30); - } - //var indexSet = new IndexSet(); - // var indexCriteria = indexSet.ToIndexCriteria(dataService, UmbracoContentIndexer.IndexFieldPolicies); - - //var i = new UmbracoContentIndexer(indexCriteria, - // luceneDir, //custom lucene directory - // dataService, - // contentService, - // mediaService, - // dataTypeService, - // userService, - // new[] { new DefaultUrlSegmentProvider() }, - // analyzer, - // false); - - //i.IndexSecondsInterval = 1; - - if (options == null) - { - options = new UmbracoContentIndexerOptions(false, false, null); - } - - if (mediaTypeService == null) - { - var mediaTypeServiceMock = new Mock(); - mediaTypeServiceMock.Setup(x => x.GetAll()) - .Returns(new List - { - new MediaType(-1) {Alias = "Folder", Name = "Folder", Id = 1031, Icon = "icon-folder"}, - new MediaType(-1) {Alias = "Image", Name = "Image", Id = 1032, Icon = "icon-picture"} - }); - mediaTypeService = mediaTypeServiceMock.Object; - } - - // fixme oops?! - //var query = new Mock>(); - //query - // .Setup(x => x.GetWhereClauses()) - // .Returns(new List> { new Tuple($"{Constants.DatabaseSchema.Tables.Document}.published", new object[] { 1 }) }); + if (validator == null) + validator = new ContentValueSetValidator(true); - //scopeProvider - // .Setup(x => x.Query()) - // .Returns(query.Object); - - var i = new UmbracoContentIndexer( + var i = new UmbracoContentIndex( "testIndexer", - Enumerable.Empty(), + UmbracoExamineIndex.UmbracoIndexFieldDefinitions, luceneDir, analyzer, profilingLogger, - contentService, - mediaService, - userService, - sqlContext, - new[] {new DefaultUrlSegmentProvider()}, - new UmbracoContentValueSetValidator(options, Mock.Of()), - options); + languageService, + validator); i.IndexingError += IndexingError; return i; } - public static LuceneSearcher GetLuceneSearcher(Directory luceneDir) - { - return new LuceneSearcher("testSearcher", luceneDir, new StandardAnalyzer(Version.LUCENE_29)); - } - - public static MultiIndexSearcher GetMultiSearcher(Directory pdfDir, Directory simpleDir, Directory conventionDir, Directory cwsDir) - { - var i = new MultiIndexSearcher("testSearcher", new[] { pdfDir, simpleDir, conventionDir, cwsDir }, new StandardAnalyzer(Version.LUCENE_29)); - return i; - } + //public static MultiIndexSearcher GetMultiSearcher(Directory pdfDir, Directory simpleDir, Directory conventionDir, Directory cwsDir) + //{ + // var i = new MultiIndexSearcher("testSearcher", new[] { pdfDir, simpleDir, conventionDir, cwsDir }, new StandardAnalyzer(Version.LUCENE_29)); + // return i; + //} internal static void IndexingError(object sender, IndexingErrorEventArgs e) diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs index db6f174f59..35e3524459 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs @@ -8,6 +8,13 @@ using Lucene.Net.Store; using NUnit.Framework; using Umbraco.Tests.Testing; using Umbraco.Examine; +using Umbraco.Core.PropertyEditors; +using LightInject; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Core.Models; +using Newtonsoft.Json; +using System.Collections.Generic; +using System; namespace Umbraco.Tests.UmbracoExamine { @@ -21,16 +28,116 @@ namespace Umbraco.Tests.UmbracoExamine { [Test] - public void Rebuild_Index() + public void Index_Property_Data_With_Value_Indexer() { + var contentValueSetBuilder = IndexInitializer.GetContentValueSetBuilder(Container.GetInstance(), false); + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, options: new UmbracoContentIndexerOptions(true, false, null))) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, + validator: new ContentValueSetValidator(false))) using (indexer.ProcessNonAsync()) { + indexer.CreateIndex(); + + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.AddPropertyType(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "grid", + Name = "Grid", + PropertyEditorAlias = Core.Constants.PropertyEditors.Aliases.Grid + }); + var content = MockedContent.CreateBasicContent(contentType); + content.Id = 555; + content.Path = "-1,555"; + var gridVal = new GridValue + { + Name = "n1", + Sections = new List + { + new GridValue.GridSection + { + Grid = "g1", + Rows = new List + { + new GridValue.GridRow + { + Id = Guid.NewGuid(), + Name = "row1", + Areas = new List + { + new GridValue.GridArea + { + Grid = "g2", + Controls = new List + { + new GridValue.GridControl + { + Editor = new GridValue.GridEditor + { + Alias = "editor1", + View = "view1" + }, + Value = "value1" + }, + new GridValue.GridControl + { + Editor = new GridValue.GridEditor + { + Alias = "editor1", + View = "view1" + }, + Value = "value2" + } + } + } + } + } + } + } + } + }; + + var json = JsonConvert.SerializeObject(gridVal); + content.Properties["grid"].SetValue(json); + + var valueSet = contentValueSetBuilder.GetValueSets(content); + indexer.IndexItems(valueSet); + + var searcher = indexer.GetSearcher(); + + var results = searcher.Search(searcher.CreateCriteria().Id(555).Compile()); + Assert.AreEqual(1, results.TotalItemCount); + + var result = results.First(); + Assert.IsTrue(result.Values.ContainsKey("grid.row1")); + Assert.AreEqual("value1", result.AllValues["grid.row1"][0]); + Assert.AreEqual("value2", result.AllValues["grid.row1"][1]); + Assert.IsTrue(result.Values.ContainsKey("grid")); + Assert.AreEqual("value1 value2 ", result["grid"]); + Assert.IsTrue(result.Values.ContainsKey($"{UmbracoExamineIndex.RawFieldPrefix}grid")); + Assert.AreEqual(json, result[$"{UmbracoExamineIndex.RawFieldPrefix}grid"]); + } + } + + [Test] + public void Rebuild_Index() + { + var contentRebuilder = IndexInitializer.GetContentIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var mediaRebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); + + using (var luceneDir = new RandomIdRamDirectory()) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, + validator: new ContentValueSetValidator(false))) + using (indexer.ProcessNonAsync()) + { + contentRebuilder.RegisterIndex(indexer.Name); + mediaRebuilder.RegisterIndex(indexer.Name); + var searcher = indexer.GetSearcher(); //create the whole thing - indexer.RebuildIndex(); + contentRebuilder.Populate(indexer); + mediaRebuilder.Populate(indexer); var result = searcher.Search(searcher.CreateCriteria().All().Compile()); @@ -45,23 +152,27 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Protected_Content_Not_Indexed() { + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir)) using (indexer.ProcessNonAsync()) using (var searcher = ((LuceneSearcher)indexer.GetSearcher()).GetLuceneSearcher()) { //create the whole thing - indexer.RebuildIndex(); + rebuilder.Populate(indexer); + var protectedQuery = new BooleanQuery(); protectedQuery.Add( new BooleanClause( - new TermQuery(new Term(LuceneIndexer.CategoryFieldName, IndexTypes.Content)), + new TermQuery(new Term(LuceneIndex.CategoryFieldName, IndexTypes.Content)), Occur.MUST)); protectedQuery.Add( new BooleanClause( - new TermQuery(new Term(LuceneIndexer.ItemIdFieldName, ExamineDemoDataContentService.ProtectedNode.ToString())), + new TermQuery(new Term(LuceneIndex.ItemIdFieldName, ExamineDemoDataContentService.ProtectedNode.ToString())), Occur.MUST)); var collector = TopScoreDocCollector.Create(100, true); @@ -76,26 +187,27 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Move_Media_From_Non_Indexable_To_Indexable_ParentID() { + // create a validator with + // publishedValuesOnly false + // parentId 1116 (only content under that parent will be indexed) + var validator = new ContentValueSetValidator(false, 1116); + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, - //make parent id 1116 - options: new UmbracoContentIndexerOptions(false, false, 1116))) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, validator: validator)) using (indexer.ProcessNonAsync()) { var searcher = indexer.GetSearcher(); //get a node from the data repo (this one exists underneath 2222) var node = _mediaService.GetLatestMediaByXpath("//*[string-length(@id)>0 and number(@id)>0]") - .Root - .Elements() - .Where(x => (int)x.Attribute("id") == 2112) - .First(); + .Root.Elements() + .First(x => (int) x.Attribute("id") == 2112); var currPath = (string)node.Attribute("path"); //should be : -1,1111,2222,2112 Assert.AreEqual("-1,1111,2222,2112", currPath); //ensure it's indexed - indexer.IndexItems(new []{ node.ConvertToValueSet(IndexTypes.Media) }); + indexer.IndexItem(node.ConvertToValueSet(IndexTypes.Media)); //it will not exist because it exists under 2222 var results = searcher.Search(searcher.CreateCriteria().Id(2112).Compile()); @@ -118,28 +230,27 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Move_Media_To_Non_Indexable_ParentID() { + // create a validator with + // publishedValuesOnly false + // parentId 2222 (only content under that parent will be indexed) + var validator = new ContentValueSetValidator(false, 2222); + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer1 = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, - //make parent id 2222 - options: new UmbracoContentIndexerOptions(false, false, 2222))) + using (var indexer1 = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, validator: validator)) using (indexer1.ProcessNonAsync()) { var searcher = indexer1.GetSearcher(); //get a node from the data repo (this one exists underneath 2222) var node = _mediaService.GetLatestMediaByXpath("//*[string-length(@id)>0 and number(@id)>0]") - .Root - .Elements() - .Where(x => (int)x.Attribute("id") == 2112) - .First(); + .Root.Elements() + .First(x => (int) x.Attribute("id") == 2112); var currPath = (string)node.Attribute("path"); //should be : -1,1111,2222,2112 Assert.AreEqual("-1,1111,2222,2112", currPath); //ensure it's indexed - indexer1.IndexItems(new[] { node.ConvertToValueSet(IndexTypes.Media) }); - - + indexer1.IndexItem(node.ConvertToValueSet(IndexTypes.Media)); //it will exist because it exists under 2222 var results = searcher.Search(searcher.CreateCriteria().Id(2112).Compile()); @@ -152,8 +263,6 @@ namespace Umbraco.Tests.UmbracoExamine //now reindex the node, this should first delete it and then NOT add it because of the parent id constraint indexer1.IndexItems(new[] { node.ConvertToValueSet(IndexTypes.Media) }); - - //now ensure it's deleted results = searcher.Search(searcher.CreateCriteria().Id(2112).Compile()); Assert.AreEqual(0, results.Count()); @@ -168,17 +277,20 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Reindex_Content() { + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, options: new UmbracoContentIndexerOptions(true, false, null))) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, + validator: new ContentValueSetValidator(false))) using (indexer.ProcessNonAsync()) { + rebuilder.RegisterIndex(indexer.Name); + var searcher = indexer.GetSearcher(); //create the whole thing - indexer.RebuildIndex(); + rebuilder.Populate(indexer); - - var result = searcher.Search(searcher.CreateCriteria().Field(LuceneIndexer.CategoryFieldName, IndexTypes.Content).Compile()); + var result = searcher.Search(searcher.CreateCriteria().Field(LuceneIndex.CategoryFieldName, IndexTypes.Content).Compile()); Assert.AreEqual(21, result.TotalItemCount); //delete all content @@ -189,15 +301,13 @@ namespace Umbraco.Tests.UmbracoExamine //ensure it's all gone - result = searcher.Search(searcher.CreateCriteria().Field(LuceneIndexer.CategoryFieldName, IndexTypes.Content).Compile()); + result = searcher.Search(searcher.CreateCriteria().Field(LuceneIndex.CategoryFieldName, IndexTypes.Content).Compile()); Assert.AreEqual(0, result.TotalItemCount); //call our indexing methods - indexer.IndexAll(IndexTypes.Content); + rebuilder.Populate(indexer); - - - result = searcher.Search(searcher.CreateCriteria().Field(LuceneIndexer.CategoryFieldName, IndexTypes.Content).Compile()); + result = searcher.Search(searcher.CreateCriteria().Field(LuceneIndex.CategoryFieldName, IndexTypes.Content).Compile()); Assert.AreEqual(21, result.TotalItemCount); } } @@ -208,15 +318,17 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Delete_Index_Item_Ensure_Heirarchy_Removed() { + + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir)) using (indexer.ProcessNonAsync()) { var searcher = indexer.GetSearcher(); //create the whole thing - indexer.RebuildIndex(); - + rebuilder.Populate(indexer); //now delete a node that has children diff --git a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs index 768d1c735c..a01d49cc22 100644 --- a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs @@ -1,17 +1,19 @@ using System; using System.Collections.Generic; using System.Linq; +using LightInject; using Examine; -using Lucene.Net.Store; using NUnit.Framework; using Examine.LuceneEngine.SearchCriteria; using Moq; using Umbraco.Core.Models; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Services; using Umbraco.Examine; using Umbraco.Tests.Testing; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Strings; namespace Umbraco.Tests.UmbracoExamine { @@ -53,13 +55,17 @@ namespace Umbraco.Tests.UmbracoExamine == allRecs); + var propertyEditors = Container.GetInstance(); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(propertyEditors, contentService, ScopeProvider.SqlContext, true); + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, ScopeProvider.SqlContext, contentService: contentService)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir)) using (indexer.ProcessNonAsync()) { - indexer.RebuildIndex(); + rebuilder.RegisterIndex(indexer.Name); + indexer.CreateIndex(); + rebuilder.Populate(indexer); - var searcher = indexer.GetSearcher(); var numberSortedCriteria = searcher.CreateCriteria() @@ -80,12 +86,12 @@ namespace Umbraco.Tests.UmbracoExamine } } - private bool IsSortedByNumber(IEnumerable results) + private bool IsSortedByNumber(IEnumerable results) { var currentSort = 0; foreach (var searchResult in results) { - var sort = int.Parse(searchResult.Fields["sortOrder"]); + var sort = int.Parse(searchResult.Values["sortOrder"]); if (currentSort >= sort) { return false; diff --git a/src/Umbraco.Tests/UmbracoExamine/TestDataService.cs b/src/Umbraco.Tests/UmbracoExamine/TestDataService.cs deleted file mode 100644 index 856c3e99aa..0000000000 --- a/src/Umbraco.Tests/UmbracoExamine/TestDataService.cs +++ /dev/null @@ -1,32 +0,0 @@ -//using System.IO; -//using Umbraco.Tests.TestHelpers; -//using UmbracoExamine.DataServices; - -//namespace Umbraco.Tests.UmbracoExamine -//{ -// public class TestDataService : IDataService -// { - -// public TestDataService() -// { -// ContentService = new TestContentService(); -// LogService = new TestLogService(); -// MediaService = new TestMediaService(); -// } - -// #region IDataService Members - -// public IContentService ContentService { get; internal set; } - -// public ILogService LogService { get; internal set; } - -// public IMediaService MediaService { get; internal set; } - -// public string MapPath(string virtualPath) -// { -// return new DirectoryInfo(TestHelper.CurrentAssemblyDirectory) + "\\" + virtualPath.Replace("/", "\\"); -// } - -// #endregion -// } -//} diff --git a/src/Umbraco.Tests/UmbracoExamine/UmbracoContentValueSetValidatorTests.cs b/src/Umbraco.Tests/UmbracoExamine/UmbracoContentValueSetValidatorTests.cs new file mode 100644 index 0000000000..8d7a446ccb --- /dev/null +++ b/src/Umbraco.Tests/UmbracoExamine/UmbracoContentValueSetValidatorTests.cs @@ -0,0 +1,300 @@ +using Examine; +using NUnit.Framework; +using Umbraco.Examine; +using Moq; +using Umbraco.Core.Services; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models; +using System; +using System.Linq; + +namespace Umbraco.Tests.UmbracoExamine +{ + [TestFixture] + public class UmbracoContentValueSetValidatorTests + { + [Test] + public void Invalid_Category() + { + var validator = new ContentValueSetValidator(false, true, Mock.Of()); + + var result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + + result = validator.Validate(ValueSet.FromObject("777", IndexTypes.Media, new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + + result = validator.Validate(ValueSet.FromObject("555", "invalid-category", new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + } + + [Test] + public void Must_Have_Path() + { + var validator = new ContentValueSetValidator(false, true, Mock.Of()); + + var result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + } + + [Test] + public void Parent_Id() + { + var validator = new ContentValueSetValidator(false, true, Mock.Of(), 555); + + var result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Filtered, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,444" })); + Assert.AreEqual(ValueSetValidationResult.Filtered, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555,777" })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555,777,999" })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + } + + [Test] + public void Inclusion_Field_List() + { + var validator = new ValueSetValidator(null, null, + new[] { "hello", "world" }, + null); + + var valueSet = ValueSet.FromObject("555", IndexTypes.Content, "test-content", new { hello = "world", path = "-1,555", world = "your oyster" }); + var result = validator.Validate(valueSet); + Assert.AreEqual(ValueSetValidationResult.Filtered, result); + + Assert.IsFalse(valueSet.Values.ContainsKey("path")); + Assert.IsTrue(valueSet.Values.ContainsKey("hello")); + Assert.IsTrue(valueSet.Values.ContainsKey("world")); + } + + [Test] + public void Exclusion_Field_List() + { + var validator = new ValueSetValidator(null, null, + null, + new[] { "hello", "world" }); + + var valueSet = ValueSet.FromObject("555", IndexTypes.Content, "test-content", new { hello = "world", path = "-1,555", world = "your oyster" }); + var result = validator.Validate(valueSet); + Assert.AreEqual(ValueSetValidationResult.Filtered, result); + + Assert.IsTrue(valueSet.Values.ContainsKey("path")); + Assert.IsFalse(valueSet.Values.ContainsKey("hello")); + Assert.IsFalse(valueSet.Values.ContainsKey("world")); + } + + [Test] + public void Inclusion_Exclusion_Field_List() + { + var validator = new ValueSetValidator(null, null, + new[] { "hello", "world" }, + new[] { "world" }); + + var valueSet = ValueSet.FromObject("555", IndexTypes.Content, "test-content", new { hello = "world", path = "-1,555", world = "your oyster" }); + var result = validator.Validate(valueSet); + Assert.AreEqual(ValueSetValidationResult.Filtered, result); + + Assert.IsFalse(valueSet.Values.ContainsKey("path")); + Assert.IsTrue(valueSet.Values.ContainsKey("hello")); + Assert.IsFalse(valueSet.Values.ContainsKey("world")); + } + + [Test] + public void Inclusion_Type_List() + { + var validator = new ContentValueSetValidator(false, true, Mock.Of(), + includeItemTypes: new List { "include-content" }); + + var result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, "test-content", new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, "include-content", new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + } + + [Test] + public void Exclusion_Type_List() + { + var validator = new ContentValueSetValidator(false, true, Mock.Of(), + excludeItemTypes: new List { "exclude-content" }); + + var result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, "test-content", new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, "exclude-content", new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + } + + [Test] + public void Inclusion_Exclusion_Type_List() + { + var validator = new ContentValueSetValidator(false, true, Mock.Of(), + includeItemTypes: new List { "include-content", "exclude-content" }, + excludeItemTypes: new List { "exclude-content" }); + + var result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, "test-content", new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, "exclude-content", new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, "include-content", new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + } + + [Test] + public void Recycle_Bin_Content() + { + var validator = new ContentValueSetValidator(true, false, Mock.Of()); + + var result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,-20,555" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,-20,555,777" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(new ValueSet("555", IndexTypes.Content, + new Dictionary + { + ["hello"] = "world", + ["path"] = "-1,555", + [UmbracoExamineIndex.PublishedFieldName] = 1 + })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + } + + [Test] + public void Recycle_Bin_Media() + { + var validator = new ContentValueSetValidator(true, false, Mock.Of()); + + var result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Media, new { hello = "world", path = "-1,-21,555" })); + Assert.AreEqual(ValueSetValidationResult.Filtered, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Media, new { hello = "world", path = "-1,-21,555,777" })); + Assert.AreEqual(ValueSetValidationResult.Filtered, result); + + result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Media, new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + + } + + [Test] + public void Published_Only() + { + var validator = new ContentValueSetValidator(true, true, Mock.Of()); + + var result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(new ValueSet("555", IndexTypes.Content, + new Dictionary + { + ["hello"] = "world", + ["path"] = "-1,555", + [UmbracoExamineIndex.PublishedFieldName] = 0 + })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(new ValueSet("555", IndexTypes.Content, + new Dictionary + { + ["hello"] = "world", + ["path"] = "-1,555", + [UmbracoExamineIndex.PublishedFieldName] = 1 + })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + } + + [Test] + public void Published_Only_With_Variants() + { + var validator = new ContentValueSetValidator(true, true, Mock.Of()); + + var result = validator.Validate(new ValueSet("555", IndexTypes.Content, + new Dictionary + { + ["hello"] = "world", + ["path"] = "-1,555", + [UmbracoContentIndex.VariesByCultureFieldName] = 1, + [UmbracoExamineIndex.PublishedFieldName] = 0 + })); + Assert.AreEqual(ValueSetValidationResult.Failed, result); + + result = validator.Validate(new ValueSet("555", IndexTypes.Content, + new Dictionary + { + ["hello"] = "world", + ["path"] = "-1,555", + [UmbracoContentIndex.VariesByCultureFieldName] = 1, + [UmbracoExamineIndex.PublishedFieldName] = 1 + })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + + var valueSet = new ValueSet("555", IndexTypes.Content, + new Dictionary + { + ["hello"] = "world", + ["path"] = "-1,555", + [UmbracoContentIndex.VariesByCultureFieldName] = 1, + [$"{UmbracoExamineIndex.PublishedFieldName}_en-us"] = 1, + ["hello_en-us"] = "world", + ["title_en-us"] = "my title", + [$"{UmbracoExamineIndex.PublishedFieldName}_es-es"] = 0, + ["hello_es-ES"] = "world", + ["title_es-ES"] = "my title", + [UmbracoExamineIndex.PublishedFieldName] = 1 + }); + Assert.AreEqual(10, valueSet.Values.Count()); + Assert.IsTrue(valueSet.Values.ContainsKey($"{UmbracoExamineIndex.PublishedFieldName}_es-es")); + Assert.IsTrue(valueSet.Values.ContainsKey("hello_es-ES")); + Assert.IsTrue(valueSet.Values.ContainsKey("title_es-ES")); + + result = validator.Validate(valueSet); + Assert.AreEqual(ValueSetValidationResult.Filtered, result); + + Assert.AreEqual(7, valueSet.Values.Count()); //filtered to 7 values (removes es-es values) + Assert.IsFalse(valueSet.Values.ContainsKey($"{UmbracoExamineIndex.PublishedFieldName}_es-es")); + Assert.IsFalse(valueSet.Values.ContainsKey("hello_es-ES")); + Assert.IsFalse(valueSet.Values.ContainsKey("title_es-ES")); + } + + [Test] + public void Non_Protected() + { + var publicAccessService = new Mock(); + publicAccessService.Setup(x => x.IsProtected("-1,555")) + .Returns(Attempt.Succeed(new PublicAccessEntry(Guid.NewGuid(), 555, 444, 333, Enumerable.Empty()))); + publicAccessService.Setup(x => x.IsProtected("-1,777")) + .Returns(Attempt.Fail()); + var validator = new ContentValueSetValidator(false, false, publicAccessService.Object); + + var result = validator.Validate(ValueSet.FromObject("555", IndexTypes.Content, new { hello = "world", path = "-1,555" })); + Assert.AreEqual(ValueSetValidationResult.Filtered, result); + + result = validator.Validate(ValueSet.FromObject("777", IndexTypes.Content, new { hello = "world", path = "-1,777" })); + Assert.AreEqual(ValueSetValidationResult.Valid, result); + } + } +} diff --git a/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs index 26a7403dac..e28d11dc4e 100644 --- a/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs @@ -211,9 +211,8 @@ namespace Umbraco.Tests.Web.Controllers var contentServiceMock = Mock.Get(Current.Services.ContentService); contentServiceMock.Setup(x => x.GetById(123)).Returns(() => null); //do not find it - var publishedSnapshot = Mock.Of(); var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty())); - var usersController = new ContentController(publishedSnapshot, propertyEditorCollection); + var usersController = new ContentController(propertyEditorCollection); Container.InjectProperties(usersController); return usersController; } @@ -239,9 +238,8 @@ namespace Umbraco.Tests.Web.Controllers var contentServiceMock = Mock.Get(Current.Services.ContentService); contentServiceMock.Setup(x => x.GetById(123)).Returns(() => GetMockedContent()); - var publishedSnapshot = Mock.Of(); var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty())); - var usersController = new ContentController(publishedSnapshot, propertyEditorCollection); + var usersController = new ContentController(propertyEditorCollection); Container.InjectProperties(usersController); return usersController; } @@ -272,9 +270,8 @@ namespace Umbraco.Tests.Web.Controllers var contentServiceMock = Mock.Get(Current.Services.ContentService); contentServiceMock.Setup(x => x.GetById(123)).Returns(() => GetMockedContent()); - var publishedSnapshot = Mock.Of(); var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty())); - var usersController = new ContentController(publishedSnapshot, propertyEditorCollection); + var usersController = new ContentController(propertyEditorCollection); Container.InjectProperties(usersController); return usersController; } @@ -311,9 +308,8 @@ namespace Umbraco.Tests.Web.Controllers contentServiceMock.Setup(x => x.Save(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new OperationResult(OperationResultType.Success, new Core.Events.EventMessages())); //success - var publishedSnapshot = Mock.Of(); var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty())); - var usersController = new ContentController(publishedSnapshot, propertyEditorCollection); + var usersController = new ContentController(propertyEditorCollection); Container.InjectProperties(usersController); return usersController; } @@ -344,9 +340,8 @@ namespace Umbraco.Tests.Web.Controllers contentServiceMock.Setup(x => x.Save(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new OperationResult(OperationResultType.Success, new Core.Events.EventMessages())); //success - var publishedSnapshot = Mock.Of(); var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty())); - var usersController = new ContentController(publishedSnapshot, propertyEditorCollection); + var usersController = new ContentController(propertyEditorCollection); Container.InjectProperties(usersController); return usersController; } @@ -381,9 +376,8 @@ namespace Umbraco.Tests.Web.Controllers contentServiceMock.Setup(x => x.Save(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new OperationResult(OperationResultType.Success, new Core.Events.EventMessages())); //success - var publishedSnapshot = Mock.Of(); var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty())); - var usersController = new ContentController(publishedSnapshot, propertyEditorCollection); + var usersController = new ContentController(propertyEditorCollection); Container.InjectProperties(usersController); return usersController; } diff --git a/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs index 21aa739e23..f55a4e593b 100644 --- a/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Editors; @@ -33,14 +34,14 @@ namespace Umbraco.Tests.Web.Controllers var userService = userServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, 1234); + var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] - public void Throws_Exception_When_No_Content_Found() + public void No_Content_Found() { //arrange var userMock = new Mock(); @@ -60,8 +61,11 @@ namespace Umbraco.Tests.Web.Controllers var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; - //act/assert - Assert.Throws(() => ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, 1234, new[] { 'F' })); + //act + var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); + + //assert + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.NotFound, result); } [Test] @@ -89,10 +93,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, 1234, new[] { 'F' }); + var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } [Test] @@ -120,10 +124,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, 1234, new[] { 'F' }); + var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } [Test] @@ -152,10 +156,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, 1234, new[] { 'F' }); + var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] @@ -174,10 +178,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -1); + var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] @@ -196,10 +200,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -20); + var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] @@ -220,10 +224,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -20); + var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } [Test] @@ -244,10 +248,10 @@ namespace Umbraco.Tests.Web.Controllers var entityService = entityServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -1); + var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } [Test] @@ -274,10 +278,10 @@ namespace Umbraco.Tests.Web.Controllers //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -1, new[] { 'A' }); + var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent, new[] { 'A' }); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] @@ -302,10 +306,10 @@ namespace Umbraco.Tests.Web.Controllers var contentService = contentServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -1, new[] { 'B' }); + var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent, new[] { 'B' }); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } [Test] @@ -332,10 +336,10 @@ namespace Umbraco.Tests.Web.Controllers var contentService = contentServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -20, new[] { 'A' }); + var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent, new[] { 'A' }); //assert - Assert.IsTrue(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); } [Test] @@ -360,10 +364,10 @@ namespace Umbraco.Tests.Web.Controllers var contentService = contentServiceMock.Object; //act - var result = ContentController.CheckPermissions(new Dictionary(), user, userService, contentService, entityService, -20, new[] { 'B' }); + var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent, new[] { 'B' }); //assert - Assert.IsFalse(result); + Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); } } diff --git a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs index 4186c56636..dce975d0c4 100644 --- a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs @@ -103,6 +103,13 @@ namespace Umbraco.Tests.Web.Mvc { var publishedSnapshot = new Mock(); publishedSnapshot.Setup(x => x.Members).Returns(Mock.Of()); + var contentCache = new Mock(); + var content = new Mock(); + content.Setup(x => x.Id).Returns(2); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns(content.Object); + var mediaCache = new Mock(); + publishedSnapshot.Setup(x => x.Content).Returns(contentCache.Object); + publishedSnapshot.Setup(x => x.Media).Returns(mediaCache.Object); var publishedSnapshotService = new Mock(); publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedSnapshot.Object); var globalSettings = TestObjects.GetGlobalSettings(); @@ -121,16 +128,11 @@ namespace Umbraco.Tests.Web.Mvc var helper = new UmbracoHelper( umbracoContext, Mock.Of(), - Mock.Of(query => query.Content(It.IsAny()) == - //return mock of IPublishedContent for any call to GetById - Mock.Of(content => content.Id == 2)), Mock.Of(), - Mock.Of(), Mock.Of(), Mock.Of(), new MembershipHelper(umbracoContext, Mock.Of(), Mock.Of()), - new ServiceContext(), - CacheHelper.CreateDisabledCacheHelper()); + new ServiceContext()); var ctrl = new TestSurfaceController { UmbracoContext = umbracoContext, Umbraco = helper }; var result = ctrl.GetContent(2) as PublishedContentResult; diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index b773025e7c..0af327d148 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -253,24 +253,6 @@ gulp.task('dependencies', function () { "src": ["./node_modules/clipboard/dist/clipboard.min.js"], "base": "./node_modules/clipboard/dist" }, - { - "name": "codemirror", - "src": [ - "./node_modules/codemirror/lib/codemirror.js", - "./node_modules/codemirror/lib/codemirror.css", - - "./node_modules/codemirror/mode/css/css.js", - "./node_modules/codemirror/mode/javascript/javascript.js", - "./node_modules/codemirror/mode/xml/xml.js", - "./node_modules/codemirror/mode/htmlmixed/htmlmixed.js", - - "./node_modules/codemirror/addon/search/*", - "./node_modules/codemirror/addon/edit/*", - "./node_modules/codemirror/addon/selection/*", - "./node_modules/codemirror/addon/dialog/*" - ], - "base": "./node_modules/codemirror" - }, { "name": "jsdiff", "src": ["./node_modules/diff/dist/diff.min.js"], @@ -300,48 +282,29 @@ gulp.task('dependencies', function () { ], "base": "./node_modules/jquery/dist" }, - { - "name": "jquery-migrate", - "src": ["./node_modules/jquery-migrate/dist/jquery-migrate.min.js"], - "base": "./node_modules/jquery-migrate/dist" - }, { "name": "jquery-ui", "src": ["./node_modules/jquery-ui-dist/jquery-ui.min.js"], "base": "./node_modules/jquery-ui-dist" }, { - "name": "jquery-validate", - "src": ["./node_modules/jquery-validation/dist/jquery.validate.min.js"], - "base": "./node_modules/jquery-validation/dist" - }, - { - "name": "jquery-validation-unobtrusive", - "src": ["./node_modules/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"], - "base": "./node_modules/jquery-validation-unobtrusive/dist" + "name": "jquery-ui-touch-punch", + "src": ["./node_modules/jquery-ui-touch-punch/jquery.ui.touch-punch.min.js"], + "base": "./node_modules/jquery-ui-touch-punch" }, { "name": "lazyload-js", "src": ["./node_modules/lazyload-js/lazyload.min.js"], "base": "./node_modules/lazyload-js" }, - // TODO: We can optimize here: - // we don't have to ship with the moment-with-locales libraries - // we lazyload the user locale { "name": "moment", - "src": [ - "./node_modules/moment/min/moment.min.js", - "./node_modules/moment/min/moment-with-locales.js", - "./node_modules/moment/min/moment-with-locales.min.js" - ], + "src": ["./node_modules/moment/min/moment.min.js"], "base": "./node_modules/moment/min" }, { "name": "moment", - "src": [ - "./node_modules/moment/locale/*.js" - ], + "src": ["./node_modules/moment/locale/*.js"], "base": "./node_modules/moment/locale" }, { @@ -349,16 +312,33 @@ gulp.task('dependencies', function () { "src": ["./node_modules/ng-file-upload/dist/ng-file-upload.min.js"], "base": "./node_modules/ng-file-upload/dist" }, + { + "name": "nouislider", + "src": [ + "./node_modules/nouislider/distribute/nouislider.min.js", + "./node_modules/nouislider/distribute/nouislider.min.css" + ], + "base": "./node_modules/nouislider/distribute" + }, { "name": "signalr", "src": ["./node_modules/signalr/jquery.signalR.js"], "base": "./node_modules/signalr" }, + { + "name": "spectrum", + "src": [ + "./node_modules/spectrum-colorpicker/spectrum.js", + "./node_modules/spectrum-colorpicker/spectrum.css" + ], + "base": "./node_modules/spectrum-colorpicker" + }, { "name": "tinymce", "src": [ "./node_modules/tinymce/tinymce.min.js", "./node_modules/tinymce/plugins/**", + "./node_modules/tinymce/skins/**", "./node_modules/tinymce/themes/**" ], "base": "./node_modules/tinymce" diff --git a/src/Umbraco.Web.UI.Client/lib/angular-bootstrap/ui-bootstrap-tpls-0.10.0.min.js b/src/Umbraco.Web.UI.Client/lib/angular-bootstrap/ui-bootstrap-tpls-0.10.0.min.js deleted file mode 100644 index 0830f35b07..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/angular-bootstrap/ui-bootstrap-tpls-0.10.0.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * angular-ui-bootstrap - * http://angular-ui.github.io/bootstrap/ - - * Version: 0.10.0 - 2014-01-13 - * License: MIT - */ -angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/popup.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(a){return{link:function(b,c,d){function e(b){function d(){j===e&&(j=void 0)}var e=a(c,b);return j&&j.cancel(),j=e,e.then(d,d),e}function f(){k?(k=!1,g()):(c.removeClass("collapse").addClass("collapsing"),e({height:c[0].scrollHeight+"px"}).then(g))}function g(){c.removeClass("collapsing"),c.addClass("collapse in"),c.css({height:"auto"})}function h(){if(k)k=!1,i(),c.css({height:0});else{c.css({height:c[0].scrollHeight+"px"});{c[0].offsetWidth}c.removeClass("collapse in").addClass("collapsing"),e({height:0}).then(i)}}function i(){c.removeClass("collapsing"),c.addClass("collapse")}var j,k=!0;b.$watch(d.collapse,function(a){a?h():f()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(this.groups.indexOf(a),1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",["$parse",function(a){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(b,c,d,e){var f,g;e.addGroup(b),b.isOpen=!1,d.isOpen&&(f=a(d.isOpen),g=f.assign,b.$parent.$watch(f,function(a){b.isOpen=!!a})),b.$watch("isOpen",function(a){a&&e.closeOthers(b),g&&g(b.$parent,a)})}}}]).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",compile:function(a,b,c){return function(a,b,d,e){e.setHeading(c(a,function(){}))}}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"=",close:"&"}}}),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){b.hasClass(e.activeClass)||a.$apply(function(){f.$setViewValue(a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$transition","$q",function(a,b,c){function d(){e();var c=+a.interval;!isNaN(c)&&c>=0&&(g=b(f,c))}function e(){g&&(b.cancel(g),g=null)}function f(){h?(a.next(),d()):a.pause()}var g,h,i=this,j=i.slides=[],k=-1;i.currentSlide=null;var l=!1;i.select=function(e,f){function g(){if(!l){if(i.currentSlide&&angular.isString(f)&&!a.noTransition&&e.$element){e.$element.addClass(f);{e.$element[0].offsetWidth}angular.forEach(j,function(a){angular.extend(a,{direction:"",entering:!1,leaving:!1,active:!1})}),angular.extend(e,{direction:f,active:!0,entering:!0}),angular.extend(i.currentSlide||{},{direction:f,leaving:!0}),a.$currentTransition=c(e.$element,{}),function(b,c){a.$currentTransition.then(function(){h(b,c)},function(){h(b,c)})}(e,i.currentSlide)}else h(e,i.currentSlide);i.currentSlide=e,k=m,d()}}function h(b,c){angular.extend(b,{direction:"",active:!0,leaving:!1,entering:!1}),angular.extend(c||{},{direction:"",active:!1,leaving:!1,entering:!1}),a.$currentTransition=null}var m=j.indexOf(e);void 0===f&&(f=m>k?"next":"prev"),e&&e!==i.currentSlide&&(a.$currentTransition?(a.$currentTransition.cancel(),b(g)):g())},a.$on("$destroy",function(){l=!0}),i.indexOfSlide=function(a){return j.indexOf(a)},a.next=function(){var b=(k+1)%j.length;return a.$currentTransition?void 0:i.select(j[b],"next")},a.prev=function(){var b=0>k-1?j.length-1:k-1;return a.$currentTransition?void 0:i.select(j[b],"prev")},a.select=function(a){i.select(a)},a.isActive=function(a){return i.currentSlide===a},a.slides=function(){return j},a.$watch("interval",d),a.$on("$destroy",e),a.play=function(){h||(h=!0,d())},a.pause=function(){a.noPause||(h=!1,e())},i.addSlide=function(b,c){b.$element=c,j.push(b),1===j.length||b.active?(i.select(j[j.length-1]),1==j.length&&a.play()):b.active=!1},i.removeSlide=function(a){var b=j.indexOf(a);j.splice(b,1),j.length>0&&a.active?b>=j.length?i.select(j[b-1]):i.select(j[b]):k>b&&k--}}]).directive("carousel",[function(){return{restrict:"EA",transclude:!0,replace:!0,controller:"CarouselController",require:"carousel",templateUrl:"template/carousel/carousel.html",scope:{interval:"=",noTransition:"=",noPause:"="}}}]).directive("slide",["$parse",function(a){return{require:"^carousel",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/carousel/slide.html",scope:{},link:function(b,c,d,e){if(d.active){var f=a(d.active),g=f.assign,h=b.active=f(b.$parent);b.$watch(function(){var a=f(b.$parent);return a!==b.active&&(a!==h?h=b.active=a:g(b.$parent,a=h=b.active)),a})}e.addSlide(b,c),b.$on("$destroy",function(){e.removeSlide(b)}),b.$watch("active",function(a){a&&e.select(b)})}}}]),angular.module("ui.bootstrap.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].body.scrollTop||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].body.scrollLeft||a[0].documentElement.scrollLeft)}}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.position"]).constant("datepickerConfig",{dayFormat:"dd",monthFormat:"MMMM",yearFormat:"yyyy",dayHeaderFormat:"EEE",dayTitleFormat:"MMMM yyyy",monthTitleFormat:"yyyy",showWeeks:!0,startingDay:0,yearRange:20,minDate:null,maxDate:null}).controller("DatepickerController",["$scope","$attrs","dateFilter","datepickerConfig",function(a,b,c,d){function e(b,c){return angular.isDefined(b)?a.$parent.$eval(b):c}function f(a,b){return new Date(a,b,0).getDate()}function g(a,b){for(var c=new Array(b),d=a,e=0;b>e;)c[e++]=new Date(d),d.setDate(d.getDate()+1);return c}function h(a,b,d,e){return{date:a,label:c(a,b),selected:!!d,secondary:!!e}}var i={day:e(b.dayFormat,d.dayFormat),month:e(b.monthFormat,d.monthFormat),year:e(b.yearFormat,d.yearFormat),dayHeader:e(b.dayHeaderFormat,d.dayHeaderFormat),dayTitle:e(b.dayTitleFormat,d.dayTitleFormat),monthTitle:e(b.monthTitleFormat,d.monthTitleFormat)},j=e(b.startingDay,d.startingDay),k=e(b.yearRange,d.yearRange);this.minDate=d.minDate?new Date(d.minDate):null,this.maxDate=d.maxDate?new Date(d.maxDate):null,this.modes=[{name:"day",getVisibleDates:function(a,b){var d=a.getFullYear(),e=a.getMonth(),k=new Date(d,e,1),l=j-k.getDay(),m=l>0?7-l:-l,n=new Date(k),o=0;m>0&&(n.setDate(-m+1),o+=m),o+=f(d,e+1),o+=(7-o%7)%7;for(var p=g(n,o),q=new Array(7),r=0;o>r;r++){var s=new Date(p[r]);p[r]=h(s,i.day,b&&b.getDate()===s.getDate()&&b.getMonth()===s.getMonth()&&b.getFullYear()===s.getFullYear(),s.getMonth()!==e)}for(var t=0;7>t;t++)q[t]=c(p[t].date,i.dayHeader);return{objects:p,title:c(a,i.dayTitle),labels:q}},compare:function(a,b){return new Date(a.getFullYear(),a.getMonth(),a.getDate())-new Date(b.getFullYear(),b.getMonth(),b.getDate())},split:7,step:{months:1}},{name:"month",getVisibleDates:function(a,b){for(var d=new Array(12),e=a.getFullYear(),f=0;12>f;f++){var g=new Date(e,f,1);d[f]=h(g,i.month,b&&b.getMonth()===f&&b.getFullYear()===e)}return{objects:d,title:c(a,i.monthTitle)}},compare:function(a,b){return new Date(a.getFullYear(),a.getMonth())-new Date(b.getFullYear(),b.getMonth())},split:3,step:{years:1}},{name:"year",getVisibleDates:function(a,b){for(var c=new Array(k),d=a.getFullYear(),e=parseInt((d-1)/k,10)*k+1,f=0;k>f;f++){var g=new Date(e+f,0,1);c[f]=h(g,i.year,b&&b.getFullYear()===g.getFullYear())}return{objects:c,title:[c[0].label,c[k-1].label].join(" - ")}},compare:function(a,b){return a.getFullYear()-b.getFullYear()},split:5,step:{years:k}}],this.isDisabled=function(b,c){var d=this.modes[c||0];return this.minDate&&d.compare(b,this.minDate)<0||this.maxDate&&d.compare(b,this.maxDate)>0||a.dateDisabled&&a.dateDisabled({date:b,mode:d.name})}}]).directive("datepicker",["dateFilter","$parse","datepickerConfig","$log",function(a,b,c,d){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/datepicker.html",scope:{dateDisabled:"&"},require:["datepicker","?^ngModel"],controller:"DatepickerController",link:function(a,e,f,g){function h(){a.showWeekNumbers=0===o&&q}function i(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c}function j(b){var c=null,e=!0;n.$modelValue&&(c=new Date(n.$modelValue),isNaN(c)?(e=!1,d.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):b&&(p=c)),n.$setValidity("date",e);var f=m.modes[o],g=f.getVisibleDates(p,c);angular.forEach(g.objects,function(a){a.disabled=m.isDisabled(a.date,o)}),n.$setValidity("date-disabled",!c||!m.isDisabled(c)),a.rows=i(g.objects,f.split),a.labels=g.labels||[],a.title=g.title}function k(a){o=a,h(),j()}function l(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}var m=g[0],n=g[1];if(n){var o=0,p=new Date,q=c.showWeeks;f.showWeeks?a.$parent.$watch(b(f.showWeeks),function(a){q=!!a,h()}):h(),f.min&&a.$parent.$watch(b(f.min),function(a){m.minDate=a?new Date(a):null,j()}),f.max&&a.$parent.$watch(b(f.max),function(a){m.maxDate=a?new Date(a):null,j()}),n.$render=function(){j(!0)},a.select=function(a){if(0===o){var b=n.$modelValue?new Date(n.$modelValue):new Date(0,0,0,0,0,0,0);b.setFullYear(a.getFullYear(),a.getMonth(),a.getDate()),n.$setViewValue(b),j(!0)}else p=a,k(o-1)},a.move=function(a){var b=m.modes[o].step;p.setMonth(p.getMonth()+a*(b.months||0)),p.setFullYear(p.getFullYear()+a*(b.years||0)),j()},a.toggleMode=function(){k((o+1)%m.modes.length)},a.getWeekNumber=function(b){return 0===o&&a.showWeekNumbers&&7===b.length?l(b[0].date):null}}}}}]).constant("datepickerPopupConfig",{dateFormat:"yyyy-MM-dd",currentText:"Today",toggleWeeksText:"Weeks",clearText:"Clear",closeText:"Done",closeOnDateSelection:!0,appendToBody:!1,showButtonBar:!0}).directive("datepickerPopup",["$compile","$parse","$document","$position","dateFilter","datepickerPopupConfig","datepickerConfig",function(a,b,c,d,e,f,g){return{restrict:"EA",require:"ngModel",link:function(h,i,j,k){function l(a){u?u(h,!!a):q.isOpen=!!a}function m(a){if(a){if(angular.isDate(a))return k.$setValidity("date",!0),a;if(angular.isString(a)){var b=new Date(a);return isNaN(b)?(k.$setValidity("date",!1),void 0):(k.$setValidity("date",!0),b)}return k.$setValidity("date",!1),void 0}return k.$setValidity("date",!0),null}function n(a,c,d){a&&(h.$watch(b(a),function(a){q[c]=a}),y.attr(d||c,c))}function o(){q.position=s?d.offset(i):d.position(i),q.position.top=q.position.top+i.prop("offsetHeight")}var p,q=h.$new(),r=angular.isDefined(j.closeOnDateSelection)?h.$eval(j.closeOnDateSelection):f.closeOnDateSelection,s=angular.isDefined(j.datepickerAppendToBody)?h.$eval(j.datepickerAppendToBody):f.appendToBody;j.$observe("datepickerPopup",function(a){p=a||f.dateFormat,k.$render()}),q.showButtonBar=angular.isDefined(j.showButtonBar)?h.$eval(j.showButtonBar):f.showButtonBar,h.$on("$destroy",function(){C.remove(),q.$destroy()}),j.$observe("currentText",function(a){q.currentText=angular.isDefined(a)?a:f.currentText}),j.$observe("toggleWeeksText",function(a){q.toggleWeeksText=angular.isDefined(a)?a:f.toggleWeeksText}),j.$observe("clearText",function(a){q.clearText=angular.isDefined(a)?a:f.clearText}),j.$observe("closeText",function(a){q.closeText=angular.isDefined(a)?a:f.closeText});var t,u;j.isOpen&&(t=b(j.isOpen),u=t.assign,h.$watch(t,function(a){q.isOpen=!!a})),q.isOpen=t?t(h):!1;var v=function(a){q.isOpen&&a.target!==i[0]&&q.$apply(function(){l(!1)})},w=function(){q.$apply(function(){l(!0)})},x=angular.element("
");x.attr({"ng-model":"date","ng-change":"dateSelection()"});var y=angular.element(x.children()[0]),z={};j.datepickerOptions&&(z=h.$eval(j.datepickerOptions),y.attr(angular.extend({},z))),k.$parsers.unshift(m),q.dateSelection=function(a){angular.isDefined(a)&&(q.date=a),k.$setViewValue(q.date),k.$render(),r&&l(!1)},i.bind("input change keyup",function(){q.$apply(function(){q.date=k.$modelValue})}),k.$render=function(){var a=k.$viewValue?e(k.$viewValue,p):"";i.val(a),q.date=k.$modelValue},n(j.min,"min"),n(j.max,"max"),j.showWeeks?n(j.showWeeks,"showWeeks","show-weeks"):(q.showWeeks="show-weeks"in z?z["show-weeks"]:g.showWeeks,y.attr("show-weeks","showWeeks")),j.dateDisabled&&y.attr("date-disabled",j.dateDisabled);var A=!1,B=!1;q.$watch("isOpen",function(a){a?(o(),c.bind("click",v),B&&i.unbind("focus",w),i[0].focus(),A=!0):(A&&c.unbind("click",v),i.bind("focus",w),B=!0),u&&u(h,a)}),q.today=function(){q.dateSelection(new Date)},q.clear=function(){q.dateSelection(null)};var C=a(x)(q);s?c.find("body").append(C):i.after(C)}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(a,b){b.bind("click",function(a){a.preventDefault(),a.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdownToggle",[]).directive("dropdownToggle",["$document","$location",function(a){var b=null,c=angular.noop;return{restrict:"CA",link:function(d,e){d.$watch("$location.path",function(){c()}),e.parent().bind("click",function(){c()}),e.bind("click",function(d){var f=e===b;d.preventDefault(),d.stopPropagation(),b&&c(),f||e.hasClass("disabled")||e.prop("disabled")||(e.parent().addClass("open"),b=e,c=function(d){d&&(d.preventDefault(),d.stopPropagation()),a.unbind("click",c),e.parent().removeClass("open"),c=angular.noop,b=null},a.bind("click",c))})}}}]),angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0)}function i(){if(k&&-1==g()){var a=l;j(k,l,150,function(){a.$destroy(),a=null}),k=void 0,l=void 0}}function j(c,d,e,f){function g(){g.done||(g.done=!0,c.remove(),f&&f())}d.animate=!1;var h=a.transitionEndEventName;if(h){var i=b(g,e);c.bind(h,function(){b.cancel(i),g(),d.$apply()})}else b(g,0)}var k,l,m="modal-open",n=f.createNew(),o={};return e.$watch(g,function(a){l&&(l.index=a)}),c.bind("keydown",function(a){var b;27===a.which&&(b=n.top(),b&&b.value.keyboard&&e.$apply(function(){o.dismiss(b.key)}))}),o.open=function(a,b){n.add(a,{deferred:b.deferred,modalScope:b.scope,backdrop:b.backdrop,keyboard:b.keyboard});var f=c.find("body").eq(0),h=g();h>=0&&!k&&(l=e.$new(!0),l.index=h,k=d("
")(l),f.append(k));var i=angular.element("
");i.attr("window-class",b.windowClass),i.attr("index",n.length()-1),i.attr("animate","animate"),i.html(b.content);var j=d(i)(b.scope);n.top().value.modalDomEl=j,f.append(j),f.addClass(m)},o.close=function(a,b){var c=n.get(a).value;c&&(c.deferred.resolve(b),h(a))},o.dismiss=function(a,b){var c=n.get(a).value;c&&(c.deferred.reject(b),h(a))},o.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},o.getTop=function(){return n.top()},o}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse","$interpolate",function(a,b,c,d){var e=this,f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(d){b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){e.itemsPerPage=parseInt(b,10),a.totalPages=e.calculateTotalPages()}):this.itemsPerPage=d},this.noPrevious=function(){return 1===this.page},this.noNext=function(){return this.page===a.totalPages},this.isActive=function(a){return this.page===a},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.getAttributeValue=function(b,c,e){return angular.isDefined(b)?e?d(b)(a.$parent):a.$parent.$eval(b):c},this.render=function(){this.page=parseInt(a.page,10)||1,this.page>0&&this.page<=a.totalPages&&(a.pages=this.getPages(this.page,a.totalPages))},a.selectPage=function(b){!e.isActive(b)&&b>0&&b<=a.totalPages&&(a.page=b,a.onSelectPage({page:b}))},a.$watch("page",function(){e.render()}),a.$watch("totalItems",function(){a.totalPages=e.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),e.page>b?a.selectPage(b):e.render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c,d){return{number:a,text:b,active:c,disabled:d}}var h,i=f.getAttributeValue(e.boundaryLinks,b.boundaryLinks),j=f.getAttributeValue(e.directionLinks,b.directionLinks),k=f.getAttributeValue(e.firstText,b.firstText,!0),l=f.getAttributeValue(e.previousText,b.previousText,!0),m=f.getAttributeValue(e.nextText,b.nextText,!0),n=f.getAttributeValue(e.lastText,b.lastText,!0),o=f.getAttributeValue(e.rotate,b.rotate);f.init(b.itemsPerPage),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){h=parseInt(a,10),f.render()}),f.getPages=function(a,b){var c=[],d=1,e=b,p=angular.isDefined(h)&&b>h;p&&(o?(d=Math.max(a-Math.floor(h/2),1),e=d+h-1,e>b&&(e=b,d=e-h+1)):(d=(Math.ceil(a/h)-1)*h+1,e=Math.min(d+h-1,b)));for(var q=d;e>=q;q++){var r=g(q,q,f.isActive(q),!1);c.push(r)}if(p&&!o){if(d>1){var s=g(d-1,"...",!1,!1);c.unshift(s)}if(b>e){var t=g(e+1,"...",!1,!1);c.push(t)}}if(j){var u=g(a-1,l,!1,f.noPrevious());c.unshift(u);var v=g(a+1,m,!1,f.noNext());c.push(v)}if(i){var w=g(1,k,!1,f.noPrevious());c.unshift(w);var x=g(b,n,!1,f.noNext());c.push(x)}return c}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){function f(a,b,c,d,e){return{number:a,text:b,disabled:c,previous:i&&d,next:i&&e}}var g=e.getAttributeValue(d.previousText,a.previousText,!0),h=e.getAttributeValue(d.nextText,a.nextText,!0),i=e.getAttributeValue(d.align,a.align);e.init(a.itemsPerPage),e.getPages=function(a){return[f(a-1,g,e.noPrevious(),!0,!1),f(a+1,h,e.noNext(),!1,!0)]}}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!z||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){b.$apply(function(){q()})}function p(){return b.tt_content?(r(),u&&g.cancel(u),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),A(),b.tt_isOpen=!0,b.$digest(),A):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),b.tt_animation?u=g(s,500):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=!1,z=angular.isDefined(d[l+"Enable"]),A=function(){var a,d,e,f;switch(a=w?j.offset(c):j.position(c),d=t.prop("offsetWidth"),e=t.prop("offsetHeight"),b.tt_placement){case"right":f={top:a.top+a.height/2-e/2,left:a.left+a.width};break;case"bottom":f={top:a.top+a.height,left:a.left+a.width/2-d/2};break;case"left":f={top:a.top+a.height/2-e/2,left:a.left-d};break;default:f={top:a.top-e,left:a.left+a.width/2-d/2}}f.top+="px",f.left+="px",t.css(f)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)?a:o.placement}),d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var B=function(){y&&(c.unbind(x.show,k),c.unbind(x.hide,m))};d.$observe(l+"Trigger",function(a){B(),x=n(a),x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m)),y=!0});var C=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(C)?!!C:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),B(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",["ui.bootstrap.transition"]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig","$transition",function(a,b,c,d){var e=this,f=[],g=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,h=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.addBar=function(a,b){var c=0,d=a.$parent.$index;angular.isDefined(d)&&f[d]&&(c=f[d].value),f.push(a),this.update(b,a.value,c),a.$watch("value",function(a,c){a!==c&&e.update(b,a,c)}),a.$on("$destroy",function(){e.removeBar(a)})},this.update=function(a,b,c){var e=this.getPercentage(b);h?(a.css("width",this.getPercentage(c)+"%"),d(a,{width:e+"%"})):a.css({transition:"none",width:e+"%"})},this.removeBar=function(a){f.splice(f.indexOf(a),1)},this.getPercentage=function(a){return Math.round(100*a/g)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},template:'
'}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","$parse","ratingConfig",function(a,b,c,d){this.maxRange=angular.isDefined(b.max)?a.$parent.$eval(b.max):d.max,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):d.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):d.stateOff,this.createRateObjects=function(a){for(var b={stateOn:this.stateOn,stateOff:this.stateOff},c=0,d=a.length;d>c;c++)a[c]=angular.extend({index:c},b,a[c]);return a},a.range=angular.isDefined(b.ratingStates)?this.createRateObjects(angular.copy(a.$parent.$eval(b.ratingStates))):this.createRateObjects(new Array(this.maxRange)),a.rate=function(b){a.value===b||a.readonly||(a.value=b) -},a.enter=function(b){a.readonly||(a.val=b),a.onHover({value:b})},a.reset=function(){a.val=angular.copy(a.value),a.onLeave()},a.$watch("value",function(b){a.val=b}),a.readonly=!1,b.readonly&&a.$parent.$watch(c(b.readonly),function(b){a.readonly=!!b})}]).directive("rating",function(){return{restrict:"EA",scope:{value:"=",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0}}),angular.module("ui.bootstrap.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(a){a.active=!1}),a.active=!0},b.addTab=function(a){c.push(a),(1===c.length||a.active)&&b.select(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1,a.type=angular.isDefined(c.type)?a.$parent.$eval(c.type):"tabs"}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){var g,h;e.active?(g=a(e.active),h=g.assign,b.$parent.$watch(g,function(a,c){a!==c&&(b.active=!!a)}),b.active=g(b.$parent)):h=g=angular.noop,b.$watch("active",function(a){h(b.$parent,a),a?(f.select(b),b.onSelect()):b.onDeselect()}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("ui.bootstrap.timepicker",[]).constant("timepickerConfig",{hourStep:1,minuteStep:1,showMeridian:!0,meridians:null,readonlyInput:!1,mousewheel:!0}).directive("timepicker",["$parse","$log","timepickerConfig","$locale",function(a,b,c,d){return{restrict:"EA",require:"?^ngModel",replace:!0,scope:{},templateUrl:"template/timepicker/timepicker.html",link:function(e,f,g,h){function i(){var a=parseInt(e.hours,10),b=e.showMeridian?a>0&&13>a:a>=0&&24>a;return b?(e.showMeridian&&(12===a&&(a=0),e.meridian===q[1]&&(a+=12)),a):void 0}function j(){var a=parseInt(e.minutes,10);return a>=0&&60>a?a:void 0}function k(a){return angular.isDefined(a)&&a.toString().length<2?"0"+a:a}function l(a){m(),h.$setViewValue(new Date(p)),n(a)}function m(){h.$setValidity("time",!0),e.invalidHours=!1,e.invalidMinutes=!1}function n(a){var b=p.getHours(),c=p.getMinutes();e.showMeridian&&(b=0===b||12===b?12:b%12),e.hours="h"===a?b:k(b),e.minutes="m"===a?c:k(c),e.meridian=p.getHours()<12?q[0]:q[1]}function o(a){var b=new Date(p.getTime()+6e4*a);p.setHours(b.getHours(),b.getMinutes()),l()}if(h){var p=new Date,q=angular.isDefined(g.meridians)?e.$parent.$eval(g.meridians):c.meridians||d.DATETIME_FORMATS.AMPMS,r=c.hourStep;g.hourStep&&e.$parent.$watch(a(g.hourStep),function(a){r=parseInt(a,10)});var s=c.minuteStep;g.minuteStep&&e.$parent.$watch(a(g.minuteStep),function(a){s=parseInt(a,10)}),e.showMeridian=c.showMeridian,g.showMeridian&&e.$parent.$watch(a(g.showMeridian),function(a){if(e.showMeridian=!!a,h.$error.time){var b=i(),c=j();angular.isDefined(b)&&angular.isDefined(c)&&(p.setHours(b),l())}else n()});var t=f.find("input"),u=t.eq(0),v=t.eq(1),w=angular.isDefined(g.mousewheel)?e.$eval(g.mousewheel):c.mousewheel;if(w){var x=function(a){a.originalEvent&&(a=a.originalEvent);var b=a.wheelDelta?a.wheelDelta:-a.deltaY;return a.detail||b>0};u.bind("mousewheel wheel",function(a){e.$apply(x(a)?e.incrementHours():e.decrementHours()),a.preventDefault()}),v.bind("mousewheel wheel",function(a){e.$apply(x(a)?e.incrementMinutes():e.decrementMinutes()),a.preventDefault()})}if(e.readonlyInput=angular.isDefined(g.readonlyInput)?e.$eval(g.readonlyInput):c.readonlyInput,e.readonlyInput)e.updateHours=angular.noop,e.updateMinutes=angular.noop;else{var y=function(a,b){h.$setViewValue(null),h.$setValidity("time",!1),angular.isDefined(a)&&(e.invalidHours=a),angular.isDefined(b)&&(e.invalidMinutes=b)};e.updateHours=function(){var a=i();angular.isDefined(a)?(p.setHours(a),l("h")):y(!0)},u.bind("blur",function(){!e.validHours&&e.hours<10&&e.$apply(function(){e.hours=k(e.hours)})}),e.updateMinutes=function(){var a=j();angular.isDefined(a)?(p.setMinutes(a),l("m")):y(void 0,!0)},v.bind("blur",function(){!e.invalidMinutes&&e.minutes<10&&e.$apply(function(){e.minutes=k(e.minutes)})})}h.$render=function(){var a=h.$modelValue?new Date(h.$modelValue):null;isNaN(a)?(h.$setValidity("time",!1),b.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(a&&(p=a),m(),n())},e.incrementHours=function(){o(60*r)},e.decrementHours=function(){o(60*-r)},e.incrementMinutes=function(){o(s)},e.decrementMinutes=function(){o(-s)},e.toggleMeridian=function(){o(720*(p.getHours()<12?1:-1))}}}}}]),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_' but got '"+c+"'.");return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?b(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=angular.element("
");w.attr({matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&w.attr("template-url",k.typeaheadTemplateUrl);var x=i.$new();i.$on("$destroy",function(){x.$destroy()});var y=function(){x.matches=[],x.activeIdx=-1},z=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){if(a===l.$viewValue&&m){if(c.length>0){x.activeIdx=0,x.matches.length=0;for(var d=0;d=n?o>0?(A&&d.cancel(A),A=d(function(){z(a)},o)):z(a):(q(i,!1),y()),p?a:a?(l.$setValidity("editable",!1),void 0):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),x.select=function(a){var b,c,d={};d[v.itemName]=c=x.matches[a].model,b=v.modelMapper(i,d),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,d)}),y(),j[0].focus()},j.bind("keydown",function(a){0!==x.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(x.activeIdx=(x.activeIdx+1)%x.matches.length,x.$digest()):38===a.which?(x.activeIdx=(x.activeIdx?x.activeIdx:x.matches.length)-1,x.$digest()):13===a.which||9===a.which?x.$apply(function(){x.select(x.activeIdx)}):27===a.which&&(a.stopPropagation(),y(),x.$digest()))}),j.bind("blur",function(){m=!1});var B=function(a){j[0]!==a.target&&(y(),x.$digest())};e.bind("click",B),i.$on("$destroy",function(){e.unbind("click",B)});var C=a(w)(x);t?e.find("body").append(C):j.after(C)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?b.replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'
\n
\n

\n {{heading}}\n

\n
\n
\n
\n
\n
')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'
')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html","
\n \n
\n
\n")}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(a){a.put("template/carousel/carousel.html",'\n')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(a){a.put("template/carousel/slide.html","
\n")}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/datepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
#{{label}}
{{ getWeekNumber(row) }}\n \n
\n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/popup.html","
    \n
  • \n"+'
  • \n \n \n \n \n \n \n
  • \n
\n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'
\n
\n
\n
\n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'
\n
\n
\n
\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
\n
\n\n
\n

\n
\n
\n
\n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'
')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
  • \n {{heading}}\n
  • \n')}]),angular.module("template/tabs/tabset-titles.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset-titles.html","
      \n
    \n")}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'\n
    \n \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("template/timepicker/timepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
     
    \n \n :\n \n
     
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html","
      \n"+'
    • \n
      \n
    • \n
    ')}]); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/js/bootstrap.2.3.2.js b/src/Umbraco.Web.UI.Client/lib/bootstrap/js/bootstrap.2.3.2.js deleted file mode 100644 index 31701ad5df..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/js/bootstrap.2.3.2.js +++ /dev/null @@ -1,2284 +0,0 @@ -/* =================================================== - * bootstrap-transition.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#transitions - * =================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) - * ======================================================= */ - - $(function () { - - $.support.transition = (function () { - - var transitionEnd = (function () { - - var el = document.createElement('bootstrap') - , transEndEventNames = { - 'WebkitTransition' : 'webkitTransitionEnd' - , 'MozTransition' : 'transitionend' - , 'OTransition' : 'oTransitionEnd otransitionend' - , 'transition' : 'transitionend' - } - , name - - for (name in transEndEventNames){ - if (el.style[name] !== undefined) { - return transEndEventNames[name] - } - } - - }()) - - return transitionEnd && { - end: transitionEnd - } - - })() - - }) - -}(window.jQuery);/* ========================================================== - * bootstrap-alert.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#alerts - * ========================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* ALERT CLASS DEFINITION - * ====================== */ - - var dismiss = '[data-dismiss="alert"]' - , Alert = function (el) { - $(el).on('click', dismiss, this.close) - } - - Alert.prototype.close = function (e) { - var $this = $(this) - , selector = $this.attr('data-target') - , $parent - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - $parent = $(selector) - - e && e.preventDefault() - - $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) - - $parent.trigger(e = $.Event('close')) - - if (e.isDefaultPrevented()) return - - $parent.removeClass('in') - - function removeElement() { - $parent - .trigger('closed') - .remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent.on($.support.transition.end, removeElement) : - removeElement() - } - - - /* ALERT PLUGIN DEFINITION - * ======================= */ - - var old = $.fn.alert - - $.fn.alert = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('alert') - if (!data) $this.data('alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - $.fn.alert.Constructor = Alert - - - /* ALERT NO CONFLICT - * ================= */ - - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this - } - - - /* ALERT DATA-API - * ============== */ - - $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) - -}(window.jQuery);/* ============================================================ - * bootstrap-button.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#buttons - * ============================================================ - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* BUTTON PUBLIC CLASS DEFINITION - * ============================== */ - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.button.defaults, options) - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - , $el = this.$element - , data = $el.data() - , val = $el.is('input') ? 'val' : 'html' - - state = state + 'Text' - data.resetText || $el.data('resetText', $el[val]()) - - $el[val](data[state] || this.options[state]) - - // push to event loop to allow forms to submit - setTimeout(function () { - state == 'loadingText' ? - $el.addClass(d).attr(d, d) : - $el.removeClass(d).removeAttr(d) - }, 0) - } - - Button.prototype.toggle = function () { - var $parent = this.$element.closest('[data-toggle="buttons-radio"]') - - $parent && $parent - .find('.active') - .removeClass('active') - - this.$element.toggleClass('active') - } - - - /* BUTTON PLUGIN DEFINITION - * ======================== */ - - var old = $.fn.button - - $.fn.button = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('button') - , options = typeof option == 'object' && option - if (!data) $this.data('button', (data = new Button(this, options))) - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - $.fn.button.defaults = { - loadingText: 'loading...' - } - - $.fn.button.Constructor = Button - - - /* BUTTON NO CONFLICT - * ================== */ - - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } - - - /* BUTTON DATA-API - * =============== */ - - $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - $btn.button('toggle') - }) - -}(window.jQuery);/* ========================================================== - * bootstrap-carousel.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#carousel - * ========================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* CAROUSEL CLASS DEFINITION - * ========================= */ - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.options.pause == 'hover' && this.$element - .on('mouseenter', $.proxy(this.pause, this)) - .on('mouseleave', $.proxy(this.cycle, this)) - } - - Carousel.prototype = { - - cycle: function (e) { - if (!e) this.paused = false - if (this.interval) clearInterval(this.interval); - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - return this - } - - , getActiveIndex: function () { - this.$active = this.$element.find('.item.active') - this.$items = this.$active.parent().children() - return this.$items.index(this.$active) - } - - , to: function (pos) { - var activeIndex = this.getActiveIndex() - , that = this - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) { - return this.$element.one('slid', function () { - that.to(pos) - }) - } - - if (activeIndex == pos) { - return this.pause().cycle() - } - - return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) - } - - , pause: function (e) { - if (!e) this.paused = true - if (this.$element.find('.next, .prev').length && $.support.transition.end) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - clearInterval(this.interval) - this.interval = null - return this - } - - , next: function () { - if (this.sliding) return - return this.slide('next') - } - - , prev: function () { - if (this.sliding) return - return this.slide('prev') - } - - , slide: function (type, next) { - var $active = this.$element.find('.item.active') - , $next = next || $active[type]() - , isCycling = this.interval - , direction = type == 'next' ? 'left' : 'right' - , fallback = type == 'next' ? 'first' : 'last' - , that = this - , e - - this.sliding = true - - isCycling && this.pause() - - $next = $next.length ? $next : this.$element.find('.item')[fallback]() - - e = $.Event('slide', { - relatedTarget: $next[0] - , direction: direction - }) - - if ($next.hasClass('active')) return - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - this.$element.one('slid', function () { - var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) - $nextIndicator && $nextIndicator.addClass('active') - }) - } - - if ($.support.transition && this.$element.hasClass('slide')) { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - this.$element.one($.support.transition.end, function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { that.$element.trigger('slid') }, 0) - }) - } else { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger('slid') - } - - isCycling && this.cycle() - - return this - } - - } - - - /* CAROUSEL PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.carousel - - $.fn.carousel = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('carousel') - , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) - , action = typeof option == 'string' ? option : options.slide - if (!data) $this.data('carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - $.fn.carousel.defaults = { - interval: 5000 - , pause: 'hover' - } - - $.fn.carousel.Constructor = Carousel - - - /* CAROUSEL NO CONFLICT - * ==================== */ - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - /* CAROUSEL DATA-API - * ================= */ - - $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { - var $this = $(this), href - , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 - , options = $.extend({}, $target.data(), $this.data()) - , slideIndex - - $target.carousel(options) - - if (slideIndex = $this.attr('data-slide-to')) { - $target.data('carousel').pause().to(slideIndex).cycle() - } - - e.preventDefault() - }) - -}(window.jQuery);/* ============================================================= - * bootstrap-collapse.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#collapse - * ============================================================= - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* COLLAPSE PUBLIC CLASS DEFINITION - * ================================ */ - - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.collapse.defaults, options) - - if (this.options.parent) { - this.$parent = $(this.options.parent) - } - - this.options.toggle && this.toggle() - } - - Collapse.prototype = { - - constructor: Collapse - - , dimension: function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - , show: function () { - var dimension - , scroll - , actives - , hasData - - if (this.transitioning || this.$element.hasClass('in')) return - - dimension = this.dimension() - scroll = $.camelCase(['scroll', dimension].join('-')) - actives = this.$parent && this.$parent.find('> .accordion-group > .in') - - if (actives && actives.length) { - hasData = actives.data('collapse') - if (hasData && hasData.transitioning) return - actives.collapse('hide') - hasData || actives.data('collapse', null) - } - - this.$element[dimension](0) - this.transition('addClass', $.Event('show'), 'shown') - $.support.transition && this.$element[dimension](this.$element[0][scroll]) - } - - , hide: function () { - var dimension - if (this.transitioning || !this.$element.hasClass('in')) return - dimension = this.dimension() - this.reset(this.$element[dimension]()) - this.transition('removeClass', $.Event('hide'), 'hidden') - this.$element[dimension](0) - } - - , reset: function (size) { - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - [dimension](size || 'auto') - [0].offsetWidth - - this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') - - return this - } - - , transition: function (method, startEvent, completeEvent) { - var that = this - , complete = function () { - if (startEvent.type == 'show') that.reset() - that.transitioning = 0 - that.$element.trigger(completeEvent) - } - - this.$element.trigger(startEvent) - - if (startEvent.isDefaultPrevented()) return - - this.transitioning = 1 - - this.$element[method]('in') - - $.support.transition && this.$element.hasClass('collapse') ? - this.$element.one($.support.transition.end, complete) : - complete() - } - - , toggle: function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - } - - - /* COLLAPSE PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.collapse - - $.fn.collapse = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('collapse') - , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.collapse.defaults = { - toggle: true - } - - $.fn.collapse.Constructor = Collapse - - - /* COLLAPSE NO CONFLICT - * ==================== */ - - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } - - - /* COLLAPSE DATA-API - * ================= */ - - $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { - var $this = $(this), href - , target = $this.attr('data-target') - || e.preventDefault() - || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 - , option = $(target).data('collapse') ? 'toggle' : $this.data() - $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') - $(target).collapse(option) - }) - -}(window.jQuery);/* ============================================================ - * bootstrap-dropdown.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#dropdowns - * ============================================================ - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* DROPDOWN CLASS DEFINITION - * ========================= */ - - var toggle = '[data-toggle=dropdown]' - , Dropdown = function (element) { - var $el = $(element).on('click.dropdown.data-api', this.toggle) - $('html').on('click.dropdown.data-api', function () { - $el.parent().removeClass('open') - }) - } - - Dropdown.prototype = { - - constructor: Dropdown - - , toggle: function (e) { - var $this = $(this) - , $parent - , isActive - - if ($this.is('.disabled, :disabled')) return - - $parent = getParent($this) - - isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement) { - // if mobile we we use a backdrop because click events don't delegate - $('