diff --git a/.editorconfig b/.editorconfig index 29e21d01ed..c63ef39430 100644 --- a/.editorconfig +++ b/.editorconfig @@ -33,4 +33,5 @@ dotnet_naming_style.prefix_underscore.required_prefix = _ [*.cs] csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_var_elsewhere = true:suggestion \ No newline at end of file +csharp_style_var_elsewhere = true:suggestion +csharp_prefer_braces = false : none diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 96014f65b7..9dc6f9457f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -16,7 +16,7 @@ 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 valuable time. We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes. @@ -82,7 +82,6 @@ You can get in touch with [the PR team](#the-pr-team) in multiple ways, we love - If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward - Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.com/forum/contributing-to-umbraco-cms/) forum, the team monitors that one closely -- We're also [active in the Gitter chatroom](https://gitter.im/umbraco/Umbraco-CMS) ## Code of Conduct diff --git a/.github/CONTRIBUTING_DETAILED.md b/.github/CONTRIBUTING_DETAILED.md index b3e34ef55d..8c2bfffd87 100644 --- a/.github/CONTRIBUTING_DETAILED.md +++ b/.github/CONTRIBUTING_DETAILED.md @@ -19,7 +19,7 @@ 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 @@ -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/CONTRIBUTION_GUIDELINES.md b/.github/CONTRIBUTION_GUIDELINES.md index 2e4a9ed334..b94feb0b6b 100644 --- a/.github/CONTRIBUTION_GUIDELINES.md +++ b/.github/CONTRIBUTION_GUIDELINES.md @@ -13,7 +13,7 @@ We’re usually able to handle small PRs pretty quickly. A community volunteer w Umbraco HQ will regularly mark newly created issues on the issue tracker with the `Up for grabs` tag. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. These issues are usually small enough to fit in the "Small PRs" category and we encourage anyone to pick them up and help out. -If you do start working on something, make sure leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has. +If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has. ## Large PRs New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGet’s packages.config, NPM’s packages.json, etc.). @@ -30,4 +30,4 @@ If a larger pull request is encouraged by Umbraco HQ, the process will be simila If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and to fix bugs. -Eventually, a package could "graduate" to be included in the CMS. \ No newline at end of file +Eventually, a package could "graduate" to be included in the CMS. 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/.gitignore b/.gitignore index 529adef976..279bdb39dd 100644 --- a/.gitignore +++ b/.gitignore @@ -107,6 +107,7 @@ src/Umbraco.Web.UI.Client/[Bb]uild/[Bb]elle/ src/Umbraco.Web.UI/[Uu]ser[Cc]ontrols/ src/Umbraco.Web.UI.Client/src/[Ll]ess/*.css +src/Umbraco.Web.UI.Client/vwd.webinfo src/Umbraco.Web.UI/App_Plugins/* src/*.psess diff --git a/build/Azure/azuregalleryrelease.ps1 b/build/Azure/azuregalleryrelease.ps1 index 502ca3010e..61505171a2 100644 --- a/build/Azure/azuregalleryrelease.ps1 +++ b/build/Azure/azuregalleryrelease.ps1 @@ -3,57 +3,57 @@ [string]$Directory ) $workingDirectory = $Directory -CD $workingDirectory +CD "$($workingDirectory)" # Clone repo -$fullGitUrl = "https://$env:GIT_URL/$env:GIT_REPOSITORYNAME.git" -git clone $fullGitUrl 2>&1 | % { $_.ToString() } +$fullGitUrl = "https://$($env:GIT_URL)/$($env:GIT_REPOSITORYNAME).git" +git clone $($fullGitUrl) $($env:GIT_REPOSITORYNAME) 2>&1 | % { $_.ToString() } # Remove everything so that unzipping the release later will update everything # Don't remove the readme file nor the git directory Write-Host "Cleaning up git directory before adding new version" -Remove-Item -Recurse $workingDirectory\$env:GIT_REPOSITORYNAME\* -Exclude README.md,.git +Remove-Item -Recurse "$($workingDirectory)\$($env:GIT_REPOSITORYNAME)\*" -Exclude README.md,.git # Find release zip -$zipsDir = "$workingDirectory\$env:BUILD_DEFINITIONNAME\zips" +$zipsDir = "$($workingDirectory)\$($env:BUILD_DEFINITIONNAME)\zips" $pattern = "UmbracoCms.([0-9]{1,2}.[0-9]{1,3}.[0-9]{1,3}).zip" -Write-Host "Searching for Umbraco release files in $workingDirectory\$zipsDir for a file with pattern $pattern" -$file = (Get-ChildItem $zipsDir | Where-Object { $_.Name -match "$pattern" }) +Write-Host "Searching for Umbraco release files in $($zipsDir) for a file with pattern $($pattern)" +$file = (Get-ChildItem "$($zipsDir)" | Where-Object { $_.Name -match "$($pattern)" }) if($file) { # Get release name - $version = [regex]::Match($file.Name, $pattern).captures.groups[1].value - $releaseName = "Umbraco $version" - Write-Host "Found $releaseName" + $version = [regex]::Match($($file.Name), $($pattern)).captures.groups[1].value + $releaseName = "Umbraco $($version)" + Write-Host "Found $($releaseName)" # Unzip into repository to update release Add-Type -AssemblyName System.IO.Compression.FileSystem - Write-Host "Unzipping $($file.FullName) to $workingDirectory\$env:GIT_REPOSITORYNAME" - [System.IO.Compression.ZipFile]::ExtractToDirectory("$($file.FullName)", "$workingDirectory\$env:GIT_REPOSITORYNAME") + Write-Host "Unzipping $($file.FullName) to $($workingDirectory)\$($env:GIT_REPOSITORYNAME)" + [System.IO.Compression.ZipFile]::ExtractToDirectory("$($file.FullName)", "$($workingDirectory)\$($env:GIT_REPOSITORYNAME)") # Telling git who we are git config --global user.email "coffee@umbraco.com" 2>&1 | % { $_.ToString() } git config --global user.name "Umbraco HQ" 2>&1 | % { $_.ToString() } # Commit - CD $env:GIT_REPOSITORYNAME - Write-Host "Committing Umbraco $version Release from Build Output" + CD "$($workingDirectory)\$($env:GIT_REPOSITORYNAME)" + Write-Host "Committing Umbraco $($version) Release from Build Output" git add . 2>&1 | % { $_.ToString() } - git commit -m " Release $releaseName from Build Output" 2>&1 | % { $_.ToString() } + git commit -m " Release $($releaseName) from Build Output" 2>&1 | % { $_.ToString() } # Tag the release - git tag -a "v$version" -m "v$version" + git tag -a "v$($version)" -m "v$($version)" # Push release to master - $fullGitAuthUrl = "https://$($env:GIT_USERNAME):$GitHubPersonalAccessToken@$env:GIT_URL/$env:GIT_REPOSITORYNAME.git" - git push $fullGitAuthUrl 2>&1 | % { $_.ToString() } + $fullGitAuthUrl = "https://$($env:GIT_USERNAME):$($GitHubPersonalAccessToken)@$($env:GIT_URL)/$($env:GIT_REPOSITORYNAME).git" + git push $($fullGitAuthUrl) 2>&1 | % { $_.ToString() } #Push tag to master - git push $fullGitAuthUrl --tags 2>&1 | % { $_.ToString() } + git push $($fullGitAuthUrl) --tags 2>&1 | % { $_.ToString() } } else { - Write-Error "Umbraco release file not found, searched in $workingDirectory\$zipsDir for a file with pattern $pattern - cancelling" + Write-Error "Umbraco release file not found, searched in $($workingDirectory)\$($zipsDir) for a file with pattern $($pattern) - canceling" } 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..30fa303b30 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -14,7 +14,7 @@ Contains the core assemblies needed to run Umbraco Cms en-US umbraco - + - + - - + + 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 1066c62876..dafae2665a 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -92,44 +92,40 @@ # so we have to take care of it else they'll bubble and kill the build if ($error.Count -gt 0) { return } - Push-Location "$($this.SolutionRoot)\src\Umbraco.Web.UI.Client" - Write-Output "" > $log + try { + Push-Location "$($this.SolutionRoot)\src\Umbraco.Web.UI.Client" + Write-Output "" > $log - Write-Output "### node version is:" > $log - &node -v >> $log 2>&1 - if (-not $?) { throw "Failed to report node version." } + Write-Output "### node version is:" > $log + node -v >> $log 2>&1 + if (-not $?) { throw "Failed to report node version." } - Write-Output "### npm version is:" >> $log 2>&1 - &npm -v >> $log 2>&1 - if (-not $?) { throw "Failed to report npm version." } + Write-Output "### npm version is:" >> $log 2>&1 + npm -v >> $log 2>&1 + if (-not $?) { throw "Failed to report npm version." } - Write-Output "### clean npm cache" >> $log 2>&1 - &npm cache clean --force >> $log 2>&1 - $error.Clear() # that one can fail 'cos security bug - ignore + Write-Output "### clean npm cache" >> $log 2>&1 + npm cache clean --force >> $log 2>&1 + $error.Clear() # that one can fail 'cos security bug - ignore - Write-Output "### npm install" >> $log 2>&1 - &npm install >> $log 2>&1 - Write-Output ">> $? $($error.Count)" >> $log 2>&1 + Write-Output "### npm install" >> $log 2>&1 + npm install >> $log 2>&1 + Write-Output ">> $? $($error.Count)" >> $log 2>&1 + # Don't really care about the messages from npm install making us think there are errors + $error.Clear() - Write-Output "### install gulp" >> $log 2>&1 - &npm install -g gulp >> $log 2>&1 - $error.Clear() # that one fails 'cos deprecated stuff - ignore + Write-Output "### gulp build for version $($this.Version.Release)" >> $log 2>&1 + npx gulp build --buildversion=$this.Version.Release >> $log 2>&1 + if (-not $?) { throw "Failed to build" } # that one is expected to work + } finally { + Pop-Location - Write-Output "### install gulp-cli" >> $log 2>&1 - &npm install -g gulp-cli --quiet >> $log 2>&1 - $error.Clear() # that one fails 'cos some files not being removed - ignore + # fixme - should we filter the log to find errors? + #get-content .\build.tmp\belle.log | %{ if ($_ -match "build") { write $_}} - Write-Output "### gulp build for version $($this.Version.Release)" >> $log 2>&1 - &gulp build --buildversion=$this.Version.Release >> $log 2>&1 - if (-not $?) { throw "Failed to build" } # that one is expected to work - - Pop-Location - - # fixme - should we filter the log to find errors? - #get-content .\build.tmp\belle.log | %{ if ($_ -match "build") { write $_}} - - # restore - $this.RestoreNode() + # restore + $this.RestoreNode() + } # setting node_modules folder to hidden # used to prevent VS13 from crashing on it while loading the websites project @@ -456,9 +452,22 @@ if ($this.OnError()) { return } $this.PrepareAzureGallery() if ($this.OnError()) { return } + $this.PostPackageHook() + if ($this.OnError()) { return } Write-Host "Done" }) + $ubuild.DefineMethod("PostPackageHook", + { + # run hook + if ($this.HasMethod("PostPackage")) + { + Write-Host "Run PostPackage hook" + $this.PostPackage(); + if (-not $?) { throw "Failed to run hook." } + } + }) + # ################################################################ # RUN # ################################################################ diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index b5af335791..ce40bd9baa 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -19,4 +19,4 @@ using System.Resources; // these are FYI and changed automatically [assembly: AssemblyFileVersion("8.0.0")] -[assembly: AssemblyInformationalVersion("8.0.0-alpha.52")] +[assembly: AssemblyInformationalVersion("8.0.0-alpha.58")] diff --git a/src/Umbraco.Core/ByteArrayExtensions.cs b/src/Umbraco.Core/ByteArrayExtensions.cs deleted file mode 100644 index dacdd509ca..0000000000 --- a/src/Umbraco.Core/ByteArrayExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Umbraco.Core -{ - public static class ByteArrayExtensions - { - private static readonly char[] BytesToHexStringLookup = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; - - public static string ToHexString(this byte[] bytes) - { - int i = 0, p = 0, bytesLength = bytes.Length; - var chars = new char[bytesLength * 2]; - while (i < bytesLength) - { - var b = bytes[i++]; - chars[p++] = BytesToHexStringLookup[b / 0x10]; - chars[p++] = BytesToHexStringLookup[b % 0x10]; - } - return new string(chars, 0, chars.Length); - } - - public static string ToHexString(this byte[] bytes, char separator, int blockSize, int blockCount) - { - int p = 0, bytesLength = bytes.Length, count = 0, size = 0; - var chars = new char[bytesLength * 2 + blockCount]; - for (var i = 0; i < bytesLength; i++) - { - var b = bytes[i++]; - chars[p++] = BytesToHexStringLookup[b / 0x10]; - chars[p++] = BytesToHexStringLookup[b % 0x10]; - if (count == blockCount) continue; - if (++size < blockSize) continue; - - chars[p++] = '/'; - size = 0; - count++; - } - return new string(chars, 0, chars.Length); - } - } -} diff --git a/src/Umbraco.Core/Cache/CacheHelper.cs b/src/Umbraco.Core/Cache/CacheHelper.cs index f99b1e847b..cd2225ae9d 100644 --- a/src/Umbraco.Core/Cache/CacheHelper.cs +++ b/src/Umbraco.Core/Cache/CacheHelper.cs @@ -4,27 +4,10 @@ using System.Web; namespace Umbraco.Core.Cache { /// - /// Class that is exposed by the ApplicationContext for application wide caching purposes + /// Represents the application-wide caches. /// public class CacheHelper { - public static CacheHelper NoCache { get; } = new CacheHelper(NullCacheProvider.Instance, NullCacheProvider.Instance, NullCacheProvider.Instance, new IsolatedRuntimeCache(_ => NullCacheProvider.Instance)); - - /// - /// Creates a cache helper with disabled caches - /// - /// - /// - /// Good for unit testing - /// - public static CacheHelper CreateDisabledCacheHelper() - { - // do *not* return NoCache - // NoCache is a special instance that is detected by RepositoryBase and disables all cache policies - // CreateDisabledCacheHelper is used in tests to use no cache, *but* keep all cache policies - return new CacheHelper(NullCacheProvider.Instance, NullCacheProvider.Instance, NullCacheProvider.Instance, new IsolatedRuntimeCache(_ => NullCacheProvider.Instance)); - } - /// /// Initializes a new instance for use in the web /// @@ -40,7 +23,6 @@ namespace Umbraco.Core.Cache /// /// Initializes a new instance for use in the web /// - /// public CacheHelper(System.Web.Caching.Cache cache) : this( new HttpRuntimeCacheProvider(cache), @@ -50,30 +32,39 @@ namespace Umbraco.Core.Cache { } - /// /// Initializes a new instance based on the provided providers /// - /// - /// - /// - /// public CacheHelper( IRuntimeCacheProvider httpCacheProvider, ICacheProvider staticCacheProvider, ICacheProvider requestCacheProvider, IsolatedRuntimeCache isolatedCacheManager) { - if (httpCacheProvider == null) throw new ArgumentNullException("httpCacheProvider"); - if (staticCacheProvider == null) throw new ArgumentNullException("staticCacheProvider"); - if (requestCacheProvider == null) throw new ArgumentNullException("requestCacheProvider"); - if (isolatedCacheManager == null) throw new ArgumentNullException("isolatedCacheManager"); - RuntimeCache = httpCacheProvider; - StaticCache = staticCacheProvider; - RequestCache = requestCacheProvider; - IsolatedRuntimeCache = isolatedCacheManager; + RuntimeCache = httpCacheProvider ?? throw new ArgumentNullException(nameof(httpCacheProvider)); + StaticCache = staticCacheProvider ?? throw new ArgumentNullException(nameof(staticCacheProvider)); + RequestCache = requestCacheProvider ?? throw new ArgumentNullException(nameof(requestCacheProvider)); + IsolatedRuntimeCache = isolatedCacheManager ?? throw new ArgumentNullException(nameof(isolatedCacheManager)); } + /// + /// Gets the special disabled instance. + /// + /// + /// When used by repositories, all cache policies apply, but the underlying caches do not cache anything. + /// Used by tests. + /// + public static CacheHelper Disabled { get; } = new CacheHelper(NullCacheProvider.Instance, NullCacheProvider.Instance, NullCacheProvider.Instance, new IsolatedRuntimeCache(_ => NullCacheProvider.Instance)); + + /// + /// Gets the special no-cache instance. + /// + /// + /// When used by repositories, all cache policies are bypassed. + /// Used by repositories that do no cache. + /// + public static CacheHelper NoCache { get; } = new CacheHelper(NullCacheProvider.Instance, NullCacheProvider.Instance, NullCacheProvider.Instance, new IsolatedRuntimeCache(_ => NullCacheProvider.Instance)); + /// /// Returns the current Request cache /// diff --git a/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs b/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs index 11ac05844b..8bae755149 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs @@ -1,15 +1,9 @@ -using System.Collections.Generic; -using LightInject; -using Umbraco.Core.Composing; +using Umbraco.Core.Composing; namespace Umbraco.Core.Cache { public class CacheRefresherCollectionBuilder : LazyCollectionBuilderBase { - public CacheRefresherCollectionBuilder(IServiceContainer container) - : base(container) - { } - protected override CacheRefresherCollectionBuilder This => this; } } 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/Components/AuditEventsComponent.cs b/src/Umbraco.Core/Components/AuditEventsComponent.cs index 134aa18414..08d4702afa 100644 --- a/src/Umbraco.Core/Components/AuditEventsComponent.cs +++ b/src/Umbraco.Core/Components/AuditEventsComponent.cs @@ -12,11 +12,36 @@ using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Components { - public sealed class AuditEventsComponent : UmbracoComponentBase, IUmbracoCoreComponent + public sealed class AuditEventsComponent : IComponent { - private IAuditService _auditService; - private IUserService _userService; - private IEntityService _entityService; + private readonly IAuditService _auditService; + private readonly IUserService _userService; + private readonly IEntityService _entityService; + + public AuditEventsComponent(IAuditService auditService, IUserService userService, IEntityService entityService) + { + _auditService = auditService; + _userService = userService; + _entityService = entityService; + } + + public void Initialize() + { + UserService.SavedUserGroup += OnSavedUserGroupWithUsers; + + UserService.SavedUser += OnSavedUser; + UserService.DeletedUser += OnDeletedUser; + UserService.UserGroupPermissionsAssigned += UserGroupPermissionAssigned; + + MemberService.Saved += OnSavedMember; + MemberService.Deleted += OnDeletedMember; + MemberService.AssignedRoles += OnAssignedRoles; + MemberService.RemovedRoles += OnRemovedRoles; + MemberService.Exported += OnMemberExported; + } + + public void Terminate() + { } private IUser CurrentPerformingUser { @@ -46,25 +71,6 @@ namespace Umbraco.Core.Components } } - public void Initialize(IAuditService auditService, IUserService userService, IEntityService entityService) - { - _auditService = auditService; - _userService = userService; - _entityService = entityService; - - UserService.SavedUserGroup += OnSavedUserGroupWithUsers; - - UserService.SavedUser += OnSavedUser; - UserService.DeletedUser += OnDeletedUser; - UserService.UserGroupPermissionsAssigned += UserGroupPermissionAssigned; - - MemberService.Saved += OnSavedMember; - MemberService.Deleted += OnDeletedMember; - MemberService.AssignedRoles += OnAssignedRoles; - MemberService.RemovedRoles += OnRemovedRoles; - MemberService.Exported += OnMemberExported; - } - private string FormatEmail(IMember member) { return member == null ? string.Empty : member.Email.IsNullOrWhiteSpace() ? "" : $"<{member.Email}>"; diff --git a/src/Umbraco.Core/Components/AuditEventsComposer.cs b/src/Umbraco.Core/Components/AuditEventsComposer.cs new file mode 100644 index 0000000000..692cb6c6dd --- /dev/null +++ b/src/Umbraco.Core/Components/AuditEventsComposer.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Core.Components +{ + public sealed class AuditEventsComposer : ComponentComposer, ICoreComposer + { } +} diff --git a/src/Umbraco.Core/Components/BootLoader.cs b/src/Umbraco.Core/Components/BootLoader.cs deleted file mode 100644 index fd292990c8..0000000000 --- a/src/Umbraco.Core/Components/BootLoader.cs +++ /dev/null @@ -1,368 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using LightInject; -using Umbraco.Core.Collections; -using Umbraco.Core.Exceptions; -using Umbraco.Core.Logging; -using Umbraco.Core.Scoping; - -namespace Umbraco.Core.Components -{ - // note: this class is NOT thread-safe in any ways - - internal class BootLoader - { - private readonly IServiceContainer _container; - private readonly ProfilingLogger _proflog; - private readonly ILogger _logger; - private IUmbracoComponent[] _components; - private bool _booted; - - private const int LogThresholdMilliseconds = 100; - - /// - /// Initializes a new instance of the class. - /// - /// The application container. - public BootLoader(IServiceContainer container) - { - _container = container ?? throw new ArgumentNullException(nameof(container)); - _proflog = container.GetInstance(); - _logger = container.GetInstance(); - } - - private class EnableInfo - { - public bool Enabled; - public int Weight = -1; - } - - public void Boot(IEnumerable componentTypes, RuntimeLevel level) - { - if (_booted) throw new InvalidOperationException("Can not boot, has already booted."); - - var orderedComponentTypes = PrepareComponentTypes(componentTypes, level); - - InstanciateComponents(orderedComponentTypes); - ComposeComponents(level); - - using (var scope = _container.GetInstance().CreateScope()) - { - InitializeComponents(); - scope.Complete(); - } - - // rejoice! - _booted = true; - } - - private IEnumerable PrepareComponentTypes(IEnumerable componentTypes, RuntimeLevel level) - { - using (_proflog.DebugDuration("Preparing component types.", "Prepared component types.")) - { - return PrepareComponentTypes2(componentTypes, level); - } - } - - private IEnumerable PrepareComponentTypes2(IEnumerable componentTypes, RuntimeLevel level) - { - // create a list, remove those that cannot be enabled due to runtime level - var componentTypeList = componentTypes - .Where(x => - { - // use the min level specified by the attribute if any - // otherwise, user components have Run min level, anything else is Unknown (always run) - var attr = x.GetCustomAttribute(); - var minLevel = attr?.MinLevel ?? (x.Implements() ? RuntimeLevel.Run : RuntimeLevel.Unknown); - return level >= minLevel; - }) - .ToList(); - - // cannot remove that one - ever - if (componentTypeList.Contains(typeof(UmbracoCoreComponent)) == false) - componentTypeList.Add(typeof(UmbracoCoreComponent)); - - // enable or disable components - EnableDisableComponents(componentTypeList); - - // sort the components according to their dependencies - var requirements = new Dictionary>(); - foreach (var type in componentTypeList) requirements[type] = null; - foreach (var type in componentTypeList) - { - GatherRequirementsFromRequireAttribute(type, componentTypeList, requirements); - GatherRequirementsFromRequiredAttribute(type, componentTypeList, requirements); - } - - // only for debugging, this is verbose - //_logger.Debug(GetComponentsReport(requirements)); - - // sort components - var graph = new TopoGraph>>(kvp => kvp.Key, kvp => kvp.Value); - graph.AddItems(requirements); - List sortedComponentTypes; - try - { - sortedComponentTypes = graph.GetSortedItems().Select(x => x.Key).ToList(); - } - catch (Exception e) - { - // in case of an error, force-dump everything to log - _logger.Info("Component Report:\r\n{ComponentReport}", GetComponentsReport(requirements)); - _logger.Error(e, "Failed to sort compontents."); - throw; - } - - // bit verbose but should help for troubleshooting - var text = "Ordered Components: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComponentTypes) + Environment.NewLine; - Console.WriteLine(text); - _logger.Debug("Ordered Components: {SortedComponentTypes}", sortedComponentTypes); - - return sortedComponentTypes; - } - - private static string GetComponentsReport(Dictionary> requirements) - { - var text = new StringBuilder(); - text.AppendLine("Components & Dependencies:"); - text.AppendLine(); - - foreach (var kvp in requirements) - { - var type = kvp.Key; - - text.AppendLine(type.FullName); - foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue - ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) - : "")); - foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" -< " + attribute.RequiringType); - foreach (var i in type.GetInterfaces()) - { - text.AppendLine(" : " + i.FullName); - foreach (var attribute in i.GetCustomAttributes()) - text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue - ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) - : "")); - foreach (var attribute in i.GetCustomAttributes()) - text.AppendLine(" -< " + attribute.RequiringType); - } - if (kvp.Value != null) - foreach (var t in kvp.Value) - text.AppendLine(" = " + t); - text.AppendLine(); - } - text.AppendLine("/"); - text.AppendLine(); - return text.ToString(); - } - - private static void EnableDisableComponents(ICollection types) - { - var enabled = new Dictionary(); - - // process the enable/disable attributes - // these two attributes are *not* inherited and apply to *classes* only (not interfaces). - // remote declarations (when a component enables/disables *another* component) - // have priority over local declarations (when a component disables itself) so that - // ppl can enable components that, by default, are disabled. - // what happens in case of conflicting remote declarations is unspecified. more - // precisely, the last declaration to be processed wins, but the order of the - // declarations depends on the type finder and is unspecified. - foreach (var componentType in types) - { - foreach (var attr in componentType.GetCustomAttributes()) - { - var type = attr.EnabledType ?? componentType; - if (enabled.TryGetValue(type, out var enableInfo) == false) enableInfo = enabled[type] = new EnableInfo(); - var weight = type == componentType ? 1 : 2; - if (enableInfo.Weight > weight) continue; - - enableInfo.Enabled = true; - enableInfo.Weight = weight; - } - foreach (var attr in componentType.GetCustomAttributes()) - { - var type = attr.DisabledType ?? componentType; - if (type == typeof(UmbracoCoreComponent)) throw new InvalidOperationException("Cannot disable UmbracoCoreComponent."); - if (enabled.TryGetValue(type, out var enableInfo) == false) enableInfo = enabled[type] = new EnableInfo(); - var weight = type == componentType ? 1 : 2; - if (enableInfo.Weight > weight) continue; - - enableInfo.Enabled = false; - enableInfo.Weight = weight; - } - } - - // remove components that end up being disabled - foreach (var kvp in enabled.Where(x => x.Value.Enabled == false)) - types.Remove(kvp.Key); - } - - private static void GatherRequirementsFromRequireAttribute(Type type, ICollection types, IDictionary> requirements) - { - // get 'require' attributes - // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var requireAttributes = type - .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces - .Concat(type.GetCustomAttributes()); // those marking the component - - // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. - foreach (var attr in requireAttributes) - { - if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below) - - // requiring an interface = require any enabled component implementing that interface - // unless strong, and then require at least one enabled component implementing that interface - if (attr.RequiredType.IsInterface) - { - var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x)).ToList(); - if (implems.Count > 0) - { - if (requirements[type] == null) requirements[type] = new List(); - requirements[type].AddRange(implems); - } - else if (attr.Weak == false) // if explicitely set to !weak, is strong, else is weak - throw new Exception($"Broken component dependency: {type.FullName} -> {attr.RequiredType.FullName}."); - } - // requiring a class = require that the component is enabled - // unless weak, and then requires it if it is enabled - else - { - if (types.Contains(attr.RequiredType)) - { - if (requirements[type] == null) requirements[type] = new List(); - requirements[type].Add(attr.RequiredType); - } - else if (attr.Weak != true) // if not explicitely set to weak, is strong - throw new Exception($"Broken component dependency: {type.FullName} -> {attr.RequiredType.FullName}."); - } - } - } - - private static void GatherRequirementsFromRequiredAttribute(Type type, ICollection types, IDictionary> requirements) - { - // get 'required' attributes - // fixme explain - var requiredAttributes = type - .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) - .Concat(type.GetCustomAttributes()); - - foreach (var attr in requiredAttributes) - { - if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below) - - if (attr.RequiringType.IsInterface) - { - var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x)).ToList(); - foreach (var implem in implems) - { - if (requirements[implem] == null) requirements[implem] = new List(); - requirements[implem].Add(type); - } - } - else - { - if (types.Contains(attr.RequiringType)) - { - if (requirements[attr.RequiringType] == null) requirements[attr.RequiringType] = new List(); - requirements[attr.RequiringType].Add(type); - } - } - } - } - - private void InstanciateComponents(IEnumerable types) - { - using (_proflog.DebugDuration("Instanciating components.", "Instanciated components.")) - { - _components = types.Select(x => (IUmbracoComponent) Activator.CreateInstance(x)).ToArray(); - } - } - - private void ComposeComponents(RuntimeLevel level) - { - using (_proflog.DebugDuration($"Composing components. (log when >{LogThresholdMilliseconds}ms)", "Composed components.")) - { - var composition = new Composition(_container, level); - foreach (var component in _components) - { - var componentType = component.GetType(); - using (_proflog.DebugDuration($"Composing {componentType.FullName}.", $"Composed {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) - { - component.Compose(composition); - } - } - } - } - - private void InitializeComponents() - { - // use a container scope to ensure that PerScope instances are disposed - // components that require instances that should not survive should register them with PerScope lifetime - using (_proflog.DebugDuration($"Initializing components. (log when >{LogThresholdMilliseconds}ms)", "Initialized components.")) - using (_container.BeginScope()) - { - foreach (var component in _components) - { - var componentType = component.GetType(); - var initializers = componentType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .Where(x => x.Name == "Initialize" && x.IsGenericMethod == false); - using (_proflog.DebugDuration($"Initializing {componentType.FullName}.", $"Initialised {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) - { - foreach (var initializer in initializers) - { - var parameters = initializer.GetParameters() - .Select(x => GetParameter(componentType, x.ParameterType)) - .ToArray(); - initializer.Invoke(component, parameters); - } - } - } - } - } - - private object GetParameter(Type componentType, Type parameterType) - { - object param; - - try - { - param = _container.TryGetInstance(parameterType); - } - catch (Exception e) - { - throw new BootFailedException($"Could not get parameter of type {parameterType.FullName} for component {componentType.FullName}.", e); - } - - if (param == null) throw new BootFailedException($"Could not get parameter of type {parameterType.FullName} for component {componentType.FullName}."); - return param; - } - - public void Terminate() - { - if (_booted == false) - { - _proflog.Logger.Warn("Cannot terminate, has not booted."); - return; - } - - using (_proflog.DebugDuration($"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated.")) - { - for (var i = _components.Length - 1; i >= 0; i--) // terminate components in reverse order - { - var component = _components[i]; - var componentType = component.GetType(); - using (_proflog.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) - { - component.Terminate(); - } - } - } - } - } -} diff --git a/src/Umbraco.Core/Components/ComponentCollection.cs b/src/Umbraco.Core/Components/ComponentCollection.cs new file mode 100644 index 0000000000..4fa81b9760 --- /dev/null +++ b/src/Umbraco.Core/Components/ComponentCollection.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.Components +{ + /// + /// Represents the collection of implementations. + /// + public class ComponentCollection : BuilderCollectionBase + { + private const int LogThresholdMilliseconds = 100; + + private readonly IProfilingLogger _logger; + + public ComponentCollection(IEnumerable items, IProfilingLogger logger) + : base(items) + { + _logger = logger; + } + + public void Initialize() + { + using (_logger.DebugDuration($"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized.")) + { + foreach (var component in this.Reverse()) // terminate components in reverse order + { + var componentType = component.GetType(); + using (_logger.DebugDuration($"Initializing {componentType.FullName}.", $"Initialized {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) + { + component.Initialize(); + } + } + } + } + + public void Terminate() + { + using (_logger.DebugDuration($"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated.")) + { + foreach (var component in this.Reverse()) // terminate components in reverse order + { + var componentType = component.GetType(); + using (_logger.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) + { + component.Terminate(); + component.DisposeIfDisposable(); + } + } + } + } + } +} diff --git a/src/Umbraco.Core/Components/ComponentCollectionBuilder.cs b/src/Umbraco.Core/Components/ComponentCollectionBuilder.cs new file mode 100644 index 0000000000..584de7a8f2 --- /dev/null +++ b/src/Umbraco.Core/Components/ComponentCollectionBuilder.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.Components +{ + /// + /// Builds a . + /// + public class ComponentCollectionBuilder : OrderedCollectionBuilderBase + { + private const int LogThresholdMilliseconds = 100; + + private IProfilingLogger _logger; + + public ComponentCollectionBuilder() + { } + + protected override ComponentCollectionBuilder This => this; + + protected override IEnumerable CreateItems(IFactory factory) + { + _logger = factory.GetInstance(); + + using (_logger.DebugDuration($"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created.")) + { + return base.CreateItems(factory); + } + } + + protected override IComponent CreateItem(IFactory factory, Type itemType) + { + using (_logger.DebugDuration($"Creating {itemType.FullName}.", $"Created {itemType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) + { + return base.CreateItem(factory, itemType); + } + } + } +} diff --git a/src/Umbraco.Core/Components/ComponentComposer.cs b/src/Umbraco.Core/Components/ComponentComposer.cs new file mode 100644 index 0000000000..792790c42f --- /dev/null +++ b/src/Umbraco.Core/Components/ComponentComposer.cs @@ -0,0 +1,20 @@ +namespace Umbraco.Core.Components +{ + /// + /// Provides a base class for composers which compose a component. + /// + /// The type of the component + public abstract class ComponentComposer : IComposer + where TComponent : IComponent + { + /// + public virtual void Compose(Composition composition) + { + composition.Components().Append(); + } + + // note: thanks to this class, a component that does not compose anything can be + // registered with one line: + // public class MyComponentComposer : ComponentComposer { } + } +} diff --git a/src/Umbraco.Core/Components/ComposeAfterAttribute.cs b/src/Umbraco.Core/Components/ComposeAfterAttribute.cs new file mode 100644 index 0000000000..a8fdfaa92b --- /dev/null +++ b/src/Umbraco.Core/Components/ComposeAfterAttribute.cs @@ -0,0 +1,59 @@ +using System; + +namespace Umbraco.Core.Components +{ + /// + /// Indicates that a composer requires another composer. + /// + /// + /// This attribute is *not* inherited. This means that a composer class inheriting from + /// another composer class does *not* inherit its requirements. However, the runtime checks + /// the *interfaces* of every composer for their requirements, so requirements declared on + /// interfaces are inherited by every composer class implementing the interface. + /// When targeting a class, indicates a dependency on the composer which must be enabled, + /// unless the requirement has explicitly been declared as weak (and then, only if the composer + /// is enabled). + /// When targeting an interface, indicates a dependency on enabled composers implementing + /// the interface. It could be no composer at all, unless the requirement has explicitly been + /// declared as strong (and at least one composer must be enabled). + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] + public sealed class ComposeAfterAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The type of the required composer. + public ComposeAfterAttribute(Type requiredType) + { + if (typeof(IComposer).IsAssignableFrom(requiredType) == false) + throw new ArgumentException($"Type {requiredType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}."); + RequiredType = requiredType; + } + + /// + /// Initializes a new instance of the class. + /// + /// The type of the required composer. + /// A value indicating whether the requirement is weak. + public ComposeAfterAttribute(Type requiredType, bool weak) + : this(requiredType) + { + Weak = weak; + } + + /// + /// Gets the required type. + /// + public Type RequiredType { get; } + + /// + /// Gets a value indicating whether the requirement is weak. + /// + /// Returns true if the requirement is weak (requires the other composer if it + /// is enabled), false if the requirement is strong (requires the other composer to be + /// enabled), and null if unspecified, in which case it is strong for classes and weak for + /// interfaces. + public bool? Weak { get; } + } +} diff --git a/src/Umbraco.Core/Components/ComposeBeforeAttribute.cs b/src/Umbraco.Core/Components/ComposeBeforeAttribute.cs new file mode 100644 index 0000000000..17065d1676 --- /dev/null +++ b/src/Umbraco.Core/Components/ComposeBeforeAttribute.cs @@ -0,0 +1,40 @@ +using System; + +namespace Umbraco.Core.Components +{ + /// + /// Indicates that a component is required by another composer. + /// + /// + /// This attribute is *not* inherited. This means that a composer class inheriting from + /// another composer class does *not* inherit its requirements. However, the runtime checks + /// the *interfaces* of every composer for their requirements, so requirements declared on + /// interfaces are inherited by every composer class implementing the interface. + /// When targeting a class, indicates a dependency on the composer which must be enabled, + /// unless the requirement has explicitly been declared as weak (and then, only if the composer + /// is enabled). + /// When targeting an interface, indicates a dependency on enabled composers implementing + /// the interface. It could be no composer at all, unless the requirement has explicitly been + /// declared as strong (and at least one composer must be enabled). + /// + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] + public sealed class ComposeBeforeAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The type of the required composer. + public ComposeBeforeAttribute(Type requiringType) + { + if (typeof(IComposer).IsAssignableFrom(requiringType) == false) + throw new ArgumentException($"Type {requiringType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}."); + RequiringType = requiringType; + } + + /// + /// Gets the required type. + /// + public Type RequiringType { get; } + } +} diff --git a/src/Umbraco.Core/Components/Composers.cs b/src/Umbraco.Core/Components/Composers.cs new file mode 100644 index 0000000000..1c836e9e5c --- /dev/null +++ b/src/Umbraco.Core/Components/Composers.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Umbraco.Core.Collections; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.Components +{ + // note: this class is NOT thread-safe in any ways + + /// + /// Handles the composers. + /// + public class Composers + { + private readonly Composition _composition; + private readonly IProfilingLogger _logger; + private readonly IEnumerable _composerTypes; + + private const int LogThresholdMilliseconds = 100; + + /// + /// Initializes a new instance of the class. + /// + /// The composition. + /// The composer types. + /// A profiling logger. + public Composers(Composition composition, IEnumerable composerTypes, IProfilingLogger logger) + { + _composition = composition ?? throw new ArgumentNullException(nameof(composition)); + _composerTypes = composerTypes ?? throw new ArgumentNullException(nameof(composerTypes)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + private class EnableInfo + { + public bool Enabled; + public int Weight = -1; + } + + /// + /// Instantiates and composes the composers. + /// + public void Compose() + { + // make sure it is there + _composition.WithCollectionBuilder(); + + IEnumerable orderedComposerTypes; + + using (_logger.DebugDuration("Preparing composer types.", "Prepared composer types.")) + { + orderedComposerTypes = PrepareComposerTypes(); + } + + var composers = InstantiateComposers(orderedComposerTypes); + + using (_logger.DebugDuration($"Composing composers. (log when >{LogThresholdMilliseconds}ms)", "Composed composers.")) + { + foreach (var composer in composers) + { + var componentType = composer.GetType(); + using (_logger.DebugDuration($"Composing {componentType.FullName}.", $"Composed {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) + { + composer.Compose(_composition); + } + } + } + } + + private IEnumerable PrepareComposerTypes() + { + // create a list, remove those that cannot be enabled due to runtime level + var composerTypeList = _composerTypes + .Where(x => + { + // use the min level specified by the attribute if any + // otherwise, user composers have Run min level, anything else is Unknown (always run) + var attr = x.GetCustomAttribute(); + var minLevel = attr?.MinLevel ?? (x.Implements() ? RuntimeLevel.Run : RuntimeLevel.Unknown); + return _composition.RuntimeState.Level >= minLevel; + }) + .ToList(); + + // enable or disable composers + EnableDisableComposers(composerTypeList); + + // sort the composers according to their dependencies + var requirements = new Dictionary>(); + foreach (var type in composerTypeList) requirements[type] = null; + foreach (var type in composerTypeList) + { + GatherRequirementsFromRequireAttribute(type, composerTypeList, requirements); + GatherRequirementsFromRequiredByAttribute(type, composerTypeList, requirements); + } + + // only for debugging, this is verbose + //_logger.Debug(GetComposersReport(requirements)); + + // sort composers + var graph = new TopoGraph>>(kvp => kvp.Key, kvp => kvp.Value); + graph.AddItems(requirements); + List sortedComposerTypes; + try + { + sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).ToList(); + } + catch (Exception e) + { + // in case of an error, force-dump everything to log + _logger.Info("Composer Report:\r\n{ComposerReport}", GetComposersReport(requirements)); + _logger.Error(e, "Failed to sort composers."); + throw; + } + + // bit verbose but should help for troubleshooting + //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; + _logger.Debug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); + + return sortedComposerTypes; + } + + private static string GetComposersReport(Dictionary> requirements) + { + var text = new StringBuilder(); + text.AppendLine("Composers & Dependencies:"); + text.AppendLine(); + + foreach (var kvp in requirements) + { + var type = kvp.Key; + + text.AppendLine(type.FullName); + foreach (var attribute in type.GetCustomAttributes()) + text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue + ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) + : "")); + foreach (var attribute in type.GetCustomAttributes()) + text.AppendLine(" -< " + attribute.RequiringType); + foreach (var i in type.GetInterfaces()) + { + text.AppendLine(" : " + i.FullName); + foreach (var attribute in i.GetCustomAttributes()) + text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue + ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) + : "")); + foreach (var attribute in i.GetCustomAttributes()) + text.AppendLine(" -< " + attribute.RequiringType); + } + if (kvp.Value != null) + foreach (var t in kvp.Value) + text.AppendLine(" = " + t); + text.AppendLine(); + } + text.AppendLine("/"); + text.AppendLine(); + return text.ToString(); + } + + private static void EnableDisableComposers(ICollection types) + { + var enabled = new Dictionary(); + + // process the enable/disable attributes + // these two attributes are *not* inherited and apply to *classes* only (not interfaces). + // remote declarations (when a composer enables/disables *another* composer) + // have priority over local declarations (when a composer disables itself) so that + // ppl can enable composers that, by default, are disabled. + // what happens in case of conflicting remote declarations is unspecified. more + // precisely, the last declaration to be processed wins, but the order of the + // declarations depends on the type finder and is unspecified. + foreach (var composerType in types) + { + foreach (var attr in composerType.GetCustomAttributes()) + { + var type = attr.EnabledType ?? composerType; + if (enabled.TryGetValue(type, out var enableInfo) == false) enableInfo = enabled[type] = new EnableInfo(); + var weight = type == composerType ? 1 : 2; + if (enableInfo.Weight > weight) continue; + + enableInfo.Enabled = true; + enableInfo.Weight = weight; + } + foreach (var attr in composerType.GetCustomAttributes()) + { + var type = attr.DisabledType ?? composerType; + if (enabled.TryGetValue(type, out var enableInfo) == false) enableInfo = enabled[type] = new EnableInfo(); + var weight = type == composerType ? 1 : 2; + if (enableInfo.Weight > weight) continue; + + enableInfo.Enabled = false; + enableInfo.Weight = weight; + } + } + + // remove composers that end up being disabled + foreach (var kvp in enabled.Where(x => x.Value.Enabled == false)) + types.Remove(kvp.Key); + } + + private static void GatherRequirementsFromRequireAttribute(Type type, ICollection types, IDictionary> requirements) + { + // get 'require' attributes + // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only + var requireAttributes = type + .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces + .Concat(type.GetCustomAttributes()); // those marking the composer + + // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. + foreach (var attr in requireAttributes) + { + if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below) + + // requiring an interface = require any enabled composer implementing that interface + // unless strong, and then require at least one enabled composer implementing that interface + if (attr.RequiredType.IsInterface) + { + var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x)).ToList(); + if (implems.Count > 0) + { + if (requirements[type] == null) requirements[type] = new List(); + requirements[type].AddRange(implems); + } + else if (attr.Weak == false) // if explicitly set to !weak, is strong, else is weak + throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); + } + // requiring a class = require that the composer is enabled + // unless weak, and then requires it if it is enabled + else + { + if (types.Contains(attr.RequiredType)) + { + if (requirements[type] == null) requirements[type] = new List(); + requirements[type].Add(attr.RequiredType); + } + else if (attr.Weak != true) // if not explicitly set to weak, is strong + throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); + } + } + } + + private static void GatherRequirementsFromRequiredByAttribute(Type type, ICollection types, IDictionary> requirements) + { + // get 'required' attributes + // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only + var requiredAttributes = type + .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces + .Concat(type.GetCustomAttributes()); // those marking the composer + + foreach (var attr in requiredAttributes) + { + if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below) + + // required by an interface = by any enabled composer implementing this that interface + if (attr.RequiringType.IsInterface) + { + var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x)).ToList(); + foreach (var implem in implems) + { + if (requirements[implem] == null) requirements[implem] = new List(); + requirements[implem].Add(type); + } + } + // required by a class + else + { + if (types.Contains(attr.RequiringType)) + { + if (requirements[attr.RequiringType] == null) requirements[attr.RequiringType] = new List(); + requirements[attr.RequiringType].Add(type); + } + } + } + } + + private IEnumerable InstantiateComposers(IEnumerable types) + { + IComposer InstantiateComposer(Type type) + { + var ctor = type.GetConstructor(Array.Empty()); + if (ctor == null) + throw new InvalidOperationException($"Composer {type.FullName} does not have a parameter-less constructor."); + return (IComposer) ctor.Invoke(Array.Empty()); + } + + using (_logger.DebugDuration("Instantiating composers.", "Instantiated composers.")) + { + return types.Select(InstantiateComposer).ToArray(); + } + } + } +} diff --git a/src/Umbraco.Core/Components/Composition.cs b/src/Umbraco.Core/Components/Composition.cs index 671469c73a..6df86d793f 100644 --- a/src/Umbraco.Core/Components/Composition.cs +++ b/src/Umbraco.Core/Components/Composition.cs @@ -1,4 +1,8 @@ -using LightInject; +using System; +using System.Collections.Generic; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; namespace Umbraco.Core.Components { @@ -10,28 +14,233 @@ namespace Umbraco.Core.Components /// avoid accessing the container. This is because everything needs to be properly registered and with /// the proper lifecycle. These methods will take care of it. Directly registering into the container /// may cause issues. - public class Composition + public class Composition : IRegister { + private readonly Dictionary _builders = new Dictionary(); + private readonly Dictionary> _uniques = new Dictionary>(); + private readonly IRegister _register; + /// /// Initializes a new instance of the class. /// - /// A container. - /// The runtime level. - public Composition(IServiceContainer container, RuntimeLevel level) + /// A register. + /// A type loader. + /// A logger. + /// The runtime state. + /// Optional configs. + public Composition(IRegister register, TypeLoader typeLoader, IProfilingLogger logger, IRuntimeState runtimeState, Configs configs = null) { - Container = container; - RuntimeLevel = level; + _register = register; + TypeLoader = typeLoader; + Logger = logger; + RuntimeState = runtimeState; + + if (configs == null) + { + configs = new Configs(); + configs.AddCoreConfigs(); + } + + Configs = configs; + } + + #region Services + + /// + /// Gets the logger. + /// + public IProfilingLogger Logger { get; } + + /// + /// Gets the type loader. + /// + public TypeLoader TypeLoader { get; } + + /// + /// Gets the runtime state. + /// + public IRuntimeState RuntimeState { get; } + + /// + /// Gets the configurations. + /// + public Configs Configs { get; } + + #endregion + + #region IRegister + + /// + public object Concrete => _register.Concrete; + + /// + public void Register(Type serviceType, Lifetime lifetime = Lifetime.Transient) + => _register.Register(serviceType, lifetime); + + /// + public void Register(Type serviceType, Type implementingType, Lifetime lifetime = Lifetime.Transient) + => _register.Register(serviceType, implementingType, lifetime); + + /// + public void Register(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class + => _register.Register(factory, lifetime); + + /// + public void Register(Type serviceType, object instance) + => _register.Register(serviceType, instance); + + /// + public void RegisterFor(Lifetime lifetime = Lifetime.Transient) + where TService : class + => _register.RegisterFor(lifetime); + + /// + public void RegisterFor(Type implementingType, Lifetime lifetime = Lifetime.Transient) + where TService : class + => _register.RegisterFor(implementingType, lifetime); + + /// + public void RegisterFor(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class + => _register.RegisterFor(factory, lifetime); + + /// + public void RegisterFor(TService instance) + where TService : class + => _register.RegisterFor(instance); + + /// + public void RegisterAuto(Type serviceBaseType) + => _register.RegisterAuto(serviceBaseType); + + /// + public void ConfigureForWeb() + => _register.ConfigureForWeb(); + + /// + public IFactory CreateFactory() + { + foreach (var onCreating in OnCreatingFactory.Values) + onCreating(); + + foreach (var unique in _uniques.Values) + unique(_register); + _uniques.Clear(); // no point keep them around + + foreach (var builder in _builders.Values) + builder.RegisterWith(_register); + _builders.Clear(); // no point keep them around + + Configs.RegisterWith(_register); + + return _register.CreateFactory(); } /// - /// Gets the container. + /// Gets a dictionary of action to execute when creating the factory. /// - /// Use with care! - public IServiceContainer Container { get; } + public Dictionary OnCreatingFactory { get; } = new Dictionary(); + + #endregion + + #region Unique + + private string GetUniqueName() + => GetUniqueName(typeof(TService)); + + private string GetUniqueName(Type serviceType) + => serviceType.FullName; + + private string GetUniqueName() + => GetUniqueName(typeof(TService), typeof(TTarget)); + + private string GetUniqueName(Type serviceType, Type targetType) + => serviceType.FullName + "::" + targetType.FullName; /// - /// Gets the runtime level. + /// Registers a unique service as its own implementation. /// - public RuntimeLevel RuntimeLevel { get; } + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUnique(Type serviceType) + => _uniques[GetUniqueName(serviceType)] = register => register.Register(serviceType, Lifetime.Singleton); + + /// + /// Registers a unique service with an implementation type. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUnique(Type serviceType, Type implementingType) + => _uniques[GetUniqueName(serviceType)] = register => register.Register(serviceType, implementingType, Lifetime.Singleton); + + /// + /// Registers a unique service with an implementation factory. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUnique(Func factory) + where TService : class + => _uniques[GetUniqueName()] = register => register.Register(factory, Lifetime.Singleton); + + /// + /// Registers a unique service with an implementing instance. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUnique(Type serviceType, object instance) + => _uniques[GetUniqueName(serviceType)] = register => register.Register(serviceType, instance); + + /// + /// Registers a unique service for a target, as its own implementation. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUniqueFor() + where TService : class + => _uniques[GetUniqueName()] = register => register.RegisterFor(Lifetime.Singleton); + + /// + /// Registers a unique service for a target, with an implementing type. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUniqueFor(Type implementingType) + where TService : class + => _uniques[GetUniqueName()] = register => register.RegisterFor(implementingType, Lifetime.Singleton); + + /// + /// Registers a unique service for a target, with an implementation factory. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUniqueFor(Func factory) + where TService : class + => _uniques[GetUniqueName()] = register => register.RegisterFor(factory, Lifetime.Singleton); + + /// + /// Registers a unique service for a target, with an implementing instance. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUniqueFor(TService instance) + where TService : class + => _uniques[GetUniqueName()] = register => register.RegisterFor(instance); + + #endregion + + #region Collection Builders + + /// + /// Gets a collection builder (and registers the collection). + /// + /// The type of the collection builder. + /// The collection builder. + public TBuilder WithCollectionBuilder() + where TBuilder: ICollectionBuilder, new() + { + var typeOfBuilder = typeof(TBuilder); + + if (_builders.TryGetValue(typeOfBuilder, out var o)) + return (TBuilder) o; + + var builder = new TBuilder(); + _builders[typeOfBuilder] = builder; + return builder; + } + + #endregion } } diff --git a/src/Umbraco.Core/Components/CompositionExtensions.cs b/src/Umbraco.Core/Components/CompositionExtensions.cs index 7e94e4dc2b..bb23e89b81 100644 --- a/src/Umbraco.Core/Components/CompositionExtensions.cs +++ b/src/Umbraco.Core/Components/CompositionExtensions.cs @@ -1,9 +1,8 @@ using System; -using System.Runtime.CompilerServices; -using LightInject; using Umbraco.Core.Cache; using Umbraco.Core.Dictionary; using Umbraco.Core.Composing; +using Umbraco.Core.IO; using Umbraco.Core.Migrations; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence.Mappers; @@ -19,6 +18,46 @@ namespace Umbraco.Core.Components /// public static class CompositionExtensions { + #region FileSystems + + /// + /// Registers a filesystem. + /// + /// The type of the filesystem. + /// The implementing type. + /// The composition. + /// The register. + public static void RegisterFileSystem(this Composition composition) + where TImplementing : FileSystemWrapper, TFileSystem + where TFileSystem : class + { + composition.RegisterUnique(factory => + { + var fileSystems = factory.GetInstance(); + var supporting = factory.GetInstance(); + return fileSystems.GetFileSystem(supporting.For()); + }); + } + + /// + /// Registers a filesystem. + /// + /// The type of the filesystem. + /// The composition. + /// The register. + public static void RegisterFileSystem(this Composition composition) + where TFileSystem : FileSystemWrapper + { + composition.RegisterUnique(factory => + { + var fileSystems = factory.GetInstance(); + var supporting = factory.GetInstance(); + return fileSystems.GetFileSystem(supporting.For()); + }); + } + + #endregion + #region Collection Builders /// @@ -26,60 +65,66 @@ namespace Umbraco.Core.Components /// /// The composition. public static CacheRefresherCollectionBuilder CacheRefreshers(this Composition composition) - => composition.Container.GetInstance(); + => composition.WithCollectionBuilder(); /// /// Gets the mappers collection builder. /// /// The composition. public static MapperCollectionBuilder Mappers(this Composition composition) - => composition.Container.GetInstance(); + => composition.WithCollectionBuilder(); /// /// Gets the package actions collection builder. /// /// The composition. internal static PackageActionCollectionBuilder PackageActions(this Composition composition) - => composition.Container.GetInstance(); + => composition.WithCollectionBuilder(); /// /// Gets the data editor collection builder. /// /// The composition. public static DataEditorCollectionBuilder DataEditors(this Composition composition) - => composition.Container.GetInstance(); + => composition.WithCollectionBuilder(); /// /// Gets the property value converters collection builder. /// /// The composition. public static PropertyValueConverterCollectionBuilder PropertyValueConverters(this Composition composition) - => composition.Container.GetInstance(); + => composition.WithCollectionBuilder(); /// /// Gets the url segment providers collection builder. /// /// The composition. public static UrlSegmentProviderCollectionBuilder UrlSegmentProviders(this Composition composition) - => composition.Container.GetInstance(); + => composition.WithCollectionBuilder(); /// /// Gets the validators collection builder. /// /// The composition. internal static ManifestValueValidatorCollectionBuilder Validators(this Composition composition) - => composition.Container.GetInstance(); + => composition.WithCollectionBuilder(); /// /// Gets the post-migrations collection builder. /// /// The composition. internal static PostMigrationCollectionBuilder PostMigrations(this Composition composition) - => composition.Container.GetInstance(); + => composition.WithCollectionBuilder(); + + /// + /// Gets the components collection builder. + /// + public static ComponentCollectionBuilder Components(this Composition composition) + => composition.WithCollectionBuilder(); #endregion - #region Singleton + #region Uniques /// /// Sets the culture dictionary factory. @@ -89,7 +134,7 @@ namespace Umbraco.Core.Components public static void SetCultureDictionaryFactory(this Composition composition) where T : ICultureDictionaryFactory { - composition.Container.RegisterSingleton(); + composition.RegisterUnique(); } /// @@ -97,9 +142,9 @@ namespace Umbraco.Core.Components /// /// The composition. /// A function creating a culture dictionary factory. - public static void SetCultureDictionaryFactory(this Composition composition, Func factory) + public static void SetCultureDictionaryFactory(this Composition composition, Func factory) { - composition.Container.RegisterSingleton(factory); + composition.RegisterUnique(factory); } /// @@ -109,7 +154,7 @@ namespace Umbraco.Core.Components /// A factory. public static void SetCultureDictionaryFactory(this Composition composition, ICultureDictionaryFactory factory) { - composition.Container.RegisterSingleton(_ => factory); + composition.RegisterUnique(_ => factory); } /// @@ -120,7 +165,7 @@ namespace Umbraco.Core.Components public static void SetPublishedContentModelFactory(this Composition composition) where T : IPublishedModelFactory { - composition.Container.RegisterSingleton(); + composition.RegisterUnique(); } /// @@ -128,9 +173,9 @@ namespace Umbraco.Core.Components /// /// The composition. /// A function creating a published content model factory. - public static void SetPublishedContentModelFactory(this Composition composition, Func factory) + public static void SetPublishedContentModelFactory(this Composition composition, Func factory) { - composition.Container.RegisterSingleton(factory); + composition.RegisterUnique(factory); } /// @@ -140,7 +185,7 @@ namespace Umbraco.Core.Components /// A published content model factory. public static void SetPublishedContentModelFactory(this Composition composition, IPublishedModelFactory factory) { - composition.Container.RegisterSingleton(_ => factory); + composition.RegisterUnique(_ => factory); } /// @@ -151,7 +196,7 @@ namespace Umbraco.Core.Components public static void SetServerRegistrar(this Composition composition) where T : IServerRegistrar { - composition.Container.RegisterSingleton(); + composition.RegisterUnique(); } /// @@ -159,9 +204,9 @@ namespace Umbraco.Core.Components /// /// The composition. /// A function creating a server registar. - public static void SetServerRegistrar(this Composition composition, Func factory) + public static void SetServerRegistrar(this Composition composition, Func factory) { - composition.Container.RegisterSingleton(factory); + composition.RegisterUnique(factory); } /// @@ -171,7 +216,7 @@ namespace Umbraco.Core.Components /// A server registrar. public static void SetServerRegistrar(this Composition composition, IServerRegistrar registrar) { - composition.Container.RegisterSingleton(_ => registrar); + composition.RegisterUnique(_ => registrar); } /// @@ -182,7 +227,7 @@ namespace Umbraco.Core.Components public static void SetServerMessenger(this Composition composition) where T : IServerMessenger { - composition.Container.RegisterSingleton(); + composition.RegisterUnique(); } /// @@ -190,9 +235,9 @@ namespace Umbraco.Core.Components /// /// The composition. /// A function creating a server messenger. - public static void SetServerMessenger(this Composition composition, Func factory) + public static void SetServerMessenger(this Composition composition, Func factory) { - composition.Container.RegisterSingleton(factory); + composition.RegisterUnique(factory); } /// @@ -202,7 +247,7 @@ namespace Umbraco.Core.Components /// A server messenger. public static void SetServerMessenger(this Composition composition, IServerMessenger registrar) { - composition.Container.RegisterSingleton(_ => registrar); + composition.RegisterUnique(_ => registrar); } /// @@ -213,7 +258,7 @@ namespace Umbraco.Core.Components public static void SetShortStringHelper(this Composition composition) where T : IShortStringHelper { - composition.Container.RegisterSingleton(); + composition.RegisterUnique(); } /// @@ -221,9 +266,9 @@ namespace Umbraco.Core.Components /// /// The composition. /// A function creating a short string helper. - public static void SetShortStringHelper(this Composition composition, Func factory) + public static void SetShortStringHelper(this Composition composition, Func factory) { - composition.Container.RegisterSingleton(factory); + composition.RegisterUnique(factory); } /// @@ -233,9 +278,25 @@ namespace Umbraco.Core.Components /// A short string helper. public static void SetShortStringHelper(this Composition composition, IShortStringHelper helper) { - composition.Container.RegisterSingleton(_ => helper); + composition.RegisterUnique(_ => helper); } + /// + /// Sets the underlying media filesystem. + /// + /// A composition. + /// A filesystem factory. + public static void SetMediaFileSystem(this Composition composition, Func filesystemFactory) + => composition.RegisterUniqueFor(filesystemFactory); + + /// + /// Sets the underlying media filesystem. + /// + /// A composition. + /// A filesystem factory. + public static void SetMediaFileSystem(this Composition composition, Func filesystemFactory) + => composition.RegisterUniqueFor(_ => filesystemFactory()); + #endregion } } diff --git a/src/Umbraco.Core/Components/DisableAttribute.cs b/src/Umbraco.Core/Components/DisableAttribute.cs new file mode 100644 index 0000000000..f9a7249b89 --- /dev/null +++ b/src/Umbraco.Core/Components/DisableAttribute.cs @@ -0,0 +1,38 @@ +using System; + +namespace Umbraco.Core.Components +{ + /// + /// Indicates that a composer should be disabled. + /// + /// + /// If a type is specified, disables the composer of that type, else disables the composer marked with the attribute. + /// This attribute is *not* inherited. + /// This attribute applies to classes only, it is not possible to enable/disable interfaces. + /// If a composer ends up being both enabled and disabled: attributes marking the composer itself have lower priority + /// than attributes on *other* composers, eg if a composer declares itself as disabled it is possible to enable it from + /// another composer. Anything else is unspecified. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] + public class DisableAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public DisableAttribute() + { } + + /// + /// Initializes a new instance of the class. + /// + public DisableAttribute(Type disabledType) + { + DisabledType = disabledType; + } + + /// + /// Gets the disabled type, or null if it is the composer marked with the attribute. + /// + public Type DisabledType { get; } + } +} diff --git a/src/Umbraco.Core/Components/DisableComponentAttribute.cs b/src/Umbraco.Core/Components/DisableComponentAttribute.cs deleted file mode 100644 index f7ff71e119..0000000000 --- a/src/Umbraco.Core/Components/DisableComponentAttribute.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; - -namespace Umbraco.Core.Components -{ - /// - /// Indicates that a component should be disabled. - /// - /// - /// If a type is specified, disables the component of that type, else disables the component marked with the attribute. - /// This attribute is *not* inherited. - /// This attribute applies to classes only, it is not possible to enable/disable interfaces. - /// If a component ends up being both enabled and disabled: attributes marking the component itself have lower priority - /// than attributes on *other* components, eg if a component declares itself as disabled it is possible to enable it from - /// another component. Anything else is unspecified. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public class DisableComponentAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - public DisableComponentAttribute() - { } - - /// - /// Initializes a new instance of the class. - /// - public DisableComponentAttribute(Type disabledType) - { - DisabledType = disabledType; - } - - /// - /// Gets the disabled type, or null if it is the component marked with the attribute. - /// - public Type DisabledType { get; } - } -} diff --git a/src/Umbraco.Core/Components/EnableAttribute.cs b/src/Umbraco.Core/Components/EnableAttribute.cs new file mode 100644 index 0000000000..edf3cbdc2e --- /dev/null +++ b/src/Umbraco.Core/Components/EnableAttribute.cs @@ -0,0 +1,38 @@ +using System; + +namespace Umbraco.Core.Components +{ + /// + /// Indicates that a composer should be enabled. + /// + /// + /// If a type is specified, enables the composer of that type, else enables the composer marked with the attribute. + /// This attribute is *not* inherited. + /// This attribute applies to classes only, it is not possible to enable/disable interfaces. + /// If a composer ends up being both enabled and disabled: attributes marking the composer itself have lower priority + /// than attributes on *other* composers, eg if a composer declares itself as disabled it is possible to enable it from + /// another composer. Anything else is unspecified. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] + public class EnableAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public EnableAttribute() + { } + + /// + /// Initializes a new instance of the class. + /// + public EnableAttribute(Type enabledType) + { + EnabledType = enabledType; + } + + /// + /// Gets the enabled type, or null if it is the composer marked with the attribute. + /// + public Type EnabledType { get; } + } +} diff --git a/src/Umbraco.Core/Components/EnableComponentAttribute.cs b/src/Umbraco.Core/Components/EnableComponentAttribute.cs deleted file mode 100644 index fa76dc2404..0000000000 --- a/src/Umbraco.Core/Components/EnableComponentAttribute.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; - -namespace Umbraco.Core.Components -{ - /// - /// Indicates that a component should be enabled. - /// - /// - /// If a type is specified, enables the component of that type, else enables the component marked with the attribute. - /// This attribute is *not* inherited. - /// This attribute applies to classes only, it is not possible to enable/disable interfaces. - /// If a component ends up being both enabled and disabled: attributes marking the component itself have lower priority - /// than attributes on *other* components, eg if a component declares itself as disabled it is possible to enable it from - /// another component. Anything else is unspecified. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public class EnableComponentAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - public EnableComponentAttribute() - { } - - /// - /// Initializes a new instance of the class. - /// - public EnableComponentAttribute(Type enabledType) - { - EnabledType = enabledType; - } - - /// - /// Gets the enabled type, or null if it is the component marked with the attribute. - /// - public Type EnabledType { get; } - } -} diff --git a/src/Umbraco.Core/Components/IComponent.cs b/src/Umbraco.Core/Components/IComponent.cs new file mode 100644 index 0000000000..b1954d821b --- /dev/null +++ b/src/Umbraco.Core/Components/IComponent.cs @@ -0,0 +1,25 @@ +namespace Umbraco.Core.Components +{ + /// + /// Represents a component. + /// + /// + /// Components are created by DI and therefore must have a public constructor. + /// All components are terminated in reverse order when Umbraco terminates, and + /// disposable components are disposed. + /// The Dispose method may be invoked more than once, and components + /// should ensure they support this. + /// + public interface IComponent + { + /// + /// Initializes the component. + /// + void Initialize(); + + /// + /// Terminates the component. + /// + void Terminate(); + } +} diff --git a/src/Umbraco.Core/Components/IComposer.cs b/src/Umbraco.Core/Components/IComposer.cs new file mode 100644 index 0000000000..ce02aa4f13 --- /dev/null +++ b/src/Umbraco.Core/Components/IComposer.cs @@ -0,0 +1,16 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Components +{ + /// + /// Represents a composer. + /// + public interface IComposer : IDiscoverable + { + /// + /// Compose. + /// + /// + void Compose(Composition composition); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Components/ICoreComposer.cs b/src/Umbraco.Core/Components/ICoreComposer.cs new file mode 100644 index 0000000000..94aa9953a9 --- /dev/null +++ b/src/Umbraco.Core/Components/ICoreComposer.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Components +{ + /// + /// Represents a core . + /// + /// + /// All core composers are required by (compose before) all user composers, + /// and require (compose after) all runtime composers. + /// + [ComposeAfter(typeof(IRuntimeComposer))] + public interface ICoreComposer : IComposer + { } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Components/IRuntimeComponent.cs b/src/Umbraco.Core/Components/IRuntimeComponent.cs deleted file mode 100644 index 7f89d21e87..0000000000 --- a/src/Umbraco.Core/Components/IRuntimeComponent.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Umbraco.Core.Components -{ - public interface IRuntimeComponent : IUmbracoComponent - { } -} diff --git a/src/Umbraco.Core/Components/IRuntimeComposer.cs b/src/Umbraco.Core/Components/IRuntimeComposer.cs new file mode 100644 index 0000000000..4b8253ee6c --- /dev/null +++ b/src/Umbraco.Core/Components/IRuntimeComposer.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Core.Components +{ + /// + /// Represents a runtime . + /// + /// + /// All runtime composers are required by (compose before) all core composers + /// + public interface IRuntimeComposer : IComposer + { } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Components/IUmbracoComponent.cs b/src/Umbraco.Core/Components/IUmbracoComponent.cs deleted file mode 100644 index d25b97c270..0000000000 --- a/src/Umbraco.Core/Components/IUmbracoComponent.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Umbraco.Core.Composing; - -namespace Umbraco.Core.Components -{ - /// - /// Represents an Umbraco component. - /// - public interface IUmbracoComponent : IDiscoverable - { - /// - /// Composes the component. - /// - /// The composition. - void Compose(Composition composition); - - /// - /// Terminates the component. - /// - void Terminate(); - } -} diff --git a/src/Umbraco.Core/Components/IUmbracoCoreComponent.cs b/src/Umbraco.Core/Components/IUmbracoCoreComponent.cs deleted file mode 100644 index 28ff286da3..0000000000 --- a/src/Umbraco.Core/Components/IUmbracoCoreComponent.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Umbraco.Core.Components -{ - [RequireComponent(typeof(IRuntimeComponent))] - public interface IUmbracoCoreComponent : IUmbracoComponent - { } -} diff --git a/src/Umbraco.Core/Components/IUmbracoUserComponent.cs b/src/Umbraco.Core/Components/IUmbracoUserComponent.cs deleted file mode 100644 index 61a07cfd48..0000000000 --- a/src/Umbraco.Core/Components/IUmbracoUserComponent.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Umbraco.Core.Components -{ - [RequireComponent(typeof(UmbracoCoreComponent))] - public interface IUmbracoUserComponent : IUmbracoComponent - { } -} diff --git a/src/Umbraco.Core/Components/IUserComposer.cs b/src/Umbraco.Core/Components/IUserComposer.cs new file mode 100644 index 0000000000..59e0023635 --- /dev/null +++ b/src/Umbraco.Core/Components/IUserComposer.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core.Components +{ + /// + /// Represents a user . + /// + /// + /// All user composers require (compose after) all core composers. + /// + [ComposeAfter(typeof(ICoreComposer))] + public interface IUserComposer : IComposer + { } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Components/ManifestWatcherComponent.cs b/src/Umbraco.Core/Components/ManifestWatcherComponent.cs index 0a8d9ccd52..2420e1d5bb 100644 --- a/src/Umbraco.Core/Components/ManifestWatcherComponent.cs +++ b/src/Umbraco.Core/Components/ManifestWatcherComponent.cs @@ -5,16 +5,24 @@ using Umbraco.Core.Manifest; namespace Umbraco.Core.Components { - [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - public class ManifestWatcherComponent : UmbracoComponentBase, IUmbracoCoreComponent + public sealed class ManifestWatcherComponent : IComponent { + private readonly IRuntimeState _runtimeState; + private readonly ILogger _logger; + // if configured and in debug mode, a ManifestWatcher watches App_Plugins folders for // package.manifest chances and restarts the application on any change private ManifestWatcher _mw; - public void Initialize(IRuntimeState runtime, ILogger logger) + public ManifestWatcherComponent(IRuntimeState runtimeState, ILogger logger) { - if (runtime.Debug == false) return; + _runtimeState = runtimeState; + _logger = logger; + } + + public void Initialize() + { + if (_runtimeState.Debug == false) return; //if (ApplicationContext.Current.IsConfigured == false || GlobalSettings.DebugMode == false) // return; @@ -22,13 +30,15 @@ namespace Umbraco.Core.Components var appPlugins = IOHelper.MapPath("~/App_Plugins/"); if (Directory.Exists(appPlugins) == false) return; - _mw = new ManifestWatcher(logger); + _mw = new ManifestWatcher(_logger); _mw.Start(Directory.GetDirectories(appPlugins)); } - public override void Terminate() + public void Terminate() { - _mw?.Dispose(); + if (_mw == null) return; + + _mw.Dispose(); _mw = null; } } diff --git a/src/Umbraco.Core/Components/ManifestWatcherComposer.cs b/src/Umbraco.Core/Components/ManifestWatcherComposer.cs new file mode 100644 index 0000000000..b08680156b --- /dev/null +++ b/src/Umbraco.Core/Components/ManifestWatcherComposer.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Core.Components +{ + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public class ManifestWatcherComposer : ComponentComposer, ICoreComposer + { } +} diff --git a/src/Umbraco.Core/Components/RelateOnCopyComponent.cs b/src/Umbraco.Core/Components/RelateOnCopyComponent.cs index bc66dccd31..404d385680 100644 --- a/src/Umbraco.Core/Components/RelateOnCopyComponent.cs +++ b/src/Umbraco.Core/Components/RelateOnCopyComponent.cs @@ -6,14 +6,16 @@ using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Components { //TODO: This should just exist in the content service/repo! - [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - public sealed class RelateOnCopyComponent : UmbracoComponentBase, IUmbracoCoreComponent + public sealed class RelateOnCopyComponent : IComponent { public void Initialize() { ContentService.Copied += ContentServiceCopied; } + public void Terminate() + { } + private static void ContentServiceCopied(IContentService sender, Events.CopyEventArgs e) { if (e.RelateToOriginal == false) return; diff --git a/src/Umbraco.Core/Components/RelateOnCopyComposer.cs b/src/Umbraco.Core/Components/RelateOnCopyComposer.cs new file mode 100644 index 0000000000..f5e9423edd --- /dev/null +++ b/src/Umbraco.Core/Components/RelateOnCopyComposer.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Core.Components +{ + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public sealed class RelateOnCopyComposer : ComponentComposer, ICoreComposer + { } +} diff --git a/src/Umbraco.Core/Components/RelateOnTrashComponent.cs b/src/Umbraco.Core/Components/RelateOnTrashComponent.cs index 8bcce50c68..6279bb98ba 100644 --- a/src/Umbraco.Core/Components/RelateOnTrashComponent.cs +++ b/src/Umbraco.Core/Components/RelateOnTrashComponent.cs @@ -7,8 +7,7 @@ using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Components { - [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - public sealed class RelateOnTrashComponent : UmbracoComponentBase, IUmbracoCoreComponent + public sealed class RelateOnTrashComponent : IComponent { public void Initialize() { @@ -18,6 +17,9 @@ namespace Umbraco.Core.Components MediaService.Trashed += MediaService_Trashed; } + public void Terminate() + { } + private static void ContentService_Moved(IContentService sender, MoveEventArgs e) { foreach (var item in e.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Constants.System.RecycleBinContent.ToInvariantString()))) diff --git a/src/Umbraco.Core/Components/RelateOnTrashComposer.cs b/src/Umbraco.Core/Components/RelateOnTrashComposer.cs new file mode 100644 index 0000000000..5d89bc0e37 --- /dev/null +++ b/src/Umbraco.Core/Components/RelateOnTrashComposer.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Core.Components +{ + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public sealed class RelateOnTrashComposer : ComponentComposer, ICoreComposer + { } +} diff --git a/src/Umbraco.Core/Components/RequireComponentAttribute.cs b/src/Umbraco.Core/Components/RequireComponentAttribute.cs deleted file mode 100644 index 2d53413f99..0000000000 --- a/src/Umbraco.Core/Components/RequireComponentAttribute.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace Umbraco.Core.Components -{ - /// - /// Indicates that a component requires another component. - /// - /// - /// This attribute is *not* inherited. This means that a component class inheriting from - /// another component class does *not* inherit its requirements. However, the bootloader checks - /// the *interfaces* of every component for their requirements, so requirements declared on - /// interfaces are inherited by every component class implementing the interface. - /// When targetting a class, indicates a dependency on the component which must be enabled, - /// unless the requirement has explicitely been declared as weak (and then, only if the component - /// is enabled). - /// When targetting an interface, indicates a dependency on enabled components implementing - /// the interface. It could be no component at all, unless the requirement has explicitely been - /// declared as strong (and at least one component must be enabled). - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] - public class RequireComponentAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The type of the required component. - public RequireComponentAttribute(Type requiredType) - { - if (typeof(IUmbracoComponent).IsAssignableFrom(requiredType) == false) - throw new ArgumentException($"Type {requiredType.FullName} is invalid here because it does not implement {typeof(IUmbracoComponent).FullName}."); - RequiredType = requiredType; - } - - /// - /// Initializes a new instance of the class. - /// - /// The type of the required component. - /// A value indicating whether the requirement is weak. - public RequireComponentAttribute(Type requiredType, bool weak) - : this(requiredType) - { - Weak = weak; - } - - /// - /// Gets the required type. - /// - public Type RequiredType { get; } - - /// - /// Gets a value indicating whether the requirement is weak. - /// - /// Returns true if the requirement is weak (requires the other component if it - /// is enabled), false if the requirement is strong (requires the other component to be - /// enabled), and null if unspecified, in which case it is strong for classes and weak for - /// interfaces. - public bool? Weak { get; } - } -} diff --git a/src/Umbraco.Core/Components/RequiredComponentAttribute.cs b/src/Umbraco.Core/Components/RequiredComponentAttribute.cs deleted file mode 100644 index 7895445179..0000000000 --- a/src/Umbraco.Core/Components/RequiredComponentAttribute.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; - -namespace Umbraco.Core.Components -{ - /// - /// Indicates that a component is required by another component. - /// - /// - /// fixme - /// This attribute is *not* inherited. This means that a component class inheriting from - /// another component class does *not* inherit its requirements. However, the bootloader checks - /// the *interfaces* of every component for their requirements, so requirements declared on - /// interfaces are inherited by every component class implementing the interface. - /// When targetting a class, indicates a dependency on the component which must be enabled, - /// unless the requirement has explicitely been declared as weak (and then, only if the component - /// is enabled). - /// When targetting an interface, indicates a dependency on enabled components implementing - /// the interface. It could be no component at all, unless the requirement has explicitely been - /// declared as strong (and at least one component must be enabled). - /// - - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] - public class RequiredComponentAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The type of the required component. - public RequiredComponentAttribute(Type requiringType) - { - if (typeof(IUmbracoComponent).IsAssignableFrom(requiringType) == false) - throw new ArgumentException($"Type {requiringType.FullName} is invalid here because it does not implement {typeof(IUmbracoComponent).FullName}."); - RequiringType = requiringType; - } - - /// - /// Gets the required type. - /// - public Type RequiringType { get; } - } -} diff --git a/src/Umbraco.Core/Components/UmbracoComponentBase.cs b/src/Umbraco.Core/Components/UmbracoComponentBase.cs deleted file mode 100644 index 476c4c6d59..0000000000 --- a/src/Umbraco.Core/Components/UmbracoComponentBase.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Umbraco.Core.Components -{ - /// - /// Provides a base class for implementations. - /// - public abstract class UmbracoComponentBase : IUmbracoComponent - { - /// - public virtual void Compose(Composition composition) - { } - - /// - public virtual void Terminate() - { } - } -} diff --git a/src/Umbraco.Core/Components/UmbracoCoreComponent.cs b/src/Umbraco.Core/Components/UmbracoCoreComponent.cs deleted file mode 100644 index 9f6709c494..0000000000 --- a/src/Umbraco.Core/Components/UmbracoCoreComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Umbraco.Core.Components -{ - // the UmbracoCoreComponent requires all IUmbracoCoreComponent - // all user-level components should require the UmbracoCoreComponent - - [RequireComponent(typeof(IUmbracoCoreComponent))] - public class UmbracoCoreComponent : UmbracoComponentBase - { } -} diff --git a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs index 3fac2d3255..41038ea4e9 100644 --- a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; -using LightInject; namespace Umbraco.Core.Composing { @@ -14,69 +12,34 @@ namespace Umbraco.Core.Composing /// The type of the items. public abstract class CollectionBuilderBase : ICollectionBuilder where TBuilder: CollectionBuilderBase - where TCollection : IBuilderCollection + where TCollection : class, IBuilderCollection { private readonly List _types = new List(); private readonly object _locker = new object(); - private Func, TCollection> _collectionCtor; - private ServiceRegistration[] _registrations; - - /// - /// Initializes a new instance of the - /// class with a service container. - /// - /// A service container. - protected CollectionBuilderBase(IServiceContainer container) - { - Container = container; - // ReSharper disable once DoNotCallOverridableMethodsInConstructor - Initialize(); - } - - /// - /// Gets the service container. - /// - protected IServiceContainer Container { get; } + private Type[] _registeredTypes; /// /// Gets the internal list of types as an IEnumerable (immutable). /// public IEnumerable GetTypes() => _types; - /// - /// Initializes a new instance of the builder. - /// - /// This is called by the constructor and, by default, registers the - /// collection automatically. - protected virtual void Initialize() + /// + public virtual void RegisterWith(IRegister register) { - // compile the auto-collection constructor - var argType = typeof(IEnumerable); - var ctorArgTypes = new[] { argType }; - var constructor = typeof(TCollection).GetConstructor(ctorArgTypes); - if (constructor != null) - { - var exprArg = Expression.Parameter(argType, "items"); - var exprNew = Expression.New(constructor, exprArg); - var expr = Expression.Lambda, TCollection>>(exprNew, exprArg); - _collectionCtor = expr.Compile(); - } - // else _collectionCtor remains null, assuming CreateCollection has been overriden - - // we just don't want to support re-registering collections here - var registration = Container.GetAvailableService(); - if (registration != null) - throw new InvalidOperationException("Collection builders cannot be registered once the collection itself has been registered."); + if (_registeredTypes != null) + throw new InvalidOperationException("This builder has already been registered."); // register the collection - Container.Register(_ => CreateCollection(), CollectionLifetime); + register.Register(CreateCollection, CollectionLifetime); + + // register the types + RegisterTypes(register); } /// /// Gets the collection lifetime. /// - /// Return null for transient collections. - protected virtual ILifetime CollectionLifetime => new PerContainerLifetime(); + protected virtual Lifetime CollectionLifetime => Lifetime.Singleton; /// /// Configures the internal list of types. @@ -87,8 +50,8 @@ namespace Umbraco.Core.Composing { lock (_locker) { - if (_registrations != null) - throw new InvalidOperationException("Cannot configure a collection builder after its types have been resolved."); + if (_registeredTypes != null) + throw new InvalidOperationException("Cannot configure a collection builder after it has been registered."); action(_types); } } @@ -104,55 +67,54 @@ namespace Umbraco.Core.Composing return types; } - private void RegisterTypes() + private void RegisterTypes(IRegister register) { lock (_locker) { - if (_registrations != null) return; + if (_registeredTypes != null) return; var types = GetRegisteringTypes(_types).ToArray(); + + // ensure they are safe foreach (var type in types) EnsureType(type, "register"); - var prefix = GetType().FullName + "_"; - var i = 0; + // register them foreach (var type in types) - { - var name = $"{prefix}{i++:00000}"; - Container.Register(typeof(TItem), type, name); - } + register.Register(type); - _registrations = Container.AvailableServices - .Where(x => x.ServiceName.StartsWith(prefix)) - .OrderBy(x => x.ServiceName) - .ToArray(); + _registeredTypes = types; } } /// /// Creates the collection items. /// - /// The arguments. /// The collection items. - protected virtual IEnumerable CreateItems(params object[] args) + protected virtual IEnumerable CreateItems(IFactory factory) { - RegisterTypes(); // will do it only once + if (_registeredTypes == null) + throw new InvalidOperationException("Cannot create items before the collection builder has been registered."); - var type = typeof (TItem); - return _registrations - .Select(x => (TItem) Container.GetInstanceOrThrow(type, x.ServiceName, x.ImplementingType, args)) + return _registeredTypes // respect order + .Select(x => CreateItem(factory, x)) .ToArray(); // safe } + /// + /// Creates a collection item. + /// + protected virtual TItem CreateItem(IFactory factory, Type itemType) + => (TItem) factory.GetInstance(itemType); + /// /// Creates a collection. /// /// A collection. /// Creates a new collection each time it is invoked. - public virtual TCollection CreateCollection() + public virtual TCollection CreateCollection(IFactory factory) { - if (_collectionCtor == null) throw new InvalidOperationException("Collection auto-creation is not possible."); - return _collectionCtor(CreateItems()); + return factory.CreateInstance(CreateItems(factory)); } protected Type EnsureType(Type type, string action) diff --git a/src/Umbraco.Core/Composing/Composers/ConfigurationComposer.cs b/src/Umbraco.Core/Composing/Composers/ConfigurationComposer.cs new file mode 100644 index 0000000000..ca86f623cc --- /dev/null +++ b/src/Umbraco.Core/Composing/Composers/ConfigurationComposer.cs @@ -0,0 +1,25 @@ +using Umbraco.Core.Components; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Core.Composing.Composers +{ + /// + /// Compose configurations. + /// + public static class ConfigurationComposer + { + public static Composition ComposeConfiguration(this Composition composition) + { + // common configurations are already registered + // register others + + composition.RegisterUnique(factory => factory.GetInstance().Content); + composition.RegisterUnique(factory => factory.GetInstance().Templates); + composition.RegisterUnique(factory => factory.GetInstance().RequestHandler); + composition.RegisterUnique(factory => factory.GetInstance().Security); + + return composition; + } + } +} diff --git a/src/Umbraco.Core/Composing/Composers/CoreMappingProfilesComposer.cs b/src/Umbraco.Core/Composing/Composers/CoreMappingProfilesComposer.cs new file mode 100644 index 0000000000..0274b8f1a9 --- /dev/null +++ b/src/Umbraco.Core/Composing/Composers/CoreMappingProfilesComposer.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using Umbraco.Core.Components; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Composing.Composers + +{ + public static class CoreMappingProfilesComposer + { + public static Composition ComposeCoreMappingProfiles(this Composition composition) + { + composition.Register(); + return composition; + } + } +} diff --git a/src/Umbraco.Core/Composing/Composers/FileSystemsComposer.cs b/src/Umbraco.Core/Composing/Composers/FileSystemsComposer.cs new file mode 100644 index 0000000000..4c598f27e4 --- /dev/null +++ b/src/Umbraco.Core/Composing/Composers/FileSystemsComposer.cs @@ -0,0 +1,98 @@ +using Umbraco.Core.Components; +using Umbraco.Core.IO; +using Umbraco.Core.IO.MediaPathSchemes; + +namespace Umbraco.Core.Composing.Composers +{ + public static class FileSystemsComposer + { + /* + * HOW TO REPLACE THE MEDIA UNDERLYING FILESYSTEM + * ---------------------------------------------- + * + * Create a component and use it to modify the composition by adding something like: + * + * composition.RegisterUniqueFor(...); + * + * and register whatever supporting filesystem you like. + * + * + * HOW TO IMPLEMENT MY OWN FILESYSTEM + * ---------------------------------- + * + * Create your filesystem class: + * + * public class MyFileSystem : FileSystemWrapper + * { + * public MyFileSystem(IFileSystem innerFileSystem) + * : base(innerFileSystem) + * { } + * } + * + * The ctor can have more parameters, that will be resolved by the container. + * + * Register your filesystem, in a component: + * + * composition.RegisterFileSystem(); + * + * Register the underlying filesystem: + * + * composition.RegisterUniqueFor(...); + * + * And that's it, you can inject MyFileSystem wherever it's needed. + * + * + * You can also declare a filesystem interface: + * + * public interface IMyFileSystem : IFileSystem + * { } + * + * Make the class implement the interface, then + * register your filesystem, in a component: + * + * composition.RegisterFileSystem(); + * composition.RegisterUniqueFor(...); + * + * And that's it, you can inject IMyFileSystem wherever it's needed. + * + * + * WHAT IS SHADOWING + * ----------------- + * + * Shadowing is the technology used for Deploy to implement some sort of + * transaction-management on top of filesystems. The plumbing explained above, + * compared to creating your own physical filesystem, ensures that your filesystem + * would participate into such transactions. + * + * + */ + + public static Composition ComposeFileSystems(this Composition composition) + { + // register FileSystems, which manages all filesystems + // it needs to be registered (not only the interface) because it provides additional + // functionality eg for scoping, and is injected in the scope provider - whereas the + // interface is really for end-users to get access to filesystems. + composition.RegisterUnique(factory => factory.CreateInstance(factory)); + + // register IFileSystems, which gives access too all filesystems + composition.RegisterUnique(factory => factory.GetInstance()); + + // register the scheme for media paths + composition.RegisterUnique(); + + // register the IMediaFileSystem implementation + composition.RegisterFileSystem(); + + // register the supporting filesystems provider + composition.Register(factory => new SupportingFileSystems(factory), Lifetime.Singleton); + + // register the IFileSystem supporting the IMediaFileSystem + // THIS IS THE ONLY THING THAT NEEDS TO CHANGE, IN ORDER TO REPLACE THE UNDERLYING FILESYSTEM + // and, SupportingFileSystem.For() returns the underlying filesystem + composition.SetMediaFileSystem(() => new PhysicalFileSystem("~/media")); + + return composition; + } + } +} diff --git a/src/Umbraco.Core/Composing/Composers/RepositoriesComposer.cs b/src/Umbraco.Core/Composing/Composers/RepositoriesComposer.cs new file mode 100644 index 0000000000..62b92081c1 --- /dev/null +++ b/src/Umbraco.Core/Composing/Composers/RepositoriesComposer.cs @@ -0,0 +1,54 @@ +using Umbraco.Core.Components; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.Repositories.Implement; + +namespace Umbraco.Core.Composing.Composers +{ + /// + /// Composes repositories. + /// + public static class RepositoriesComposer + { + public static Composition ComposeRepositories(this Composition composition) + { + // repositories + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + + return composition; + } + } +} diff --git a/src/Umbraco.Core/Composing/Composers/ServicesComposer.cs b/src/Umbraco.Core/Composing/Composers/ServicesComposer.cs new file mode 100644 index 0000000000..1b77aaa7d6 --- /dev/null +++ b/src/Umbraco.Core/Composing/Composers/ServicesComposer.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using System.Linq; +using Umbraco.Core.Cache; +using Umbraco.Core.Components; +using Umbraco.Core.Events; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; + +namespace Umbraco.Core.Composing.Composers +{ + public static class ServicesComposer + { + public static Composition ComposeServices(this Composition composition) + { + // register a transient messages factory, which will be replaced by the web + // boot manager when running in a web context + composition.RegisterUnique(); + + // register the service context + composition.RegisterUnique(); + + // register the special idk map + composition.RegisterUnique(); + + // register the services + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.Register(SourcesFactory); + composition.RegisterUnique(factory => new LocalizedTextService( + factory.GetInstance>(), + factory.GetInstance())); + + //TODO: These are replaced in the web project - we need to declare them so that + // something is wired up, just not sure this is very nice but will work for now. + composition.RegisterUnique(); + composition.RegisterUnique(); + + return composition; + } + + private static LocalizedTextServiceFileSources SourcesFactory(IFactory container) + { + var mainLangFolder = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Umbraco + "/config/lang/")); + var appPlugins = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.AppPlugins)); + var configLangFolder = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Config + "/lang/")); + + var pluginLangFolders = appPlugins.Exists == false + ? Enumerable.Empty() + : appPlugins.GetDirectories() + .SelectMany(x => x.GetDirectories("Lang")) + .SelectMany(x => x.GetFiles("*.xml", SearchOption.TopDirectoryOnly)) + .Where(x => Path.GetFileNameWithoutExtension(x.FullName).Length == 5) + .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, false)); + + //user defined langs that overwrite the default, these should not be used by plugin creators + var userLangFolders = configLangFolder.Exists == false + ? Enumerable.Empty() + : configLangFolder + .GetFiles("*.user.xml", SearchOption.TopDirectoryOnly) + .Where(x => Path.GetFileNameWithoutExtension(x.FullName).Length == 10) + .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, true)); + + return new LocalizedTextServiceFileSources( + container.GetInstance(), + container.GetInstance().RuntimeCache, + mainLangFolder, + pluginLangFolders.Concat(userLangFolders)); + } + } +} diff --git a/src/Umbraco.Core/Composing/CompositionExtensions.cs b/src/Umbraco.Core/Composing/CompositionExtensions.cs new file mode 100644 index 0000000000..2307d757c9 --- /dev/null +++ b/src/Umbraco.Core/Composing/CompositionExtensions.cs @@ -0,0 +1,69 @@ +using Umbraco.Core.Cache; +using Umbraco.Core.Components; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Composing +{ + /// + /// Provides extension methods to the class. + /// + public static class CompositionExtensions + { + #region Essentials + + /// + /// Registers essential services. + /// + public static void RegisterEssentials(this Composition composition, + ILogger logger, IProfiler profiler, IProfilingLogger profilingLogger, + IMainDom mainDom, + CacheHelper appCaches, + IUmbracoDatabaseFactory databaseFactory, + TypeLoader typeLoader, + IRuntimeState state) + { + composition.RegisterUnique(logger); + composition.RegisterUnique(profiler); + composition.RegisterUnique(profilingLogger); + composition.RegisterUnique(mainDom); + composition.RegisterUnique(appCaches); + composition.RegisterUnique(factory => factory.GetInstance().RuntimeCache); + composition.RegisterUnique(databaseFactory); + composition.RegisterUnique(factory => factory.GetInstance().SqlContext); + composition.RegisterUnique(typeLoader); + composition.RegisterUnique(state); + } + + #endregion + + #region Unique + + /// + /// Registers a unique service as its own implementation. + /// + public static void RegisterUnique(this Composition composition) + => composition.RegisterUnique(typeof(TService), typeof(TService)); + + /// + /// Registers a unique service with an implementation type. + /// + public static void RegisterUnique(this Composition composition) + => composition.RegisterUnique(typeof(TService), typeof(TImplementing)); + + /// + /// Registers a unique service with an implementation type, for a target. + /// + public static void RegisterUniqueFor(this Composition composition) + where TService : class + => composition.RegisterUniqueFor(typeof(TImplementing)); + + /// + /// Registers a unique service with an implementing instance. + /// + public static void RegisterUnique(this Composition composition, TService instance) + => composition.RegisterUnique(typeof(TService), instance); + + #endregion + } +} diff --git a/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs b/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs deleted file mode 100644 index 82912163b6..0000000000 --- a/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs +++ /dev/null @@ -1,23 +0,0 @@ -using LightInject; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; - -namespace Umbraco.Core.Composing.CompositionRoots -{ - /// - /// Sets up IoC container for Umbraco configuration classes - /// - public sealed class ConfigurationCompositionRoot : ICompositionRoot - { - public void Compose(IServiceRegistry container) - { - container.Register(factory => UmbracoConfig.For.UmbracoSettings()); - container.Register(factory => factory.GetInstance().Content); - container.Register(factory => factory.GetInstance().Templates); - container.Register(factory => factory.GetInstance().RequestHandler); - container.Register(factory => UmbracoConfig.For.GlobalSettings()); - - // fixme - other sections we need to add? - } - } -} diff --git a/src/Umbraco.Core/Composing/CompositionRoots/CoreMappingProfilesCompositionRoot.cs b/src/Umbraco.Core/Composing/CompositionRoots/CoreMappingProfilesCompositionRoot.cs deleted file mode 100644 index 6b55a4af7e..0000000000 --- a/src/Umbraco.Core/Composing/CompositionRoots/CoreMappingProfilesCompositionRoot.cs +++ /dev/null @@ -1,13 +0,0 @@ -using LightInject; -using Umbraco.Core.Models.Identity; - -namespace Umbraco.Core.Composing.CompositionRoots -{ - public sealed class CoreMappingProfilesCompositionRoot : ICompositionRoot - { - public void Compose(IServiceRegistry container) - { - container.Register(); - } - } -} diff --git a/src/Umbraco.Core/Composing/CompositionRoots/RepositoryCompositionRoot.cs b/src/Umbraco.Core/Composing/CompositionRoots/RepositoryCompositionRoot.cs deleted file mode 100644 index 9c36bf5cec..0000000000 --- a/src/Umbraco.Core/Composing/CompositionRoots/RepositoryCompositionRoot.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using LightInject; -using Umbraco.Core.Cache; -using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Persistence.Repositories.Implement; - -namespace Umbraco.Core.Composing.CompositionRoots -{ - /// - /// Sets the IoC container for the umbraco data layer/repositories/sql/database/etc... - /// - public sealed class RepositoryCompositionRoot : ICompositionRoot - { - public const string DisabledCache = "DisabledCache"; - - public void Compose(IServiceRegistry container) - { - // register cache helpers - // the main cache helper is registered by CoreBootManager and is used by most repositories - // the disabled one is used by those repositories that have an annotated ctor parameter - container.RegisterSingleton(factory => CacheHelper.CreateDisabledCacheHelper(), DisabledCache); - - // resolve ctor dependency from GetInstance() runtimeArguments, if possible - 'factory' is - // the container, 'info' describes the ctor argument, and 'args' contains the args that - // were passed to GetInstance() - use first arg if it is the right type, - // - // for ... - //container.RegisterConstructorDependency((factory, info, args) => - //{ - // if (info.Member.DeclaringType != typeof(EntityContainerRepository)) return default; - // return args.Length > 0 && args[0] is Guid guid ? guid : default; - //}); - - // register repositories - // repos depend on various things, - // some repositories have an annotated ctor parameter to pick the right cache helper - - // repositories - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - - // repositories that depend on a filesystem - // these have an annotated ctor parameter to pick the right file system - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - } - } -} diff --git a/src/Umbraco.Core/Composing/CompositionRoots/ServicesCompositionRoot.cs b/src/Umbraco.Core/Composing/CompositionRoots/ServicesCompositionRoot.cs deleted file mode 100644 index 92b8139a04..0000000000 --- a/src/Umbraco.Core/Composing/CompositionRoots/ServicesCompositionRoot.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using LightInject; -using Umbraco.Core.Cache; -using Umbraco.Core.Events; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Services; -using Umbraco.Core.Services.Implement; - -namespace Umbraco.Core.Composing.CompositionRoots -{ - public sealed class ServicesCompositionRoot : ICompositionRoot - { - public void Compose(IServiceRegistry container) - { - // register a transient messages factory, which will be replaced by the web - // boot manager when running in a web context - container.RegisterSingleton(); - - // register the service context - container.RegisterSingleton(); - - // register the special idk map - container.RegisterSingleton(); - - // register the services - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.Register(factory => - { - var mainLangFolder = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Umbraco + "/config/lang/")); - var appPlugins = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.AppPlugins)); - var configLangFolder = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Config + "/lang/")); - - var pluginLangFolders = appPlugins.Exists == false - ? Enumerable.Empty() - : appPlugins.GetDirectories() - .SelectMany(x => x.GetDirectories("Lang")) - .SelectMany(x => x.GetFiles("*.xml", SearchOption.TopDirectoryOnly)) - .Where(x => Path.GetFileNameWithoutExtension(x.FullName).Length == 5) - .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, false)); - - //user defined langs that overwrite the default, these should not be used by plugin creators - var userLangFolders = configLangFolder.Exists == false - ? Enumerable.Empty() - : configLangFolder - .GetFiles("*.user.xml", SearchOption.TopDirectoryOnly) - .Where(x => Path.GetFileNameWithoutExtension(x.FullName).Length == 10) - .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, true)); - - return new LocalizedTextServiceFileSources( - factory.GetInstance(), - factory.GetInstance().RuntimeCache, - mainLangFolder, - pluginLangFolders.Concat(userLangFolders)); - }); - container.RegisterSingleton(factory => new LocalizedTextService( - factory.GetInstance>(), - factory.GetInstance())); - - //TODO: These are replaced in the web project - we need to declare them so that - // something is wired up, just not sure this is very nice but will work for now. - container.RegisterSingleton(); - container.RegisterSingleton(); - } - } -} diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index e3bce53a3c..cf67409925 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -1,5 +1,4 @@ using System; -using LightInject; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; @@ -20,47 +19,61 @@ namespace Umbraco.Core.Composing /// Provides a static service locator for most singletons. /// /// - /// This class is initialized with the container via LightInjectExtensions.ConfigureUmbracoCore, + /// This class is initialized with the container in UmbracoApplicationBase, /// right after the container is created in UmbracoApplicationBase.HandleApplicationStart. /// Obviously, this is a service locator, which some may consider an anti-pattern. And yet, /// practically, it works. /// public static class Current { - private static IServiceContainer _container; + private static IFactory _factory; + + // fixme - refactor + // we don't want Umbraco tests to die because the container has not been properly initialized, + // for some too-important things such as IShortStringHelper or loggers, so if it's not + // registered we setup a default one. We should really refactor our tests so that it does + // not happen. private static IShortStringHelper _shortStringHelper; private static ILogger _logger; private static IProfiler _profiler; - private static ProfilingLogger _profilingLogger; + private static IProfilingLogger _profilingLogger; private static IPublishedValueFallback _publishedValueFallback; + private static Configs _configs; /// - /// Gets or sets the DI container. + /// Gets or sets the factory. /// - internal static IServiceContainer Container + public static IFactory Factory { get { - if (_container == null) throw new Exception("No container has been set."); - return _container; + if (_factory == null) throw new InvalidOperationException("No factory has been set."); + return _factory; } set { - if (_container != null) throw new Exception("A container has already been set."); - _container = value; + if (_factory != null) throw new InvalidOperationException("A factory has already been set."); + if (_configs != null) throw new InvalidOperationException("Configs are unlocked."); + _factory = value; } } - internal static bool HasContainer => _container != null; + internal static bool HasFactory => _factory != null; - // for UNIT TESTS exclusively! - // resets *everything* that is 'current' - internal static void Reset() + /// + /// Resets . Indented for testing only, and not supported in production code. + /// + /// + /// For UNIT TESTS exclusively. + /// Resets everything that is 'current'. + /// + public static void Reset() { - _container?.Dispose(); - _container = null; + _factory.DisposeIfDisposable(); + _factory = null; + _configs = null; _shortStringHelper = null; _logger = null; _profiler = null; @@ -70,97 +83,119 @@ namespace Umbraco.Core.Composing Resetted?.Invoke(null, EventArgs.Empty); } + /// + /// Unlocks . Intended for testing only, and not supported in production code. + /// + /// + /// For UNIT TESTS exclusively. + /// Unlocks so that it is possible to add configurations + /// directly to without having to wire composition. + /// + public static void UnlockConfigs() + { + if (_factory != null) + throw new InvalidOperationException("Cannot unlock configs when a factory has been set."); + _configs = new Configs(); + } + internal static event EventHandler Resetted; #region Getters - // fixme - refactor - // we don't want Umbraco to die because the container has not been properly initialized, - // for some too-important things such as IShortStringHelper or loggers, so if it's not - // registered we setup a default one. We should really refactor our tests so that it does - // not happen. Will do when we get rid of IShortStringHelper. - public static IShortStringHelper ShortStringHelper - => _shortStringHelper ?? (_shortStringHelper = _container?.TryGetInstance() - ?? new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(UmbracoConfig.For.UmbracoSettings()))); + => _shortStringHelper ?? (_shortStringHelper = _factory?.TryGetInstance() + ?? new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(Configs.Settings()))); public static ILogger Logger - => _logger ?? (_logger = _container?.TryGetInstance() - ?? new DebugDiagnosticsLogger()); + => _logger ?? (_logger = _factory?.TryGetInstance() + ?? new DebugDiagnosticsLogger()); public static IProfiler Profiler - => _profiler ?? (_profiler = _container?.TryGetInstance() - ?? new LogProfiler(Logger)); + => _profiler ?? (_profiler = _factory?.TryGetInstance() + ?? new LogProfiler(Logger)); - public static ProfilingLogger ProfilingLogger - => _profilingLogger ?? (_profilingLogger = _container?.TryGetInstance()) + public static IProfilingLogger ProfilingLogger + => _profilingLogger ?? (_profilingLogger = _factory?.TryGetInstance()) ?? new ProfilingLogger(Logger, Profiler); public static IRuntimeState RuntimeState - => Container.GetInstance(); + => Factory.GetInstance(); public static TypeLoader TypeLoader - => Container.GetInstance(); + => Factory.GetInstance(); - public static FileSystems FileSystems - => Container.GetInstance(); + public static Configs Configs + { + get + { + if (_configs != null) return _configs; + if (_factory == null) throw new InvalidOperationException("Can not get Current.Config during composition. Use composition.Config."); + return _factory.GetInstance(); + } + } + + public static IFileSystems FileSystems + => Factory.GetInstance(); + + public static IMediaFileSystem MediaFileSystem + => Factory.GetInstance(); public static UrlSegmentProviderCollection UrlSegmentProviders - => Container.GetInstance(); + => Factory.GetInstance(); public static CacheRefresherCollection CacheRefreshers - => Container.GetInstance(); + => Factory.GetInstance(); public static DataEditorCollection DataEditors - => Container.GetInstance(); + => Factory.GetInstance(); public static PropertyEditorCollection PropertyEditors - => Container.GetInstance(); + => Factory.GetInstance(); public static ParameterEditorCollection ParameterEditors - => Container.GetInstance(); + => Factory.GetInstance(); internal static ManifestValueValidatorCollection ManifestValidators - => Container.GetInstance(); + => Factory.GetInstance(); internal static PackageActionCollection PackageActions - => Container.GetInstance(); + => Factory.GetInstance(); internal static PropertyValueConverterCollection PropertyValueConverters - => Container.GetInstance(); + => Factory.GetInstance(); internal static IPublishedModelFactory PublishedModelFactory - => Container.GetInstance(); + => Factory.GetInstance(); public static IServerMessenger ServerMessenger - => Container.GetInstance(); + => Factory.GetInstance(); public static IServerRegistrar ServerRegistrar - => Container.GetInstance(); + => Factory.GetInstance(); public static ICultureDictionaryFactory CultureDictionaryFactory - => Container.GetInstance(); + => Factory.GetInstance(); public static CacheHelper ApplicationCache - => Container.GetInstance(); + => Factory.GetInstance(); public static ServiceContext Services - => Container.GetInstance(); + => Factory.GetInstance(); public static IScopeProvider ScopeProvider - => Container.GetInstance(); + => Factory.GetInstance(); public static ISqlContext SqlContext - => Container.GetInstance(); + => Factory.GetInstance(); public static IPublishedContentTypeFactory PublishedContentTypeFactory - => Container.GetInstance(); + => Factory.GetInstance(); public static IPublishedValueFallback PublishedValueFallback - => _publishedValueFallback ?? Container.GetInstance() ?? new NoopPublishedValueFallback(); + => _publishedValueFallback ?? Factory.GetInstance() ?? new NoopPublishedValueFallback(); public static IVariationContextAccessor VariationContextAccessor - => Container.GetInstance(); + => Factory.GetInstance(); #endregion } diff --git a/src/Umbraco.Core/Composing/FactoryExtensions.cs b/src/Umbraco.Core/Composing/FactoryExtensions.cs new file mode 100644 index 0000000000..8027f2c7a1 --- /dev/null +++ b/src/Umbraco.Core/Composing/FactoryExtensions.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Umbraco.Core.Composing +{ + /// + /// Provides extension methods to the class. + /// + public static class FactoryExtensions + { + /// + /// Gets an instance of a service. + /// + /// The type of the service. + /// The factory. + /// An instance of the specified type. + /// Throws an exception if the factory failed to get an instance of the specified type. + public static T GetInstance(this IFactory factory) + where T : class + => (T)factory.GetInstance(typeof(T)); + + /// + /// Tries to get an instance of a service. + /// + /// The type of the service. + /// An instance of the specified type, or null. + /// Returns null if the factory does not know how to get an instance + /// of the specified type. Throws an exception if the factory does know how + /// to get an instance of the specified type, but failed to do so. + public static T TryGetInstance(this IFactory factory) + where T : class + => (T)factory.TryGetInstance(typeof(T)); + + /// + /// Creates an instance with arguments. + /// + /// The type of the instance. + /// The factory. + /// Arguments. + /// An instance of the specified type. + /// + /// Throws an exception if the factory failed to get an instance of the specified type. + /// The arguments are used as dependencies by the factory. + /// + public static T CreateInstance(this IFactory factory, params object[] args) + where T : class + => (T)factory.CreateInstance(typeof(T), args); + + /// + /// Creates an instance of a service, with arguments. + /// + /// + /// The type of the instance. + /// Named arguments. + /// An instance of the specified type. + /// + /// The instance type does not need to be registered into the factory. + /// The arguments are used as dependencies by the factory. Other dependencies + /// are retrieved from the factory. + /// + public static object CreateInstance(this IFactory factory, Type type, params object[] args) + { + // LightInject has this, but then it requires RegisterConstructorDependency etc and has various oddities + // including the most annoying one, which is that it does not work on singletons (hard to fix) + //return factory.GetInstance(type, args); + + // this method is essentially used to build singleton instances, so it is assumed that it would be + // more expensive to build and cache a dynamic method ctor than to simply invoke the ctor, as we do + // here - this can be discussed + + // TODO: we currently try the ctor with most parameters, but we could want to fall back to others + + var ctor = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public).OrderByDescending(x => x.GetParameters().Length).FirstOrDefault(); + if (ctor == null) throw new InvalidOperationException($"Could not find a public constructor for type {type.FullName}."); + + var ctorParameters = ctor.GetParameters(); + var ctorArgs = new object[ctorParameters.Length]; + var i = 0; + foreach (var parameter in ctorParameters) + { + // no! IsInstanceOfType is not ok here + // ReSharper disable once UseMethodIsInstanceOfType + var arg = args?.FirstOrDefault(a => parameter.ParameterType.IsAssignableFrom(a.GetType())); + ctorArgs[i++] = arg ?? factory.GetInstance(parameter.ParameterType); + } + return ctor.Invoke(ctorArgs); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Composing/ICollectionBuilder.cs b/src/Umbraco.Core/Composing/ICollectionBuilder.cs index 5efc03c9ac..84ff3ba747 100644 --- a/src/Umbraco.Core/Composing/ICollectionBuilder.cs +++ b/src/Umbraco.Core/Composing/ICollectionBuilder.cs @@ -1,11 +1,23 @@ namespace Umbraco.Core.Composing { + /// + /// Represents a collection builder. + /// + public interface ICollectionBuilder + { + /// + /// Registers the builder so it can build the collection, by + /// registering the collection and the types. + /// + void RegisterWith(IRegister register); + } + /// /// Represents a collection builder. /// /// The type of the collection. /// The type of the items. - public interface ICollectionBuilder + public interface ICollectionBuilder : ICollectionBuilder where TCollection : IBuilderCollection { /// @@ -13,6 +25,6 @@ /// /// A collection. /// Creates a new collection each time it is invoked. - TCollection CreateCollection(); + TCollection CreateCollection(IFactory factory); } } diff --git a/src/Umbraco.Core/Composing/IFactory.cs b/src/Umbraco.Core/Composing/IFactory.cs new file mode 100644 index 0000000000..768b9207a3 --- /dev/null +++ b/src/Umbraco.Core/Composing/IFactory.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Composing +{ + /// + /// Defines a service factory for Umbraco. + /// + public interface IFactory + { + /// + /// Gets the concrete factory. + /// + object Concrete { get; } + + /// + /// Gets an instance of a service. + /// + /// The type of the service. + /// An instance of the specified type. + /// Throws an exception if the container failed to get an instance of the specified type. + object GetInstance(Type type); + + /// + /// Gets a targeted instance of a service. + /// + /// The type of the service. + /// The type of the target. + /// The instance of the specified type for the specified target. + /// Throws an exception if the container failed to get an instance of the specified type. + TService GetInstanceFor(); + + /// + /// Tries to get an instance of a service. + /// + /// The type of the service. + /// An instance of the specified type, or null. + /// Returns null if the container does not know how to get an instance + /// of the specified type. Throws an exception if the container does know how + /// to get an instance of the specified type, but failed to do so. + object TryGetInstance(Type type); + + /// + /// Gets all instances of a service. + /// + /// The type of the service. + IEnumerable GetAllInstances(Type serviceType); + + /// + /// Gets all instances of a service. + /// + /// The type of the service. + IEnumerable GetAllInstances() + where TService : class; + + /// + /// Releases an instance. + /// + /// The instance. + /// + /// See https://stackoverflow.com/questions/14072208 and http://kozmic.net/2010/08/27/must-i-release-everything-when-using-windsor/, + /// you only need to release instances you specifically resolved, and even then, if done right, that might never be needed. For + /// instance, LightInject does not require this and does not support it - should work with scopes. + /// + void Release(object instance); + + /// + /// Begins a scope. + /// + /// + /// When the scope is disposed, scoped instances that have been created during the scope are disposed. + /// Scopes can be nested. Each instance is disposed individually. + /// + IDisposable BeginScope(); + + /// + /// Enables per-request scope. + /// + /// + /// Ties scopes to web requests. + /// + void EnablePerWebRequestScope(); + } +} diff --git a/src/Umbraco.Core/Composing/IRegister.cs b/src/Umbraco.Core/Composing/IRegister.cs new file mode 100644 index 0000000000..cbf12f54a3 --- /dev/null +++ b/src/Umbraco.Core/Composing/IRegister.cs @@ -0,0 +1,106 @@ +using System; + +namespace Umbraco.Core.Composing +{ + /// + /// Defines a service register for Umbraco. + /// + public interface IRegister + { + /// + /// Gets the concrete container. + /// + object Concrete { get; } + + /// + /// Registers a service as its own implementation. + /// + void Register(Type serviceType, Lifetime lifetime = Lifetime.Transient); + + /// + /// Registers a service with an implementation type. + /// + void Register(Type serviceType, Type implementingType, Lifetime lifetime = Lifetime.Transient); + + /// + /// Registers a service with an implementation factory. + /// + void Register(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class; + + /// + /// Registers a service with an implementing instance. + /// + void Register(Type serviceType, object instance); + + /// + /// Registers a service for a target, as its own implementation. + /// + /// + /// There can only be one implementation or instanced registered for a service and target; + /// what happens if many are registered is not specified. + /// + void RegisterFor(Lifetime lifetime = Lifetime.Transient) + where TService : class; + + /// + /// Registers a service for a target, with an implementation type. + /// + /// + /// There can only be one implementation or instanced registered for a service and target; + /// what happens if many are registered is not specified. + /// + void RegisterFor(Type implementingType, Lifetime lifetime = Lifetime.Transient) + where TService : class; + + /// + /// Registers a service for a target, with an implementation factory. + /// + /// + /// There can only be one implementation or instanced registered for a service and target; + /// what happens if many are registered is not specified. + /// + void RegisterFor(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class; + + /// + /// Registers a service for a target, with an implementing instance. + /// + /// + /// There can only be one implementation or instanced registered for a service and target; + /// what happens if many are registered is not specified. + /// + void RegisterFor(TService instance) + where TService : class; + + /// + /// Registers a base type for auto-registration. + /// + /// + /// Auto-registration means that anytime the container is asked to create an instance + /// of a type deriving from , it will first register that + /// type automatically. + /// This can be used for instance for views or controllers. Then, one just needs to + /// register a common base class or interface, and the container knows how to create instances. + /// + void RegisterAuto(Type serviceBaseType); + + #region Control + + /// + /// Configures the container for web support. + /// + /// + /// Enables support for MVC, WebAPI, but *not* per-request scope. This is used early in the boot + /// process, where anything "scoped" should not be linked to a web request. + /// + void ConfigureForWeb(); + + /// + /// Creates the factory. + /// + IFactory CreateFactory(); + + #endregion + } +} diff --git a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs index 52e5a764fd..46b06daf7d 100644 --- a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using LightInject; namespace Umbraco.Core.Composing { @@ -13,33 +12,24 @@ namespace Umbraco.Core.Composing /// The type of the items. public abstract class LazyCollectionBuilderBase : CollectionBuilderBase where TBuilder : LazyCollectionBuilderBase - where TCollection : IBuilderCollection + where TCollection : class, IBuilderCollection { - private readonly List>> _producers1 = new List>>(); - private readonly List>> _producers2 = new List>>(); + private readonly List>> _producers = new List>>(); private readonly List _excluded = new List(); - /// - /// Initializes a new instance of the class. - /// - protected LazyCollectionBuilderBase(IServiceContainer container) - : base(container) - { } - protected abstract TBuilder This { get; } /// /// Clears all types in the collection. /// - /// The buidler. + /// The builder. public TBuilder Clear() { Configure(types => { types.Clear(); - _producers1.Clear(); - _producers2.Clear(); - _excluded.Clear(); + _producers.Clear(); + _excluded.Clear(); }); return This; } @@ -75,37 +65,6 @@ namespace Umbraco.Core.Composing return This; } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove() - where T : TItem - { - Configure(types => - { - var type = typeof(T); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } - - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove(Type type) - { - Configure(types => - { - EnsureType(type, "remove"); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } - /// /// Adds a types producer to the collection. /// @@ -115,21 +74,7 @@ namespace Umbraco.Core.Composing { Configure(types => { - _producers1.Add(producer); - }); - return This; - } - - /// - /// Adds a types producer to the collection. - /// - /// The types producer. - /// The builder. - public TBuilder Add(Func> producer) - { - Configure(types => - { - _producers2.Add(producer); + _producers.Add(producer); }); return This; } @@ -168,8 +113,7 @@ namespace Umbraco.Core.Composing protected override IEnumerable GetRegisteringTypes(IEnumerable types) { return types - .Union(_producers1.SelectMany(x => x())) - .Union(_producers2.SelectMany(x => x(Container))) + .Union(_producers.SelectMany(x => x())) .Distinct() .Select(x => EnsureType(x, "register")) .Except(_excluded); diff --git a/src/Umbraco.Core/Composing/Lifetime.cs b/src/Umbraco.Core/Composing/Lifetime.cs new file mode 100644 index 0000000000..e1b9950c39 --- /dev/null +++ b/src/Umbraco.Core/Composing/Lifetime.cs @@ -0,0 +1,49 @@ +namespace Umbraco.Core.Composing +{ + /// + /// Specifies the lifetime of a registered instance. + /// + public enum Lifetime + { + /// + /// Always get a new instance. + /// + /// Corresponds to Transient in LightInject, Castle Windsor + /// or MS.DI, PerDependency in Autofac. + Transient, + + /// + /// One unique instance per request. + /// + // fixme - not what you think! + // currently, corresponds to 'Request' in LightInject which is 'Transient + disposed by Scope' + // but NOT (in LightInject) a per-web-request lifetime, more a TransientScoped + // + // we use it for controllers, httpContextBase and umbracoContext + // - so that they are automatically disposed at the end of the scope (ie request) + // - not sure they should not be simply 'scoped'? + // + // Castle has an extra PerWebRequest something, and others use scope + // what about Request before first request ie during application startup? + // see http://blog.ploeh.dk/2009/11/17/UsingCastleWindsor'sPerWebRequestlifestylewithASP.NETMVConIIS7/ + // Castle ends up requiring a special scope manager too + // see https://groups.google.com/forum/#!topic/castle-project-users/1E2W9LVIYR4 + // + // but maybe also - why are we requiring scoped services at startup? + Request, + + /// + /// One unique instance per container scope. + /// + /// Corresponds to Scope in LightInject, Scoped in MS.DI + /// or Castle Windsor, PerLifetimeScope in Autofac. + Scope, + + /// + /// One unique instance per container. + /// + /// Corresponds to Singleton in LightInject, Castle Windsor + /// or MS.DI and to SingleInstance in Autofac. + Singleton + } +} diff --git a/src/Umbraco.Core/Composing/LightInject/LightInjectContainer.cs b/src/Umbraco.Core/Composing/LightInject/LightInjectContainer.cs new file mode 100644 index 0000000000..d8a554ee8c --- /dev/null +++ b/src/Umbraco.Core/Composing/LightInject/LightInjectContainer.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using LightInject; + +namespace Umbraco.Core.Composing.LightInject +{ + /// + /// Implements DI with LightInject. + /// + public class LightInjectContainer : IRegister, IFactory, IDisposable + { + private int _disposed; + + /// + /// Initializes a new instance of the with a LightInject container. + /// + protected LightInjectContainer(ServiceContainer container) + { + Container = container; + } + + /// + /// Creates a new instance of the class. + /// + public static LightInjectContainer Create() + => new LightInjectContainer(CreateServiceContainer()); + + /// + /// Creates a new instance of the LightInject service container. + /// + protected static ServiceContainer CreateServiceContainer() + { + var container = new ServiceContainer(new ContainerOptions { EnablePropertyInjection = false }); + + // note: the block below is disabled, as it is too LightInject-specific + // + // supports annotated constructor injections + // eg to specify the service name on some services + //container.EnableAnnotatedConstructorInjection(); + + // note: the block below is disabled, we do not allow property injection at all anymore + // (see options in CreateServiceContainer) + // + // from the docs: "LightInject considers all read/write properties a dependency, but implements + // a loose strategy around property dependencies, meaning that it will NOT throw an exception + // in the case of an unresolved property dependency." + // + // in Umbraco we do NOT want to do property injection by default, so we have to disable it. + // from the docs, the following line will cause the container to "now only try to inject + // dependencies for properties that is annotated with the InjectAttribute." + // + // could not find it documented, but tests & code review shows that LightInject considers a + // property to be "injectable" when its setter exists and is not static, nor private, nor + // it is an index property. which means that eg protected or internal setters are OK. + //Container.EnableAnnotatedPropertyInjection(); + + // ensure that we do *not* scan assemblies + // we explicitly RegisterFrom our own composition roots and don't want them scanned + container.AssemblyScanner = new AssemblyScanner(/*container.AssemblyScanner*/); + + // see notes in MixedLightInjectScopeManagerProvider + container.ScopeManagerProvider = new MixedLightInjectScopeManagerProvider(); + + // note: the block below is disabled, because it does not work, because collection builders + // are singletons, and constructor dependencies don't work on singletons, see + // https://github.com/seesharper/LightInject/issues/294 + // + // if looking for a IContainer, and one was passed in args, use it + // this is for collection builders which require the IContainer + //container.RegisterConstructorDependency((c, i, a) => a.OfType().FirstOrDefault()); + // + // and, the block below is also disabled, because it is ugly + // + //// which means that the only way to inject the container into builders is to register it + //container.RegisterInstance(this); + // + // instead, we use an explicit GetInstance with arguments implementation + + return container; + } + + /// + /// Gets the LightInject container. + /// + protected ServiceContainer Container { get; } + + /// + /// + public object Concrete => Container; + + /// + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) == 1) + return; + + Container.Dispose(); + } + + /// + public IFactory CreateFactory() => this; + + private static string GetTargetedServiceName() => "TARGET:" + typeof(TTarget).FullName; + + #region Factory + + /// + public object GetInstance(Type type) + => Container.GetInstance(type); + + /// + public TService GetInstanceFor() + => Container.GetInstance(GetTargetedServiceName()); + + /// + public object TryGetInstance(Type type) + => Container.TryGetInstance(type); + + /// + public IEnumerable GetAllInstances() + where T : class + => Container.GetAllInstances(); + + /// + public IEnumerable GetAllInstances(Type type) + => Container.GetAllInstances(type); + + /// + public void Release(object instance) + { + // nothing to release with LightInject + } + + // notes: + // we may want to look into MS code, eg: + // TypeActivatorCache in MVC at https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Core/Internal/TypeActivatorCache.cs + // which relies onto + // ActivatorUtilities at https://github.com/aspnet/DependencyInjection/blob/master/shared/Microsoft.Extensions.ActivatorUtilities.Sources/ActivatorUtilities.cs + + #endregion + + #region Registry + + /// + public void Register(Type serviceType, Lifetime lifetime = Lifetime.Transient) + => Container.Register(serviceType, GetLifetime(lifetime)); + + /// + public void Register(Type serviceType, Type implementingType, Lifetime lifetime = Lifetime.Transient) + { + switch (lifetime) + { + case Lifetime.Transient: + Container.Register(serviceType, implementingType, implementingType.Name); + break; + case Lifetime.Request: + case Lifetime.Scope: + case Lifetime.Singleton: + Container.Register(serviceType, implementingType, GetLifetime(lifetime)); + break; + default: + throw new NotSupportedException($"Lifetime {lifetime} is not supported."); + } + } + + /// + public void Register(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class + { + Container.Register(f => factory(this), GetLifetime(lifetime)); + } + + /// + public void Register(Type serviceType, object instance) + => Container.RegisterInstance(serviceType, instance); + + /// + public void RegisterFor(Lifetime lifetime = Lifetime.Transient) + where TService : class + => RegisterFor(typeof(TService), lifetime); + + /// + public void RegisterFor(Type implementingType, Lifetime lifetime = Lifetime.Transient) + where TService : class + { + // note that there can only be one implementation or instance registered "for" a service + Container.Register(typeof(TService), implementingType, GetTargetedServiceName(), GetLifetime(lifetime)); + } + + /// + public void RegisterFor(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class + { + // note that there can only be one implementation or instance registered "for" a service + Container.Register(f => factory(this), GetTargetedServiceName(), GetLifetime(lifetime)); + } + + /// + public void RegisterFor(TService instance) + where TService : class + => Container.RegisterInstance(typeof(TService), instance, GetTargetedServiceName()); + + private ILifetime GetLifetime(Lifetime lifetime) + { + switch (lifetime) + { + case Lifetime.Transient: + return null; + case Lifetime.Request: + return new PerRequestLifeTime(); + case Lifetime.Scope: + return new PerScopeLifetime(); + case Lifetime.Singleton: + return new PerContainerLifetime(); + default: + throw new NotSupportedException($"Lifetime {lifetime} is not supported."); + } + } + + /// + public void RegisterAuto(Type serviceBaseType) + { + Container.RegisterFallback((serviceType, serviceName) => + { + // https://github.com/seesharper/LightInject/issues/173 + if (serviceBaseType.IsAssignableFromGtd(serviceType)) + Container.Register(serviceType); + return false; + }, null); + } + + #endregion + + #region Control + + /// + public IDisposable BeginScope() + => Container.BeginScope(); + + /// + public virtual void ConfigureForWeb() + { } + + /// + public void EnablePerWebRequestScope() + { + if (!(Container.ScopeManagerProvider is MixedLightInjectScopeManagerProvider smp)) + throw new Exception("Container.ScopeManagerProvider is not MixedLightInjectScopeManagerProvider."); + smp.EnablePerWebRequestScope(); + } + + private class AssemblyScanner : IAssemblyScanner + { + public void Scan(Assembly assembly, IServiceRegistry serviceRegistry, Func lifetime, Func shouldRegister, Func serviceNameProvider) + { + // nothing - we don't want LightInject to scan + } + + public void Scan(Assembly assembly, IServiceRegistry serviceRegistry) + { + // nothing - we don't want LightInject to scan + } + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Exceptions/LightInjectException.cs b/src/Umbraco.Core/Composing/LightInject/LightInjectException.cs similarity index 96% rename from src/Umbraco.Core/Exceptions/LightInjectException.cs rename to src/Umbraco.Core/Composing/LightInject/LightInjectException.cs index 03fd9f2f9f..fa0aed21ca 100644 --- a/src/Umbraco.Core/Exceptions/LightInjectException.cs +++ b/src/Umbraco.Core/Composing/LightInject/LightInjectException.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -namespace Umbraco.Core.Exceptions +namespace Umbraco.Core.Composing.LightInject { /// /// Represents errors that occur due to LightInject. diff --git a/src/Umbraco.Core/Composing/MixedLightInjectScopeManagerProvider.cs b/src/Umbraco.Core/Composing/LightInject/MixedLightInjectScopeManagerProvider.cs similarity index 88% rename from src/Umbraco.Core/Composing/MixedLightInjectScopeManagerProvider.cs rename to src/Umbraco.Core/Composing/LightInject/MixedLightInjectScopeManagerProvider.cs index 05bdbc446d..897c58dd43 100644 --- a/src/Umbraco.Core/Composing/MixedLightInjectScopeManagerProvider.cs +++ b/src/Umbraco.Core/Composing/LightInject/MixedLightInjectScopeManagerProvider.cs @@ -1,7 +1,7 @@ using LightInject; using LightInject.Web; -namespace Umbraco.Core.Composing +namespace Umbraco.Core.Composing.LightInject { // by default, the container's scope manager provider is PerThreadScopeManagerProvider, // and then container.EnablePerWebRequestScope() replaces it with PerWebRequestScopeManagerProvider, @@ -13,6 +13,9 @@ namespace Umbraco.Core.Composing // of PerWebRequestScopeManagerProvider - but all delegates see is the mixed one - and therefore // they can transition without issues. // + // The PerWebRequestScopeManager maintains the scope in HttpContext and LightInject registers a + // module (PreApplicationStartMethod) which disposes it on EndRequest + // // the mixed provider is installed in container.ConfigureUmbracoCore() and then, // when doing eg container.EnableMvc() or anything that does container.EnablePerWebRequestScope() // we need to take great care to preserve the mixed scope manager provider! diff --git a/src/Umbraco.Core/Composing/LightInjectExtensions.cs b/src/Umbraco.Core/Composing/LightInjectExtensions.cs deleted file mode 100644 index 68ba48c803..0000000000 --- a/src/Umbraco.Core/Composing/LightInjectExtensions.cs +++ /dev/null @@ -1,392 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using LightInject; -using Umbraco.Core.Exceptions; - -namespace Umbraco.Core.Composing -{ - /// - /// Provides extensions to LightInject. - /// - public static class LightInjectExtensions - { - /// - /// Configure the container for Umbraco Core usage and assign to Current. - /// - /// The container. - /// The container is now the unique application container and is now accessible via Current.Container. - internal static void ConfigureUmbracoCore(this ServiceContainer container) - { - // supports annotated constructor injections - // eg to specify the service name on some services - container.EnableAnnotatedConstructorInjection(); - - // from the docs: "LightInject considers all read/write properties a dependency, but implements - // a loose strategy around property dependencies, meaning that it will NOT throw an exception - // in the case of an unresolved property dependency." - // - // in Umbraco we do NOT want to do property injection by default, so we have to disable it. - // from the docs, the following line will cause the container to "now only try to inject - // dependencies for properties that is annotated with the InjectAttribute." - // - // could not find it documented, but tests & code review shows that LightInject considers a - // property to be "injectable" when its setter exists and is not static, nor private, nor - // it is an index property. which means that eg protected or internal setters are OK. - container.EnableAnnotatedPropertyInjection(); - - // ensure that we do *not* scan assemblies - // we explicitely RegisterFrom our own composition roots and don't want them scanned - container.AssemblyScanner = new AssemblyScanner(/*container.AssemblyScanner*/); - - // see notes in MixedLightInjectScopeManagerProvider - container.ScopeManagerProvider = new MixedLightInjectScopeManagerProvider(); - - // self-register - container.Register(_ => container); - - // configure the current container - Current.Container = container; - } - - private class AssemblyScanner : IAssemblyScanner - { - //private readonly IAssemblyScanner _scanner; - - //public AssemblyScanner(IAssemblyScanner scanner) - //{ - // _scanner = scanner; - //} - - public void Scan(Assembly assembly, IServiceRegistry serviceRegistry, Func lifetime, Func shouldRegister, Func serviceNameProvider) - { - // nothing - we *could* scan non-Umbraco assemblies, though - } - - public void Scan(Assembly assembly, IServiceRegistry serviceRegistry) - { - // nothing - we *could* scan non-Umbraco assemblies, though - } - } - - /// - /// Registers a service implementation with a specified lifetime. - /// - /// The type of the service. - /// The type of the implementation. - /// The type of the lifetime. - /// The container. - public static void Register(this IServiceContainer container) - where TImplementation : TService - where TLifetime : ILifetime, new() - { - container.Register(new TLifetime()); - } - - /// - /// Registers a service implementation with a specified lifetime. - /// - /// The type of the service. - /// The type of the lifetime. - /// The container. - /// A factory. - public static void Register(this IServiceContainer container, Func factory) - where TLifetime : ILifetime, new() - { - container.Register(factory, new TLifetime()); - } - - /// - /// Registers several service implementations with a specified lifetime. - /// - /// The type of the service. - /// The type of the lifetime. - /// The container. - /// The types of the implementations. - public static void RegisterMany(this IServiceContainer container, IEnumerable implementations) - where TLifeTime : ILifetime, new() - { - foreach (var implementation in implementations) - { - // if "typeof (TService)" is there then "implementation.FullName" MUST be there too - container.Register(typeof(TService), implementation, implementation.FullName, new TLifeTime()); - } - } - - /// - /// Registers the TService with the factory that describes the dependencies of the service, as a singleton. - /// - public static void RegisterSingleton(this IServiceRegistry container, Func factory, string serviceName) - { - var registration = container.GetAvailableService(serviceName); - if (registration == null) - { - container.Register(factory, serviceName, new PerContainerLifetime()); - } - else - { - if (registration.Lifetime is PerContainerLifetime == false) - throw new InvalidOperationException("Existing registration lifetime is not PerContainer."); - UpdateRegistration(registration, null, factory); - } - } - - /// - /// Registers the TService with the TImplementation as a singleton. - /// - public static void RegisterSingleton(this IServiceRegistry container) - where TImplementation : TService - { - var registration = container.GetAvailableService(); - - if (registration == null) - { - container.Register(new PerContainerLifetime()); - } - else - { - if (registration.Lifetime is PerContainerLifetime == false) - throw new InvalidOperationException("Existing registration lifetime is not PerContainer."); - UpdateRegistration(registration, typeof(TImplementation), null); - } - } - - /// - /// Registers a concrete type as a singleton service. - /// - public static void RegisterSingleton(this IServiceRegistry container) - { - var registration = container.GetAvailableService(); - if (registration == null) - { - container.Register(new PerContainerLifetime()); - } - else - { - if (registration.Lifetime is PerContainerLifetime == false) - throw new InvalidOperationException("Existing registration lifetime is not PerContainer."); - UpdateRegistration(registration, typeof(TImplementation), null); - } - - } - - /// - /// Registers the TService with the factory that describes the dependencies of the service, as a singleton. - /// - /// - /// - /// - public static void RegisterSingleton(this IServiceRegistry container, Func factory) - { - var registration = container.GetAvailableService(); - if (registration == null) - container.Register(factory, new PerContainerLifetime()); - else - UpdateRegistration(registration, null, factory); - } - - // note - what's below ALSO applies to non-singleton ie transient services - // - // see https://github.com/seesharper/LightInject/issues/133 - // - // note: we *could* use tracking lifetimes for singletons to ensure they have not been resolved - // already but that would not work for transient as the transient lifetime is null (and that is - // optimized in LightInject) - // - // also, RegisterSingleton above is dangerous because ppl could still use Register with a - // PerContainerLifetime and it will not work + the default Register would not work either for other - // lifetimes - // - // all in all, not sure we want to let ppl have direct access to the container - // we might instead want to expose some methods in UmbracoComponentBase or whatever? - - /// - /// Updates a registration. - /// - private static void UpdateRegistration(Registration registration, Type implementingType, Delegate factoryExpression) - { - // if the container has compiled already then the registrations have been captured, - // and re-registering - although updating available services - does not modify the - // output of GetInstance - // - // so we have to rely on different methods - // - // assuming the service has NOT been resolved, both methods below work, but the first - // one looks simpler. it would be good to check whether the service HAS been resolved - // but I am not sure how to do it right now, depending on the lifetime - // - // if the service HAS been resolved then updating is probably a bad idea - - // not sure which is best? that one works, though, and looks simpler - registration.ImplementingType = implementingType; - registration.FactoryExpression = factoryExpression; - - //container.Override( - // r => r.ServiceType == typeof (TService) && (registration.ServiceName == null || r.ServiceName == registration.ServiceName), - // (f, r) => - // { - // r.ImplementingType = implementingType; - // r.FactoryExpression = factoryExpression; - // return r; - // }); - } - - /// - /// Gets the available service registrations for a service type. - /// - /// The service type. - /// The container. - /// The service registrations for the service type. - public static IEnumerable GetAvailableServices(this IServiceRegistry container) - { - var typeofTService = typeof(TService); - return container.AvailableServices.Where(x => x.ServiceType == typeofTService); - } - - /// - /// Gets the unique available service registration for a service type. - /// - /// The service type. - /// The container. - /// The unique service registration for the service type. - /// Can return null, but throws if more than one registration exist for the service type. - public static ServiceRegistration GetAvailableService(this IServiceRegistry container) - { - var typeofTService = typeof(TService); - return container.AvailableServices.SingleOrDefault(x => x.ServiceType == typeofTService); - } - - /// - /// Gets the unique available service registration for a service type and a name. - /// - /// The service type. - /// The container. - /// The name. - /// The unique service registration for the service type and the name. - /// Can return null, but throws if more than one registration exist for the service type and the name. - public static ServiceRegistration GetAvailableService(this IServiceRegistry container, string name) - { - var typeofTService = typeof(TService); - return container.AvailableServices.SingleOrDefault(x => x.ServiceType == typeofTService && x.ServiceName == name); - } - - /// - /// Gets an instance of a TService or throws a meaningful exception. - /// - /// The service type. - /// The container. - /// The instance. - public static TService GetInstanceOrThrow(this IServiceFactory factory) - { - if (factory == null) - throw new ArgumentNullException(nameof(factory)); - - try - { - return factory.GetInstance(); - } - catch (Exception e) - { - LightInjectException.TryThrow(e); - throw; - } - } - - /// - /// Gets an instance of a TService or throws a meaningful exception. - /// - /// The container. - /// The type of the service. - /// The name of the service. - /// The implementing type. - /// Arguments. - /// The instance. - internal static object GetInstanceOrThrow(this IServiceFactory factory, Type tService, string serviceName, Type implementingType, object[] args) - { - if (factory == null) - throw new ArgumentNullException(nameof(factory)); - - // fixme temp - STOP doing this, it confuses LightInject and then we get ugly exception traces - // we HAVE to let LightInject throw - and catch at THE OUTERMOST if InvalidOperationException in LightInject.Anything! - - return factory.GetInstance(tService, serviceName, args); - //try - //{ - // return factory.GetInstance(tService, serviceName, args); - //} - //catch (Exception e) - //{ - // LightInjectException.TryThrow(e, implementingType); - // throw; - //} - } - - /// - /// Registers a base type for auto-registration. - /// - /// The base type. - /// The container. - /// - /// Any type that inherits/implements the base type will be auto-registered on-demand. - /// This methods works with actual types. Use the other overload for eg generic definitions. - /// - public static void RegisterAuto(this IServiceContainer container) - { - container.RegisterFallback((serviceType, serviceName) => - { - //Current.Logger.Debug(typeof(LightInjectExtensions), $"Fallback for type {serviceType.FullName}."); - // https://github.com/seesharper/LightInject/issues/173 - - if (typeof(T).IsAssignableFrom(serviceType)) - container.Register(serviceType); - return false; - }, null); - } - - /// - /// Registers a base type for auto-registration. - /// - /// The container. - /// The base type. - /// - /// Any type that inherits/implements the base type will be auto-registered on-demand. - /// This methods works with actual types, as well as generic definitions eg typeof(MyBase{}). - /// - public static void RegisterAuto(this IServiceContainer container, Type type) - { - container.RegisterFallback((serviceType, serviceName) => - { - //Current.Logger.Debug(typeof(LightInjectExtensions), $"Fallback for type {serviceType.FullName}."); - // https://github.com/seesharper/LightInject/issues/173 - - if (type.IsAssignableFromGtd(serviceType)) - container.Register(serviceType); - return false; - }, null); - } - - /// - /// Registers and instanciates a collection builder. - /// - /// The type of the collection builder. - /// The container. - /// The collection builder. - public static TBuilder RegisterCollectionBuilder(this IServiceContainer container) - { - // make sure it's not already registered - // we just don't want to support re-registering collection builders - var registration = container.GetAvailableService(); - if (registration != null) - throw new InvalidOperationException("Collection builders should be registered only once."); - - // register the builder - per container - var builderLifetime = new PerContainerLifetime(); - container.Register(builderLifetime); - - // return the builder - // (also initializes the builder) - return container.GetInstance(); - } - } -} diff --git a/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs index 4811551cd2..241b84d8d2 100644 --- a/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using LightInject; namespace Umbraco.Core.Composing { @@ -12,22 +11,14 @@ namespace Umbraco.Core.Composing /// The type of the items. public abstract class OrderedCollectionBuilderBase : CollectionBuilderBase where TBuilder : OrderedCollectionBuilderBase - where TCollection : IBuilderCollection + where TCollection : class, IBuilderCollection { - /// - /// Initializes a new instance of the class. - /// - /// - protected OrderedCollectionBuilderBase(IServiceContainer container) - : base (container) - { } - protected abstract TBuilder This { get; } /// /// Clears all types in the collection. /// - /// The buidler. + /// The builder. public TBuilder Clear() { Configure(types => types.Clear()); @@ -87,26 +78,6 @@ namespace Umbraco.Core.Composing return This; } - /// - /// Appends types to the collections. - /// - /// The types to append. - /// The builder. - public TBuilder Append(Func> types) - { - Configure(list => - { - foreach (var type in types(Container)) - { - // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast - EnsureType(type, "register"); - if (list.Contains(type)) list.Remove(type); - list.Add(type); - } - }); - return This; - } - /// /// Appends a type after another type. /// diff --git a/src/Umbraco.Core/Composing/RegisterExtensions.cs b/src/Umbraco.Core/Composing/RegisterExtensions.cs new file mode 100644 index 0000000000..d1eacc0c0f --- /dev/null +++ b/src/Umbraco.Core/Composing/RegisterExtensions.cs @@ -0,0 +1,42 @@ +namespace Umbraco.Core.Composing +{ + /// + /// Provides extension methods to the class. + /// + public static class RegisterExtensions + { + /// + /// Registers a service with an implementation type. + /// + public static void Register(this IRegister register, Lifetime lifetime = Lifetime.Transient) + => register.Register(typeof(TService), typeof(TImplementing), lifetime); + + /// + /// Registers a service with an implementation type, for a target. + /// + public static void RegisterFor(this IRegister register, Lifetime lifetime = Lifetime.Transient) + where TService : class + => register.RegisterFor(typeof(TImplementing), lifetime); + + /// + /// Registers a service as its own implementation. + /// + public static void Register(this IRegister register, Lifetime lifetime = Lifetime.Transient) + where TService : class + => register.Register(typeof(TService), lifetime); + + /// + /// Registers a service with an implementing instance. + /// + public static void Register(this IRegister register, TService instance) + where TService : class + => register.Register(typeof(TService), instance); + + /// + /// Registers a base type for auto-registration. + /// + public static void RegisterAuto(this IRegister register) + where TServiceBase : class + => register.RegisterAuto(typeof(TServiceBase)); + } +} diff --git a/src/Umbraco.Core/Composing/RegisterFactory.cs b/src/Umbraco.Core/Composing/RegisterFactory.cs new file mode 100644 index 0000000000..8ee6e5a94c --- /dev/null +++ b/src/Umbraco.Core/Composing/RegisterFactory.cs @@ -0,0 +1,56 @@ +using System; +using System.Configuration; +using System.Reflection; + +namespace Umbraco.Core.Composing +{ + /// + /// Creates the container. + /// + public static class RegisterFactory + { + // cannot use typeof().AssemblyQualifiedName on the web container - we don't reference it + // a normal Umbraco site should run on the web container, but an app may run on the core one + private const string CoreLightInjectContainerTypeName = "Umbraco.Core.Composing.LightInject.LightInjectContainer,Umbraco.Core"; + private const string WebLightInjectContainerTypeName = "Umbraco.Web.Composing.LightInject.LightInjectContainer,Umbraco.Web"; + + /// + /// Creates a new instance of the configured container. + /// + /// + /// To override the default LightInjectContainer, add an appSetting named umbracoContainerType with + /// a fully qualified type name to a class with a static method "Create" returning an IRegister. + /// + public static IRegister Create() + { + Type type; + + var configuredTypeName = ConfigurationManager.AppSettings["umbracoRegisterType"]; + if (configuredTypeName.IsNullOrWhiteSpace()) + { + // try to get the web LightInject container type, + // else the core LightInject container type + type = Type.GetType(configuredTypeName = WebLightInjectContainerTypeName) ?? + Type.GetType(configuredTypeName = CoreLightInjectContainerTypeName); + } + else + { + // try to get the configured type + type = Type.GetType(configuredTypeName); + } + + if (type == null) + throw new Exception($"Cannot find register factory class '{configuredTypeName}'."); + + var factoryMethod = type.GetMethod("Create", BindingFlags.Public | BindingFlags.Static); + if (factoryMethod == null) + throw new Exception($"Register factory class '{configuredTypeName}' does not have a public static method named Create."); + + var container = factoryMethod.Invoke(null, Array.Empty()) as IRegister; + if (container == null) + throw new Exception($"Register factory '{configuredTypeName}' did not return an IRegister implementation."); + + return container; + } + } +} diff --git a/src/Umbraco.Core/Composing/TargetedServiceFactory.cs b/src/Umbraco.Core/Composing/TargetedServiceFactory.cs new file mode 100644 index 0000000000..53022c0043 --- /dev/null +++ b/src/Umbraco.Core/Composing/TargetedServiceFactory.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Composing +{ + /// + /// Provides a base class for targeted service factories. + /// + /// + public abstract class TargetedServiceFactory + { + private readonly IFactory _factory; + + protected TargetedServiceFactory(IFactory factory) + { + _factory = factory; + } + + public TService For() => _factory.GetInstanceFor(); + } +} diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index a42b84e0c5..86d3863994 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 @@ -616,6 +618,8 @@ namespace Umbraco.Core.Composing // doesn't actualy load in all assemblies, only the types that have been referenced so far. // However, in a web context, the BuildManager will have executed which will force all assemblies // to be loaded so it's fine for now. + // It could be fairly easy to parse the typeName to get the assembly name and then do Assembly.Load and + // find the type from there. //now try fall back procedures. type = Type.GetType(typeName); diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index 304638e017..3121e869c3 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Web; using System.Web.Compilation; using Umbraco.Core.Cache; +using Umbraco.Core.Collections; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -16,7 +17,7 @@ using File = System.IO.File; namespace Umbraco.Core.Composing { /// - /// Provides methods to find and instanciate types. + /// Provides methods to find and instantiate types. /// /// /// This class should be used to get all types, the class should never be used directly. @@ -29,30 +30,42 @@ namespace Umbraco.Core.Composing private const string CacheKey = "umbraco-types.list"; private readonly IRuntimeCacheProvider _runtimeCache; - private readonly IGlobalSettings _globalSettings; - private readonly ProfilingLogger _logger; + private readonly IProfilingLogger _logger; - private readonly object _typesLock = new object(); - private readonly Dictionary _types = new Dictionary(); + private readonly Dictionary _types = new Dictionary(); + private readonly object _locko = new object(); + private readonly object _timerLock = new object(); + private Timer _timer; + private bool _timing; private string _cachedAssembliesHash; private string _currentAssembliesHash; private IEnumerable _assemblies; private bool _reportedChange; - private static LocalTempStorage _localTempStorage = LocalTempStorage.Unknown; + private static LocalTempStorage _localTempStorage; private static string _fileBasePath; /// /// Initializes a new instance of the class. /// /// The application runtime cache. - /// + /// Files storage mode. + /// A profiling logger. + public TypeLoader(IRuntimeCacheProvider runtimeCache, LocalTempStorage localTempStorage, IProfilingLogger logger) + : this(runtimeCache, localTempStorage, logger, true) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The application runtime cache. + /// Files storage mode. /// A profiling logger. /// Whether to detect changes using hashes. - internal TypeLoader(IRuntimeCacheProvider runtimeCache, IGlobalSettings globalSettings, ProfilingLogger logger, bool detectChanges = true) + internal TypeLoader(IRuntimeCacheProvider runtimeCache, LocalTempStorage localTempStorage, IProfilingLogger logger, bool detectChanges) { _runtimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache)); - _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); + _localTempStorage = localTempStorage == LocalTempStorage.Unknown ? LocalTempStorage.Default : localTempStorage; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); if (detectChanges) @@ -67,8 +80,7 @@ namespace Umbraco.Core.Composing // rescanning of all types including lazy ones. // http://issues.umbraco.org/issue/U4-4789 var typesListFilePath = GetTypesListFilePath(); - if (File.Exists(typesListFilePath)) - File.Delete(typesListFilePath); + DeleteFile(typesListFilePath, FileDeleteTimeout); WriteCacheTypesHash(); } @@ -79,14 +91,20 @@ namespace Umbraco.Core.Composing // rescanning of all types including lazy ones. // http://issues.umbraco.org/issue/U4-4789 var typesListFilePath = GetTypesListFilePath(); - if (File.Exists(typesListFilePath)) - File.Delete(typesListFilePath); + DeleteFile(typesListFilePath, FileDeleteTimeout); // always set to true if we're not detecting (generally only for testing) RequiresRescanning = true; } } + /// + /// Initializes a new, test/blank, instance of the class. + /// + /// The initialized instance cannot get types. + internal TypeLoader() + { } + /// /// Gets or sets the set of assemblies to scan. /// @@ -117,7 +135,8 @@ namespace Umbraco.Core.Composing // internal for tests internal void AddTypeList(TypeList typeList) { - _types[new TypeListKey(typeList.BaseType, typeList.AttributeType)] = typeList; + var tobject = typeof(object); // CompositeTypeTypeKey does not support null values + _types[new CompositeTypeTypeKey(typeList.BaseType ?? tobject, typeList.AttributeType ?? tobject)] = typeList; } #region Hashing @@ -192,7 +211,7 @@ namespace Umbraco.Core.Composing /// The hash. /// Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the /// file properties (false) or the file contents (true). - private static string GetFileHash(IEnumerable> filesAndFolders, ProfilingLogger logger) + private static string GetFileHash(IEnumerable> filesAndFolders, IProfilingLogger logger) { using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined")) { @@ -250,7 +269,7 @@ namespace Umbraco.Core.Composing /// A profiling logger. /// The hash. // internal for tests - internal static string GetFileHash(IEnumerable filesAndFolders, ProfilingLogger logger) + internal static string GetFileHash(IEnumerable filesAndFolders, IProfilingLogger logger) { using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined")) { @@ -276,11 +295,14 @@ namespace Umbraco.Core.Composing private const int ListFileOpenReadTimeout = 4000; // milliseconds private const int ListFileOpenWriteTimeout = 2000; // milliseconds + private const int ListFileWriteThrottle = 500; // milliseconds - throttle before writing + private const int ListFileCacheDuration = 2 * 60; // seconds - duration we cache the entire list + private const int FileDeleteTimeout = 4000; // milliseconds // internal for tests internal Attempt> TryGetCached(Type baseType, Type attributeType) { - var cache = _runtimeCache.GetCacheItem, IEnumerable>>(CacheKey, ReadCacheSafe, TimeSpan.FromMinutes(4)); + var cache = _runtimeCache.GetCacheItem, IEnumerable>>(CacheKey, ReadCacheSafe, TimeSpan.FromSeconds(ListFileCacheDuration)); cache.TryGetValue(Tuple.Create(baseType == null ? string.Empty : baseType.FullName, attributeType == null ? string.Empty : attributeType.FullName), out IEnumerable types); return types == null @@ -299,7 +321,7 @@ namespace Umbraco.Core.Composing try { var typesListFilePath = GetTypesListFilePath(); - File.Delete(typesListFilePath); + DeleteFile(typesListFilePath, FileDeleteTimeout); } catch { @@ -363,14 +385,15 @@ namespace Umbraco.Core.Composing private string GetFileBasePath() { - var localTempStorage = _globalSettings.LocalTempStorageLocation; - if (_localTempStorage != localTempStorage) + lock (_locko) { - string path; - switch (_globalSettings.LocalTempStorageLocation) + if (_fileBasePath != null) + return _fileBasePath; + + switch (_localTempStorage) { case LocalTempStorage.AspNetTemp: - path = Path.Combine(HttpRuntime.CodegenDir, "UmbracoData", "umbraco-types"); + _fileBasePath = Path.Combine(HttpRuntime.CodegenDir, "UmbracoData", "umbraco-types"); break; case LocalTempStorage.EnvironmentTemp: // include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back @@ -378,65 +401,30 @@ namespace Umbraco.Core.Composing // utilizing an old path - assuming we cannot have SHA1 collisions on AppDomainAppId var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", appDomainHash); - path = Path.Combine(cachePath, "umbraco-types"); + _fileBasePath = Path.Combine(cachePath, "umbraco-types"); break; case LocalTempStorage.Default: default: var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/TypesCache"); - path = Path.Combine(tempFolder, "umbraco-types." + NetworkHelper.FileSafeMachineName); + _fileBasePath = Path.Combine(tempFolder, "umbraco-types." + NetworkHelper.FileSafeMachineName); break; } - _fileBasePath = path; - _localTempStorage = localTempStorage; + // ensure that the folder exists + var directory = Path.GetDirectoryName(_fileBasePath); + if (directory == null) + throw new InvalidOperationException($"Could not determine folder for path \"{_fileBasePath}\"."); + if (Directory.Exists(directory) == false) + Directory.CreateDirectory(directory); + + return _fileBasePath; } - - // ensure that the folder exists - var directory = Path.GetDirectoryName(_fileBasePath); - if (directory == null) - throw new InvalidOperationException($"Could not determine folder for path \"{_fileBasePath}\"."); - if (Directory.Exists(directory) == false) - Directory.CreateDirectory(directory); - - return _fileBasePath; } - //private string GetFilePath(string extension) - //{ - // string path; - // switch (_globalSettings.LocalTempStorageLocation) - // { - // case LocalTempStorage.AspNetTemp: - // path = Path.Combine(HttpRuntime.CodegenDir, "UmbracoData", "umbraco-types." + extension); - // break; - // case LocalTempStorage.EnvironmentTemp: - // // include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back - // // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not - // // utilizing an old path - assuming we cannot have SHA1 collisions on AppDomainAppId - // var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); - // var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", appDomainHash); - // path = Path.Combine(cachePath, "umbraco-types." + extension); - // break; - // case LocalTempStorage.Default: - // default: - // var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/TypesCache"); - // path = Path.Combine(tempFolder, "umbraco-types." + NetworkHelper.FileSafeMachineName + "." + extension); - // break; - // } - - // // ensure that the folder exists - // var directory = Path.GetDirectoryName(path); - // if (directory == null) - // throw new InvalidOperationException($"Could not determine folder for file \"{path}\"."); - // if (Directory.Exists(directory) == false) - // Directory.CreateDirectory(directory); - - // return path; - //} - // internal for tests internal void WriteCache() { + _logger.Debug("Writing cache file."); var typesListFilePath = GetTypesListFilePath(); using (var stream = GetFileStream(typesListFilePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout)) using (var writer = new StreamWriter(stream)) @@ -455,11 +443,27 @@ namespace Umbraco.Core.Composing // internal for tests internal void UpdateCache() { - // note - // at the moment we write the cache to disk every time we update it. ideally we defer the writing - // since all the updates are going to happen in a row when Umbraco starts. that being said, the - // file is small enough, so it is not a priority. - WriteCache(); + void TimerRelease(object o) + { + lock (_timerLock) + { + try + { + WriteCache(); + } + catch { /* bah - just don't die */ } + if (!_timing) _timer = null; + } + } + + lock (_timerLock) + { + if (_timer == null) + _timer = new Timer(TimerRelease, null, ListFileWriteThrottle, Timeout.Infinite); + else + _timer.Change(ListFileWriteThrottle, Timeout.Infinite); + _timing = true; + } } /// @@ -469,12 +473,10 @@ namespace Umbraco.Core.Composing public void ClearTypesCache() { var typesListFilePath = GetTypesListFilePath(); - if (File.Exists(typesListFilePath)) - File.Delete(typesListFilePath); + DeleteFile(typesListFilePath, FileDeleteTimeout); var typesHashFilePath = GetTypesHashFilePath(); - if (File.Exists(typesHashFilePath)) - File.Delete(typesHashFilePath); + DeleteFile(typesHashFilePath, FileDeleteTimeout); _runtimeCache.ClearCacheItem(CacheKey); } @@ -494,7 +496,28 @@ namespace Umbraco.Core.Composing if (--attempts == 0) throw; - _logger.Logger.Debug("Attempted to get filestream for file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds); + _logger.Debug("Attempted to get filestream for file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds); + Thread.Sleep(pauseMilliseconds); + } + } + } + + private void DeleteFile(string path, int timeoutMilliseconds) + { + const int pauseMilliseconds = 250; + var attempts = timeoutMilliseconds / pauseMilliseconds; + while (File.Exists(path)) + { + try + { + File.Delete(path); + } + catch + { + if (--attempts == 0) + throw; + + _logger.Debug("Attempted to delete file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds); Thread.Sleep(pauseMilliseconds); } } @@ -514,31 +537,43 @@ namespace Umbraco.Core.Composing /// Caching is disabled when using specific assemblies. public IEnumerable GetTypes(bool cache = true, IEnumerable specificAssemblies = null) { + if (_logger == null) + throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + // do not cache anything from specific assemblies cache &= specificAssemblies == null; - // if not caching, or not IDiscoverable, directly get types - if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) + // if not IDiscoverable, directly get types + if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) { + // warn + _logger.Debug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} (slow).", typeof(T).FullName); + return GetTypesInternal( - typeof (T), null, + typeof(T), null, () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", cache); } - // if caching and IDiscoverable - // filter the cached discovered types (and cache the result) - + // get IDiscoverable and always cache var discovered = GetTypesInternal( typeof (IDiscoverable), null, () => TypeFinder.FindClassesOfType(AssembliesToScan), + "scanning assemblies", true); + // warn + if (!cache) + _logger.Debug("Running a non-cached, filter for discoverable type {TypeName} (slowish).", typeof(T).FullName); + + // filter the cached discovered types (and maybe cache the result) return GetTypesInternal( typeof (T), null, () => discovered .Where(x => typeof (T).IsAssignableFrom(x)), - true); + "filtering IDiscoverable", + cache); } /// @@ -553,32 +588,43 @@ namespace Umbraco.Core.Composing public IEnumerable GetTypesWithAttribute(bool cache = true, IEnumerable specificAssemblies = null) where TAttribute : Attribute { + if (_logger == null) + throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + // do not cache anything from specific assemblies cache &= specificAssemblies == null; - // if not caching, or not IDiscoverable, directly get types - if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) + // if not IDiscoverable, directly get types + if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) { + _logger.Debug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).", typeof(T).FullName, typeof(TAttribute).FullName); + return GetTypesInternal( - typeof (T), typeof (TAttribute), + typeof(T), typeof(TAttribute), () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", cache); } - // if caching and IDiscoverable - // filter the cached discovered types (and cache the result) - + // get IDiscoverable and always cache var discovered = GetTypesInternal( typeof (IDiscoverable), null, () => TypeFinder.FindClassesOfType(AssembliesToScan), + "scanning assemblies", true); + // warn + if (!cache) + _logger.Debug("Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).", typeof(T).FullName, typeof(TAttribute).FullName); + + // filter the cached discovered types (and maybe cache the result) return GetTypesInternal( typeof (T), typeof (TAttribute), () => discovered .Where(x => typeof(T).IsAssignableFrom(x)) .Where(x => x.GetCustomAttributes(false).Any()), - true); + "filtering IDiscoverable", + cache); } /// @@ -592,18 +638,26 @@ namespace Umbraco.Core.Composing public IEnumerable GetAttributedTypes(bool cache = true, IEnumerable specificAssemblies = null) where TAttribute : Attribute { + if (_logger == null) + throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + // do not cache anything from specific assemblies cache &= specificAssemblies == null; + if (!cache) + _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), + "scanning assemblies", cache); } private IEnumerable GetTypesInternal( Type baseType, Type attributeType, Func> finder, + string action, bool cache) { // using an upgradeable lock makes little sense here as only one thread can enter the upgradeable @@ -613,13 +667,13 @@ namespace Umbraco.Core.Composing var name = GetName(baseType, attributeType); - lock (_typesLock) + lock (_locko) using (_logger.TraceDuration( "Getting " + name, "Got " + name)) // cannot contain typesFound.Count as it's evaluated before the find { // get within a lock & timer - return GetTypesInternalLocked(baseType, attributeType, finder, cache); + return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); } } @@ -633,10 +687,12 @@ namespace Umbraco.Core.Composing private IEnumerable GetTypesInternalLocked( Type baseType, Type attributeType, Func> finder, + string action, bool cache) { // check if the TypeList already exists, if so return it, if not we'll create it - var listKey = new TypeListKey(baseType, attributeType); + var tobject = typeof(object); // CompositeTypeTypeKey does not support null values + var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject); TypeList typeList = null; if (cache) _types.TryGetValue(listKey, out typeList); // else null @@ -645,7 +701,7 @@ namespace Umbraco.Core.Composing if (typeList != null) { // need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 - _logger.Logger.Debug("Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType)); + _logger.Debug("Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType)); return typeList.Types; } @@ -661,7 +717,7 @@ namespace Umbraco.Core.Composing // report (only once) and scan and update the cache file if (_reportedChange == false) { - _logger.Logger.Debug("Assemblies changes detected, need to rescan everything."); + _logger.Debug("Assemblies changes detected, need to rescan everything."); _reportedChange = true; } } @@ -676,7 +732,7 @@ namespace Umbraco.Core.Composing // so in this instance there will never be a result. if (cacheResult.Exception is CachedTypeNotFoundInFileException || cacheResult.Success == false) { - _logger.Logger.Debug("Getting {TypeName}: failed to load from cache file, must scan assemblies.", GetName(baseType, attributeType)); + _logger.Debug("Getting {TypeName}: failed to load from cache file, must scan assemblies.", GetName(baseType, attributeType)); scan = true; } else @@ -695,7 +751,7 @@ namespace Umbraco.Core.Composing catch (Exception ex) { // in case of any exception, we have to exit, and revert to scanning - _logger.Logger.Error(ex, "Getting {TypeName}: failed to load cache file type {CacheType}, reverting to scanning assemblies.", GetName(baseType, attributeType), type); + _logger.Error(ex, "Getting {TypeName}: failed to load cache file type {CacheType}, reverting to scanning assemblies.", GetName(baseType, attributeType), type); scan = true; break; } @@ -703,7 +759,7 @@ namespace Umbraco.Core.Composing if (scan == false) { - _logger.Logger.Debug("Getting {TypeName}: loaded types from cache file.", GetName(baseType, attributeType)); + _logger.Debug("Getting {TypeName}: loaded types from cache file.", GetName(baseType, attributeType)); } } } @@ -711,7 +767,7 @@ namespace Umbraco.Core.Composing if (scan) { // either we had to scan, or we could not get the types from the cache file - scan now - _logger.Logger.Debug("Getting {TypeName}: scanning assemblies.", GetName(baseType, attributeType)); + _logger.Debug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType)); foreach (var t in finder()) typeList.Add(t); @@ -729,11 +785,11 @@ namespace Umbraco.Core.Composing UpdateCache(); } - _logger.Logger.Debug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); + _logger.Debug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); } else { - _logger.Logger.Debug("Got {TypeName}.", GetName(baseType, attributeType)); + _logger.Debug("Got {TypeName}.", GetName(baseType, attributeType)); } return typeList.Types; @@ -743,41 +799,6 @@ namespace Umbraco.Core.Composing #region Nested classes and stuff - /// - /// Groups a type and a resolution kind into a key. - /// - private struct TypeListKey - { - // ReSharper disable MemberCanBePrivate.Local - public readonly Type BaseType; - public readonly Type AttributeType; - // ReSharper restore MemberCanBePrivate.Local - - public TypeListKey(Type baseType, Type attributeType) - { - BaseType = baseType ?? typeof (object); - AttributeType = attributeType; - } - - public override bool Equals(object obj) - { - if (obj == null || obj is TypeListKey == false) return false; - var o = (TypeListKey)obj; - return BaseType == o.BaseType && AttributeType == o.AttributeType; - } - - public override int GetHashCode() - { - // in case AttributeType is null we need something else, using typeof (TypeListKey) - // which does not really "mean" anything, it's just a value... - - var hash = 5381; - hash = ((hash << 5) + hash) ^ BaseType.GetHashCode(); - hash = ((hash << 5) + hash) ^ (AttributeType ?? typeof (TypeListKey)).GetHashCode(); - return hash; - } - } - /// /// Represents a list of types obtained by looking for types inheriting/implementing a /// specified type, and/or marked with a specified attribute type. diff --git a/src/Umbraco.Core/Composing/TypeLoaderExtensions.cs b/src/Umbraco.Core/Composing/TypeLoaderExtensions.cs index 6177151a00..ba57243071 100644 --- a/src/Umbraco.Core/Composing/TypeLoaderExtensions.cs +++ b/src/Umbraco.Core/Composing/TypeLoaderExtensions.cs @@ -42,13 +42,5 @@ namespace Umbraco.Core.Composing { return mgr.GetTypesWithAttribute(); } - - /// - /// Gets all classes implementing ISqlSyntaxProvider and marked with the SqlSyntaxProviderAttribute. - /// - public static IEnumerable GetSqlSyntaxProviders(this TypeLoader mgr) - { - return mgr.GetTypesWithAttribute(); - } } } diff --git a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs index 99fa2d3eb9..f8ecc11d98 100644 --- a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using LightInject; namespace Umbraco.Core.Composing { @@ -13,22 +12,14 @@ namespace Umbraco.Core.Composing /// The type of the items. public abstract class WeightedCollectionBuilderBase : CollectionBuilderBase where TBuilder : WeightedCollectionBuilderBase - where TCollection : IBuilderCollection + where TCollection : class, IBuilderCollection { - /// - /// Initializes a new instance of the class. - /// - /// - protected WeightedCollectionBuilderBase(IServiceContainer container) - : base(container) - { } - protected abstract TBuilder This { get; } /// /// Clears all types in the collection. /// - /// The buidler. + /// The builder. public TBuilder Clear() { Configure(types => types.Clear()); diff --git a/src/Umbraco.Core/ConfigsExtensions.cs b/src/Umbraco.Core/ConfigsExtensions.cs new file mode 100644 index 0000000000..1414dbc852 --- /dev/null +++ b/src/Umbraco.Core/ConfigsExtensions.cs @@ -0,0 +1,52 @@ +using System.IO; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Dashboard; +using Umbraco.Core.Configuration.Grid; +using Umbraco.Core.Configuration.HealthChecks; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; + +namespace Umbraco.Core +{ + /// + /// Provides extension methods for the class. + /// + public static class ConfigsExtensions + { + public static IGlobalSettings Global(this Configs configs) + => configs.GetConfig(); + + public static IUmbracoSettingsSection Settings(this Configs configs) + => configs.GetConfig(); + + public static IDashboardSection Dashboards(this Configs configs) + => configs.GetConfig(); + + public static IHealthChecks HealthChecks(this Configs configs) + => configs.GetConfig(); + + public static IGridConfig Grids(this Configs configs) + => configs.GetConfig(); + + internal static CoreDebug CoreDebug(this Configs configs) + => configs.GetConfig(); + + public static void AddCoreConfigs(this Configs configs) + { + var configDir = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Config)); + + configs.Add(() => new GlobalSettings()); + configs.Add("umbracoConfiguration/settings"); + configs.Add("umbracoConfiguration/dashBoard"); + configs.Add("umbracoConfiguration/HealthChecks"); + + configs.Add(() => new CoreDebug()); + + // GridConfig depends on runtime caches, manifest parsers... and cannot be available during composition + configs.Add(factory => new GridConfig(factory.GetInstance(), factory.GetInstance(), configDir, factory.GetInstance().Debug)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/Configs.cs b/src/Umbraco.Core/Configuration/Configs.cs new file mode 100644 index 0000000000..51e1a327a0 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Configs.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.Configuration +{ + /// + /// Represents Umbraco configurations. + /// + /// + /// During composition, use composition.Configs. When running, either inject the required configuration, + /// or use Current.Configs. + /// + public class Configs + { + private readonly Dictionary> _configs = new Dictionary>(); + private Dictionary> _registerings = new Dictionary>(); + + /// + /// Gets a configuration. + /// + public TConfig GetConfig() + where TConfig : class + { + if (!_configs.TryGetValue(typeof(TConfig), out var configFactory)) + throw new InvalidOperationException($"No configuration of type {typeof(TConfig)} has been added."); + + return (TConfig) configFactory.Value; + } + + /// + /// Adds a configuration, provided by a factory. + /// + public void Add(Func configFactory) + where TConfig : class + { + // make sure it is not too late + if (_registerings == null) + throw new InvalidOperationException("Configurations have already been registered."); + + var typeOfConfig = typeof(TConfig); + + var lazyConfigFactory = _configs[typeOfConfig] = new Lazy(configFactory); + _registerings[typeOfConfig] = register => register.Register(_ => (TConfig) lazyConfigFactory.Value, Lifetime.Singleton); + } + + /// + /// Adds a configuration, provided by a factory. + /// + public void Add(Func configFactory) + where TConfig : class + { + // make sure it is not too late + if (_registerings == null) + throw new InvalidOperationException("Configurations have already been registered."); + + var typeOfConfig = typeof(TConfig); + + _configs[typeOfConfig] = new Lazy(() => + { + if (Current.HasFactory) return Current.Factory.GetInstance(); + throw new InvalidOperationException($"Cannot get configuration of type {typeOfConfig} during composition."); + }); + _registerings[typeOfConfig] = register => register.Register(configFactory, Lifetime.Singleton); + } + + /// + /// Adds a configuration, provided by a configuration section. + /// + public void Add(string sectionName) + where TConfig : class + { + Add(() => GetConfig(sectionName)); + } + + private static TConfig GetConfig(string sectionName) + where TConfig : class + { + // note: need to use SafeCallContext here because ConfigurationManager.GetSection ends up getting AppDomain.Evidence + // which will want to serialize the call context including anything that is in there - what a mess! + + using (new SafeCallContext()) + { + if ((ConfigurationManager.GetSection(sectionName) is TConfig config)) + return config; + var ex = new ConfigurationErrorsException($"Could not get configuration section \"{sectionName}\" from config files."); + Current.Logger.Error(ex, "Config error"); + throw ex; + } + } + + /// + /// Registers configurations in a register. + /// + public void RegisterWith(IRegister register) + { + // do it only once + if (_registerings == null) + throw new InvalidOperationException("Configurations have already been registered."); + + register.Register(this); + + foreach (var registering in _registerings.Values) + registering(register); + + // no need to keep them around + _registerings = null; + } + } +} diff --git a/src/Umbraco.Core/Configuration/CoreDebug.cs b/src/Umbraco.Core/Configuration/CoreDebug.cs index 71d0f24941..a71b311d7c 100644 --- a/src/Umbraco.Core/Configuration/CoreDebug.cs +++ b/src/Umbraco.Core/Configuration/CoreDebug.cs @@ -2,16 +2,6 @@ namespace Umbraco.Core.Configuration { - internal static class CoreDebugExtensions - { - private static CoreDebug _coreDebug; - - public static CoreDebug CoreDebug(this UmbracoConfig config) - { - return _coreDebug ?? (_coreDebug = new CoreDebug()); - } - } - internal class CoreDebug { public CoreDebug() 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/Grid/GridConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs index beade2d6d1..6c16a5e7ef 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs @@ -6,11 +6,11 @@ namespace Umbraco.Core.Configuration.Grid { class GridConfig : IGridConfig { - public GridConfig(ILogger logger, IRuntimeCacheProvider runtimeCache, DirectoryInfo appPlugins, DirectoryInfo configFolder, bool isDebug) + public GridConfig(ILogger logger, IRuntimeCacheProvider runtimeCache, DirectoryInfo configFolder, bool isDebug) { - EditorsConfig = new GridEditorsConfig(logger, runtimeCache, appPlugins, configFolder, isDebug); + EditorsConfig = new GridEditorsConfig(logger, runtimeCache, configFolder, isDebug); } - public IGridEditorsConfig EditorsConfig { get; private set; } + public IGridEditorsConfig EditorsConfig { get; } } } diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index 708c563d9d..94249aa135 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -14,15 +14,13 @@ namespace Umbraco.Core.Configuration.Grid { private readonly ILogger _logger; private readonly IRuntimeCacheProvider _runtimeCache; - private readonly DirectoryInfo _appPlugins; private readonly DirectoryInfo _configFolder; private readonly bool _isDebug; - public GridEditorsConfig(ILogger logger, IRuntimeCacheProvider runtimeCache, DirectoryInfo appPlugins, DirectoryInfo configFolder, bool isDebug) + public GridEditorsConfig(ILogger logger, IRuntimeCacheProvider runtimeCache, DirectoryInfo configFolder, bool isDebug) { _logger = logger; _runtimeCache = runtimeCache; - _appPlugins = appPlugins; _configFolder = configFolder; _isDebug = isDebug; } @@ -31,7 +29,7 @@ namespace Umbraco.Core.Configuration.Grid { get { - Func> getResult = () => + List GetResult() { // fixme - should use the common one somehow! + ignoring _appPlugins here! var parser = new ManifestParser(_runtimeCache, Current.ManifestValidators, _logger); @@ -55,20 +53,16 @@ namespace Umbraco.Core.Configuration.Grid // add manifest editors, skip duplicates foreach (var gridEditor in parser.Manifest.GridEditors) { - if (editors.Contains(gridEditor) == false) - editors.Add(gridEditor); + if (editors.Contains(gridEditor) == false) editors.Add(gridEditor); } return editors; - }; + } //cache the result if debugging is disabled var result = _isDebug - ? getResult() - : _runtimeCache.GetCacheItem>( - typeof(GridEditorsConfig) + "Editors", - () => getResult(), - TimeSpan.FromMinutes(10)); + ? GetResult() + : _runtimeCache.GetCacheItem>(typeof(GridEditorsConfig) + ".Editors",GetResult, TimeSpan.FromMinutes(10)); return result; } diff --git a/src/Umbraco.Core/Configuration/UmbracoConfig.cs b/src/Umbraco.Core/Configuration/UmbracoConfig.cs deleted file mode 100644 index 6a1203313e..0000000000 --- a/src/Umbraco.Core/Configuration/UmbracoConfig.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.Configuration; -using System.IO; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration.Dashboard; -using Umbraco.Core.Configuration.Grid; -using Umbraco.Core.Configuration.HealthChecks; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Composing; -using Umbraco.Core.Logging; - -namespace Umbraco.Core.Configuration -{ - /// - /// The gateway to all umbraco configuration - /// - public class UmbracoConfig - { - #region Singleton - - private static readonly Lazy Lazy = new Lazy(() => new UmbracoConfig()); - - public static UmbracoConfig For => Lazy.Value; - - #endregion - - /// - /// Default constructor - /// - private UmbracoConfig() - { - // note: need to use SafeCallContext here because ConfigurationManager.GetSection ends up getting AppDomain.Evidence - // which will want to serialize the call context including anything that is in there - what a mess! - - if (_umbracoSettings == null) - { - IUmbracoSettingsSection umbracoSettings; - using (new SafeCallContext()) - { - umbracoSettings = ConfigurationManager.GetSection("umbracoConfiguration/settings") as IUmbracoSettingsSection; - } - SetUmbracoSettings(umbracoSettings); - } - - if (_dashboardSection == null) - { - IDashboardSection dashboardConfig; - using (new SafeCallContext()) - { - dashboardConfig = ConfigurationManager.GetSection("umbracoConfiguration/dashBoard") as IDashboardSection; - } - SetDashboardSettings(dashboardConfig); - } - - if (_healthChecks == null) - { - var healthCheckConfig = ConfigurationManager.GetSection("umbracoConfiguration/HealthChecks") as IHealthChecks; - SetHealthCheckSettings(healthCheckConfig); - } - } - - /// - /// Constructor - can be used for testing - /// - /// - /// - /// - /// - public UmbracoConfig(IUmbracoSettingsSection umbracoSettings, IDashboardSection dashboardSettings, IHealthChecks healthChecks, IGlobalSettings globalSettings) - { - SetHealthCheckSettings(healthChecks); - SetUmbracoSettings(umbracoSettings); - SetDashboardSettings(dashboardSettings); - SetGlobalConfig(globalSettings); - } - - private IHealthChecks _healthChecks; - private IDashboardSection _dashboardSection; - private IUmbracoSettingsSection _umbracoSettings; - private IGridConfig _gridConfig; - private IGlobalSettings _globalSettings; - - /// - /// Gets the IHealthCheck config - /// - public IHealthChecks HealthCheck() - { - if (_healthChecks == null) - { - var ex = new ConfigurationErrorsException("Could not load the " + typeof(IHealthChecks) + " from config file, ensure the web.config and healthchecks.config files are formatted correctly"); - Current.Logger.Error(ex, "Config error"); - throw ex; - } - - return _healthChecks; - } - - /// - /// Gets the IDashboardSection - /// - public IDashboardSection DashboardSettings() - { - if (_dashboardSection == null) - { - var ex = new ConfigurationErrorsException("Could not load the " + typeof(IDashboardSection) + " from config file, ensure the web.config and Dashboard.config files are formatted correctly"); - Current.Logger.Error(ex, "Config error"); - throw ex; - } - - return _dashboardSection; - } - - /// - /// Only for testing - /// - /// - public void SetDashboardSettings(IDashboardSection value) - { - _dashboardSection = value; - } - - /// - /// Only for testing - /// - /// - public void SetHealthCheckSettings(IHealthChecks value) - { - _healthChecks = value; - } - - /// - /// Only for testing - /// - /// - public void SetUmbracoSettings(IUmbracoSettingsSection value) - { - _umbracoSettings = value; - } - - /// - /// Only for testing - /// - /// - public void SetGlobalConfig(IGlobalSettings value) - { - _globalSettings = value; - } - - /// - /// Gets the IGlobalSettings - /// - public IGlobalSettings GlobalSettings() - { - return _globalSettings ?? (_globalSettings = new GlobalSettings()); - } - - /// - /// Gets the IUmbracoSettings - /// - public IUmbracoSettingsSection UmbracoSettings() - { - if (_umbracoSettings == null) - { - var ex = new ConfigurationErrorsException("Could not load the " + typeof (IUmbracoSettingsSection) + " from config file, ensure the web.config and umbracoSettings.config files are formatted correctly"); - Current.Logger.Error(ex, "Config error"); - throw ex; - } - - return _umbracoSettings; - } - - /// - /// Only for testing - /// - /// - public void SetGridConfig(IGridConfig value) - { - _gridConfig = value; - } - - /// - /// Gets the IGridConfig - /// - public IGridConfig GridConfig(ILogger logger, IRuntimeCacheProvider runtimeCache, DirectoryInfo appPlugins, DirectoryInfo configFolder, bool isDebug) - { - if (_gridConfig == null) - { - _gridConfig = new GridConfig(logger, runtimeCache, appPlugins, configFolder, isDebug); - } - - return _gridConfig; - } - - //TODO: Add other configurations here ! - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index d2236bab70..91627edb8b 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { internal class ContentElement : UmbracoConfigurationElement, IContentSection { - private const string DefaultPreviewBadge = @"In Preview Mode - click to end"; + private const string DefaultPreviewBadge = @"In Preview Mode - click to end"; [ConfigurationProperty("imaging")] internal ContentImagingElement Imaging => (ContentImagingElement) this["imaging"]; diff --git a/src/Umbraco.Core/Constants-Composing.cs b/src/Umbraco.Core/Constants-Composing.cs index 1e8c9886d2..e65629a278 100644 --- a/src/Umbraco.Core/Constants-Composing.cs +++ b/src/Umbraco.Core/Constants-Composing.cs @@ -9,20 +9,6 @@ /// Defines constants for composition. /// public static class Composing - { - /// - /// Defines file system names. - /// - public static class FileSystems - { - public const string ScriptFileSystem = "ScriptFileSystem"; - public const string PartialViewFileSystem = "PartialViewFileSystem"; - public const string PartialViewMacroFileSystem = "PartialViewMacroFileSystem"; - public const string StylesheetFileSystem = "StylesheetFileSystem"; - public const string MasterpageFileSystem = "MasterpageFileSystem"; - public const string ViewFileSystem = "ViewFileSystem"; - public const string JavascriptLibraryFileSystem = "JavascriptLibraryFileSystem"; - } - } + { } } } diff --git a/src/Umbraco.Core/Constants-Examine.cs b/src/Umbraco.Core/Constants-Examine.cs deleted file mode 100644 index ddc3500066..0000000000 --- a/src/Umbraco.Core/Constants-Examine.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Umbraco.Core -{ - public static partial class Constants - { - public static class Examine - { - /// - /// The alias of the internal member indexer - /// - public const string InternalMemberIndexer = "InternalMemberIndexer"; - - /// - /// The alias of the internal content indexer - /// - public const string InternalIndexer = "InternalIndexer"; - - /// - /// The alias of the external content indexer - /// - public const string ExternalIndexer = "ExternalIndexer"; - - } - } -} 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 b15e371e87..5e2d44c90d 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -5,6 +5,8 @@ using System.IO; using System.Linq; using System.Web; using System.Xml.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core.Composing; using Umbraco.Core.IO; using Umbraco.Core.Models; @@ -18,8 +20,8 @@ namespace Umbraco.Core public static class ContentExtensions { // this ain't pretty - private static MediaFileSystem _mediaFileSystem; - private static MediaFileSystem MediaFileSystem => _mediaFileSystem ?? (_mediaFileSystem = Current.FileSystems.MediaFileSystem); + private static IMediaFileSystem _mediaFileSystem; + private static IMediaFileSystem MediaFileSystem => _mediaFileSystem ?? (_mediaFileSystem = Current.MediaFileSystem); #region IContent @@ -189,7 +191,21 @@ namespace Umbraco.Core private static void SetUploadFile(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null) { var property = GetProperty(content, propertyTypeAlias); - var oldpath = property.GetValue(culture, segment) is string svalue ? MediaFileSystem.GetRelativePath(svalue) : null; + + // Fixes https://github.com/umbraco/Umbraco-CMS/issues/3937 - Assigning a new file to an + // existing IMedia with extension SetValue causes exception 'Illegal characters in path' + string oldpath = null; + if (property.GetValue(culture, segment) is string svalue) + { + if (svalue.DetectIsJson()) + { + // the property value is a JSON serialized image crop data set - grab the "src" property as the file source + var jObject = JsonConvert.DeserializeObject(svalue); + svalue = jObject != null ? jObject.GetValueAsString("src") : svalue; + } + oldpath = MediaFileSystem.GetRelativePath(svalue); + } + var filepath = MediaFileSystem.StoreFile(content, property.PropertyType, filename, filestream, oldpath); property.SetValue(MediaFileSystem.GetUrl(filepath), culture, segment); } diff --git a/src/Umbraco.Core/Deploy/IValueConnector.cs b/src/Umbraco.Core/Deploy/IValueConnector.cs index 35304e3fde..92589ab6cf 100644 --- a/src/Umbraco.Core/Deploy/IValueConnector.cs +++ b/src/Umbraco.Core/Deploy/IValueConnector.cs @@ -17,19 +17,19 @@ namespace Umbraco.Core.Deploy IEnumerable PropertyEditorAliases { get; } /// - /// Gets the deploy property corresponding to a content property. + /// Gets the deploy property value corresponding to a content property value, and gather dependencies. /// - /// The content property. + /// The content property value. /// The content dependencies. /// The deploy property value. - string GetValue(Property property, ICollection dependencies); + string ToArtifact(object value, ICollection dependencies); /// - /// Sets a content property value using a deploy property. + /// Gets the content property value corresponding to a deploy property value. /// - /// The content item. - /// The property alias. /// The deploy property value. - void SetValue(IContentBase content, string alias, string value); + /// The current content property value. + /// The content property value. + object FromArtifact(string value, object currentValue); } } 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/Events/QueuingEventDispatcher.cs b/src/Umbraco.Core/Events/QueuingEventDispatcher.cs index e0d3847c17..b31b64e435 100644 --- a/src/Umbraco.Core/Events/QueuingEventDispatcher.cs +++ b/src/Umbraco.Core/Events/QueuingEventDispatcher.cs @@ -32,9 +32,9 @@ namespace Umbraco.Core.Events } } - private MediaFileSystem _mediaFileSystem; + private IMediaFileSystem _mediaFileSystem; // fixme inject - private MediaFileSystem MediaFileSystem => _mediaFileSystem ?? (_mediaFileSystem = Current.FileSystems.MediaFileSystem); + private IMediaFileSystem MediaFileSystem => _mediaFileSystem ?? (_mediaFileSystem = Current.MediaFileSystem); } } diff --git a/src/Umbraco.Core/Exceptions/BootFailedException.cs b/src/Umbraco.Core/Exceptions/BootFailedException.cs index ec07389d37..10a648fd76 100644 --- a/src/Umbraco.Core/Exceptions/BootFailedException.cs +++ b/src/Umbraco.Core/Exceptions/BootFailedException.cs @@ -1,9 +1,10 @@ using System; +using System.Text; namespace Umbraco.Core.Exceptions { /// - /// An exception that is thrown if the Umbraco application cannnot boot. + /// An exception that is thrown if the Umbraco application cannot boot. /// public class BootFailedException : Exception { @@ -29,5 +30,31 @@ namespace Umbraco.Core.Exceptions public BootFailedException(string message, Exception inner) : base(message, inner) { } + + /// + /// Rethrows a captured . + /// + /// The exception can be null, in which case a default message is used. + public static void Rethrow(BootFailedException bootFailedException) + { + if (bootFailedException == null) + throw new BootFailedException(DefaultMessage); + + // see https://stackoverflow.com/questions/57383 + // would that be the correct way to do it? + //ExceptionDispatchInfo.Capture(bootFailedException).Throw(); + + Exception e = bootFailedException; + var m = new StringBuilder(); + m.Append(DefaultMessage); + while (e != null) + { + m.Append($"\n\n-> {e.GetType().FullName}: {e.Message}"); + if (string.IsNullOrWhiteSpace(e.StackTrace) == false) + m.Append($"\n{e.StackTrace}"); + e = e.InnerException; + } + throw new BootFailedException(m.ToString()); + } } } diff --git a/src/Umbraco.Core/GuidUtils.cs b/src/Umbraco.Core/GuidUtils.cs new file mode 100644 index 0000000000..3768e1385d --- /dev/null +++ b/src/Umbraco.Core/GuidUtils.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Umbraco.Core +{ + /// + /// Utility methods for the struct. + /// + internal static class GuidUtils + { + /// + /// Combines two guid instances utilizing an exclusive disjunction. + /// The resultant guid is not guaranteed to be unique since the number of unique bits is halved. + /// + /// The first guid. + /// The seconds guid. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid Combine(Guid a, Guid b) + { + var ad = new DecomposedGuid(a); + var bd = new DecomposedGuid(b); + + ad.Hi ^= bd.Hi; + ad.Lo ^= bd.Lo; + + return ad.Value; + } + + /// + /// A decomposed guid. Allows access to the high and low bits without unsafe code. + /// + [StructLayout(LayoutKind.Explicit)] + private struct DecomposedGuid + { + [FieldOffset(00)] public Guid Value; + [FieldOffset(00)] public long Hi; + [FieldOffset(08)] public long Lo; + + public DecomposedGuid(Guid value) : this() => this.Value = value; + } + } +} diff --git a/src/Umbraco.Core/HexEncoder.cs b/src/Umbraco.Core/HexEncoder.cs new file mode 100644 index 0000000000..073dc8b543 --- /dev/null +++ b/src/Umbraco.Core/HexEncoder.cs @@ -0,0 +1,84 @@ +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Umbraco.Core +{ + /// + /// Provides methods for encoding byte arrays into hexidecimal strings. + /// + internal static class HexEncoder + { + // LUT's that provide the hexidecimal representation of each possible byte value. + private static readonly char[] HexLutBase = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + // The base LUT arranged in 16x each item order. 0 * 16, 1 * 16, .... F * 16 + private static readonly char[] HexLutHi = Enumerable.Range(0, 256).Select(x => HexLutBase[x / 0x10]).ToArray(); + + // The base LUT repeated 16x. + private static readonly char[] HexLutLo = Enumerable.Range(0, 256).Select(x => HexLutBase[x % 0x10]).ToArray(); + + /// + /// Converts a to a hexidecimal formatted padded to 2 digits. + /// + /// The bytes. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Encode(byte[] bytes) + { + var length = bytes.Length; + var chars = new char[length * 2]; + + var index = 0; + for (var i = 0; i < length; i++) + { + var byteIndex = bytes[i]; + chars[index++] = HexLutHi[byteIndex]; + chars[index++] = HexLutLo[byteIndex]; + } + + return new string(chars, 0, chars.Length); + } + + /// + /// Converts a to a hexidecimal formatted padded to 2 digits + /// and split into blocks with the given char separator. + /// + /// The bytes. + /// The separator. + /// The block size. + /// The block count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Encode(byte[] bytes, char separator, int blockSize, int blockCount) + { + var length = bytes.Length; + var chars = new char[(length * 2) + blockCount]; + var count = 0; + var size = 0; + var index = 0; + + for (var i = 0; i < length; i++) + { + var byteIndex = bytes[i]; + chars[index++] = HexLutHi[byteIndex]; + chars[index++] = HexLutLo[byteIndex]; + + if (count == blockCount) + { + continue; + } + + if (++size < blockSize) + { + continue; + } + + chars[index++] = separator; + size = 0; + count++; + } + + return new string(chars, 0, chars.Length); + } + } +} diff --git a/src/Umbraco.Core/IMainDom.cs b/src/Umbraco.Core/IMainDom.cs new file mode 100644 index 0000000000..3a8cd13ff1 --- /dev/null +++ b/src/Umbraco.Core/IMainDom.cs @@ -0,0 +1,38 @@ +using System; + +namespace Umbraco.Core +{ + /// + /// Represents the main AppDomain running for a given application. + /// + /// + /// There can be only one "main" AppDomain running for a given application at a time. + /// It is possible to register against the MainDom and be notified when it is released. + /// + public interface IMainDom + { + /// + /// Gets a value indicating whether the current domain is the main domain. + /// + bool IsMainDom { get; } + + /// + /// Registers a resource that requires the current AppDomain to be the main domain to function. + /// + /// An action to execute before the AppDomain releases the main domain status. + /// An optional weight (lower goes first). + /// A value indicating whether it was possible to register. + bool Register(Action release, int weight = 100); + + /// + /// Registers a resource that requires the current AppDomain to be the main domain to function. + /// + /// An action to execute when registering. + /// An action to execute before the AppDomain releases the main domain status. + /// An optional weight (lower goes first). + /// A value indicating whether it was possible to register. + /// If registering is successful, then the action + /// is guaranteed to execute before the AppDomain releases the main domain status. + bool Register(Action install, Action release, int weight = 100); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 4b73e64e80..ade2c58b38 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -65,5 +65,33 @@ namespace Umbraco.Core.IO } fs.DeleteFile(tempFile); } + + /// + /// Unwraps a filesystem. + /// + /// + /// A filesystem can be wrapped in a (public) or a (internal), + /// and this method deals with the various wrappers and + /// + public static IFileSystem Unwrap(this IFileSystem filesystem) + { + var unwrapping = true; + while (unwrapping) + { + switch (filesystem) + { + case FileSystemWrapper wrapper: + filesystem = wrapper.InnerFileSystem; + break; + case ShadowWrapper shadow: + filesystem = shadow.InnerFileSystem; + break; + default: + unwrapping = false; + break; + } + } + return filesystem; + } } } diff --git a/src/Umbraco.Core/IO/FileSystemProviderAttribute.cs b/src/Umbraco.Core/IO/FileSystemProviderAttribute.cs deleted file mode 100644 index b3b6cb6b79..0000000000 --- a/src/Umbraco.Core/IO/FileSystemProviderAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Umbraco.Core.CodeAnnotations; - -namespace Umbraco.Core.IO -{ - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public class FileSystemProviderAttribute : Attribute - { - public string Alias { get; private set; } - - public FileSystemProviderAttribute(string alias) - { - Alias = alias; - } - } -} diff --git a/src/Umbraco.Core/IO/FileSystemWrapper.cs b/src/Umbraco.Core/IO/FileSystemWrapper.cs index 2c4377b89b..14d028c16d 100644 --- a/src/Umbraco.Core/IO/FileSystemWrapper.cs +++ b/src/Umbraco.Core/IO/FileSystemWrapper.cs @@ -16,103 +16,103 @@ namespace Umbraco.Core.IO /// public abstract class FileSystemWrapper : IFileSystem { - protected FileSystemWrapper(IFileSystem wrapped) + protected FileSystemWrapper(IFileSystem innerFileSystem) { - Wrapped = wrapped; + InnerFileSystem = innerFileSystem; } - internal IFileSystem Wrapped { get; set; } + internal IFileSystem InnerFileSystem { get; set; } public IEnumerable GetDirectories(string path) { - return Wrapped.GetDirectories(path); + return InnerFileSystem.GetDirectories(path); } public void DeleteDirectory(string path) { - Wrapped.DeleteDirectory(path); + InnerFileSystem.DeleteDirectory(path); } public void DeleteDirectory(string path, bool recursive) { - Wrapped.DeleteDirectory(path, recursive); + InnerFileSystem.DeleteDirectory(path, recursive); } public bool DirectoryExists(string path) { - return Wrapped.DirectoryExists(path); + return InnerFileSystem.DirectoryExists(path); } public void AddFile(string path, Stream stream) { - Wrapped.AddFile(path, stream); + InnerFileSystem.AddFile(path, stream); } public void AddFile(string path, Stream stream, bool overrideExisting) { - Wrapped.AddFile(path, stream, overrideExisting); + InnerFileSystem.AddFile(path, stream, overrideExisting); } public IEnumerable GetFiles(string path) { - return Wrapped.GetFiles(path); + return InnerFileSystem.GetFiles(path); } public IEnumerable GetFiles(string path, string filter) { - return Wrapped.GetFiles(path, filter); + return InnerFileSystem.GetFiles(path, filter); } public Stream OpenFile(string path) { - return Wrapped.OpenFile(path); + return InnerFileSystem.OpenFile(path); } public void DeleteFile(string path) { - Wrapped.DeleteFile(path); + InnerFileSystem.DeleteFile(path); } public bool FileExists(string path) { - return Wrapped.FileExists(path); + return InnerFileSystem.FileExists(path); } public string GetRelativePath(string fullPathOrUrl) { - return Wrapped.GetRelativePath(fullPathOrUrl); + return InnerFileSystem.GetRelativePath(fullPathOrUrl); } public string GetFullPath(string path) { - return Wrapped.GetFullPath(path); + return InnerFileSystem.GetFullPath(path); } public string GetUrl(string path) { - return Wrapped.GetUrl(path); + return InnerFileSystem.GetUrl(path); } public DateTimeOffset GetLastModified(string path) { - return Wrapped.GetLastModified(path); + return InnerFileSystem.GetLastModified(path); } public DateTimeOffset GetCreated(string path) { - return Wrapped.GetCreated(path); + return InnerFileSystem.GetCreated(path); } public long GetSize(string path) { - return Wrapped.GetSize(path); + return InnerFileSystem.GetSize(path); } - public bool CanAddPhysical => Wrapped.CanAddPhysical; + public bool CanAddPhysical => InnerFileSystem.CanAddPhysical; public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) { - Wrapped.AddFile(path, physicalPath, overrideIfExists, copy); + InnerFileSystem.AddFile(path, physicalPath, overrideIfExists, copy); } } } diff --git a/src/Umbraco.Core/IO/FileSystems.cs b/src/Umbraco.Core/IO/FileSystems.cs index 62ce25dff0..7fc846319b 100644 --- a/src/Umbraco.Core/IO/FileSystems.cs +++ b/src/Umbraco.Core/IO/FileSystems.cs @@ -1,25 +1,18 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Configuration; -using System.IO; -using System.Linq; -using System.Reflection; using System.Threading; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Composing; namespace Umbraco.Core.IO { - public class FileSystems + public class FileSystems : IFileSystems { - private readonly IFileSystemProvidersSection _config; - private readonly ConcurrentSet _wrappers = new ConcurrentSet(); + private readonly IFactory _container; private readonly ILogger _logger; - private readonly ConcurrentDictionary _providerLookup = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _filesystems = new ConcurrentDictionary(); + private readonly ConcurrentDictionary> _filesystems = new ConcurrentDictionary>(); // wrappers for shadow support private ShadowWrapper _macroPartialFileSystem; @@ -28,40 +21,48 @@ namespace Umbraco.Core.IO private ShadowWrapper _scriptsFileSystem; private ShadowWrapper _masterPagesFileSystem; private ShadowWrapper _mvcViewsFileSystem; - + // well-known file systems lazy initialization private object _wkfsLock = new object(); private bool _wkfsInitialized; - private object _wkfsObject; + private object _wkfsObject; // unused - private MediaFileSystem _mediaFileSystem; - + // shadow support + private readonly List _shadowWrappers = new List(); + private readonly object _shadowLocker = new object(); + private static Guid _shadowCurrentId = Guid.Empty; // static - unique!! #region Constructor // DI wants a public ctor - // but IScopeProviderInternal is not public - public FileSystems(ILogger logger) + public FileSystems(IFactory container, ILogger logger) { - // fixme inject config section => can be used by tests - _config = (FileSystemProvidersSection) ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders"); + _container = container; _logger = logger; } // for tests only, totally unsafe internal void Reset() { - _wrappers.Clear(); - _providerLookup.Clear(); + _shadowWrappers.Clear(); _filesystems.Clear(); Volatile.Write(ref _wkfsInitialized, false); + _shadowCurrentId = Guid.Empty; } + // for tests only, totally unsafe + internal static void ResetShadowId() + { + _shadowCurrentId = Guid.Empty; + } + + // set by the scope provider when taking control of filesystems internal Func IsScoped { get; set; } = () => false; #endregion #region Well-Known FileSystems + /// public IFileSystem MacroPartialsFileSystem { get @@ -71,6 +72,7 @@ namespace Umbraco.Core.IO } } + /// public IFileSystem PartialViewsFileSystem { get @@ -80,6 +82,7 @@ namespace Umbraco.Core.IO } } + /// public IFileSystem StylesheetsFileSystem { get @@ -89,6 +92,7 @@ namespace Umbraco.Core.IO } } + /// public IFileSystem ScriptsFileSystem { get @@ -97,16 +101,18 @@ namespace Umbraco.Core.IO return _scriptsFileSystem; } } - + + /// public IFileSystem MasterPagesFileSystem { get { if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); - return _masterPagesFileSystem;// fixme - see 7.6?! + return _masterPagesFileSystem; } } + /// public IFileSystem MvcViewsFileSystem { get @@ -116,15 +122,6 @@ namespace Umbraco.Core.IO } } - public MediaFileSystem MediaFileSystem - { - get - { - if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); - return _mediaFileSystem; - } - } - private void EnsureWellKnownFileSystems() { LazyInitializer.EnsureInitialized(ref _wkfsObject, ref _wkfsInitialized, ref _wkfsLock, CreateWellKnownFileSystems); @@ -141,15 +138,20 @@ namespace Umbraco.Core.IO var masterPagesFileSystem = new PhysicalFileSystem(SystemDirectories.Masterpages); var mvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews); - _macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "Views/MacroPartials", () => IsScoped()); - _partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "Views/Partials", () => IsScoped()); - _stylesheetsFileSystem = new ShadowWrapper(stylesheetsFileSystem, "css", () => IsScoped()); - _scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", () => IsScoped()); - _masterPagesFileSystem = new ShadowWrapper(masterPagesFileSystem, "masterpages", () => IsScoped()); - _mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "Views", () => IsScoped()); + _macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "Views/MacroPartials", IsScoped); + _partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "Views/Partials", IsScoped); + _stylesheetsFileSystem = new ShadowWrapper(stylesheetsFileSystem, "css", IsScoped); + _scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", IsScoped); + _masterPagesFileSystem = new ShadowWrapper(masterPagesFileSystem, "masterpages", IsScoped); + _mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "Views", IsScoped); - // filesystems obtained from GetFileSystemProvider are already wrapped and do not need to be wrapped again - _mediaFileSystem = GetFileSystemProvider(); + // fixme locking? + _shadowWrappers.Add(_macroPartialFileSystem); + _shadowWrappers.Add(_partialViewsFileSystem); + _shadowWrappers.Add(_stylesheetsFileSystem); + _shadowWrappers.Add(_scriptsFileSystem); + _shadowWrappers.Add(_masterPagesFileSystem); + _shadowWrappers.Add(_mvcViewsFileSystem); return null; } @@ -158,156 +160,28 @@ namespace Umbraco.Core.IO #region Providers - /// - /// used to cache the lookup of how to construct this object so we don't have to reflect each time. - /// - private class ProviderConstructionInfo - { - public object[] Parameters { get; set; } - public ConstructorInfo Constructor { get; set; } - //public string ProviderAlias { get; set; } - } - - /// - /// Gets an underlying (non-typed) filesystem supporting a strongly-typed filesystem. - /// - /// The alias of the strongly-typed filesystem. - /// The non-typed filesystem supporting the strongly-typed filesystem with the specified alias. - /// This method should not be used directly, used instead. - public IFileSystem GetUnderlyingFileSystemProvider(string alias) - { - return GetUnderlyingFileSystemProvider(alias, null); - } - - /// - /// Gets an underlying (non-typed) filesystem supporting a strongly-typed filesystem. - /// - /// The alias of the strongly-typed filesystem. - /// A fallback creator for the filesystem. - /// The non-typed filesystem supporting the strongly-typed filesystem with the specified alias. - /// This method should not be used directly, used instead. - internal IFileSystem GetUnderlyingFileSystemProvider(string alias, Func fallback) - { - // either get the constructor info from cache or create it and add to cache - var ctorInfo = _providerLookup.GetOrAdd(alias, _ => GetUnderlyingFileSystemCtor(alias, fallback)); - return ctorInfo == null ? fallback() : (IFileSystem) ctorInfo.Constructor.Invoke(ctorInfo.Parameters); - } - - private IFileSystem GetUnderlyingFileSystemNoCache(string alias, Func fallback) - { - var ctorInfo = GetUnderlyingFileSystemCtor(alias, fallback); - return ctorInfo == null ? fallback() : (IFileSystem) ctorInfo.Constructor.Invoke(ctorInfo.Parameters); - } - - private ProviderConstructionInfo GetUnderlyingFileSystemCtor(string alias, Func fallback) - { - // get config - if (_config.Providers.TryGetValue(alias, out var providerConfig) == false) - { - if (fallback != null) return null; - throw new ArgumentException($"No provider found with alias {alias}."); - } - - // get the filesystem type - var providerType = Type.GetType(providerConfig.Type); - if (providerType == null) - throw new InvalidOperationException($"Could not find type {providerConfig.Type}."); - - // ensure it implements IFileSystem - if (providerType.IsAssignableFrom(typeof (IFileSystem))) - throw new InvalidOperationException($"Type {providerType.FullName} does not implement IFileSystem."); - - // find a ctor matching the config parameters - var paramCount = providerConfig.Parameters?.Count ?? 0; - var constructor = providerType.GetConstructors().SingleOrDefault(x - => x.GetParameters().Length == paramCount && x.GetParameters().All(y => providerConfig.Parameters.Keys.Contains(y.Name))); - if (constructor == null) - throw new InvalidOperationException($"Type {providerType.FullName} has no ctor matching the {paramCount} configuration parameter(s)."); - - var parameters = new object[paramCount]; - if (providerConfig.Parameters != null) // keeps ReSharper happy - { - var allKeys = providerConfig.Parameters.Keys.ToArray(); - for (var i = 0; i < paramCount; i++) - parameters[i] = providerConfig.Parameters[allKeys[i]]; - } - - return new ProviderConstructionInfo - { - Constructor = constructor, - Parameters = parameters, - //ProviderAlias = s - }; - } - /// /// Gets a strongly-typed filesystem. /// /// The type of the filesystem. /// A strongly-typed filesystem of the specified type. /// - /// Ideally, this should cache the instances, but that would break backward compatibility, so we - /// only do it for our own MediaFileSystem - for everything else, it's the responsibility of the caller - /// to ensure that they maintain singletons. This is important for singletons, as each filesystem maintains - /// its own shadow and having multiple instances would lead to inconsistencies. /// Note that any filesystem created by this method *after* shadowing begins, will *not* be /// shadowing (and an exception will be thrown by the ShadowWrapper). /// - // fixme - should it change for v8? - public TFileSystem GetFileSystemProvider() + public TFileSystem GetFileSystem(IFileSystem supporting) where TFileSystem : FileSystemWrapper { - return GetFileSystemProvider(null); - } + if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); - /// - /// Gets a strongly-typed filesystem. - /// - /// The type of the filesystem. - /// A fallback creator for the inner filesystem. - /// A strongly-typed filesystem of the specified type. - /// - /// The fallback creator is used only if nothing is configured. - /// Ideally, this should cache the instances, but that would break backward compatibility, so we - /// only do it for our own MediaFileSystem - for everything else, it's the responsibility of the caller - /// to ensure that they maintain singletons. This is important for singletons, as each filesystem maintains - /// its own shadow and having multiple instances would lead to inconsistencies. - /// Note that any filesystem created by this method *after* shadowing begins, will *not* be - /// shadowing (and an exception will be thrown by the ShadowWrapper). - /// - public TFileSystem GetFileSystemProvider(Func fallback) - where TFileSystem : FileSystemWrapper - { - var alias = GetFileSystemAlias(); - return (TFileSystem)_filesystems.GetOrAdd(alias, _ => + return (TFileSystem) _filesystems.GetOrAdd(typeof(TFileSystem), _ => new Lazy(() => { - // gets the inner fs, create the strongly-typed fs wrapping the inner fs, register & return - // so we are double-wrapping here - // could be optimized by having FileSystemWrapper inherit from ShadowWrapper, maybe - var innerFs = GetUnderlyingFileSystemNoCache(alias, fallback); - var shadowWrapper = new ShadowWrapper(innerFs, "typed/" + alias, () => IsScoped()); - var fs = (IFileSystem) Activator.CreateInstance(typeof(TFileSystem), shadowWrapper); - _wrappers.Add(shadowWrapper); // keeping a reference to the wrapper - return fs; - }); - } + var name = typeof(TFileSystem).FullName; + if (name == null) throw new Exception("panic!"); - private string GetFileSystemAlias() - { - var fsType = typeof(TFileSystem); - - // validate the ctor - var constructor = fsType.GetConstructors().SingleOrDefault(x - => x.GetParameters().Length == 1 && TypeHelper.IsTypeAssignableFrom(x.GetParameters().Single().ParameterType)); - if (constructor == null) - throw new InvalidOperationException("Type " + fsType.FullName + " must inherit from FileSystemWrapper and have a constructor that accepts one parameter of type " + typeof(IFileSystem).FullName + "."); - - // find the attribute and get the alias - var attr = (FileSystemProviderAttribute)fsType.GetCustomAttributes(typeof(FileSystemProviderAttribute), false).SingleOrDefault(); - if (attr == null) - throw new InvalidOperationException("Type " + fsType.FullName + "is missing the required FileSystemProviderAttribute."); - - return attr.Alias; + var shadowWrapper = CreateShadowWrapper(supporting, "typed/" + name); + return _container.CreateInstance(shadowWrapper); + })).Value; } #endregion @@ -318,68 +192,75 @@ namespace Umbraco.Core.IO // shadowing is thread-safe, but entering and exiting shadow mode is not, and there is only one // global shadow for the entire application, so great care should be taken to ensure that the // application is *not* doing anything else when using a shadow. - // shadow applies to well-known filesystems *only* - at the moment, any other filesystem that would - // be created directly (via ctor) or via GetFileSystem is *not* shadowed. - - // shadow must be enabled in an app event handler before anything else ie before any filesystem - // is actually created and used - after, it is too late - enabling shadow has a neglictible perfs - // impact. - // NO! by the time an app event handler is instanciated it is already too late, see note in ctor. - //internal void EnableShadow() - //{ - // if (_mvcViewsFileSystem != null) // test one of the fs... - // throw new InvalidOperationException("Cannot enable shadow once filesystems have been created."); - // _shadowEnabled = true; - //} internal ICompletable Shadow(Guid id) { if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); - var typed = _wrappers.ToArray(); - var wrappers = new ShadowWrapper[typed.Length + 6]; - var i = 0; - while (i < typed.Length) wrappers[i] = typed[i++]; - wrappers[i++] = _macroPartialFileSystem; - wrappers[i++] = _partialViewsFileSystem; - wrappers[i++] = _stylesheetsFileSystem; - wrappers[i++] = _scriptsFileSystem; - wrappers[i++] = _masterPagesFileSystem; - wrappers[i] = _mvcViewsFileSystem; + return new ShadowFileSystems(this, id); // will invoke BeginShadow and EndShadow + } - return new ShadowFileSystems(id, wrappers, _logger); + internal void BeginShadow(Guid id) + { + lock (_shadowLocker) + { + // if we throw here, it means that something very wrong happened. + if (_shadowCurrentId != Guid.Empty) + throw new InvalidOperationException("Already shadowing."); + _shadowCurrentId = id; + + _logger.Debug("Shadow '{ShadowId}'", id); + + foreach (var wrapper in _shadowWrappers) + wrapper.Shadow(id); + } + } + + internal void EndShadow(Guid id, bool completed) + { + lock (_shadowLocker) + { + // if we throw here, it means that something very wrong happened. + if (_shadowCurrentId == Guid.Empty) + throw new InvalidOperationException("Not shadowing."); + if (id != _shadowCurrentId) + throw new InvalidOperationException("Not the current shadow."); + + _logger.Debug("UnShadow '{ShadowId}' {Status}", id, completed ? "complete" : "abort"); + + var exceptions = new List(); + foreach (var wrapper in _shadowWrappers) + { + try + { + // this may throw an AggregateException if some of the changes could not be applied + wrapper.UnShadow(completed); + } + catch (AggregateException ae) + { + exceptions.Add(ae); + } + } + + _shadowCurrentId = Guid.Empty; + + if (exceptions.Count > 0) + throw new AggregateException(completed ? "Failed to apply all changes (see exceptions)." : "Failed to abort (see exceptions).", exceptions); + } + } + + private ShadowWrapper CreateShadowWrapper(IFileSystem filesystem, string shadowPath) + { + lock (_shadowLocker) + { + var wrapper = new ShadowWrapper(filesystem, shadowPath, IsScoped); + if (_shadowCurrentId != Guid.Empty) + wrapper.Shadow(_shadowCurrentId); + _shadowWrappers.Add(wrapper); + return wrapper; + } } #endregion - - private class ConcurrentSet - where T : class - { - private readonly HashSet _set = new HashSet(); - - public void Add(T item) - { - lock (_set) - { - _set.Add(item); - } - } - - public void Clear() - { - lock (_set) - { - _set.Clear(); - } - } - - public T[] ToArray() - { - lock (_set) - { - return _set.ToArray(); - } - } - } } } diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 115cb8a5c1..14b015a468 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -5,7 +5,7 @@ using System.IO; namespace Umbraco.Core.IO { /// - /// Provides methods allowing the manipulation of files within an Umbraco application. + /// Provides methods allowing the manipulation of files. /// public interface IFileSystem { diff --git a/src/Umbraco.Core/IO/IFileSystems.cs b/src/Umbraco.Core/IO/IFileSystems.cs new file mode 100644 index 0000000000..d74ad48145 --- /dev/null +++ b/src/Umbraco.Core/IO/IFileSystems.cs @@ -0,0 +1,38 @@ +namespace Umbraco.Core.IO +{ + /// + /// Provides the system filesystems. + /// + public interface IFileSystems + { + /// + /// Gets the macro partials filesystem. + /// + IFileSystem MacroPartialsFileSystem { get; } + + /// + /// Gets the partial views filesystem. + /// + IFileSystem PartialViewsFileSystem { get; } + + /// + /// Gets the stylesheets filesystem. + /// + IFileSystem StylesheetsFileSystem { get; } + + /// + /// Gets the scripts filesystem. + /// + IFileSystem ScriptsFileSystem { get; } + + /// + /// Gets the masterpages filesystem. + /// + IFileSystem MasterPagesFileSystem { get; } + + /// + /// Gets the MVC views filesystem. + /// + IFileSystem MvcViewsFileSystem { get; } + } +} diff --git a/src/Umbraco.Core/IO/IMediaFileSystem.cs b/src/Umbraco.Core/IO/IMediaFileSystem.cs new file mode 100644 index 0000000000..ed88516135 --- /dev/null +++ b/src/Umbraco.Core/IO/IMediaFileSystem.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Umbraco.Core.Models; + +namespace Umbraco.Core.IO +{ + /// + /// Provides methods allowing the manipulation of media files. + /// + public interface IMediaFileSystem : IFileSystem + { + /// + /// Delete media files. + /// + /// Files to delete (filesystem-relative paths). + void DeleteMediaFiles(IEnumerable files); + + /// + /// Gets the file path of a media file. + /// + /// The file name. + /// The unique identifier of the content/media owning the file. + /// The unique identifier of the property type owning the file. + /// The filesystem-relative path to the media file. + /// With the old media path scheme, this CREATES a new media path each time it is invoked. + string GetMediaPath(string filename, Guid cuid, Guid puid); + + /// + /// Gets the file path of a media file. + /// + /// The file name. + /// A previous file path. + /// The unique identifier of the content/media owning the file. + /// The unique identifier of the property type owning the file. + /// The filesystem-relative path to the media file. + /// In the old, legacy, number-based scheme, we try to re-use the media folder + /// specified by . Else, we CREATE a new one. Each time we are invoked. + string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid); + + /// + /// Stores a media file associated to a property of a content item. + /// + /// The content item owning the media file. + /// The property type owning the media file. + /// The media file name. + /// A stream containing the media bytes. + /// An optional filesystem-relative filepath to the previous media file. + /// The filesystem-relative filepath to the media file. + /// + /// The file is considered "owned" by the content/propertyType. + /// If an is provided then that file (and associated thumbnails if any) is deleted + /// before the new file is saved, and depending on the media path scheme, the folder may be reused for the new file. + /// + string StoreFile(IContentBase content, PropertyType propertyType, string filename, Stream filestream, string oldpath); + + /// + /// Copies a media file as a new media file, associated to a property of a content item. + /// + /// The content item owning the copy of the media file. + /// The property type owning the copy of the media file. + /// The filesystem-relative path to the source media file. + /// The filesystem-relative path to the copy of the media file. + string CopyFile(IContentBase content, PropertyType propertyType, string sourcepath); + } +} diff --git a/src/Umbraco.Core/IO/IMediaPathScheme.cs b/src/Umbraco.Core/IO/IMediaPathScheme.cs index 5cfb43ed77..9a38cdc74f 100644 --- a/src/Umbraco.Core/IO/IMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/IMediaPathScheme.cs @@ -7,35 +7,27 @@ namespace Umbraco.Core.IO /// public interface IMediaPathScheme { - // fixme - // to anyone finding this code: YES the Initialize() method is CompletelyBroken™ (temporal whatever) - // but at the moment, the media filesystem wants a scheme which wants a filesystem, and it's all - // convoluted due to how filesystems are managed in FileSystems - clear that part first! - - /// - /// Initialize. - /// - void Initialize(IFileSystem filesystem); - /// /// Gets a media file path. /// + /// The media filesystem. /// The (content, media) item unique identifier. /// The property type unique identifier. /// The file name. /// A previous filename. /// The filesystem-relative complete file path. - string GetFilePath(Guid itemGuid, Guid propertyGuid, string filename, string previous = null); + string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null); /// /// Gets the directory that can be deleted when the file is deleted. /// + /// The media filesystem. /// The filesystem-relative path of the file. /// The filesystem-relative path of the directory. /// /// The directory, and anything below it, will be deleted. /// Can return null (or empty) when no directory should be deleted. /// - string GetDeleteDirectory(string filepath); + string GetDeleteDirectory(IMediaFileSystem fileSystem, string filepath); } } diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 2f1675e08a..7773f378a5 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -284,11 +284,12 @@ namespace Umbraco.Core.IO binFolder = Path.Combine(GetRootDirectorySafe(), "bin"); -#if DEBUG + // do this all the time (no #if DEBUG) because Umbraco release + // can be used in tests by an app (eg Deploy) being debugged var debugFolder = Path.Combine(binFolder, "debug"); if (Directory.Exists(debugFolder)) return debugFolder; -#endif + var releaseFolder = Path.Combine(binFolder, "release"); if (Directory.Exists(releaseFolder)) return releaseFolder; diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index fd58f573cc..2ce1230bcc 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -1,15 +1,15 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.IO; using System.Linq; using System.Threading.Tasks; -using LightInject; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Media; - using Umbraco.Core.Models; namespace Umbraco.Core.IO @@ -17,78 +17,32 @@ namespace Umbraco.Core.IO /// /// A custom file system provider for media /// - [FileSystemProvider("media")] - public class MediaFileSystem : FileSystemWrapper + public class MediaFileSystem : FileSystemWrapper, IMediaFileSystem { - public MediaFileSystem(IFileSystem wrapped) - : base(wrapped) - { - // due to how FileSystems is written at the moment, the ctor cannot be used to inject - // dependencies, so we have to rely on property injection for anything we might need - Current.Container.InjectProperties(this); - MediaPathScheme.Initialize(this); - } + private readonly IMediaPathScheme _mediaPathScheme; + private readonly IContentSection _contentConfig; + private readonly ILogger _logger; - [Inject] - internal IMediaPathScheme MediaPathScheme { get; set; } - - [Inject] - internal IContentSection ContentConfig { get; set; } - - [Inject] - internal ILogger Logger { get; set; } - /// - /// Deletes all files passed in. + /// Initializes a new instance of the class. /// - /// - /// - /// - internal bool DeleteFiles(IEnumerable files, Action onError = null) + public MediaFileSystem(IFileSystem innerFileSystem, IContentSection contentConfig, IMediaPathScheme mediaPathScheme, ILogger logger) + : base(innerFileSystem) { - //ensure duplicates are removed - files = files.Distinct(); - - var allsuccess = true; - var rootRelativePath = GetRelativePath("/"); - - Parallel.ForEach(files, file => - { - try - { - if (file.IsNullOrWhiteSpace()) return; - - var relativeFilePath = GetRelativePath(file); - if (FileExists(relativeFilePath) == false) return; - - var parentDirectory = Path.GetDirectoryName(relativeFilePath); - - // don't want to delete the media folder if not using directories. - if (ContentConfig.UploadAllowDirectories && parentDirectory != rootRelativePath) - { - //issue U4-771: if there is a parent directory the recursive parameter should be true - DeleteDirectory(parentDirectory, string.IsNullOrEmpty(parentDirectory) == false); - } - else - { - DeleteFile(file); - } - } - catch (Exception e) - { - onError?.Invoke(file, e); - allsuccess = false; - } - }); - - return allsuccess; + _contentConfig = contentConfig; + _mediaPathScheme = mediaPathScheme; + _logger = logger; } + /// public void DeleteMediaFiles(IEnumerable files) { files = files.Distinct(); - Parallel.ForEach(files, file => + // kinda try to keep things under control + var options = new ParallelOptions { MaxDegreeOfParallelism = 20 }; + + Parallel.ForEach(files, options, file => { try { @@ -96,73 +50,44 @@ namespace Umbraco.Core.IO if (FileExists(file) == false) return; DeleteFile(file); - var directory = MediaPathScheme.GetDeleteDirectory(file); + var directory = _mediaPathScheme.GetDeleteDirectory(this, file); if (!directory.IsNullOrWhiteSpace()) DeleteDirectory(directory, true); } catch (Exception e) { - Logger.Error(e, "Failed to delete attached file '{File}'", file); + _logger.Error(e, "Failed to delete media file '{File}'.", file); } }); } #region Media Path - /// - /// Gets the file path of a media file. - /// - /// The file name. - /// The unique identifier of the content/media owning the file. - /// The unique identifier of the property type owning the file. - /// The filesystem-relative path to the media file. - /// With the old media path scheme, this CREATES a new media path each time it is invoked. + /// public string GetMediaPath(string filename, Guid cuid, Guid puid) { filename = Path.GetFileName(filename); if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); - return MediaPathScheme.GetFilePath(cuid, puid, filename); + return _mediaPathScheme.GetFilePath(this, cuid, puid, filename); } - /// - /// Gets the file path of a media file. - /// - /// The file name. - /// A previous file path. - /// The unique identifier of the content/media owning the file. - /// The unique identifier of the property type owning the file. - /// The filesystem-relative path to the media file. - /// In the old, legacy, number-based scheme, we try to re-use the media folder - /// specified by . Else, we CREATE a new one. Each time we are invoked. + /// public string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid) { filename = Path.GetFileName(filename); if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); - return MediaPathScheme.GetFilePath(cuid, puid, filename, prevpath); + return _mediaPathScheme.GetFilePath(this, cuid, puid, filename, prevpath); } #endregion #region Associated Media Files - /// - /// Stores a media file associated to a property of a content item. - /// - /// The content item owning the media file. - /// The property type owning the media file. - /// The media file name. - /// A stream containing the media bytes. - /// An optional filesystem-relative filepath to the previous media file. - /// The filesystem-relative filepath to the media file. - /// - /// The file is considered "owned" by the content/propertyType. - /// If an is provided then that file (and associated thumbnails if any) is deleted - /// before the new file is saved, and depending on the media path scheme, the folder may be reused for the new file. - /// + /// public string StoreFile(IContentBase content, PropertyType propertyType, string filename, Stream filestream, string oldpath) { if (content == null) throw new ArgumentNullException(nameof(content)); @@ -181,13 +106,7 @@ namespace Umbraco.Core.IO return filepath; } - /// - /// Copies a media file as a new media file, associated to a property of a content item. - /// - /// The content item owning the copy of the media file. - /// The property type owning the copy of the media file. - /// The filesystem-relative path to the source media file. - /// The filesystem-relative path to the copy of the media file. + /// public string CopyFile(IContentBase content, PropertyType propertyType, string sourcepath) { if (content == null) throw new ArgumentNullException(nameof(content)); @@ -204,9 +123,6 @@ namespace Umbraco.Core.IO return filepath; } - - #endregion - } } diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs index ef71aff3bc..59f8213414 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs @@ -12,32 +12,15 @@ namespace Umbraco.Core.IO.MediaPathSchemes public class CombinedGuidsMediaPathScheme : IMediaPathScheme { /// - public void Initialize(IFileSystem filesystem) - { } - - /// - public string GetFilePath(Guid itemGuid, Guid propertyGuid, string filename, string previous = null) + public string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null) { // assumes that cuid and puid keys can be trusted - and that a single property type // for a single content cannot store two different files with the same name - var directory = Combine(itemGuid, propertyGuid).ToHexString(/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... + var directory = HexEncoder.Encode(GuidUtils.Combine(itemGuid, propertyGuid).ToByteArray()/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... return Path.Combine(directory, filename).Replace('\\', '/'); } /// - public string GetDeleteDirectory(string filepath) - { - return Path.GetDirectoryName(filepath); - } - - private static byte[] Combine(Guid guid1, Guid guid2) - { - var bytes1 = guid1.ToByteArray(); - var bytes2 = guid2.ToByteArray(); - var bytes = new byte[bytes1.Length]; - for (var i = 0; i < bytes1.Length; i++) - bytes[i] = (byte) (bytes1[i] ^ bytes2[i]); - return bytes; - } + public string GetDeleteDirectory(IMediaFileSystem fileSystem, string filepath) => Path.GetDirectoryName(filepath); } } diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/OriginalMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/OriginalMediaPathScheme.cs index 1ed2ea59ce..1948a9f662 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/OriginalMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/OriginalMediaPathScheme.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.IO; using System.Threading; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; namespace Umbraco.Core.IO.MediaPathSchemes @@ -17,18 +18,11 @@ namespace Umbraco.Core.IO.MediaPathSchemes public class OriginalMediaPathScheme : IMediaPathScheme { private readonly object _folderCounterLock = new object(); - private IFileSystem _filesystem; private long _folderCounter; private bool _folderCounterInitialized; /// - public void Initialize(IFileSystem filesystem) - { - _filesystem = filesystem; - } - - /// - public string GetFilePath(Guid itemGuid, Guid propertyGuid, string filename, string previous = null) + public string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null) { string directory; if (previous != null) @@ -37,45 +31,45 @@ namespace Umbraco.Core.IO.MediaPathSchemes // prevpath should be "/" OR "-" // and we want to reuse the "" part, so try to find it - var sep = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? "/" : "-"; + var sep = Current.Configs.Settings().Content.UploadAllowDirectories ? "/" : "-"; var pos = previous.IndexOf(sep, StringComparison.Ordinal); var s = pos > 0 ? previous.Substring(0, pos) : null; - directory = pos > 0 && int.TryParse(s, out _) ? s : GetNextDirectory(); + directory = pos > 0 && int.TryParse(s, out _) ? s : GetNextDirectory(fileSystem); } else { - directory = GetNextDirectory(); + directory = GetNextDirectory(fileSystem); } if (directory == null) throw new InvalidOperationException("Cannot use a null directory."); - return UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories + return Current.Configs.Settings().Content.UploadAllowDirectories ? Path.Combine(directory, filename).Replace('\\', '/') : directory + "-" + filename; } /// - public string GetDeleteDirectory(string filepath) + public string GetDeleteDirectory(IMediaFileSystem fileSystem, string filepath) { return Path.GetDirectoryName(filepath); } - private string GetNextDirectory() + private string GetNextDirectory(IFileSystem fileSystem) { - EnsureFolderCounterIsInitialized(); + EnsureFolderCounterIsInitialized(fileSystem); return Interlocked.Increment(ref _folderCounter).ToString(CultureInfo.InvariantCulture); } - private void EnsureFolderCounterIsInitialized() + private void EnsureFolderCounterIsInitialized(IFileSystem fileSystem) { lock (_folderCounterLock) { if (_folderCounterInitialized) return; _folderCounter = 1000; // seed - var directories = _filesystem.GetDirectories(""); + var directories = fileSystem.GetDirectories(""); foreach (var directory in directories) { if (long.TryParse(directory, out var folderNumber) && folderNumber > _folderCounter) diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs index 4a6fdfcdbe..3c06e295e6 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs @@ -12,17 +12,13 @@ namespace Umbraco.Core.IO.MediaPathSchemes public class TwoGuidsMediaPathScheme : IMediaPathScheme { /// - public void Initialize(IFileSystem filesystem) - { } - - /// - public string GetFilePath(Guid itemGuid, Guid propertyGuid, string filename, string previous = null) + public string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null) { return Path.Combine(itemGuid.ToString("N"), propertyGuid.ToString("N"), filename).Replace('\\', '/'); } /// - public string GetDeleteDirectory(string filepath) + public string GetDeleteDirectory(IMediaFileSystem fileSystem, string filepath) { return Path.GetDirectoryName(filepath); } diff --git a/src/Umbraco.Core/IO/ShadowFileSystems.cs b/src/Umbraco.Core/IO/ShadowFileSystems.cs index 289667b0db..bce0cc6df7 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystems.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystems.cs @@ -1,158 +1,36 @@ using System; -using System.Collections.Generic; -using Umbraco.Core.Logging; namespace Umbraco.Core.IO { + // shadow filesystems is definitively ... too convoluted + internal class ShadowFileSystems : ICompletable { - // note: taking a reference to the _manager instead of using manager.Current - // to avoid using Current everywhere but really, we support only 1 scope at - // a time, not multiple scopes in case of multiple managers (not supported) - - // fixme - why are we managing logical call context here? should be bound - // to the current scope, always => REFACTOR! but there should be something in - // place (static?) to ensure we only have one concurrent shadow FS? - // - // => yes, that's _currentId - need to cleanup this entirely - // and, we probably need a way to stop shadowing entirely without cycling the app - - private const string ItemKey = "Umbraco.Core.IO.ShadowFileSystems"; - - private static readonly object Locker = new object(); - private static Guid _currentId = Guid.Empty; - - private readonly Guid _id; - private readonly ShadowWrapper[] _wrappers; - private readonly ILogger _logger; - + private readonly FileSystems _fileSystems; private bool _completed; - //static ShadowFileSystems() - //{ - // SafeCallContext.Register( - // () => - // { - // var scope = CallContext.LogicalGetData(ItemKey); - // CallContext.FreeNamedDataSlot(ItemKey); - // return scope; - // }, - // o => - // { - // if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException(); - // if (o != null) CallContext.LogicalSetData(ItemKey, o); - // }); - //} - - public ShadowFileSystems(Guid id, ShadowWrapper[] wrappers, ILogger logger) + // invoked by the filesystems when shadowing + public ShadowFileSystems(FileSystems fileSystems, Guid id) { - lock (Locker) - { - if (_currentId != Guid.Empty) - throw new InvalidOperationException("Already shadowing."); - _currentId = id; - } + _fileSystems = fileSystems; + Id = id; - _logger = logger; - _logger.Debug("Shadow '{ShadowId}'", id); - _id = id; - - _wrappers = wrappers; - foreach (var wrapper in _wrappers) - wrapper.Shadow(id); - } - - // fixme - remove - //// internal for tests + FileSystems - //// do NOT use otherwise - //internal static ShadowFileSystems CreateScope(Guid id, ShadowWrapper[] wrappers, ILogger logger) - //{ - // lock (Locker) - // { - // if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException("Already shadowing."); - // CallContext.LogicalSetData(ItemKey, ItemKey); // value does not matter - // } - // return new ShadowFileSystems(id, wrappers, logger); - //} - - //internal static bool InScope => NoScope == false; - - //internal static bool NoScope => CallContext.LogicalGetData(ItemKey) == null; - - public void Complete() - { - _completed = true; - //lock (Locker) - //{ - // _logger.Debug("UnShadow " + _id + " (complete)."); - - // var exceptions = new List(); - // foreach (var wrapper in _wrappers) - // { - // try - // { - // // this may throw an AggregateException if some of the changes could not be applied - // wrapper.UnShadow(true); - // } - // catch (AggregateException ae) - // { - // exceptions.Add(ae); - // } - // } - - // if (exceptions.Count > 0) - // throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); - - // // last, & *only* if successful (otherwise we'll unshadow & cleanup as best as we can) - // CallContext.FreeNamedDataSlot(ItemKey); - //} - } - - public void Dispose() - { - lock (Locker) - { - _logger.Debug("UnShadow '{ShadowId}' {Status}", _id, _completed ? "complete" : "abort"); - - var exceptions = new List(); - foreach (var wrapper in _wrappers) - { - try - { - // this may throw an AggregateException if some of the changes could not be applied - wrapper.UnShadow(_completed); - } - catch (AggregateException ae) - { - exceptions.Add(ae); - } - } - - _currentId = Guid.Empty; - - if (exceptions.Count > 0) - throw new AggregateException(_completed ? "Failed to apply all changes (see exceptions)." : "Failed to abort (see exceptions).", exceptions); - - //if (CallContext.LogicalGetData(ItemKey) == null) return; - - //try - //{ - // _logger.Debug("UnShadow " + _id + " (abort)"); - // foreach (var wrapper in _wrappers) - // wrapper.UnShadow(false); // should not throw - //} - //finally - //{ - // // last, & always - // CallContext.FreeNamedDataSlot(ItemKey); - //} - } + _fileSystems.BeginShadow(id); } // for tests - internal static void ResetId() + public Guid Id { get; } + + // invoked by the scope when exiting, if completed + public void Complete() { - _currentId = Guid.Empty; + _completed = true; + } + + // invoked by the scope when exiting + public void Dispose() + { + _fileSystems.EndShadow(Id, _completed); } } } diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index 94bd61b162..6493238391 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -75,6 +75,8 @@ namespace Umbraco.Core.IO } } + public IFileSystem InnerFileSystem => _innerFileSystem; + private IFileSystem FileSystem { get diff --git a/src/Umbraco.Core/IO/SupportingFileSystems.cs b/src/Umbraco.Core/IO/SupportingFileSystems.cs new file mode 100644 index 0000000000..43ac2ba85a --- /dev/null +++ b/src/Umbraco.Core/IO/SupportingFileSystems.cs @@ -0,0 +1,11 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.IO +{ + public class SupportingFileSystems : TargetedServiceFactory + { + public SupportingFileSystems(IFactory factory) + : base(factory) + { } + } +} diff --git a/src/Umbraco.Core/IRuntime.cs b/src/Umbraco.Core/IRuntime.cs index 37b265ad77..d433dde12d 100644 --- a/src/Umbraco.Core/IRuntime.cs +++ b/src/Umbraco.Core/IRuntime.cs @@ -1,4 +1,4 @@ -using LightInject; +using Umbraco.Core.Composing; namespace Umbraco.Core { @@ -10,8 +10,14 @@ namespace Umbraco.Core /// /// Boots the runtime. /// - /// The application service container. - void Boot(ServiceContainer container); + /// The application register. + /// The application factory. + IFactory Boot(IRegister register); + + /// + /// Gets the runtime state. + /// + IRuntimeState State { get; } /// /// Terminates the runtime. diff --git a/src/Umbraco.Core/Logging/IProfilingLogger.cs b/src/Umbraco.Core/Logging/IProfilingLogger.cs new file mode 100644 index 0000000000..1d7c231e95 --- /dev/null +++ b/src/Umbraco.Core/Logging/IProfilingLogger.cs @@ -0,0 +1,40 @@ +using System; + +namespace Umbraco.Core.Logging +{ + /// + /// Defines the profiling logging service. + /// + public interface IProfilingLogger : ILogger + { + /// + /// Profiles an action and log as information messages. + /// + DisposableTimer TraceDuration(string startMessage); + + /// + /// Profiles an action and log as information messages. + /// + DisposableTimer TraceDuration(string startMessage, string completeMessage, string failMessage = null); + + /// + /// Profiles an action and log as information messages. + /// + DisposableTimer TraceDuration(Type loggerType, string startMessage, string completeMessage, string failMessage = null); + + /// + /// Profiles an action and log as debug messages. + /// + DisposableTimer DebugDuration(string startMessage); + + /// + /// Profiles an action and log as debug messages. + /// + DisposableTimer DebugDuration(string startMessage, string completeMessage, string failMessage = null, int thresholdMilliseconds = 0); + + /// + /// Profiles an action and log as debug messages. + /// + DisposableTimer DebugDuration(Type loggerType, string startMessage, string completeMessage, string failMessage = null, int thresholdMilliseconds = 0); + } +} diff --git a/src/Umbraco.Core/Logging/MessageTemplates.cs b/src/Umbraco.Core/Logging/MessageTemplates.cs index 194d47c339..47de1230ff 100644 --- a/src/Umbraco.Core/Logging/MessageTemplates.cs +++ b/src/Umbraco.Core/Logging/MessageTemplates.cs @@ -1,6 +1,10 @@ using System; +using System.IO; using System.Linq; +using System.Text; using Serilog; +using Serilog.Events; +using Serilog.Parsing; namespace Umbraco.Core.Logging { @@ -29,7 +33,23 @@ namespace Umbraco.Core.Logging if (!bound) throw new FormatException($"Could not format message \"{messageTemplate}\" with {args.Length} args."); - return parsedTemplate.Render(boundProperties.ToDictionary(x => x.Name, x => x.Value)); + var values = boundProperties.ToDictionary(x => x.Name, x => x.Value); + + // this ends up putting every string parameter between quotes + //return parsedTemplate.Render(values); + + // this does not + var tw = new StringWriter(); + foreach (var t in parsedTemplate.Tokens) + { + if (t is PropertyToken pt && + values.TryGetValue(pt.PropertyName, out var propVal) && + (propVal as ScalarValue)?.Value is string s) + tw.Write(s); + else + t.Render(values, tw); + } + return tw.ToString(); } } } diff --git a/src/Umbraco.Core/Logging/ProfilingLogger.cs b/src/Umbraco.Core/Logging/ProfilingLogger.cs index 80560e923a..d642926147 100644 --- a/src/Umbraco.Core/Logging/ProfilingLogger.cs +++ b/src/Umbraco.Core/Logging/ProfilingLogger.cs @@ -3,14 +3,23 @@ namespace Umbraco.Core.Logging { /// - /// Provides debug or trace logging with duration management. + /// Provides logging and profiling services. /// - public sealed class ProfilingLogger + public sealed class ProfilingLogger : IProfilingLogger { + /// + /// Gets the underlying implementation. + /// public ILogger Logger { get; } + /// + /// Gets the underlying implementation. + /// public IProfiler Profiler { get; } + /// + /// Initializes a new instance of the class. + /// public ProfilingLogger(ILogger logger, IProfiler profiler) { Logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -52,5 +61,72 @@ namespace Umbraco.Core.Logging ? new DisposableTimer(Logger, LogLevel.Debug, Profiler, loggerType, startMessage, completeMessage, failMessage, thresholdMilliseconds) : null; } + + #region ILogger + + public bool IsEnabled(Type reporting, LogLevel level) + => Logger.IsEnabled(reporting, level); + + public void Fatal(Type reporting, Exception exception, string message) + => Logger.Fatal(reporting, exception, message); + + public void Fatal(Type reporting, Exception exception) + => Logger.Fatal(reporting, exception); + + public void Fatal(Type reporting, string message) + => Logger.Fatal(reporting, message); + + public void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) + => Logger.Fatal(reporting, exception, messageTemplate, propertyValues); + + public void Fatal(Type reporting, string messageTemplate, params object[] propertyValues) + => Logger.Fatal(reporting, messageTemplate, propertyValues); + + public void Error(Type reporting, Exception exception, string message) + => Logger.Error(reporting, exception, message); + + public void Error(Type reporting, Exception exception) + => Logger.Error(reporting, exception); + + public void Error(Type reporting, string message) + => Logger.Error(reporting, message); + + public void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) + => Logger.Error(reporting, exception, messageTemplate, propertyValues); + + public void Error(Type reporting, string messageTemplate, params object[] propertyValues) + => Logger.Error(reporting, messageTemplate, propertyValues); + + public void Warn(Type reporting, string message) + => Logger.Warn(reporting, message); + + public void Warn(Type reporting, string messageTemplate, params object[] propertyValues) + => Logger.Warn(reporting, messageTemplate, propertyValues); + + public void Warn(Type reporting, Exception exception, string message) + => Logger.Warn(reporting, exception, message); + + public void Warn(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) + => Logger.Warn(reporting, exception, messageTemplate, propertyValues); + + public void Info(Type reporting, string message) + => Logger.Info(reporting, message); + + public void Info(Type reporting, string messageTemplate, params object[] propertyValues) + => Logger.Info(reporting, messageTemplate, propertyValues); + + public void Debug(Type reporting, string message) + => Logger.Debug(reporting, message); + + public void Debug(Type reporting, string messageTemplate, params object[] propertyValues) + => Logger.Debug(reporting, messageTemplate, propertyValues); + + public void Verbose(Type reporting, string message) + => Logger.Verbose(reporting, message); + + public void Verbose(Type reporting, string messageTemplate, params object[] propertyValues) + => Logger.Verbose(reporting, messageTemplate, propertyValues); + + #endregion } } diff --git a/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs index 6d3b2b28b3..17d86b45e1 100644 --- a/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Core/Logging/Serilog/SerilogLogger.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Threading; using Serilog; using Serilog.Events; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Diagnostics; @@ -165,7 +166,7 @@ namespace Umbraco.Core.Logging.Serilog messageTemplate += "\r\nThe thread has been aborted, because the request has timed out."; // dump if configured, or if stacktrace contains Monitor.ReliableEnter - dump = UmbracoConfig.For.CoreDebug().DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(exception); + dump = Current.Configs.CoreDebug().DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(exception); // dump if it is ok to dump (might have a cap on number of dump...) dump &= MiniDump.OkToDump(); @@ -202,7 +203,6 @@ namespace Umbraco.Core.Logging.Serilog private static bool IsTimeoutThreadAbortException(Exception exception) { if (!(exception is ThreadAbortException abort)) return false; - if (abort.ExceptionState == null) return false; var stateType = abort.ExceptionState.GetType(); @@ -217,9 +217,9 @@ namespace Umbraco.Core.Logging.Serilog /// public void Warn(Type reporting, string message) { - LoggerFor(reporting).Warning(message); + LoggerFor(reporting).Warning(message); } - + /// public void Warn(Type reporting, string message, params object[] propertyValues) { @@ -231,7 +231,7 @@ namespace Umbraco.Core.Logging.Serilog { LoggerFor(reporting).Warning(exception, message); } - + /// public void Warn(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) { @@ -255,7 +255,7 @@ namespace Umbraco.Core.Logging.Serilog { LoggerFor(reporting).Debug(message); } - + /// public void Debug(Type reporting, string messageTemplate, params object[] propertyValues) { diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index eb036fd441..0b4551a7cc 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -8,15 +8,13 @@ using Umbraco.Core.Logging; namespace Umbraco.Core { /// - /// Represents the main AppDomain running for a given application. + /// Provides the full implementation of . /// /// - /// There can be only one "main" AppDomain running for a given application at a time. /// When an AppDomain starts, it tries to acquire the main domain status. /// When an AppDomain stops (eg the application is restarting) it should release the main domain status. - /// It is possible to register against the MainDom and be notified when it is released. /// - internal class MainDom : IRegisteredObject + internal class MainDom : IMainDom, IRegisteredObject { #region Vars @@ -84,9 +82,7 @@ namespace Umbraco.Core /// An optional weight (lower goes first). /// A value indicating whether it was possible to register. public bool Register(Action release, int weight = 100) - { - return Register(null, release, weight); - } + => Register(null, release, weight); /// /// Registers a resource that requires the current AppDomain to be the main domain to function. @@ -195,7 +191,9 @@ namespace Umbraco.Core } } - // gets a value indicating whether we are the main domain + /// + /// Gets a value indicating whether the current domain is the main domain. + /// public bool IsMainDom => _isMainDom; // IRegisteredObject 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/Execute/ExecuteBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Execute/ExecuteBuilder.cs index 0637d2e597..0ba2499c44 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Execute/ExecuteBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Execute/ExecuteBuilder.cs @@ -1,6 +1,7 @@ using NPoco; using Umbraco.Core.Migrations.Expressions.Common; using Umbraco.Core.Migrations.Expressions.Execute.Expressions; +using Umbraco.Core.Persistence; namespace Umbraco.Core.Migrations.Expressions.Execute { @@ -12,7 +13,16 @@ namespace Umbraco.Core.Migrations.Expressions.Execute { } /// - public void Do() => Expression.Execute(); + public void Do() + { + // slightly awkward, but doing it right would mean a *lot* + // of changes for MigrationExpressionBase + + if (Expression.SqlObject == null) + Expression.Execute(); + else + Expression.ExecuteSqlObject(); + } /// public IExecutableBuilder Sql(string sqlStatement) @@ -20,5 +30,12 @@ namespace Umbraco.Core.Migrations.Expressions.Execute Expression.SqlStatement = sqlStatement; return this; } + + /// + public IExecutableBuilder Sql(Sql sql) + { + Expression.SqlObject = sql; + return this; + } } } diff --git a/src/Umbraco.Core/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs b/src/Umbraco.Core/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs index b5c1fbdf6b..8b5da4f270 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs @@ -1,4 +1,5 @@ using NPoco; +using Umbraco.Core.Persistence; namespace Umbraco.Core.Migrations.Expressions.Execute.Expressions { @@ -10,6 +11,13 @@ namespace Umbraco.Core.Migrations.Expressions.Execute.Expressions public virtual string SqlStatement { get; set; } + public virtual Sql SqlObject { get; set; } + + public void ExecuteSqlObject() + { + Execute(SqlObject); + } + protected override string GetSql() { return SqlStatement; diff --git a/src/Umbraco.Core/Migrations/Expressions/Execute/IExecuteBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Execute/IExecuteBuilder.cs index 5747eb2c1a..7f575fd3f8 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Execute/IExecuteBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Execute/IExecuteBuilder.cs @@ -1,4 +1,6 @@ -using Umbraco.Core.Migrations.Expressions.Common; +using NPoco; +using Umbraco.Core.Migrations.Expressions.Common; +using Umbraco.Core.Persistence; namespace Umbraco.Core.Migrations.Expressions.Execute { @@ -12,5 +14,10 @@ namespace Umbraco.Core.Migrations.Expressions.Execute /// Specifies the Sql statement to execute. /// IExecutableBuilder Sql(string sqlStatement); + + /// + /// Specifies the Sql statement to execute. + /// + IExecutableBuilder Sql(Sql sql); } } diff --git a/src/Umbraco.Core/Migrations/IPostMigration.cs b/src/Umbraco.Core/Migrations/IPostMigration.cs index 827a09e882..15daf0fc74 100644 --- a/src/Umbraco.Core/Migrations/IPostMigration.cs +++ b/src/Umbraco.Core/Migrations/IPostMigration.cs @@ -1,10 +1,11 @@ using Semver; +using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; namespace Umbraco.Core.Migrations { - public interface IPostMigration + public interface IPostMigration : IDiscoverable { void Execute(string name, IScope scope, SemVersion originVersion, SemVersion targetVersion, ILogger logger); } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs index 4986eeab02..e2c34c31cb 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs @@ -33,6 +33,9 @@ namespace Umbraco.Core.Migrations.Install private DatabaseSchemaResult _databaseSchemaValidationResult; + /// + /// Initializes a new instance of the class. + /// public DatabaseBuilder(IScopeProvider scopeProvider, IGlobalSettings globalSettings, IUmbracoDatabaseFactory databaseFactory, IRuntimeState runtime, ILogger logger, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations) { _scopeProvider = scopeProvider; @@ -49,23 +52,20 @@ namespace Umbraco.Core.Migrations.Install /// /// Gets a value indicating whether the database is configured. It does not necessarily - /// mean that it is possible to connect, nor that Umbraco is installed, nor - /// up-to-date. + /// mean that it is possible to connect, nor that Umbraco is installed, nor up-to-date. /// public bool IsDatabaseConfigured => _databaseFactory.Configured; /// - /// Gets a value indicating whether it is possible to connect to the database. + /// Gets a value indicating whether it is possible to connect to the configured database. + /// It does not necessarily mean that Umbraco is installed, nor up-to-date. /// - public bool CanConnect => _databaseFactory.CanConnect; + public bool CanConnectToDatabase => _databaseFactory.CanConnect; - // that method was originally created by Per in DatabaseHelper- tests the db connection for install - // fixed by Shannon to not-ignore the provider - // fixed by Stephan as part of the v8 persistence cleanup, now using provider names + SqlCe exception - // moved by Stephan to DatabaseBuilder - // probably needs to be cleaned up - - public bool CheckConnection(string databaseType, string connectionString, string server, string database, string login, string password, bool integratedAuth) + /// + /// Verifies whether a it is possible to connect to a database. + /// + public bool CanConnect(string databaseType, string connectionString, string server, string database, string login, string password, bool integratedAuth) { // we do not test SqlCE connection if (databaseType.InvariantContains("sqlce")) @@ -93,7 +93,7 @@ namespace Umbraco.Core.Migrations.Install return DbConnectionExtensions.IsConnectionAvailable(connectionString, providerName); } - public bool HasSomeNonDefaultUser() + internal bool HasSomeNonDefaultUser() { using (var scope = _scopeProvider.CreateScope()) { @@ -120,7 +120,7 @@ namespace Umbraco.Core.Migrations.Install #region Configure Connection String - private const string EmbeddedDatabaseConnectionString = @"Data Source=|DataDirectory|\Umbraco.sdf;Flush Interval=1;"; + public const string EmbeddedDatabaseConnectionString = @"Data Source=|DataDirectory|\Umbraco.sdf;Flush Interval=1;"; /// /// Configures a connection string for the embedded database. @@ -369,65 +369,26 @@ namespace Umbraco.Core.Migrations.Install #endregion - #region Utils - - internal static void GiveLegacyAChance(IUmbracoDatabaseFactory factory, ILogger logger) - { - // look for the legacy appSettings key - var legacyConnString = ConfigurationManager.AppSettings[Constants.System.UmbracoConnectionName]; - if (string.IsNullOrWhiteSpace(legacyConnString)) return; - - var test = legacyConnString.ToLowerInvariant(); - if (test.Contains("sqlce4umbraco")) - { - // sql ce - ConfigureEmbeddedDatabaseConnection(factory, logger); - } - else if (test.Contains("tcp:")) - { - // sql azure - SaveConnectionString(legacyConnString, Constants.DbProviderNames.SqlServer, logger); - factory.Configure(legacyConnString, Constants.DbProviderNames.SqlServer); - } - else if (test.Contains("datalayer=mysql")) - { - // mysql - - // strip the datalayer part off - var connectionStringWithoutDatalayer = string.Empty; - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var variable in legacyConnString.Split(';').Where(x => x.ToLowerInvariant().StartsWith("datalayer") == false)) - connectionStringWithoutDatalayer = $"{connectionStringWithoutDatalayer}{variable};"; - - SaveConnectionString(connectionStringWithoutDatalayer, Constants.DbProviderNames.MySql, logger); - factory.Configure(connectionStringWithoutDatalayer, Constants.DbProviderNames.MySql); - } - else - { - // sql server - SaveConnectionString(legacyConnString, Constants.DbProviderNames.SqlServer, logger); - factory.Configure(legacyConnString, Constants.DbProviderNames.SqlServer); - } - - // remove the legacy connection string, so we don't end up in a loop if something goes wrong - GlobalSettings.RemoveSetting(Constants.System.UmbracoConnectionName); - } - - #endregion - #region Database Schema - internal DatabaseSchemaResult ValidateDatabaseSchema() + /// + /// Validates the database schema. + /// + /// + /// This assumes that the database exists and the connection string is + /// configured and it is possible to connect to the database. + /// + internal DatabaseSchemaResult ValidateSchema() { using (var scope = _scopeProvider.CreateScope()) { - var result = ValidateDatabaseSchema(scope); + var result = ValidateSchema(scope); scope.Complete(); return result; } } - private DatabaseSchemaResult ValidateDatabaseSchema(IScope scope) + private DatabaseSchemaResult ValidateSchema(IScope scope) { if (_databaseFactory.Configured == false) return new DatabaseSchemaResult(_databaseFactory.SqlContext.SqlSyntax); @@ -442,17 +403,24 @@ namespace Umbraco.Core.Migrations.Install return _databaseSchemaValidationResult; } - internal Result CreateDatabaseSchemaAndData() + /// + /// Creates the database schema and inserts initial data. + /// + /// + /// This assumes that the database exists and the connection string is + /// configured and it is possible to connect to the database. + /// + public Result CreateSchemaAndData() { using (var scope = _scopeProvider.CreateScope()) { - var result = CreateDatabaseSchemaAndData(scope); + var result = CreateSchemaAndData(scope); scope.Complete(); return result; } } - private Result CreateDatabaseSchemaAndData(IScope scope) + private Result CreateSchemaAndData(IScope scope) { try { @@ -468,28 +436,14 @@ namespace Umbraco.Core.Migrations.Install // If MySQL, we're going to ensure that database calls are maintaining proper casing as to remove the necessity for checks // for case insensitive queries. In an ideal situation (which is what we're striving for), all calls would be case sensitive. - - /* - var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database); - if (supportsCaseInsensitiveQueries == false) - { - message = "

 

The database you're trying to use does not support case insensitive queries.
We currently do not support these types of databases.

" + - "

You can fix this by changing the following setting in your my.ini file in your MySQL installation directory:

" + - "
lower_case_table_names=1

" + - "

Note: Make sure to check with your hosting provider if they support case insensitive queries as well.

" + - "

For more technical information on case sensitivity in MySQL, have a look at " + - "the documentation on the subject

"; - - return new Result { Message = message, Success = false, Percentage = "15" }; - } - */ - - var message = GetResultMessageForMySql(); - var schemaResult = ValidateDatabaseSchema(); - var installedSchemaVersion = schemaResult.DetermineInstalledVersion(); + var message = database.DatabaseType.IsMySql() ? ResultMessageForMySql : ""; + var schemaResult = ValidateSchema(); + var hasInstalledVersion = schemaResult.DetermineHasInstalledVersion(); + //var installedSchemaVersion = schemaResult.DetermineInstalledVersion(); + //var hasInstalledVersion = !installedSchemaVersion.Equals(new Version(0, 0, 0)); //If Configuration Status is empty and the determined version is "empty" its a new install - otherwise upgrade the existing - if (string.IsNullOrEmpty(_globalSettings.ConfigurationStatus) && installedSchemaVersion.Equals(new Version(0, 0, 0))) + if (string.IsNullOrEmpty(_globalSettings.ConfigurationStatus) && !hasInstalledVersion) { if (_runtime.Level == RuntimeLevel.Run) throw new Exception("Umbraco is already configured!"); @@ -521,8 +475,15 @@ namespace Umbraco.Core.Migrations.Install } } - // This assumes all of the previous checks are done! - internal Result UpgradeSchemaAndData() + /// + /// Upgrades the database schema and data by running migrations. + /// + /// + /// This assumes that the database exists and the connection string is + /// configured and it is possible to connect to the database. + /// Runs whichever migrations need to run. + /// + public Result UpgradeSchemaAndData() { try { @@ -534,56 +495,11 @@ namespace Umbraco.Core.Migrations.Install _logger.Info("Database upgrade started"); - //var database = scope.Database; - //var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database); - - var message = GetResultMessageForMySql(); - - // fixme - remove this code - //var schemaResult = ValidateDatabaseSchema(); - // - //var installedSchemaVersion = new SemVersion(schemaResult.DetermineInstalledVersion()); - //var installedMigrationVersion = schemaResult.DetermineInstalledVersionByMigrations(migrationEntryService); - //var targetVersion = UmbracoVersion.Current; - // - ////In some cases - like upgrading from 7.2.6 -> 7.3, there will be no migration information in the database and therefore it will - //// return a version of 0.0.0 and we don't necessarily want to run all migrations from 0 -> 7.3, so we'll just ensure that the - //// migrations are run for the target version - //if (installedMigrationVersion == new SemVersion(new Version(0, 0, 0)) && installedSchemaVersion > new SemVersion(new Version(0, 0, 0))) - //{ - // //set the installedMigrationVersion to be one less than the target so the latest migrations are guaranteed to execute - // installedMigrationVersion = new SemVersion(targetVersion.SubtractRevision()); - //} - // - ////Figure out what our current installed version is. If the web.config doesn't have a version listed, then we'll use the minimum - //// version detected between the schema installed and the migrations listed in the migration table. - //// If there is a version in the web.config, we'll take the minimum between the listed migration in the db and what - //// is declared in the web.config. - // - //var currentInstalledVersion = string.IsNullOrEmpty(GlobalSettings.ConfigurationStatus) - // //Take the minimum version between the detected schema version and the installed migration version - // ? new[] { installedSchemaVersion, installedMigrationVersion }.Min() - // //Take the minimum version between the installed migration version and the version specified in the config - // : new[] { SemVersion.Parse(GlobalSettings.ConfigurationStatus), installedMigrationVersion }.Min(); - // - ////Ok, another edge case here. If the current version is a pre-release, - //// then we want to ensure all migrations for the current release are executed. - //if (currentInstalledVersion.Prerelease.IsNullOrWhiteSpace() == false) - //{ - // currentInstalledVersion = new SemVersion(currentInstalledVersion.GetVersion().SubtractRevision()); - //} + var message = _scopeProvider.SqlContext.DatabaseType.IsMySql() ? ResultMessageForMySql : ""; // upgrade - var upgrader = new UmbracoUpgrader(_scopeProvider, _migrationBuilder, _keyValueService, _postMigrations, _logger); - upgrader.Execute(); - - // fixme remove this code - //var runner = new MigrationRunner(_scopeProvider, builder, migrationEntryService, _logger, currentInstalledVersion, UmbracoVersion.SemanticVersion, Constants.System.UmbracoMigrationName); - //var upgraded = runner.Execute(/*upgrade:true*/); - //if (upgraded == false) - //{ - // throw new ApplicationException("Upgrading failed, either an error occurred during the upgrade process or an event canceled the upgrade process, see log for full details"); - //} + var upgrader = new UmbracoUpgrader(); + upgrader.Execute(_scopeProvider, _migrationBuilder, _keyValueService, _logger, _postMigrations); message = message + "

Upgrade completed!

"; @@ -599,47 +515,14 @@ namespace Umbraco.Core.Migrations.Install } } - private string GetResultMessageForMySql() - { - if (_databaseFactory.GetType() == typeof(MySqlSyntaxProvider)) - { - return "

 

Congratulations, the database step ran successfully!

" + - "

Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.

" + - "

However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries

" + - "

Make sure to check with your hosting provider if they support case insensitive queries as well.

" + - "

They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:

" + - "
lower_case_table_names=1

" + - "

For more technical information on case sensitivity in MySQL, have a look at " + - "the documentation on the subject

"; - } - return string.Empty; - } - - /* - private string GetResultMessageForMySql(bool? supportsCaseInsensitiveQueries) - { - if (supportsCaseInsensitiveQueries == null) - { - return "

 

Warning! Could not check if your database type supports case insensitive queries.
We currently do not support these databases that do not support case insensitive queries.

" + - "

You can check this by looking for the following setting in your my.ini file in your MySQL installation directory:

" + - "
lower_case_table_names=1

" + - "

Note: Make sure to check with your hosting provider if they support case insensitive queries as well.

" + - "

For more technical information on case sensitivity in MySQL, have a look at " + - "the documentation on the subject

"; - } - if (SqlSyntax.GetType() == typeof(MySqlSyntaxProvider)) - { - return "

 

Congratulations, the database step ran successfully!

" + - "

Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.

" + - "

However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries

" + - "

Make sure to check with your hosting provider if they support case insensitive queries as well.

" + - "

They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:

" + - "
lower_case_table_names=1

" + - "

For more technical information on case sensitivity in MySQL, have a look at " + - "the documentation on the subject

"; - } - return string.Empty; - }*/ + private const string ResultMessageForMySql = "

 

Congratulations, the database step ran successfully!

" + + "

Note: You're using MySQL and the database instance you're connecting to seems to support case insensitive queries.

" + + "

However, your hosting provider may not support this option. Umbraco does not currently support MySQL installs that do not support case insensitive queries

" + + "

Make sure to check with your hosting provider if they support case insensitive queries as well.

" + + "

They can check this by looking for the following setting in the my.ini file in their MySQL installation directory:

" + + "
lower_case_table_names=1

" + + "

For more technical information on case sensitivity in MySQL, have a look at " + + "the documentation on the subject

"; private Attempt CheckReadyForInstall() { @@ -675,11 +558,29 @@ namespace Umbraco.Core.Migrations.Install }; } - internal class Result + /// + /// Represents the result of a database creation or upgrade. + /// + public class Result { + /// + /// Gets or sets a value indicating whether an upgrade is required. + /// public bool RequiresUpgrade { get; set; } + + /// + /// Gets or sets the message returned by the operation. + /// public string Message { get; set; } + + /// + /// Gets or sets a value indicating whether the operation succeeded. + /// public bool Success { get; set; } + + /// + /// Gets or sets an install progress pseudo-percentage. + /// public string Percentage { get; set; } } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index eb7cafcb01..f32ea1cb6f 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -321,9 +321,9 @@ namespace Umbraco.Core.Migrations.Install { // on install, initialize the umbraco migration plan with the final state - var plan = new UmbracoPlan(); - var stateValueKey = Upgrader.GetStateValueKey(plan); - var finalState = plan.FinalState; + var upgrader = new UmbracoUpgrader(); + var stateValueKey = upgrader.StateValueKey; + var finalState = upgrader.Plan.FinalState; _database.Insert(Constants.DatabaseSchema.Tables.KeyValue, "key", false, new KeyValueDto { Key = stateValueKey, Value = finalState, Updated = DateTime.Now }); } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index 64be8161f2..5525cc4a50 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -116,8 +116,12 @@ namespace Umbraco.Core.Migrations.Install /// /// Initializes the database by creating the umbraco db schema. /// + /// This needs to execute as part of a transaction. public void InitializeDatabaseSchema() { + if (!_database.InTransaction) + throw new InvalidOperationException("Database is not in a transaction."); + var e = new DatabaseCreationEventArgs(); FireBeforeCreation(e); @@ -138,9 +142,8 @@ namespace Umbraco.Core.Migrations.Install { var result = new DatabaseSchemaResult(SqlSyntax); - //get the db index defs - result.DbIndexDefinitions = SqlSyntax.GetDefinedIndexes(_database) - .Select(x => new DbIndexDefinition(x)).ToArray(); + result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database) + .Select(x => new DbIndexDefinition(x))); result.TableDefinitions.AddRange(OrderedTables .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); @@ -279,7 +282,7 @@ namespace Umbraco.Core.Migrations.Install { //These are just column indexes NOT constraints or Keys //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); - var colIndexesInDatabase = result.DbIndexDefinitions.Select(x => x.IndexName).ToList(); + var colIndexesInDatabase = result.IndexDefinitions.Select(x => x.IndexName).ToList(); var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); //Add valid and invalid index differences to the result object @@ -410,9 +413,14 @@ namespace Umbraco.Core.Migrations.Install /// If a table with the same name already exists, the parameter will determine /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will /// not do anything if the parameter is false. + /// + /// This need to execute as part of a transaction. /// public void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) { + if (!_database.InTransaction) + throw new InvalidOperationException("Database is not in a transaction."); + var tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); var tableName = tableDefinition.Name; @@ -430,70 +438,64 @@ namespace Umbraco.Core.Migrations.Install tableExist = false; } - if (tableExist == false) - { - using (var transaction = _database.GetTransaction()) - { - //Execute the Create Table sql - var created = _database.Execute(new Sql(createSql)); - _logger.Info("Create Table '{TableName}' ({Created}): \n {Sql}", tableName, created, createSql); - - //If any statements exists for the primary key execute them here - if (string.IsNullOrEmpty(createPrimaryKeySql) == false) - { - var createdPk = _database.Execute(new Sql(createPrimaryKeySql)); - _logger.Info("Create Primary Key ({CreatedPk}):\n {Sql}", createdPk, createPrimaryKeySql); - } - - //Turn on identity insert if db provider is not mysql - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); - - //Call the NewTable-event to trigger the insert of base/default data - //OnNewTable(tableName, _db, e, _logger); - - dataCreation.InitializeBaseData(tableName); - - //Turn off identity insert if db provider is not mysql - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); - - //Special case for MySql - if (SqlSyntax is MySqlSyntaxProvider && tableName.Equals("umbracoUser")) - { - _database.Update("SET id = @IdAfter WHERE id = @IdBefore AND userLogin = @Login", new { IdAfter = 0, IdBefore = 1, Login = "admin" }); - } - - //Loop through index statements and execute sql - foreach (var sql in indexSql) - { - var createdIndex = _database.Execute(new Sql(sql)); - _logger.Info("Create Index ({CreatedIndex}):\n {Sql}", createdIndex, sql); - } - - //Loop through foreignkey statements and execute sql - foreach (var sql in foreignSql) - { - var createdFk = _database.Execute(new Sql(sql)); - _logger.Info("Create Foreign Key ({CreatedFk}):\n {Sql}", createdFk, sql); - } - - transaction.Complete(); - - if (overwrite) - { - _logger.Info("Table '{TableName}' was recreated", tableName); - } - else - { - _logger.Info("New table '{TableName}' was created", tableName); - } - } - } - else + if (tableExist) { // The table exists and was not recreated/overwritten. _logger.Info("Table '{TableName}' already exists - no changes were made", tableName); + return; + } + + //Execute the Create Table sql + var created = _database.Execute(new Sql(createSql)); + _logger.Info("Create Table '{TableName}' ({Created}): \n {Sql}", tableName, created, createSql); + + //If any statements exists for the primary key execute them here + if (string.IsNullOrEmpty(createPrimaryKeySql) == false) + { + var createdPk = _database.Execute(new Sql(createPrimaryKeySql)); + _logger.Info("Create Primary Key ({CreatedPk}):\n {Sql}", createdPk, createPrimaryKeySql); + } + + //Turn on identity insert if db provider is not mysql + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); + + //Call the NewTable-event to trigger the insert of base/default data + //OnNewTable(tableName, _db, e, _logger); + + dataCreation.InitializeBaseData(tableName); + + //Turn off identity insert if db provider is not mysql + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); + + //Special case for MySql + if (SqlSyntax is MySqlSyntaxProvider && tableName.Equals("umbracoUser")) + { + _database.Update("SET id = @IdAfter WHERE id = @IdBefore AND userLogin = @Login", new { IdAfter = 0, IdBefore = 1, Login = "admin" }); + } + + //Loop through index statements and execute sql + foreach (var sql in indexSql) + { + var createdIndex = _database.Execute(new Sql(sql)); + _logger.Info("Create Index ({CreatedIndex}):\n {Sql}", createdIndex, sql); + } + + //Loop through foreignkey statements and execute sql + foreach (var sql in foreignSql) + { + var createdFk = _database.Execute(new Sql(sql)); + _logger.Info("Create Foreign Key ({CreatedFk}):\n {Sql}", createdFk, sql); + } + + if (overwrite) + { + _logger.Info("Table '{TableName}' was recreated", tableName); + } + else + { + _logger.Info("New table '{TableName}' was created", tableName); } } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs index 0ec27cf0b1..4c68addebc 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs @@ -2,153 +2,55 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Umbraco.Core.Configuration; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Migrations.Install { - public class DatabaseSchemaResult + /// + /// Represents ... + /// + internal class DatabaseSchemaResult { - private readonly ISqlSyntaxProvider _sqlSyntax; + private readonly bool _isMySql; public DatabaseSchemaResult(ISqlSyntaxProvider sqlSyntax) { - _sqlSyntax = sqlSyntax; + _isMySql = sqlSyntax is MySqlSyntaxProvider; + Errors = new List>(); TableDefinitions = new List(); ValidTables = new List(); ValidColumns = new List(); ValidConstraints = new List(); ValidIndexes = new List(); + IndexDefinitions = new List(); } - public List> Errors { get; set; } + public List> Errors { get; } - public List TableDefinitions { get; set; } + public List TableDefinitions { get; } - public List ValidTables { get; set; } + // fixme TableDefinitions are those that should be there, IndexDefinitions are those that... are in DB? + internal List IndexDefinitions { get; } - public List ValidColumns { get; set; } + public List ValidTables { get; } - public List ValidConstraints { get; set; } + public List ValidColumns { get; } - public List ValidIndexes { get; set; } + public List ValidConstraints { get; } - internal IEnumerable DbIndexDefinitions { get; set; } + public List ValidIndexes { get; } /// - /// Determines the version of the currently installed database by detecting the current database structure + /// Determines whether the database contains an installed version. /// - /// - /// A with Major and Minor values for - /// non-empty database, otherwise "0.0.0" for empty databases. - /// - public Version DetermineInstalledVersion() + /// + /// A database contains an installed version when it contains at least one valid table. + /// + public bool DetermineHasInstalledVersion() { - // v8 = kill versions older than 7 - - //If (ValidTables.Count == 0) database is empty and we return -> new Version(0, 0, 0); - if (ValidTables.Count == 0) - return new Version(0, 0, 0); - - // FIXME - but the whole detection is borked really - return new Version(8, 0, 0); - - //If Errors is empty or if TableDefinitions tables + columns correspond to valid tables + columns then we're at current version - if (Errors.Any() == false || - (TableDefinitions.All(x => ValidTables.Contains(x.Name)) - && TableDefinitions.SelectMany(definition => definition.Columns).All(x => ValidColumns.Contains(x.Name)))) - return UmbracoVersion.Current; - - //If Errors contains umbracoApp or umbracoAppTree its pre-6.0.0 -> new Version(4, 10, 0); - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoApp") || x.Item2.InvariantEquals("umbracoAppTree")))) - { - //If Errors contains umbracoUser2app or umbracoAppTree foreignkey to umbracoApp exists its pre-4.8.0 -> new Version(4, 7, 0); - if (Errors.Any(x => - x.Item1.Equals("Constraint") - && (x.Item2.InvariantContains("umbracoUser2app_umbracoApp") - || x.Item2.InvariantContains("umbracoAppTree_umbracoApp")))) - { - return new Version(4, 7, 0); - } - - return new Version(4, 8, 0); - } - - //if the error is for umbracoServer - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoServer")))) - { - return new Version(6, 0, 0); - } - - //if the error indicates a problem with the column cmsMacroProperty.macroPropertyType then it is not version 7 - // since these columns get removed in v7 - if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMacroProperty,macroPropertyType")))) - { - //if the error is for this IX_umbracoNodeTrashed which is added in 6.2 AND in 7.1 but we do not have the above columns - // then it must mean that we aren't on 6.2 so must be 6.1 - if (Errors.Any(x => x.Item1.Equals("Index") && (x.Item2.InvariantEquals("IX_umbracoNodeTrashed")))) - { - return new Version(6, 1, 0); - } - else - { - //if there are no errors for that index, then the person must have 6.2 installed - return new Version(6, 2, 0); - } - } - - //if the error indicates a problem with the constraint FK_cms-OBSOLETE-Content_cmsContentType_nodeId then it is not version 7.2 - // since this gets added in 7.2.0 so it must be the previous version - if (Errors.Any(x => x.Item1.Equals("Constraint") && (x.Item2.InvariantEquals("FK_cms-OBSOLETE-Content_cmsContentType_nodeId")))) - { - return new Version(7, 0, 0); - } - - //if the error is for umbracoAccess it must be the previous version to 7.3 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoAccess")))) - { - return new Version(7, 2, 0); - } - - //if the error is for cms-OBSOLETE-PropertyData.dataDecimal it must be the previous version to 7.4 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cms-OBSOLETE-PropertyData,dataDecimal")))) - { - return new Version(7, 3, 0); - } - - //if the error is for umbracoRedirectUrl it must be the previous version to 7.5 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoRedirectUrl")))) - { - return new Version(7, 4, 0); - } - - //if the error indicates a problem with the column cmsMacroProperty.uniquePropertyId then it is not version 7.6 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMacroProperty,uniquePropertyId")))) - { - return new Version(7, 5, 0); - } - - //if the error is for umbracoUserGroup it must be the previous version to 7.7 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoUserStartNode")))) - { - return new Version(7, 6, 0); - } - - //if the error is for cmsMedia it must be the previous version to 7.8 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoMedia")))) - { - return new Version(7, 7, 0); - } - - //if the error is for isSensitive column it must be the previous version to 7.9 since that is when it is added - if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMemberType,isSensitive")))) - { - return new Version(7, 8, 0); - } - - return UmbracoVersion.Current; + return ValidTables.Count > 0; } /// @@ -200,9 +102,9 @@ namespace Umbraco.Core.Migrations.Install sb.AppendLine(" "); } - if (_sqlSyntax is MySqlSyntaxProvider) + if (_isMySql) { - sb.AppendLine("Please note that the constraints could not be validated because the current dataprovider is MySql."); + sb.AppendLine("Please note that the constraints could not be validated because the current data provider is MySql."); } return sb.ToString(); diff --git a/src/Umbraco.Core/Migrations/MergeBuilder.cs b/src/Umbraco.Core/Migrations/MergeBuilder.cs new file mode 100644 index 0000000000..f1eeea9dfa --- /dev/null +++ b/src/Umbraco.Core/Migrations/MergeBuilder.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Migrations +{ + /// + /// Represents a migration plan builder for merges. + /// + public class MergeBuilder + { + private readonly MigrationPlan _plan; + private readonly List _migrations = new List(); + private string _withLast; + private bool _with; + + /// + /// Initializes a new instance of the class. + /// + internal MergeBuilder(MigrationPlan plan) + { + _plan = plan; + } + + /// + /// Adds a transition to a target state through an empty migration. + /// + public MergeBuilder To(string targetState) + => To(targetState); + + /// + /// Adds a transition to a target state through a migration. + /// + public MergeBuilder To(string targetState) + where TMigration : IMigration + => To(targetState, typeof(TMigration)); + + /// + /// Adds a transition to a target state through a migration. + /// + public MergeBuilder To(string targetState, Type migration) + { + if (_with) + { + _withLast = targetState; + targetState = _plan.CreateRandomState(); + } + else + { + _migrations.Add(migration); + } + + _plan.To(targetState, migration); + return this; + } + + /// + /// Begins the second branch of the merge. + /// + public MergeBuilder With() + { + if (_with) + throw new InvalidOperationException("Cannot invoke With() twice."); + _with = true; + return this; + } + + /// + /// Completes the merge. + /// + public MigrationPlan As(string targetState) + { + if (!_with) + throw new InvalidOperationException("Cannot invoke As() without invoking With() first."); + + // reach final state + _plan.To(targetState); + + // restart at former end of branch2 + _plan.From(_withLast); + // and replay all branch1 migrations + foreach (var migration in _migrations) + _plan.To(_plan.CreateRandomState(), migration); + // reaching final state + _plan.To(targetState); + + return _plan; + } + } +} diff --git a/src/Umbraco.Core/Migrations/MigrationBuilder.cs b/src/Umbraco.Core/Migrations/MigrationBuilder.cs index 3d8c88c771..d2d2b7d32a 100644 --- a/src/Umbraco.Core/Migrations/MigrationBuilder.cs +++ b/src/Umbraco.Core/Migrations/MigrationBuilder.cs @@ -1,36 +1,20 @@ using System; -using LightInject; +using Umbraco.Core.Composing; namespace Umbraco.Core.Migrations { public class MigrationBuilder : IMigrationBuilder { - private readonly IServiceContainer _container; + private readonly IFactory _container; - public MigrationBuilder(IServiceContainer container) + public MigrationBuilder(IFactory container) { _container = container; - - // because the builder should be "per container" this ctor should run only once per container. - // - // note: constructor dependencies do NOT work with lifetimes other than transient - // see https://github.com/seesharper/LightInject/issues/294 - // - // resolve ctor dependency from GetInstance() runtimeArguments, if possible - 'factory' is - // the container, 'info' describes the ctor argument, and 'args' contains the args that - // were passed to GetInstance() - use first arg if it is the right type. - // - // for IMigrationContext - container.RegisterConstructorDependency((factory, info, args) => args.Length > 0 ? args[0] as IMigrationContext : null); } public IMigration Build(Type migrationType, IMigrationContext context) { - // LightInject .Create() is a shortcut for .Register() + .GetInstance() - // but it does not support parameters, so we do it ourselves here - - _container.Register(migrationType); - return (IMigration) _container.GetInstance(migrationType, new object[] { context }); + return (IMigration) _container.CreateInstance(migrationType, context); } } } diff --git a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs index 6ac92a07aa..8b5d9cc78c 100644 --- a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs +++ b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using NPoco; using Umbraco.Core.Logging; @@ -88,6 +87,31 @@ namespace Umbraco.Core.Migrations expression.Execute(); } + protected void Execute(Sql sql) + { + if (_executed) + throw new InvalidOperationException("This expression has already been executed."); + _executed = true; + + if (sql == null) + { + Logger.Info(GetType(), $"SQL [{Context.Index}]: "); + } + else + { + Logger.Info(GetType(), $"SQL [{Context.Index}]: {sql.ToText()}"); + Database.Execute(sql); + } + + Context.Index++; + + if (_expressions == null) + return; + + foreach (var expression in _expressions) + expression.Execute(); + } + private void ExecuteStatement(StringBuilder stmtBuilder) { var stmt = stmtBuilder.ToString(); diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs index 5c999ad6ef..85d9c1d2cc 100644 --- a/src/Umbraco.Core/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs @@ -4,6 +4,7 @@ using System.Linq; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; +using Type = System.Type; namespace Umbraco.Core.Migrations { @@ -12,8 +13,6 @@ namespace Umbraco.Core.Migrations /// public class MigrationPlan { - private readonly IMigrationBuilder _migrationBuilder; - private readonly ILogger _logger; private readonly Dictionary _transitions = new Dictionary(); private string _prevState; @@ -23,64 +22,24 @@ namespace Umbraco.Core.Migrations /// Initializes a new instance of the class. ///
/// The name of the plan. - /// The plan cannot be executed. Use this constructor e.g. when only validating the plan, - /// or trying to get its final state, without actually needing to execute it. public MigrationPlan(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); Name = name; - - // ReSharper disable once VirtualMemberCallInConstructor - // (accepted) - DefinePlan(); } /// - /// Initializes a new instance of the class. + /// Gets the transitions. /// - /// The name of the plan. - /// A migration builder. - /// A logger. - /// The plan can be executed. - public MigrationPlan(string name, IMigrationBuilder migrationBuilder, ILogger logger) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); - Name = name; - _migrationBuilder = migrationBuilder ?? throw new ArgumentNullException(nameof(migrationBuilder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - // ReSharper disable once VirtualMemberCallInConstructor - // (accepted) - DefinePlan(); - } - - /// - /// Defines the plan. - /// - protected virtual void DefinePlan() { } + public IReadOnlyDictionary Transitions => _transitions; /// /// Gets the name of the plan. /// public string Name { get; } - /// - /// Adds an empty migration from source to target state. - /// - public MigrationPlan Add(string sourceState, string targetState) - => Add(sourceState, targetState); - - /// - /// Adds a migration from source to target state. - /// - public MigrationPlan Add(string sourceState, string targetState) - where TMigration : IMigration - => Add(sourceState, targetState, typeof(TMigration)); - - /// - /// Adds a migration from source to target state. - /// - public MigrationPlan Add(string sourceState, string targetState, Type migration) + // adds a transition + private MigrationPlan Add(string sourceState, string targetState, Type migration) { if (sourceState == null) throw new ArgumentNullException(nameof(sourceState)); if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentNullOrEmptyException(nameof(targetState)); @@ -113,26 +72,26 @@ namespace Umbraco.Core.Migrations } /// - /// Chains an empty migration from chain to target state. + /// Adds a transition to a target state through an empty migration. /// - public MigrationPlan Chain(string targetState) - => Chain(targetState); + public MigrationPlan To(string targetState) + => To(targetState); /// - /// Chains a migration from chain to target state. + /// Adds a transition to a target state through a migration. /// - public MigrationPlan Chain(string targetState) + public MigrationPlan To(string targetState) where TMigration : IMigration - => Chain(targetState, typeof(TMigration)); + => To(targetState, typeof(TMigration)); /// - /// Chains a migration from chain to target state. + /// Adds a transition to a target state through a migration. /// - public MigrationPlan Chain(string targetState, Type migration) + public MigrationPlan To(string targetState, Type migration) => Add(_prevState, targetState, migration); /// - /// Sets the chain state. + /// Sets the starting state. /// public MigrationPlan From(string sourceState) { @@ -141,19 +100,46 @@ namespace Umbraco.Core.Migrations } /// - /// Copies a chain. + /// Adds a transition to a target state through a migration, replacing a previous migration. /// - /// Copies the chain going from startState to endState, with new states going from sourceState to targetState. - public MigrationPlan CopyChain(string sourceState, string startState, string endState, string targetState) + /// The new migration. + /// The migration to use to recover from the previous target state. + /// The previous target state, which we need to recover from through . + /// The new target state. + public MigrationPlan ToWithReplace(string recoverState, string targetState) + where TMigrationNew: IMigration + where TMigrationRecover : IMigration + { + To(targetState); + From(recoverState).To(targetState); + return this; + } + + /// + /// Adds a transition to a target state through a migration, replacing a previous migration. + /// + /// The new migration. + /// The previous target state, which we can recover from directly. + /// The new target state. + public MigrationPlan ToWithReplace(string recoverState, string targetState) + where TMigrationNew : IMigration + { + To(targetState); + From(recoverState).To(targetState); + return this; + } + + /// + /// Adds transitions to a target state by cloning transitions from a start state to an end state. + /// + public MigrationPlan ToWithClone(string startState, string endState, string targetState) { - if (sourceState == null) throw new ArgumentNullException(nameof(sourceState)); if (string.IsNullOrWhiteSpace(startState)) throw new ArgumentNullOrEmptyException(nameof(startState)); if (string.IsNullOrWhiteSpace(endState)) throw new ArgumentNullOrEmptyException(nameof(endState)); if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentNullOrEmptyException(nameof(targetState)); - if (sourceState == targetState) throw new ArgumentException("Source and target states cannot be identical."); + if (startState == endState) throw new ArgumentException("Start and end states cannot be identical."); - sourceState = sourceState.Trim(); startState = startState.Trim(); endState = endState.Trim(); targetState = targetState.Trim(); @@ -168,13 +154,12 @@ namespace Umbraco.Core.Migrations visited.Add(state); if (!_transitions.TryGetValue(state, out var transition)) - throw new InvalidOperationException($"There is no transition from state \"{sourceState}\"."); + throw new InvalidOperationException($"There is no transition from state \"{state}\"."); var newTargetState = transition.TargetState == endState ? targetState - : Guid.NewGuid().ToString("B").ToUpper(); - Add(sourceState, newTargetState, transition.MigrationType); - sourceState = newTargetState; + : CreateRandomState(); + To(newTargetState, transition.MigrationType); state = transition.TargetState; } @@ -182,11 +167,15 @@ namespace Umbraco.Core.Migrations } /// - /// Copies a chain. + /// Creates a random, unique state. /// - /// Copies the chain going from startState to endState, with new states going from chain to targetState. - public MigrationPlan CopyChain(string startState, string endState, string targetState) - => CopyChain(_prevState, startState, endState, targetState); + public virtual string CreateRandomState() + => Guid.NewGuid().ToString("B").ToUpper(); + + /// + /// Begins a merge. + /// + public MergeBuilder Merge() => new MergeBuilder(this); /// /// Gets the initial state. @@ -260,50 +249,94 @@ namespace Umbraco.Core.Migrations /// /// A scope. /// The state to start execution at. + /// A migration builder. + /// A logger. /// The final state. /// The plan executes within the scope, which must then be completed. - public string Execute(IScope scope, string fromState) + public string Execute(IScope scope, string fromState, IMigrationBuilder migrationBuilder, ILogger logger) { Validate(); - if (_migrationBuilder == null || _logger == null) - throw new InvalidOperationException("Cannot execute a non-executing plan."); + if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); - _logger.Info("Starting '{MigrationName}'...", Name); + logger.Info("Starting '{MigrationName}'...", Name); var origState = fromState ?? string.Empty; - _logger.Info("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState); + logger.Info("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState); if (!_transitions.TryGetValue(origState, out var transition)) throw new Exception($"Unknown state \"{origState}\"."); - var context = new MigrationContext(scope.Database, _logger); + var context = new MigrationContext(scope.Database, logger); while (transition != null) { - var migration = _migrationBuilder.Build(transition.MigrationType, context); + var migration = migrationBuilder.Build(transition.MigrationType, context); migration.Migrate(); var nextState = transition.TargetState; origState = nextState; - _logger.Info("At {OrigState}", origState); + logger.Info("At {OrigState}", origState); if (!_transitions.TryGetValue(origState, out transition)) throw new Exception($"Unknown state \"{origState}\"."); } - _logger.Info("Done (pending scope completion)."); + logger.Info("Done (pending scope completion)."); + + // safety check + if (origState != _finalState) + throw new Exception($"Internal error, reached state {origState} which is not final state {_finalState}"); - // fixme - what about post-migrations? return origState; } + /// + /// Follows a path (for tests and debugging). + /// + /// Does the same thing Execute does, but does not actually execute migrations. + internal IReadOnlyList FollowPath(string fromState = null, string toState = null) + { + toState = toState.NullOrWhiteSpaceAsNull(); + + Validate(); + + var origState = fromState ?? string.Empty; + var states = new List { origState }; + + if (!_transitions.TryGetValue(origState, out var transition)) + throw new Exception($"Unknown state \"{origState}\"."); + + while (transition != null) + { + var nextState = transition.TargetState; + origState = nextState; + states.Add(origState); + + if (nextState == toState) + { + transition = null; + continue; + } + + if (!_transitions.TryGetValue(origState, out transition)) + throw new Exception($"Unknown state \"{origState}\"."); + } + + // safety check + if (origState != (toState ?? _finalState)) + throw new Exception($"Internal error, reached state {origState} which is not state {toState ?? _finalState}"); + + return states; + } + /// /// Represents a plan transition. /// - private class Transition + public class Transition { /// /// Initializes a new instance of the class. diff --git a/src/Umbraco.Core/Migrations/PostMigrationCollectionBuilder.cs b/src/Umbraco.Core/Migrations/PostMigrationCollectionBuilder.cs index 63cdaf4454..b23d4f1c9c 100644 --- a/src/Umbraco.Core/Migrations/PostMigrationCollectionBuilder.cs +++ b/src/Umbraco.Core/Migrations/PostMigrationCollectionBuilder.cs @@ -1,16 +1,11 @@ -using LightInject; -using Umbraco.Core.Composing; +using Umbraco.Core.Composing; namespace Umbraco.Core.Migrations { public class PostMigrationCollectionBuilder : LazyCollectionBuilderBase { - public PostMigrationCollectionBuilder(IServiceContainer container) - : base(container) - { } - protected override PostMigrationCollectionBuilder This => this; - protected override ILifetime CollectionLifetime => null; // transient + protected override Lifetime CollectionLifetime => Lifetime.Transient; } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index ec49544976..51935e6517 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -2,7 +2,6 @@ using System.Configuration; using Semver; using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Upgrade.V_7_12_0; using Umbraco.Core.Migrations.Upgrade.V_8_0_0; @@ -18,14 +17,9 @@ namespace Umbraco.Core.Migrations.Upgrade /// public UmbracoPlan() : base(Constants.System.UmbracoUpgradePlanName) - { } - - /// - /// Initializes a new instance of the class. - /// - public UmbracoPlan(IMigrationBuilder migrationBuilder, ILogger logger) - : base(Constants.System.UmbracoUpgradePlanName, migrationBuilder, logger) - { } + { + DefinePlan(); + } /// /// @@ -61,8 +55,8 @@ namespace Umbraco.Core.Migrations.Upgrade } } - /// - protected override void DefinePlan() + // define the plan + protected void DefinePlan() { // MODIFYING THE PLAN // @@ -85,65 +79,50 @@ namespace Umbraco.Core.Migrations.Upgrade // upgrades from 7 to 8, and then takes care of all eventual upgrades // From("{init-7.10.0}"); - Chain("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); - Chain("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); - Chain("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}"); - Chain("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); - Chain("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); - Chain("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); - Chain("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}"); - Chain("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); - Chain("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); - Chain("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); + To("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); + To("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); + To("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}"); + To("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); + To("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); + To("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); + To("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}"); + To("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); + To("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); + To("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); + ToWithReplace("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1 + To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); - // AddVariationTables1 has been superceeded by AddVariationTables2 - //Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); - Chain("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); - // however, provide a path out of the old state - Add("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); - // resume at {76DF5CD7-A884-41A5-8DC6-7860D95B1DF5} ... + Merge().To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") // shannon added that one + .With().To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") // stephan added that one + .As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); - Chain("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); + To("{1350617A-4930-4D61-852F-E3AA9E692173}"); + To("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0 - Chain("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path - //Chain("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove, - Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state, - Add("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}", "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // and provide a path out of the conflict state - // resume at {4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4} ... + Merge() + .To("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}") // from 7.12.0 + .To("{EB34B5DC-BB87-4005-985E-D983EA496C38}") // from 7.12.0 + .To("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}") // from 7.12.0 + .To("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}") // from 7.12.0 + .With() + .To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // andy added that one + .As("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); - Chain("{1350617A-4930-4D61-852F-E3AA9E692173}"); - Chain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0 - //Chain("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove - Chain("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0 - Chain("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0 - 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 + ToWithReplace("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}", "{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // merge + ToWithReplace("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}", "{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); // merge + To("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); + To("{77874C77-93E5-4488-A404-A630907CEEF0}"); + To("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); + To("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); + To("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); + To("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); + To("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); + To("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); + To("{648A2D5F-7467-48F8-B309-E99CEEE00E2A}"); // fixed version + To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); + To("{64EBCE53-E1F0-463A-B40B-E98EFCCA8AE2}"); + To("{0009109C-A0B8-4F3F-8FEB-C137BBDDA268}"); - 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 - // resume at {8B14CEBD-EE47-4AAD-A841-93551D917F11} ... - - Chain("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // add stephan's after others, with a new target state - From("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}") // and provide a path out of stephan's - .Chain("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next - // resume at {5F4597F4-A4E0-4AFE-90B5-6D2F896830EB} ... - - //Chain("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}"); - Chain("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); - From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}") - .Chain("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); - // resume at {290C18EE-B3DE-4769-84F1-1F467F3F76DA}... - - Chain("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); - Chain("{77874C77-93E5-4488-A404-A630907CEEF0}"); - 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 @@ -152,20 +131,26 @@ namespace Umbraco.Core.Migrations.Upgrade // and then, need to support upgrading from more recent 7.x // - From("{init-7.10.1}").Chain("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.10.2}").Chain("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.10.3}").Chain("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.10.4}").Chain("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.11.0}").Chain("{init-7.10.0}"); // same as 7.10.0 - From("{init-7.11.1}").Chain("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.1}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.2}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.3}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.4}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.5}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.11.0}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.11.1}").To("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.11.2}").To("{init-7.10.0}"); // same as 7.10.0 // 7.12.0 has migrations, define a custom chain which copies the chain // going from {init-7.10.0} to former final (1350617A) , and then goes straight to // main chain, skipping the migrations // From("{init-7.12.0}"); - // copy from copy to (former final) main chain - CopyChain("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); + // start stop target + ToWithClone("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); + + From("{init-7.12.1}").To("{init-7.10.0}"); // same as 7.12.0 + From("{init-7.12.2}").To("{init-7.10.0}"); // same as 7.12.0 + From("{init-7.12.3}").To("{init-7.10.0}"); // same as 7.12.0 } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs index b24ad2a20e..fa29e80a6b 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoUpgrader.cs @@ -8,24 +8,41 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Migrations.Upgrade { + /// + /// Represents the Umbraco upgrader. + /// public class UmbracoUpgrader : Upgrader { - public UmbracoUpgrader(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations, ILogger logger) - : base(scopeProvider, migrationBuilder, keyValueService, postMigrations, logger) + private PostMigrationCollection _postMigrations; + + /// + /// Initializes a new instance of the class. + /// + public UmbracoUpgrader() + : base(new UmbracoPlan()) { } - protected override MigrationPlan GetPlan() + /// + /// Executes. + /// + public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger, PostMigrationCollection postMigrations) { - return new UmbracoPlan(MigrationBuilder, Logger); + _postMigrations = postMigrations; + Execute(scopeProvider, migrationBuilder, keyValueService, logger); } - protected override (SemVersion, SemVersion) GetVersions() + /// + public override void AfterMigrations(IScope scope, ILogger logger) { - // assume we have something in web.config that makes some sense - if (!SemVersion.TryParse(ConfigurationManager.AppSettings["umbracoConfigurationStatus"], out var currentVersion)) + // assume we have something in web.config that makes some sense = the origin version + if (!SemVersion.TryParse(ConfigurationManager.AppSettings["umbracoConfigurationStatus"], out var originVersion)) throw new InvalidOperationException("Could not get current version from web.config umbracoConfigurationStatus appSetting."); - return (currentVersion, UmbracoVersion.SemanticVersion); + // target version is the code version + var targetVersion = UmbracoVersion.SemanticVersion; + + foreach (var postMigration in _postMigrations) + postMigration.Execute(Name, scope, originVersion, targetVersion, logger); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs index 974ed7b4f8..f6df52bc1e 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs @@ -1,49 +1,60 @@ using System; -using Semver; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; using Umbraco.Core.Services; namespace Umbraco.Core.Migrations.Upgrade { - public abstract class Upgrader + /// + /// Represents an upgrader. + /// + public class Upgrader { - private readonly IKeyValueService _keyValueService; - private readonly PostMigrationCollection _postMigrations; - private MigrationPlan _plan; - - protected Upgrader(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, PostMigrationCollection postMigrations, ILogger logger) + /// + /// Initializes a new instance of the class. + /// + public Upgrader(MigrationPlan plan) { - ScopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); - MigrationBuilder = migrationBuilder ?? throw new ArgumentNullException(nameof(migrationBuilder)); - _keyValueService = keyValueService ?? throw new ArgumentNullException(nameof(keyValueService)); - _postMigrations = postMigrations ?? throw new ArgumentNullException(nameof(postMigrations)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + Plan = plan; } + /// + /// Gets the name of the migration plan. + /// public string Name => Plan.Name; - public string StateValueKey => GetStateValueKey(Plan); + /// + /// Gets the migration plan. + /// + public MigrationPlan Plan { get; } - protected IScopeProvider ScopeProvider { get; } + /// + /// Gets the key for the state value. + /// + public virtual string StateValueKey => "Umbraco.Core.Upgrader.State+" + Name; - protected IMigrationBuilder MigrationBuilder { get; } - - protected ILogger Logger { get; } - - protected MigrationPlan Plan => _plan ?? (_plan = GetPlan()); - - protected abstract MigrationPlan GetPlan(); - protected abstract (SemVersion, SemVersion) GetVersions(); - - public void Execute() + /// + /// Executes. + /// + /// A scope provider. + /// A migration builder. + /// A key-value service. + /// A logger. + public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger) { + if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider)); + if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder)); + if (keyValueService == null) throw new ArgumentNullException(nameof(keyValueService)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + var plan = Plan; - using (var scope = ScopeProvider.CreateScope()) + using (var scope = scopeProvider.CreateScope()) { + BeforeMigrations(scope, logger); + // read current state - var currentState = _keyValueService.GetValue(StateValueKey); + var currentState = keyValueService.GetValue(StateValueKey); var forceState = false; if (currentState == null) @@ -53,25 +64,33 @@ namespace Umbraco.Core.Migrations.Upgrade } // execute plan - var state = plan.Execute(scope, currentState); + var state = plan.Execute(scope, currentState, migrationBuilder, logger); if (string.IsNullOrWhiteSpace(state)) throw new Exception("Plan execution returned an invalid null or empty state."); // save new state if (forceState) - _keyValueService.SetValue(StateValueKey, state); + keyValueService.SetValue(StateValueKey, state); else if (currentState != state) - _keyValueService.SetValue(StateValueKey, currentState, state); + keyValueService.SetValue(StateValueKey, currentState, state); - // run post-migrations - (var originVersion, var targetVersion) = GetVersions(); - foreach (var postMigration in _postMigrations) - postMigration.Execute(Name, scope, originVersion, targetVersion, Logger); + AfterMigrations(scope, logger); scope.Complete(); } } - public static string GetStateValueKey(MigrationPlan plan) => "Umbraco.Core.Upgrader.State+" + plan.Name; + /// + /// Executes as part of the upgrade scope and before all migrations have executed. + /// + public virtual void BeforeMigrations(IScope scope, ILogger logger) + { } + + /// + /// Executes as part of the upgrade scope and after all migrations have executed. + /// + public virtual void AfterMigrations(IScope scope, ILogger logger) + { } + } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs new file mode 100644 index 0000000000..1df11a3e99 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs @@ -0,0 +1,15 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class AddContentTypeIsElementColumn : MigrationBase + { + public AddContentTypeIsElementColumn(IMigrationContext context) : base(context) + { } + + public override void Migrate() + { + AddColumn("isElement"); + } + } +} 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 e8fd4f409e..008b3e4b5f 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 @@ -9,10 +9,10 @@ public override void Migrate() { - if (TableExists("cmsTaskType")) - Delete.Table("cmsTaskType"); if (TableExists("cmsTask")) - Delete.Table("cmsTask"); + Delete.Table("cmsTask").Do(); + if (TableExists("cmsTaskType")) + Delete.Table("cmsTaskType").Do(); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs new file mode 100644 index 0000000000..2e366c7c14 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs @@ -0,0 +1,29 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class MakeRedirectUrlVariant : MigrationBase + { + public MakeRedirectUrlVariant(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + AddColumn("culture"); + + Delete.Index("IX_umbracoRedirectUrl").OnTable(Constants.DatabaseSchema.Tables.RedirectUrl).Do(); + Create.Index("IX_umbracoRedirectUrl").OnTable(Constants.DatabaseSchema.Tables.RedirectUrl) + .OnColumn("urlHash") + .Ascending() + .OnColumn("contentKey") + .Ascending() + .OnColumn("culture") + .Ascending() + .OnColumn("createDateUtc") + .Ascending() + .WithOptions().Unique() + .Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs new file mode 100644 index 0000000000..9ccd6d5e76 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs @@ -0,0 +1,27 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class MakeTagsVariant : MigrationBase + { + public MakeTagsVariant(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + AddColumn("languageId"); + + Delete.Index($"IX_{Constants.DatabaseSchema.Tables.Tag}").OnTable(Constants.DatabaseSchema.Tables.Tag).Do(); + Create.Index($"IX_{Constants.DatabaseSchema.Tables.Tag}").OnTable(Constants.DatabaseSchema.Tables.Tag) + .OnColumn("group") + .Ascending() + .OnColumn("tag") + .Ascending() + .OnColumn("languageId") + .Ascending() + .WithOptions().Unique() + .Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs index aa498583ff..6ddd49841d 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs @@ -11,7 +11,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - Delete.Column("edited").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + if (ColumnExists(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "edited")) + Delete.Column("edited").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); // add available column diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 3f6e387dec..0c679e5e70 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Models public class Content : ContentBase, IContent { private IContentType _contentType; - private ITemplate _template; + private int? _templateId; private ContentScheduleCollection _schedule; private bool _published; private PublishedState _publishedState; @@ -83,7 +83,7 @@ namespace Umbraco.Core.Models // ReSharper disable once ClassNeverInstantiated.Local private class PropertySelectors { - public readonly PropertyInfo TemplateSelector = ExpressionHelper.GetPropertyInfo(x => x.Template); + public readonly PropertyInfo TemplateSelector = ExpressionHelper.GetPropertyInfo(x => x.TemplateId); public readonly PropertyInfo PublishedSelector = ExpressionHelper.GetPropertyInfo(x => x.Published); public readonly PropertyInfo ContentScheduleSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentSchedule); public readonly PropertyInfo PublishCultureInfosSelector = ExpressionHelper.GetPropertyInfo>(x => x.PublishCultureInfos); @@ -131,10 +131,10 @@ namespace Umbraco.Core.Models /// the Default template from the ContentType will be returned. /// [DataMember] - public ITemplate Template + public int? TemplateId { - get => _template ?? _contentType.DefaultTemplate; - set => SetPropertyValueAndDetectChanges(value, ref _template, Ps.Value.TemplateSelector); + get => _templateId; + set => SetPropertyValueAndDetectChanges(value, ref _templateId, Ps.Value.TemplateSelector); } @@ -193,7 +193,7 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] - public ITemplate PublishTemplate { get; internal set; } // set by persistence + public int? PublishTemplateId { get; internal set; } // set by persistence /// [IgnoreDataMember] @@ -457,9 +457,7 @@ namespace Umbraco.Core.Models public override void ResetDirtyProperties(bool rememberDirty) { base.ResetDirtyProperties(rememberDirty); - - if (Template != null) - Template.ResetDirtyProperties(rememberDirty); + if (ContentType != null) ContentType.ResetDirtyProperties(rememberDirty); 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/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs index 46813bdb45..4c06f8927d 100644 --- a/src/Umbraco.Core/Models/ContentScheduleCollection.cs +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -169,6 +169,7 @@ namespace Umbraco.Core.Models /// Gets the schedule for a culture /// /// + /// /// public IEnumerable GetSchedule(string culture, ContentScheduleAction? action = null) { diff --git a/src/Umbraco.Core/Models/ContentTagsExtensions.cs b/src/Umbraco.Core/Models/ContentTagsExtensions.cs index 8aceaac762..dd7a716520 100644 --- a/src/Umbraco.Core/Models/ContentTagsExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTagsExtensions.cs @@ -15,10 +15,10 @@ namespace Umbraco.Core.Models /// The property alias. /// The tags. /// A value indicating whether to merge the tags with existing tags instead of replacing them. - /// Tags do not support variants. - public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool merge = false) + /// A culture, for multi-lingual properties. + public static void AssignTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool merge = false, string culture = null) { - content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge); + content.GetTagProperty(propertyTypeAlias).AssignTags(tags, merge, culture); } /// @@ -27,10 +27,10 @@ namespace Umbraco.Core.Models /// The content item. /// The property alias. /// The tags. - /// Tags do not support variants. - public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags) + /// A culture, for multi-lingual properties. + public static void RemoveTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, string culture = null) { - content.GetTagProperty(propertyTypeAlias).RemoveTags(tags); + content.GetTagProperty(propertyTypeAlias).RemoveTags(tags, culture); } // gets and validates the property diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index caa63d7526..b6ea9f50a0 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -26,6 +26,7 @@ namespace Umbraco.Core.Models private string _thumbnail = "folder.png"; private bool _allowedAsRoot; // note: only one that's not 'pure element type' private bool _isContainer; + private bool _isElement; private PropertyGroupCollection _propertyGroups; private PropertyTypeCollection _noGroupPropertyTypes; private IEnumerable _allowedContentTypes; @@ -90,10 +91,11 @@ namespace Umbraco.Core.Models public readonly PropertyInfo IconSelector = ExpressionHelper.GetPropertyInfo(x => x.Icon); public readonly PropertyInfo ThumbnailSelector = ExpressionHelper.GetPropertyInfo(x => x.Thumbnail); public readonly PropertyInfo AllowedAsRootSelector = ExpressionHelper.GetPropertyInfo(x => x.AllowedAsRoot); + public readonly PropertyInfo IsElementSelector = ExpressionHelper.GetPropertyInfo(x => x.IsElement); 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 +108,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); } /// @@ -180,6 +182,14 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _isContainer, Ps.Value.IsContainerSelector); } + /// + [DataMember] + public bool IsElement + { + get => _isElement; + set => SetPropertyValueAndDetectChanges(value, ref _isElement, Ps.Value.IsElementSelector); + } + /// /// Gets or sets a list of integer Ids for allowed ContentTypes /// @@ -263,6 +273,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 +388,7 @@ namespace Umbraco.Core.Models if (!HasPropertyTypeBeenRemoved) { HasPropertyTypeBeenRemoved = true; - OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyTypesSelector); } break; } @@ -388,7 +400,7 @@ namespace Umbraco.Core.Models if (!HasPropertyTypeBeenRemoved) { HasPropertyTypeBeenRemoved = true; - OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyTypesSelector); } } } @@ -412,7 +424,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/ContentTypeBaseExtensions.cs b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs index 8af48bb881..adbc3de54f 100644 --- a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs @@ -15,7 +15,8 @@ namespace Umbraco.Core.Models { var type = contentType.GetType(); var itemType = PublishedItemType.Unknown; - if (typeof(IContentType).IsAssignableFrom(type)) itemType = PublishedItemType.Content; + if (contentType.IsElement) itemType = PublishedItemType.Element; + else if (typeof(IContentType).IsAssignableFrom(type)) itemType = PublishedItemType.Content; else if (typeof(IMediaType).IsAssignableFrom(type)) itemType = PublishedItemType.Media; else if (typeof(IMemberType).IsAssignableFrom(type)) itemType = PublishedItemType.Member; return itemType; 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 a414a03d2f..056602f007 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -18,9 +18,9 @@ namespace Umbraco.Core.Models ContentScheduleCollection ContentSchedule { get; set; } /// - /// Gets or sets the template used to render the content. + /// Gets or sets the template id used to render the content. /// - ITemplate Template { get; set; } + int? TemplateId { get; set; } /// /// Gets a value indicating whether the content is published. @@ -45,10 +45,10 @@ namespace Umbraco.Core.Models bool Blueprint { get; } /// - /// Gets the template used to render the published version of the content. + /// Gets the template id used to render the published version of the content. /// /// When editing the content, the template can change, but this will not until the content is published. - ITemplate PublishTemplate { get; } + int? PublishTemplateId { get; } /// /// Gets the name of the published version of the content. diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index a1d4aee02f..787e347b37 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -25,7 +25,7 @@ namespace Umbraco.Core.Models /// the icon (eg. icon-home) along with an optional CSS class name representing the /// color (eg. icon-blue). Put together, the value for this scenario would be /// icon-home color-blue. - /// + /// /// If a class name for the color isn't specified, the icon color will default to black. /// string Icon { get; set; } @@ -48,6 +48,16 @@ namespace Umbraco.Core.Models /// bool IsContainer { get; set; } + /// + /// Gets or sets a value indicating whether this content type is for an element. + /// + /// + /// By default a content type is for a true media, member or document, but + /// it can also be for an element, ie a subset that can for instance be used in + /// nested content. + /// + bool IsElement { get; set; } + /// /// Gets or sets the content variation of the content type. /// diff --git a/src/Umbraco.Core/Models/IRedirectUrl.cs b/src/Umbraco.Core/Models/IRedirectUrl.cs index f3c65fe89c..e066881645 100644 --- a/src/Umbraco.Core/Models/IRedirectUrl.cs +++ b/src/Umbraco.Core/Models/IRedirectUrl.cs @@ -27,11 +27,18 @@ namespace Umbraco.Core.Models [DataMember] DateTime CreateDateUtc { get; set; } + /// + /// Gets or sets the culture. + /// + [DataMember] + string Culture { get; set; } + /// /// Gets or sets the redirect url route. /// /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo. [DataMember] string Url { get; set; } + } } diff --git a/src/Umbraco.Core/Models/ITag.cs b/src/Umbraco.Core/Models/ITag.cs index 6f492a2d78..f2c30b2644 100644 --- a/src/Umbraco.Core/Models/ITag.cs +++ b/src/Umbraco.Core/Models/ITag.cs @@ -20,6 +20,12 @@ namespace Umbraco.Core.Models [DataMember] string Text { get; set; } + /// + /// Gets or sets the tag language. + /// + [DataMember] + int? LanguageId { get; set; } + /// /// Gets the number of nodes tagged with this tag. /// diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 23c232324a..dcf86a0b42 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Security.Claims; using System.Threading.Tasks; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; @@ -66,7 +67,7 @@ namespace Umbraco.Core.Models.Identity _startContentIds = new int[] { }; _groups = new IReadOnlyUserGroup[] { }; _allowedSections = new string[] { }; - _culture = UmbracoConfig.For.GlobalSettings().DefaultUILanguage; //fixme inject somehow? + _culture = Current.Configs.Global().DefaultUILanguage; //fixme inject somehow? _groups = new IReadOnlyUserGroup[0]; _roles = new ObservableCollection>(); _roles.CollectionChanged += _roles_CollectionChanged; @@ -83,7 +84,7 @@ namespace Umbraco.Core.Models.Identity _startContentIds = new int[] { }; _groups = new IReadOnlyUserGroup[] { }; _allowedSections = new string[] { }; - _culture = UmbracoConfig.For.GlobalSettings().DefaultUILanguage; //fixme inject somehow? + _culture = Current.Configs.Global().DefaultUILanguage; //fixme inject somehow? _groups = groups.ToArray(); _roles = new ObservableCollection>(_groups.Select(x => new IdentityUserRole { @@ -442,6 +443,6 @@ namespace Umbraco.Core.Models.Identity groups => groups.GetHashCode()); } - + } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index e190c8ad3b..03f8f87cd3 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -88,7 +88,7 @@ namespace Umbraco.Core.Models try { - var globalSettings = (IGlobalSettings) Composing.Current.Container.GetInstance(typeof(IGlobalSettings)); + var globalSettings = (IGlobalSettings) Composing.Current.Factory.GetInstance(typeof(IGlobalSettings)); var defaultUiCulture = CultureInfo.GetCultureInfo(globalSettings.DefaultUILanguage); Thread.CurrentThread.CurrentUICulture = defaultUiCulture; diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 0694194996..650aa6cb29 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -27,7 +27,7 @@ namespace Umbraco.Core.Models.Membership { SessionTimeout = 60; _userGroups = new HashSet(); - _language = UmbracoConfig.For.GlobalSettings().DefaultUILanguage; //fixme inject somehow? + _language = Current.Configs.Global().DefaultUILanguage; //fixme inject somehow? _isApproved = true; _isLockedOut = false; _startContentIds = new int[] { }; @@ -453,7 +453,7 @@ namespace Umbraco.Core.Models.Membership base.PerformDeepClone(clone); var clonedEntity = (User)clone; - + //manually clone the start node props clonedEntity._startContentIds = _startContentIds.ToArray(); clonedEntity._startMediaIds = _startMediaIds.ToArray(); @@ -483,7 +483,7 @@ namespace Umbraco.Core.Models.Membership //need to create new collections otherwise they'll get copied by ref clonedEntity._userGroups = new HashSet(_userGroups); clonedEntity._allowedSections = _allowedSections != null ? new List(_allowedSections) : null; - + } /// 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/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs index 26779161a1..39172fff34 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -38,13 +38,13 @@ namespace Umbraco.Core.Models } /// - /// Assign default tags. + /// Assign tags. /// /// The property. /// The tags. /// A value indicating whether to merge the tags with existing tags instead of replacing them. - /// Tags do not support variants. - public static void AssignTags(this Property property, IEnumerable tags, bool merge = false) + /// A culture, for multi-lingual properties. + public static void AssignTags(this Property property, IEnumerable tags, bool merge = false, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); @@ -52,11 +52,11 @@ namespace Umbraco.Core.Models if (configuration == null) throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - property.AssignTags(tags, merge, configuration.StorageType, configuration.Delimiter); + property.AssignTags(tags, merge, configuration.StorageType, configuration.Delimiter, culture); } // assumes that parameters are consistent with the datatype configuration - internal static void AssignTags(this Property property, IEnumerable tags, bool merge, TagsStorageType storageType, char delimiter) + private static void AssignTags(this Property property, IEnumerable tags, bool merge, TagsStorageType storageType, char delimiter, string culture) { // set the property value var trimmedTags = tags.Select(x => x.Trim()).ToArray(); @@ -68,11 +68,11 @@ namespace Umbraco.Core.Models switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags))); // csv string + property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)), culture); // csv string break; case TagsStorageType.Json: - property.SetValue(JsonConvert.SerializeObject(currentTags.Union(trimmedTags).ToArray())); // json array + property.SetValue(JsonConvert.SerializeObject(currentTags.Union(trimmedTags).ToArray()), culture); // json array break; } } @@ -81,23 +81,23 @@ namespace Umbraco.Core.Models switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), trimmedTags)); // csv string + property.SetValue(string.Join(delimiter.ToString(), trimmedTags), culture); // csv string break; case TagsStorageType.Json: - property.SetValue(JsonConvert.SerializeObject(trimmedTags)); // json array + property.SetValue(JsonConvert.SerializeObject(trimmedTags), culture); // json array break; } } } /// - /// Removes default tags. + /// Removes tags. /// /// The property. /// The tags. - /// Tags do not support variants. - public static void RemoveTags(this Property property, IEnumerable tags) + /// A culture, for multi-lingual properties. + public static void RemoveTags(this Property property, IEnumerable tags, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); @@ -105,33 +105,33 @@ namespace Umbraco.Core.Models if (configuration == null) throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - property.RemoveTags(tags, configuration.StorageType, configuration.Delimiter); + property.RemoveTags(tags, configuration.StorageType, configuration.Delimiter, culture); } // assumes that parameters are consistent with the datatype configuration - private static void RemoveTags(this Property property, IEnumerable tags, TagsStorageType storageType, char delimiter) + private static void RemoveTags(this Property property, IEnumerable tags, TagsStorageType storageType, char delimiter, string culture) { // already empty = nothing to do - //fixme doesn't take into account variants - var value = property.GetValue()?.ToString(); + var value = property.GetValue(culture)?.ToString(); if (string.IsNullOrWhiteSpace(value)) return; // set the property value var trimmedTags = tags.Select(x => x.Trim()).ToArray(); - var currentTags = property.GetTagsValue(storageType, delimiter); + var currentTags = property.GetTagsValue(storageType, delimiter, culture); switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags))); // csv string + property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)), culture); // csv string break; case TagsStorageType.Json: - property.SetValue(JsonConvert.SerializeObject(currentTags.Except(trimmedTags).ToArray())); // json array + property.SetValue(JsonConvert.SerializeObject(currentTags.Except(trimmedTags).ToArray()), culture); // json array break; } } - internal static IEnumerable GetTagsValue(this Property property) + // used by ContentRepositoryBase + internal static IEnumerable GetTagsValue(this Property property, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); @@ -139,15 +139,14 @@ namespace Umbraco.Core.Models if (configuration == null) throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - return property.GetTagsValue(configuration.StorageType, configuration.Delimiter); + return property.GetTagsValue(configuration.StorageType, configuration.Delimiter, culture); } - internal static IEnumerable GetTagsValue(this Property property, TagsStorageType storageType, char delimiter) + private static IEnumerable GetTagsValue(this Property property, TagsStorageType storageType, char delimiter, string culture = null) { if (property == null) throw new ArgumentNullException(nameof(property)); - //fixme doesn't take into account variants - var value = property.GetValue()?.ToString(); + var value = property.GetValue(culture)?.ToString(); if (string.IsNullOrWhiteSpace(value)) return Enumerable.Empty(); switch (storageType) @@ -158,7 +157,6 @@ namespace Umbraco.Core.Models case TagsStorageType.Json: try { - //fixme doesn't take into account variants return JsonConvert.DeserializeObject(value).Select(x => x.ToString().Trim()); } catch (JsonException) @@ -178,34 +176,33 @@ namespace Umbraco.Core.Models /// The property. /// The property value. /// The datatype configuration. - /// + /// A culture, for multi-lingual properties. + /// /// The value is either a string (delimited string) or an enumeration of strings (tag list). /// This is used both by the content repositories to initialize a property with some tag values, and by the /// content controllers to update a property with values received from the property editor. /// - internal static void SetTagsValue(this Property property, object value, TagConfiguration tagConfiguration) + internal static void SetTagsValue(this Property property, object value, TagConfiguration tagConfiguration, string culture) { if (property == null) throw new ArgumentNullException(nameof(property)); if (tagConfiguration == null) throw new ArgumentNullException(nameof(tagConfiguration)); - var merge = false; // fixme always! var storageType = tagConfiguration.StorageType; var delimiter = tagConfiguration.Delimiter; - SetTagsValue(property, value, merge, storageType, delimiter); + SetTagsValue(property, value, storageType, delimiter, culture); } // assumes that parameters are consistent with the datatype configuration // value can be an enumeration of string, or a serialized value using storageType format - // fixme merge always false here?! - private static void SetTagsValue(Property property, object value, bool merge, TagsStorageType storageType, char delimiter) + private static void SetTagsValue(Property property, object value, TagsStorageType storageType, char delimiter, string culture) { if (value == null) value = Enumerable.Empty(); // if value is already an enumeration of strings, just use it if (value is IEnumerable tags1) { - property.AssignTags(tags1, merge, storageType, delimiter); + property.AssignTags(tags1, false, storageType, delimiter, culture); return; } @@ -214,14 +211,14 @@ namespace Umbraco.Core.Models { case TagsStorageType.Csv: var tags2 = value.ToString().Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); - property.AssignTags(tags2, merge, storageType, delimiter); + property.AssignTags(tags2, false, storageType, delimiter, culture); break; case TagsStorageType.Json: try { var tags3 = JsonConvert.DeserializeObject>(value.ToString()); - property.AssignTags(tags3 ?? Enumerable.Empty(), merge, storageType, delimiter); + property.AssignTags(tags3 ?? Enumerable.Empty(), false, storageType, delimiter, culture); } catch (Exception ex) { 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/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index 5b604eff3f..4e1ce7ddd7 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -62,7 +62,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the identifier of the template to use to render the content item. /// - int TemplateId { get; } + int? TemplateId { get; } /// /// Gets the identifier of the user who created the content item. @@ -151,7 +151,7 @@ namespace Umbraco.Core.Models.PublishedContent /// is the edited version) or false (document is published, and has not been edited, and /// what is returned is the published version). /// - bool IsDraft { get; } + bool IsDraft(string culture = null); // fixme - consider having an IsPublished flag too // so that when IsDraft is true, we can check whether there is a published version? diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs index df3213eb07..bfc65b70d6 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core.Models.PublishedContent // in order to provide a nice, "fluent" experience, this extension method // needs to access Current, which is not always initialized in tests - not // very elegant, but works - if (!Current.HasContainer) return content; + if (!Current.HasFactory) return content; // get model // if factory returns nothing, throw diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index 5bdeb3685d..36755c8944 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -73,7 +73,7 @@ namespace Umbraco.Core.Models.PublishedContent public virtual string Path => _content.Path; /// - public virtual int TemplateId => _content.TemplateId; + public virtual int? TemplateId => _content.TemplateId; /// public virtual int CreatorId => _content.CreatorId; @@ -109,7 +109,7 @@ namespace Umbraco.Core.Models.PublishedContent public virtual PublishedItemType ItemType => _content.ItemType; /// - public virtual bool IsDraft => _content.IsDraft; + public virtual bool IsDraft(string culture = null) => _content.IsDraft(culture); #endregion diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs index e55fe66945..42e9c9538d 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs @@ -4,13 +4,18 @@ /// The type of published element. /// /// Can be a simple element, or a document, a media, a member. - public enum PublishedItemType // fixme - need to rename to PublishedElementType but then conflicts? + public enum PublishedItemType { /// /// Unknown. /// Unknown = 0, + /// + /// An element. + /// + Element, + /// /// A document. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs index 67758c1c69..ee3fd62985 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs @@ -112,7 +112,7 @@ namespace Umbraco.Core.Models.PublishedContent if (ctor != null) return ctor(); var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); - ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstuctor>(declaring: listType); + ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); return ctor(); } diff --git a/src/Umbraco.Core/Models/RedirectUrl.cs b/src/Umbraco.Core/Models/RedirectUrl.cs index 187d9fdd6e..55b799244e 100644 --- a/src/Umbraco.Core/Models/RedirectUrl.cs +++ b/src/Umbraco.Core/Models/RedirectUrl.cs @@ -28,12 +28,14 @@ namespace Umbraco.Core.Models public readonly PropertyInfo ContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentId); public readonly PropertyInfo ContentKeySelector = ExpressionHelper.GetPropertyInfo(x => x.ContentKey); public readonly PropertyInfo CreateDateUtcSelector = ExpressionHelper.GetPropertyInfo(x => x.CreateDateUtc); + public readonly PropertyInfo CultureSelector = ExpressionHelper.GetPropertyInfo(x => x.Culture); public readonly PropertyInfo UrlSelector = ExpressionHelper.GetPropertyInfo(x => x.Url); } private int _contentId; private Guid _contentKey; private DateTime _createDateUtc; + private string _culture; private string _url; /// @@ -57,6 +59,13 @@ namespace Umbraco.Core.Models set { SetPropertyValueAndDetectChanges(value, ref _createDateUtc, Ps.Value.CreateDateUtcSelector); } } + /// + public string Culture + { + get { return _culture; } + set { SetPropertyValueAndDetectChanges(value, ref _culture, Ps.Value.CultureSelector); } + } + /// public string Url { diff --git a/src/Umbraco.Core/Models/Tag.cs b/src/Umbraco.Core/Models/Tag.cs index 867d43c257..e9707e587d 100644 --- a/src/Umbraco.Core/Models/Tag.cs +++ b/src/Umbraco.Core/Models/Tag.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Models private string _group; private string _text; + private int? _languageId; /// /// Initializes a new instance of the class. @@ -26,11 +27,12 @@ namespace Umbraco.Core.Models /// /// Initializes a new instance of the class. /// - public Tag(int id, string group, string text) + public Tag(int id, string group, string text, int? languageId = null) { Id = id; Text = text; Group = group; + LanguageId = languageId; } private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors()); @@ -39,6 +41,7 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo Group = ExpressionHelper.GetPropertyInfo(x => x.Group); public readonly PropertyInfo Text = ExpressionHelper.GetPropertyInfo(x => x.Text); + public readonly PropertyInfo LanguageId = ExpressionHelper.GetPropertyInfo(x => x.LanguageId); } /// @@ -55,6 +58,13 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _text, Selectors.Text); } + /// + public int? LanguageId + { + get => _languageId; + set => SetPropertyValueAndDetectChanges(value, ref _languageId, Selectors.LanguageId); + } + /// public int NodeCount { get; internal set; } } diff --git a/src/Umbraco.Core/Models/TaggedEntity.cs b/src/Umbraco.Core/Models/TaggedEntity.cs index ac194c15cd..8c4695555d 100644 --- a/src/Umbraco.Core/Models/TaggedEntity.cs +++ b/src/Umbraco.Core/Models/TaggedEntity.cs @@ -5,10 +5,13 @@ namespace Umbraco.Core.Models /// /// Represents a tagged entity. /// - /// Note that it is the properties of an entity (like Content, Media, Members, etc.) that is tagged, - /// which is why this class is composed of a list of tagged properties and an Id reference to the actual entity. + /// Note that it is the properties of an entity (like Content, Media, Members, etc.) that are tagged, + /// which is why this class is composed of a list of tagged properties and the identifier the actual entity. public class TaggedEntity { + /// + /// Initializes a new instance of the class. + /// public TaggedEntity(int entityId, IEnumerable taggedProperties) { EntityId = entityId; @@ -16,13 +19,13 @@ namespace Umbraco.Core.Models } /// - /// Id of the entity, which is tagged + /// Gets the identifier of the entity. /// - public int EntityId { get; private set; } + public int EntityId { get; } /// - /// An enumerable list of tagged properties + /// Gets the tagged properties. /// - public IEnumerable TaggedProperties { get; private set; } + public IEnumerable TaggedProperties { get; } } } diff --git a/src/Umbraco.Core/Models/TaggedProperty.cs b/src/Umbraco.Core/Models/TaggedProperty.cs index 2b9650b432..2d9fda9a4f 100644 --- a/src/Umbraco.Core/Models/TaggedProperty.cs +++ b/src/Umbraco.Core/Models/TaggedProperty.cs @@ -7,6 +7,9 @@ namespace Umbraco.Core.Models /// public class TaggedProperty { + /// + /// Initializes a new instance of the class. + /// public TaggedProperty(int propertyTypeId, string propertyTypeAlias, IEnumerable tags) { PropertyTypeId = propertyTypeId; @@ -15,18 +18,18 @@ namespace Umbraco.Core.Models } /// - /// Id of the PropertyType, which this tagged property is based on + /// Gets the identifier of the property type. /// - public int PropertyTypeId { get; private set; } + public int PropertyTypeId { get; } /// - /// Alias of the PropertyType, which this tagged property is based on + /// Gets the alias of the property type. /// - public string PropertyTypeAlias { get; private set; } + public string PropertyTypeAlias { get; } /// - /// An enumerable list of Tags for the property + /// Gets the tags. /// - public IEnumerable Tags { get; private set; } + public IEnumerable Tags { get; } } } diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index ea61228864..ba4d8cf590 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -3,6 +3,7 @@ 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; @@ -55,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]; } @@ -101,7 +105,7 @@ namespace Umbraco.Core.Models } //use the custom avatar - var avatarUrl = Current.FileSystems.MediaFileSystem.GetUrl(user.Avatar); + var avatarUrl = Current.MediaFileSystem.GetUrl(user.Avatar); return new[] { avatarUrl + "?width=30&height=30&mode=crop", diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 44e5968a9f..1bc2fb48a5 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -595,7 +595,6 @@ namespace Umbraco.Core return null; } - /// /// Attempts to serialize the value to an XmlString using ToXmlString /// @@ -724,8 +723,8 @@ namespace Umbraco.Core { return typeConverter; } - - TypeConverter converter = TypeDescriptor.GetConverter(target); + + var converter = TypeDescriptor.GetConverter(target); if (converter.CanConvertFrom(source)) { return DestinationTypeConverterCache[key] = converter; @@ -788,5 +787,7 @@ namespace Umbraco.Core return BoolConvertCache[type] = false; } + + } } diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs index d930abc54c..4f3a67aa91 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs @@ -41,6 +41,10 @@ namespace Umbraco.Core.Persistence.Dtos [Constraint(Default = "0")] public bool IsContainer { get; set; } + [Column("isElement")] + [Constraint(Default = "0")] + public bool IsElement { get; set; } + [Column("allowAtRoot")] [Constraint(Default = "0")] public bool AllowAtRoot { get; set; } diff --git a/src/Umbraco.Core/Persistence/Dtos/RedirectUrlDto.cs b/src/Umbraco.Core/Persistence/Dtos/RedirectUrlDto.cs index b2bc990f6b..57e7138827 100644 --- a/src/Umbraco.Core/Persistence/Dtos/RedirectUrlDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/RedirectUrlDto.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Persistence.Dtos // notes // - // we want a unique, non-clustered index on (url ASC, contentId ASC, createDate DESC) but the + // we want a unique, non-clustered index on (url ASC, contentId ASC, culture ASC, createDate DESC) but the // problem is that the index key must be 900 bytes max. should we run without an index? done // some perfs comparisons, and running with an index on a hash is only slightly slower on // inserts, and much faster on reads, so... we have an index on a hash. @@ -41,9 +41,13 @@ namespace Umbraco.Core.Persistence.Dtos [NullSetting(NullSetting = NullSettings.NotNull)] public string Url { get; set; } + [Column("culture")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Culture { get; set; } + [Column("urlHash")] [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "urlHash, contentKey, createDateUtc")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "urlHash, contentKey, culture, createDateUtc")] [Length(40)] public string UrlHash { get; set; } } diff --git a/src/Umbraco.Core/Persistence/Dtos/TagDto.cs b/src/Umbraco.Core/Persistence/Dtos/TagDto.cs index 15c309d9e5..f6296e4bd0 100644 --- a/src/Umbraco.Core/Persistence/Dtos/TagDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/TagDto.cs @@ -3,11 +3,13 @@ using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos { - [TableName(Constants.DatabaseSchema.Tables.Tag)] + [TableName(TableName)] [PrimaryKey("id")] [ExplicitColumns] internal class TagDto { + public const string TableName = Constants.DatabaseSchema.Tables.Tag; + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } @@ -16,9 +18,15 @@ namespace Umbraco.Core.Persistence.Dtos [Length(100)] public string Group { get; set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get;set; } + [Column("tag")] [Length(200)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag", Name = "IX_cmsTags")] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag,languageId", Name = "IX_cmsTags")] public string Text { get; set; } //[Column("key")] diff --git a/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs b/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs index 4a07b16a07..cbe4cf0cd4 100644 --- a/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs @@ -3,11 +3,13 @@ using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos { - [TableName(Constants.DatabaseSchema.Tables.TagRelationship)] + [TableName(TableName)] [PrimaryKey("nodeId", AutoIncrement = false)] [ExplicitColumns] internal class TagRelationshipDto { + public const string TableName = Constants.DatabaseSchema.Tables.TagRelationship; + [Column("nodeId")] [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsTagRelationship", OnColumns = "nodeId, propertyTypeId, tagId")] [ForeignKey(typeof(ContentDto), Name = "FK_cmsTagRelationship_cmsContent", Column = "nodeId")] diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index c8467f47e2..7fe1d44921 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -282,7 +282,7 @@ namespace Umbraco.Core.Persistence.Factories var dto = new DocumentVersionDto { Id = entity.VersionId, - TemplateId = entity.Template?.Id, + TemplateId = entity.TemplateId, Published = false, // always building the current, unpublished one ContentVersionDto = BuildContentVersionDto(entity, contentDto) diff --git a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs index 38a1aa2aab..7a04a6d0d9 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs @@ -107,6 +107,7 @@ namespace Umbraco.Core.Persistence.Factories entity.CreatorId = dto.NodeDto.UserId ?? Constants.Security.UnknownUserId; entity.AllowedAsRoot = dto.AllowAtRoot; entity.IsContainer = dto.IsContainer; + entity.IsElement = dto.IsElement; entity.Trashed = dto.NodeDto.Trashed; entity.Variations = (ContentVariation) dto.Variations; } @@ -132,6 +133,7 @@ namespace Umbraco.Core.Persistence.Factories NodeId = entity.Id, AllowAtRoot = entity.AllowedAsRoot, IsContainer = entity.IsContainer, + IsElement = entity.IsElement, Variations = (byte) entity.Variations, NodeDto = BuildNodeDto(entity, nodeObjectType) }; diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index c920a18c3b..728441964a 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -93,18 +93,36 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - public static IEnumerable BuildDtos(int currentVersionId, int publishedVersionId, IEnumerable properties, ILanguageRepository languageRepository, out bool edited, out HashSet editedCultures) + /// + /// Creates a collection of from a collection of + /// + /// + /// The of the entity containing the collection of + /// + /// + /// + /// The properties to map + /// + /// out parameter indicating that one or more properties have been edited + /// out parameter containing a collection of of edited cultures when the contentVariation varies by culture + /// + public static IEnumerable BuildDtos(ContentVariation contentVariation, int currentVersionId, int publishedVersionId, IEnumerable properties, + ILanguageRepository languageRepository, out bool edited, out HashSet editedCultures) { var propertyDataDtos = new List(); edited = false; editedCultures = null; // don't allocate unless necessary + string defaultCulture = null; //don't allocate unless necessary + + var entityVariesByCulture = contentVariation.VariesByCulture(); foreach (var property in properties) { if (property.PropertyType.IsPublishing) { - var editingCultures = property.PropertyType.VariesByCulture(); - if (editingCultures && editedCultures == null) editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + //create the resulting hashset if it's not created and the entity varies by culture + if (entityVariesByCulture && editedCultures == null) + editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); // publishing = deal with edit and published values foreach (var propertyValue in property.Values) @@ -125,12 +143,24 @@ namespace Umbraco.Core.Persistence.Factories var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); edited |= !sameValues; - if (editingCultures && // cultures can be edited, ie CultureNeutral is supported - propertyValue.Culture != null && propertyValue.Segment == null && // and value is CultureNeutral - !sameValues) // and edited and published are different + if (entityVariesByCulture // cultures can be edited, ie CultureNeutral is supported + && propertyValue.Culture != null && propertyValue.Segment == null // and value is CultureNeutral + && !sameValues) // and edited and published are different { editedCultures.Add(propertyValue.Culture); // report culture as edited } + + // flag culture as edited if it contains an edited invariant property + if (propertyValue.Culture == null //invariant property + && !sameValues // and edited and published are different + && entityVariesByCulture) //only when the entity is variant + { + if (defaultCulture == null) + defaultCulture = languageRepository.GetDefaultIsoCode(); + + editedCultures.Add(defaultCulture); + } + } } else diff --git a/src/Umbraco.Core/Persistence/Factories/TagFactory.cs b/src/Umbraco.Core/Persistence/Factories/TagFactory.cs index 867e6b0ae3..10441707ec 100644 --- a/src/Umbraco.Core/Persistence/Factories/TagFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/TagFactory.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Factories { public static ITag BuildEntity(TagDto dto) { - var entity = new Tag(dto.Id, dto.Group, dto.Text) { NodeCount = dto.NodeCount }; + var entity = new Tag(dto.Id, dto.Group, dto.Text, dto.LanguageId) { NodeCount = dto.NodeCount }; // reset dirty initial properties (U4-1946) entity.ResetDirtyProperties(false); return entity; @@ -20,6 +20,7 @@ namespace Umbraco.Core.Persistence.Factories Id = entity.Id, Group = entity.Group, Text = entity.Text, + LanguageId = entity.LanguageId //Key = entity.Group + "/" + entity.Text // de-normalize }; } diff --git a/src/Umbraco.Core/Persistence/Mappers/ContentTypeMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ContentTypeMapper.cs index c692a75474..a24963bace 100644 --- a/src/Umbraco.Core/Persistence/Mappers/ContentTypeMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/ContentTypeMapper.cs @@ -35,6 +35,7 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.Description, dto => dto.Description); CacheMap(src => src.Icon, dto => dto.Icon); CacheMap(src => src.IsContainer, dto => dto.IsContainer); + CacheMap(src => src.IsElement, dto => dto.IsElement); CacheMap(src => src.Thumbnail, dto => dto.Thumbnail); } } diff --git a/src/Umbraco.Core/Persistence/Mappers/MapperCollectionBuilder.cs b/src/Umbraco.Core/Persistence/Mappers/MapperCollectionBuilder.cs index 6d641cab3d..80819933f5 100644 --- a/src/Umbraco.Core/Persistence/Mappers/MapperCollectionBuilder.cs +++ b/src/Umbraco.Core/Persistence/Mappers/MapperCollectionBuilder.cs @@ -1,19 +1,14 @@ -using LightInject; -using Umbraco.Core.Composing; +using Umbraco.Core.Composing; namespace Umbraco.Core.Persistence.Mappers { public class MapperCollectionBuilder : LazyCollectionBuilderBase { - public MapperCollectionBuilder(IServiceContainer container) - : base(container) - { } - protected override MapperCollectionBuilder This => this; - protected override void Initialize() + public override void RegisterWith(IRegister register) { - base.Initialize(); + base.RegisterWith(register); // default initializer registers // - service MapperCollectionBuilder, returns MapperCollectionBuilder @@ -21,10 +16,10 @@ namespace Umbraco.Core.Persistence.Mappers // we want to register extra // - service IMapperCollection, returns MappersCollectionBuilder's collection - Container.Register(factory => factory.GetInstance()); + register.Register(factory => factory.GetInstance()); } - public MapperCollectionBuilder AddCore() + public MapperCollectionBuilder AddCoreMappers() { Add(); Add(); diff --git a/src/Umbraco.Core/Persistence/Mappers/MediaTypeMapper.cs b/src/Umbraco.Core/Persistence/Mappers/MediaTypeMapper.cs index 3f5a6e24bc..6cf83bc7aa 100644 --- a/src/Umbraco.Core/Persistence/Mappers/MediaTypeMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/MediaTypeMapper.cs @@ -35,6 +35,7 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.Description, dto => dto.Description); CacheMap(src => src.Icon, dto => dto.Icon); CacheMap(src => src.IsContainer, dto => dto.IsContainer); + CacheMap(src => src.IsElement, dto => dto.IsElement); CacheMap(src => src.Thumbnail, dto => dto.Thumbnail); } } diff --git a/src/Umbraco.Core/Persistence/Mappers/MemberTypeMapper.cs b/src/Umbraco.Core/Persistence/Mappers/MemberTypeMapper.cs index 28dc19171f..9a4e4ec040 100644 --- a/src/Umbraco.Core/Persistence/Mappers/MemberTypeMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/MemberTypeMapper.cs @@ -35,6 +35,7 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.Description, dto => dto.Description); CacheMap(src => src.Icon, dto => dto.Icon); CacheMap(src => src.IsContainer, dto => dto.IsContainer); + CacheMap(src => src.IsElement, dto => dto.IsElement); CacheMap(src => src.Thumbnail, dto => dto.Thumbnail); } } diff --git a/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs b/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs index 8cd2ab27d7..63f73d060a 100644 --- a/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/TagMapper.cs @@ -23,6 +23,7 @@ namespace Umbraco.Core.Persistence.Mappers CacheMap(src => src.Id, dto => dto.Id); CacheMap(src => src.Text, dto => dto.Text); CacheMap(src => src.Group, dto => dto.Group); + CacheMap(src => src.LanguageId, dto => dto.LanguageId); } } } diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs index a5ab62d25f..7aa8b707be 100644 --- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs @@ -76,7 +76,7 @@ namespace Umbraco.Core.Persistence var (s, a) = sql.SqlContext.VisitDto(predicate, alias); return sql.Where(s, a); } - + /// /// Appends a WHERE clause to the Sql statement. /// @@ -589,11 +589,14 @@ namespace Umbraco.Core.Persistence /// Creates a SELECT COUNT(*) Sql statement. /// /// The origin sql. + /// An optional alias. /// The Sql statement. - public static Sql SelectCount(this Sql sql) + public static Sql SelectCount(this Sql sql, string alias = null) { if (sql == null) throw new ArgumentNullException(nameof(sql)); - return sql.Select("COUNT(*)"); + var text = "COUNT(*)"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Select(text); } /// @@ -607,13 +610,29 @@ namespace Umbraco.Core.Persistence /// If is empty, all columns are counted. /// public static Sql SelectCount(this Sql sql, params Expression>[] fields) + => sql.SelectCount(null, fields); + + /// + /// Creates a SELECT COUNT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// An alias. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql SelectCount(this Sql sql, string alias, params Expression>[] fields) { if (sql == null) throw new ArgumentNullException(nameof(sql)); var sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - return sql.Select("COUNT (" + string.Join(", ", columns) + ")"); + var text = "COUNT (" + string.Join(", ", columns) + ")"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Select(text); } /// @@ -643,6 +662,26 @@ namespace Umbraco.Core.Persistence return sql.Select(sql.GetColumns(columnExpressions: fields)); } + /// + /// Creates a SELECT DISTINCT Sql statement. + /// + /// The type of the DTO to select. + /// The origin sql. + /// Expressions indicating the columns to select. + /// The Sql statement. + /// + /// If is empty, all columns are selected. + /// + public static Sql SelectDistinct(this Sql sql, params Expression>[] fields) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + var columns = sql.GetColumns(columnExpressions: fields); + sql.Append("SELECT DISTINCT " + string.Join(", ", columns)); + return sql; + } + + //this.Append("SELECT " + string.Join(", ", columns), new object[0]); + /// /// Creates a SELECT Sql statement. /// @@ -705,6 +744,56 @@ namespace Umbraco.Core.Persistence return sql.Append(", " + string.Join(", ", sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields))); } + /// + /// Adds a COUNT(*) to a SELECT Sql statement. + /// + /// The origin sql. + /// An optional alias. + /// The Sql statement. + public static Sql AndSelectCount(this Sql sql, string alias = null) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + var text = ", COUNT(*)"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Append(text); + } + + /// + /// Adds a COUNT to a SELECT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql AndSelectCount(this Sql sql, params Expression>[] fields) + => sql.AndSelectCount(null, fields); + + /// + /// Adds a COUNT to a SELECT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// An alias. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql AndSelectCount(this Sql sql, string alias = null, params Expression>[] fields) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + var sqlSyntax = sql.SqlContext.SqlSyntax; + var columns = fields.Length == 0 + ? sql.GetColumns(withAlias: false) + : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); + var text = ", COUNT (" + string.Join(", ", columns) + ")"; + if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + return sql.Append(text); + } + /// /// Creates a SELECT Sql statement with a referenced Dto. /// @@ -1115,12 +1204,37 @@ namespace Umbraco.Core.Persistence return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; } - internal static void WriteToConsole(this Sql sql) + internal static string ToText(this Sql sql) { - Console.WriteLine(sql.SQL); + var text = new StringBuilder(); + sql.ToText(text); + return text.ToString(); + } + + internal static void ToText(this Sql sql, StringBuilder text) + { + ToText(sql.SQL, sql.Arguments, text); + } + + internal static void ToText(string sql, object[] arguments, StringBuilder text) + { + text.AppendLine(sql); + + if (arguments == null || arguments.Length == 0) + return; + + text.Append(" --"); + var i = 0; - foreach (var arg in sql.Arguments) - Console.WriteLine($" @{i++}: {arg}"); + foreach (var arg in arguments) + { + text.Append(" @"); + text.Append(i++); + text.Append(":"); + text.Append(arg); + } + + text.AppendLine(); } #endregion diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index d313d27bbc..16bfc9b164 100644 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -653,6 +653,23 @@ namespace Umbraco.Core.Persistence.Querying else throw new NotSupportedException("Expression is not a proper lambda."); + // c# 'x == null' becomes sql 'x IS NULL' which is fine + // c# 'x == y' becomes sql 'x = @0' which is fine - unless they are nullable types, + // because sql 'x = NULL' is always false and the 'IS NULL' syntax is required, + // so for comparing nullable types, we use x.SqlNullableEquals(y, fb) where fb is a fallback + // value which will be used when values are null - turning the comparison into + // sql 'COALESCE(x,fb) = COALESCE(y,fb)' - of course, fb must be a value outside + // of x and y range - and if that is not possible, then a manual comparison need + // to be written + //TODO support SqlNullableEquals with 0 parameters, using the full syntax below + case "SqlNullableEquals": + var compareTo = Visit(m.Arguments[1]); + var fallback = Visit(m.Arguments[2]); + // that would work without a fallback value but is more cumbersome + //return Visited ? string.Empty : $"((({compareTo} is null) AND ({visitedMethodObject} is null)) OR (({compareTo} is not null) AND ({visitedMethodObject} = {compareTo})))"; + // use a fallback value + return Visited ? string.Empty : $"(COALESCE({visitedMethodObject},{fallback}) = COALESCE({compareTo},{fallback}))"; + default: throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); diff --git a/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs b/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs index 0f9eb47d77..710997472c 100644 --- a/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs +++ b/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs @@ -9,6 +9,21 @@ namespace Umbraco.Core.Persistence.Querying /// internal static class SqlExpressionExtensions { + /// + /// Indicates whether two nullable values are equal, substituting a fallback value for nulls. + /// + /// The nullable type. + /// The value to compare. + /// The value to compare to. + /// The value to use when any value is null. + /// Do not use outside of Sql expressions. + // see usage in ExpressionVisitorBase + public static bool SqlNullableEquals(this T? value, T? other, T fallbackValue) + where T : struct + { + return (value ?? fallbackValue).Equals(other ?? fallbackValue); + } + public static bool SqlIn(this IEnumerable collection, T item) { return collection.Contains(item); 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/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index b53b117a1a..69f6ef4c5f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { @@ -30,6 +31,6 @@ namespace Umbraco.Core.Persistence.Repositories bool Exists(Guid key); IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, IQuery filter = null); + IQuery filter, Ordering ordering); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs index 47a56bb530..d05f4e007c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs @@ -14,8 +14,9 @@ namespace Umbraco.Core.Persistence.Repositories /// /// The Umbraco redirect url route. /// The content unique key. + /// The culture. /// - IRedirectUrl Get(string url, Guid contentKey); + IRedirectUrl Get(string url, Guid contentKey, string culture); /// /// Deletes a redirect url. diff --git a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs index 782f3f1b89..c3e6dc028b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs @@ -19,7 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories /// When is false, the tags specified in are added to those already assigned. /// When is empty and is true, all assigned tags are removed. /// - void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags); + // TODO: replaceTags is used as 'false' in tests exclusively - should get rid of it + void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true); /// /// Removes assigned tags from a content property. @@ -46,54 +47,48 @@ namespace Umbraco.Core.Persistence.Repositories #region Queries + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityByKey(Guid key); + + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityById(int id); - IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup); - - IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null); + /// Gets all entities of a type, tagged with any tag in the specified group. + IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null); /// - /// Returns all tags for an entity type (content/media/member) + /// Gets all entities of a type, tagged with the specified tag. /// - /// Entity type - /// Optional group - /// - IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null); + IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item - Content/Media/Member + /// Gets all tags for an entity type. /// - /// The content item id to get tags for - /// Optional group - /// - IEnumerable GetTagsForEntity(int contentId, string group = null); + IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item - Content/Media/Member + /// Gets all tags attached to an entity. /// - /// The content item id to get tags for - /// Optional group - /// - IEnumerable GetTagsForEntity(Guid contentId, string group = null); + IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item for the property specified - Content/Media/Member + /// Gets all tags attached to an entity. /// - /// The content item id to get tags for - /// The property alias to get tags for - /// Optional group - /// - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null); + IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null); /// - /// Returns all tags that exist on the content item for the property specified - Content/Media/Member + /// Gets all tags attached to an entity via a property. /// - /// The content item id to get tags for - /// The property alias to get tags for - /// Optional group - /// - IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null); + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null); + + /// + /// Gets all tags attached to an entity via a property. + /// + IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null); #endregion } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs index 6c61fe7ad5..45f083bc6b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using LightInject; using NPoco; using Umbraco.Core.Cache; -using Umbraco.Core.Composing.CompositionRoots; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -16,8 +14,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { internal class AuditRepository : NPocoRepositoryBase, IAuditRepository { - public AuditRepository(IScopeAccessor scopeAccessor, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger) - : base(scopeAccessor, cache, logger) + public AuditRepository(IScopeAccessor scopeAccessor, ILogger logger) + : base(scopeAccessor, CacheHelper.NoCache, logger) { } protected override void PersistNewItem(IAuditItem entity) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 58f58c3d84..bd7943ff1d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -217,8 +217,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var property in entity.Properties) { var tagConfiguration = property.GetTagConfiguration(); - if (tagConfiguration == null) continue; - tagRepo.Assign(entity.Id, property.PropertyTypeId, property.GetTagsValue().Select(x => new Tag { Group = tagConfiguration.Group, Text = x }), true); + if (tagConfiguration == null) continue; // not a tags property + + if (property.PropertyType.VariesByCulture()) + { + var tags = new List(); + foreach (var pvalue in property.Values) + { + var tagsValue = property.GetTagsValue(pvalue.Culture); + var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture); + var cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId }); + tags.AddRange(cultureTags); + } + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } + else + { + var tagsValue = property.GetTagsValue(); // strings + var tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } } } @@ -541,16 +559,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement propertyDataDtos.AddRange(propertyDataDtos2); var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId, LanguageRepository).ToList(); - // deal with tags - foreach (var property in properties) - { - if (!tagConfigurations.TryGetValue(property.PropertyType.PropertyEditorAlias, out var tagConfiguration)) - continue; - - //fixme doesn't take into account variants - property.SetTagsValue(property.GetValue(), tagConfiguration); - } - if (result.ContainsKey(temp.VersionId)) { if (ContentRepositoryBase.ThrowOnWarning) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 3184c69dfe..683df047f8 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); } @@ -663,9 +663,11 @@ AND umbracoNode.id <> @id", { case ContentVariation.Culture: CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); + CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); break; case ContentVariation.Nothing: CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); + CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -690,7 +692,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) @@ -757,6 +759,139 @@ AND umbracoNode.id <> @id", } } + /// + private void CopyTagData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) + { + // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers + + // fixme - should we batch then? + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > 2000) + throw new NotSupportedException("Too many property/content types."); + + // delete existing relations (for target language) + // do *not* delete existing tags + + var sqlSelectTagsToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + + if (contentTypeIds != null) + sqlSelectTagsToDelete + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + + sqlSelectTagsToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + + var sqlDeleteRelations = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlSelectTagsToDelete); + + Database.Execute(sqlDeleteRelations); + + // do *not* delete the tags - they could be used by other content types / property types + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + + // copy tags from source language to target language + // target tags may exist already, so we have to check for existence here + // + // select tags to insert: tags pointed to by a relation ship, for proper property/content types, + // and of source language, and where we cannot left join to an existing tag with same text, + // group and languageId + + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; + var sqlSelectTagsToInsert = Sql() + .SelectDistinct(x => x.Text, x => x.Group) + .Append(", " + targetLanguageIdS) + .From(); + + sqlSelectTagsToInsert + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .LeftJoin("xtags").On((tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && xtag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "xtags"); + + if (contentTypeIds != null) + sqlSelectTagsToInsert + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + + sqlSelectTagsToInsert + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .WhereNull(x => x.Id, "xtags") // ie, not exists + .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); + + var cols = Sql().Columns(x => x.Text, x => x.Group, x => x.LanguageId); + var sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert); + + Database.Execute(sqlInsertTags); + + // create relations to new tags + // any existing relations have been deleted above, no need to check for existence here + // + // select node id and property type id from existing relations to tags of source language, + // for proper property/content types, and select new tag id from tags, with matching text, + // and group, but for the target language + + var sqlSelectRelationsToInsert = Sql() + .SelectDistinct(x => x.NodeId, x => x.PropertyTypeId) + .AndSelect("otag", x => x.Id) + .From() + .InnerJoin().On((rel, tag) => rel.TagId == tag.Id) + .InnerJoin("otag").On((tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && otag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "otag"); + + if (contentTypeIds != null) + sqlSelectRelationsToInsert + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + + sqlSelectRelationsToInsert + .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + var relationColumnsToInsert = Sql().Columns(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); + var sqlInsertRelations = Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})").Append(sqlSelectRelationsToInsert); + + Database.Execute(sqlInsertRelations); + + // delete original relations - *not* the tags - all of them + // cannot really "go back" with relations, would have to do it with property values + + sqlSelectTagsToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + + if (contentTypeIds != null) + sqlSelectTagsToDelete + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + + sqlSelectTagsToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => !x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + + sqlDeleteRelations = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlSelectTagsToDelete); + + Database.Execute(sqlDeleteRelations); + + // no + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + } + /// /// Copies property data from one language to another. /// @@ -766,6 +901,8 @@ AND umbracoNode.id <> @id", /// The content type identifiers. private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) { + // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers + // // fixme - should we batch then? var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); if (whereInArgsCount > 2000) @@ -793,11 +930,7 @@ AND umbracoNode.id <> @id", sqlDelete.WhereIn(x => x.VersionId, inSql); } - // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it - if (targetLanguageId == null) - sqlDelete.Where(x => x.LanguageId == null); - else - sqlDelete.Where(x => x.LanguageId == targetLanguageId); + sqlDelete.Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); sqlDelete .WhereIn(x => x.PropertyTypeId, propertyTypeIds); @@ -821,11 +954,7 @@ AND umbracoNode.id <> @id", .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); - // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it - if (sourceLanguageId == null) - sqlSelectData.Where(x => x.LanguageId == null); - else - sqlSelectData.Where(x => x.LanguageId == sourceLanguageId); + sqlSelectData.Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); sqlSelectData .WhereIn(x => x.PropertyTypeId, propertyTypeIds); @@ -1154,7 +1283,7 @@ AND umbracoNode.id <> @id", if (db == null) throw new ArgumentNullException(nameof(db)); var sql = @"SELECT cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, cmsContentType.variations as ctVariations, - cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, + cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.IsElement as ctIsElement, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, ParentTypes.parentContentTypeId as chtParentId, ParentTypes.parentContentTypeKey as chtParentKey, umbracoNode.createDate as nCreateDate, umbracoNode." + sqlSyntax.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, @@ -1255,6 +1384,7 @@ AND umbracoNode.id <> @id", Description = currCt.ctDesc, Icon = currCt.ctIcon, IsContainer = currCt.ctIsContainer, + IsElement = currCt.ctIsElement, NodeId = currCt.ctId, PrimaryKey = currCt.ctPk, Thumbnail = currCt.ctThumb, @@ -1293,7 +1423,7 @@ AND umbracoNode.id <> @id", var sql = @"SELECT cmsDocumentType.IsDefault as dtIsDefault, cmsDocumentType.templateNodeId as dtTemplateId, cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, cmsContentType.variations as ctVariations, - cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, + cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.IsElement as ctIsElement, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, ParentTypes.parentContentTypeId as chtParentId,ParentTypes.parentContentTypeKey as chtParentKey, umbracoNode.createDate as nCreateDate, umbracoNode." + sqlSyntax.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, @@ -1430,6 +1560,7 @@ AND umbracoNode.id <> @id", Description = currCt.ctDesc, Icon = currCt.ctIcon, IsContainer = currCt.ctIsContainer, + IsElement = currCt.ctIsElement, NodeId = currCt.ctId, PrimaryKey = currCt.ctPk, Thumbnail = currCt.ctThumb, diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 35496aaba7..054ab8cb4b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -273,8 +273,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var publishing = content.PublishedState == PublishedState.Publishing; // ensure that the default template is assigned - if (entity.Template == null) - entity.Template = entity.ContentType.DefaultTemplate; + if (entity.TemplateId.HasValue == false) + entity.TemplateId = entity.ContentType.DefaultTemplate?.Id; // sanitize names SanitizeNames(content, publishing); @@ -352,7 +352,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, content.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out var editedCultures); + var propertyDataDtos = PropertyFactory.BuildDtos(content.ContentType.Variations, content.VersionId, content.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out var editedCultures); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -404,7 +404,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (content.PublishedState == PublishedState.Publishing) { content.Published = true; - content.PublishTemplate = content.Template; + content.PublishTemplateId = content.TemplateId; content.PublisherId = content.WriterId; content.PublishName = content.Name; content.PublishDate = content.UpdateDate; @@ -414,7 +414,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement else if (content.PublishedState == PublishedState.Unpublishing) { content.Published = false; - content.PublishTemplate = null; + content.PublishTemplateId = null; content.PublisherId = null; content.PublishName = null; content.PublishDate = null; @@ -527,7 +527,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(deletePropertyDataSql); // insert property data - var propertyDataDtos = PropertyFactory.BuildDtos(content.VersionId, publishing ? content.PublishedVersionId : 0, entity.Properties, LanguageRepository, out var edited, out var editedCultures); + var propertyDataDtos = PropertyFactory.BuildDtos(content.ContentType.Variations, content.VersionId, publishing ? content.PublishedVersionId : 0, + entity.Properties, LanguageRepository, out var edited, out var editedCultures); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -608,7 +609,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (content.PublishedState == PublishedState.Publishing) { content.Published = true; - content.PublishTemplate = content.Template; + content.PublishTemplateId = content.TemplateId; content.PublisherId = content.WriterId; content.PublishName = content.Name; content.PublishDate = content.UpdateDate; @@ -618,7 +619,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement else if (content.PublishedState == PublishedState.Unpublishing) { content.Published = false; - content.PublishTemplate = null; + content.PublishTemplateId = null; content.PublisherId = null; content.PublishName = null; content.PublishDate = null; @@ -1077,18 +1078,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // assign templates and properties foreach (var temp in temps) { - // complete the item - if (temp.Template1Id.HasValue && templates.TryGetValue(temp.Template1Id.Value, out var template)) - temp.Content.Template = template; - if (temp.Template2Id.HasValue && templates.TryGetValue(temp.Template2Id.Value, out template)) - temp.Content.PublishTemplate = template; + // set the template ID if it matches an existing template + if (temp.Template1Id.HasValue && templates.ContainsKey(temp.Template1Id.Value)) + temp.Content.TemplateId = temp.Template1Id; + if (temp.Template2Id.HasValue && templates.ContainsKey(temp.Template2Id.Value)) + temp.Content.PublishTemplateId = temp.Template2Id; + // set properties if (properties.ContainsKey(temp.VersionId)) temp.Content.Properties = properties[temp.VersionId]; else throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); - //load in the schedule + // load in the schedule if (schedule.TryGetValue(temp.Content.Id, out var s)) temp.Content.ContentSchedule = s; } @@ -1122,7 +1124,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get template if (dto.DocumentVersionDto.TemplateId.HasValue && dto.DocumentVersionDto.TemplateId.Value > 0) - content.Template = _templateRepository.Get(dto.DocumentVersionDto.TemplateId.Value); + content.TemplateId = dto.DocumentVersionDto.TemplateId; // get properties - indexed by version id var versionId = dto.DocumentVersionDto.Id; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 8d6f67e9db..2be27deb0a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -25,12 +26,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement internal class EntityRepository : IEntityRepository { private readonly IScopeAccessor _scopeAccessor; - private readonly ILanguageRepository _langRepository; - public EntityRepository(IScopeAccessor scopeAccessor, ILanguageRepository langRepository) + public EntityRepository(IScopeAccessor scopeAccessor) { _scopeAccessor = scopeAccessor; - _langRepository = langRepository; } protected IUmbracoDatabase Database => _scopeAccessor.AmbientScope.Database; @@ -41,7 +40,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get a page of entities public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, IQuery filter = null) + IQuery filter, Ordering ordering) { var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectType == Constants.ObjectTypes.Media; @@ -53,11 +52,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement x.Where(filterClause.Item1, filterClause.Item2); }, objectType); + ordering = ordering ?? Ordering.ByDefault(); + var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, sql); + sql = AddGroupBy(isContent, isMedia, sql, ordering.IsEmpty); + + if (!ordering.IsEmpty) + { + // apply ordering + ApplyOrdering(ref sql, ordering); + } + //fixme - we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently - sql = sql.OrderBy("NodeId"); + //no matter what we always must have node id ordered at the end + sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); var page = Database.Page(pageIndex + 1, pageSize, sql); var dtos = page.Items; @@ -81,6 +90,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return dto == null ? null : BuildEntity(false, false, dto); } + private IEntitySlim GetEntity(Sql sql, bool isContent, bool isMedia) { //isContent is going to return a 1:M result now with the variants so we need to do different things @@ -200,7 +210,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sqlClause = GetBase(false, false, null); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); - sql = AddGroupBy(false, false, sql); + sql = AddGroupBy(false, false, sql, true); var dtos = Database.Fetch(sql); return dtos.Select(x => BuildEntity(false, false, x)).ToList(); } @@ -214,7 +224,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, sql); + sql = AddGroupBy(isContent, isMedia, sql, true); return GetEntities(sql, isContent, isMedia, true); } @@ -229,7 +239,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, sql); + sql = AddGroupBy(isContent, isMedia, sql, true); return GetEntities(sql, isContent, isMedia, false); } @@ -361,21 +371,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Guid uniqueId) { var sql = GetBaseWhere(isContent, isMedia, false, objectType, uniqueId); - return AddGroupBy(isContent, isMedia, sql); + return AddGroupBy(isContent, isMedia, sql, true); } // gets the full sql for a given object type and a given node id protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, int nodeId) { var sql = GetBaseWhere(isContent, isMedia, false, objectType, nodeId); - return AddGroupBy(isContent, isMedia, sql); + return AddGroupBy(isContent, isMedia, sql, true); } // gets the full sql for a given object type, with a given filter protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Action> filter) { var sql = GetBaseWhere(isContent, isMedia, false, filter, objectType); - return AddGroupBy(isContent, isMedia, sql); + return AddGroupBy(isContent, isMedia, sql, true); } private Sql GetPropertyData(int versionId) @@ -401,7 +411,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version - protected virtual Sql GetBase(bool isContent, bool isMedia, Action> filter, bool isCount = false) + protected Sql GetBase(bool isContent, bool isMedia, Action> filter, bool isCount = false) { var sql = Sql(); @@ -460,7 +470,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the base SELECT + FROM [+ filter] + WHERE sql // for a given object type, with a given filter - protected virtual Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Action> filter, Guid objectType) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Action> filter, Guid objectType) { return GetBase(isContent, isMedia, filter, isCount) .Where(x => x.NodeObjectType == objectType); @@ -468,25 +478,25 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the base SELECT + FROM + WHERE sql // for a given node id - protected virtual Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, int id) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, int id) { var sql = GetBase(isContent, isMedia, null, isCount) .Where(x => x.NodeId == id); - return AddGroupBy(isContent, isMedia, sql); + return AddGroupBy(isContent, isMedia, sql, true); } // gets the base SELECT + FROM + WHERE sql // for a given unique id - protected virtual Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid uniqueId) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid uniqueId) { var sql = GetBase(isContent, isMedia, null, isCount) .Where(x => x.UniqueId == uniqueId); - return AddGroupBy(isContent, isMedia, sql); + return AddGroupBy(isContent, isMedia, sql, true); } // gets the base SELECT + FROM + WHERE sql // for a given object type and node id - protected virtual Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, int nodeId) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, int nodeId) { return GetBase(isContent, isMedia, null, isCount) .Where(x => x.NodeId == nodeId && x.NodeObjectType == objectType); @@ -494,7 +504,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the base SELECT + FROM + WHERE sql // for a given object type and unique id - protected virtual Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, Guid uniqueId) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, Guid uniqueId) { return GetBase(isContent, isMedia, null, isCount) .Where(x => x.UniqueId == uniqueId && x.NodeObjectType == objectType); @@ -502,7 +512,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the GROUP BY / ORDER BY sql // required in order to count children - protected virtual Sql AddGroupBy(bool isContent, bool isMedia, Sql sql, bool sort = true) + protected Sql AddGroupBy(bool isContent, bool isMedia, Sql sql, bool defaultSort) { sql .GroupBy(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) @@ -520,12 +530,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .AndBy(x => x.Id) .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); - if (sort) + if (defaultSort) sql.OrderBy(x => x.SortOrder); return sql; } + private void ApplyOrdering(ref Sql sql, Ordering ordering) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (ordering == null) throw new ArgumentNullException(nameof(ordering)); + + //fixme - although this works for name, it probably doesn't work for others without an alias of some sort + var orderBy = ordering.OrderBy; + + if (ordering.Direction == Direction.Ascending) + sql.OrderBy(orderBy); + else + sql.OrderByDescending(orderBy); + } + #endregion #region Classes diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 09fb664ffe..e236670e74 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -111,7 +111,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement "DELETE FROM umbracoPropertyData WHERE languageId = @id", "DELETE FROM umbracoContentVersionCultureVariation WHERE languageId = @id", "DELETE FROM umbracoDocumentCultureVariation WHERE languageId = @id", - "DELETE FROM umbracoLanguage WHERE id = @id" + "DELETE FROM umbracoLanguage WHERE id = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id" }; return list; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index dbfdc8e980..3e665e321f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -284,7 +284,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(mediaVersionDto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -341,7 +341,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // replace the property data var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == media.VersionId); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index fd79b231de..bfadebd61b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -310,7 +310,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(dto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(member.ContentType.Variations, member.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -375,7 +375,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // replace the property data var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == member.VersionId); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(member.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(member.ContentType.Variations, member.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/PartialViewMacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/PartialViewMacroRepository.cs index 741bb98e7c..d707bcee10 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/PartialViewMacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/PartialViewMacroRepository.cs @@ -1,14 +1,12 @@ -using LightInject; -using Umbraco.Core.IO; +using Umbraco.Core.IO; using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories.Implement { internal class PartialViewMacroRepository : PartialViewRepository, IPartialViewMacroRepository { - - public PartialViewMacroRepository([Inject("PartialViewMacroFileSystem")] IFileSystem fileSystem) - : base(fileSystem) + public PartialViewMacroRepository(IFileSystems fileSystems) + : base(fileSystems.MacroPartialsFileSystem) { } protected override PartialViewType ViewType => PartialViewType.PartialViewMacro; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/PartialViewRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/PartialViewRepository.cs index 2aa63813e5..d04bc47cd8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/PartialViewRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/PartialViewRepository.cs @@ -2,7 +2,6 @@ using System.IO; using System.Linq; using System.Text; -using LightInject; using Umbraco.Core.IO; using Umbraco.Core.Models; @@ -10,7 +9,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { internal class PartialViewRepository : FileRepository, IPartialViewRepository { - public PartialViewRepository([Inject("PartialViewFileSystem")] IFileSystem fileSystem) + public PartialViewRepository(IFileSystems fileSystems) + : base(fileSystems.PartialViewsFileSystem) + { } + + protected PartialViewRepository(IFileSystem fileSystem) : base(fileSystem) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs index 5ec7fd6f3c..3ca937ffcd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -104,6 +104,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); ContentKey = redirectUrl.ContentKey, CreateDateUtc = redirectUrl.CreateDateUtc, Url = redirectUrl.Url, + Culture = redirectUrl.Culture, UrlHash = redirectUrl.Url.ToSHA1() }; } @@ -121,6 +122,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); url.ContentId = dto.ContentId; url.ContentKey = dto.ContentKey; url.CreateDateUtc = dto.CreateDateUtc; + url.Culture = dto.Culture; url.Url = dto.Url; return url; } @@ -130,10 +132,10 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); } } - public IRedirectUrl Get(string url, Guid contentKey) + public IRedirectUrl Get(string url, Guid contentKey, string culture) { var urlHash = url.ToSHA1(); - var sql = GetBaseQuery(false).Where(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey); + var sql = GetBaseQuery(false).Where(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); var dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : Map(dto); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index f79344028f..fb5ba00ea0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using LightInject; using NPoco; using Umbraco.Core.Cache; -using Umbraco.Core.Composing.CompositionRoots; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -22,8 +20,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { private readonly IRelationTypeRepository _relationTypeRepository; - public RelationRepository(IScopeAccessor scopeAccessor, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger, IRelationTypeRepository relationTypeRepository) - : base(scopeAccessor, cache, logger) + public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository) + : base(scopeAccessor, CacheHelper.NoCache, logger) { _relationTypeRepository = relationTypeRepository; } 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/ScriptRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ScriptRepository.cs index d5719554c9..85b41a2a1c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ScriptRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ScriptRepository.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using LightInject; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Models; @@ -16,8 +15,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { private readonly IContentSection _contentConfig; - public ScriptRepository([Inject("ScriptFileSystem")] IFileSystem fileSystem, IContentSection contentConfig) - : base(fileSystem) + public ScriptRepository(IFileSystems fileSystems, IContentSection contentConfig) + : base(fileSystems.ScriptsFileSystem) { _contentConfig = contentConfig ?? throw new ArgumentNullException(nameof(contentConfig)); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs index 531df1ba13..2679f8f92f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs @@ -14,9 +14,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { internal class ServerRegistrationRepository : NPocoRepositoryBase, IServerRegistrationRepository { - // fixme - should we use NoCache instead of CreateDisabledCacheHelper?! public ServerRegistrationRepository(IScopeAccessor scopeAccessor, ILogger logger) - : base(scopeAccessor, CacheHelper.CreateDisabledCacheHelper(), logger) + : base(scopeAccessor, CacheHelper.NoCache, logger) { } protected override IRepositoryCachePolicy CreateCachePolicy() diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs index 73dcb44fef..4c02a8f4b5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/StylesheetRepository.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using LightInject; using Umbraco.Core.IO; using Umbraco.Core.Models; @@ -12,8 +11,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class StylesheetRepository : FileRepository, IStylesheetRepository { - public StylesheetRepository([Inject("StylesheetFileSystem")] IFileSystem fileSystem) - : base(fileSystem) + public StylesheetRepository(IFileSystems fileSystems) + : base(fileSystems.StylesheetsFileSystem) { } #region Overrides of FileRepository diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs index 418e3d8ac3..77e474be08 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -109,74 +110,65 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Assign and Remove Tags /// - public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags) + // only invoked from ContentRepositoryBase with all cultures + replaceTags being true + public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true) { // to no-duplicates array var tagsA = tags.Distinct(new TagComparer()).ToArray(); - // no tags? - if (tagsA.Length == 0) + // replacing = clear all + if (replaceTags) { - // replacing = clear all - if (replaceTags) - { - var sql0 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); - Database.Execute(sql0); - } - - // nothing else to do - return; + var sql0 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); + Database.Execute(sql0); } + // no tags? nothing else to do + if (tagsA.Length == 0) + return; + // tags // using some clever logic (?) to insert tags that don't exist in 1 query + // must coalesce languageId because equality of NULLs does not exist var tagSetSql = GetTagSet(tagsA); var group = SqlSyntax.GetQuotedColumnName("group"); // insert tags - var sql1 = $@"INSERT INTO cmsTags (tag, {group}) -SELECT tagSet.tag, tagSet.{group} + var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId) +SELECT tagSet.tag, tagSet.{group}, tagSet.languageId FROM {tagSetSql} -LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group}) +LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1)) WHERE cmsTags.id IS NULL"; Database.Execute(sql1); - // if replacing, remove everything first - if (replaceTags) - { - var sql2 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); - Database.Execute(sql2); - } - // insert relations - var sql3 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) + var sql2 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) SELECT {contentId}, {propertyTypeId}, tagSet2.Id FROM ( SELECT t.Id FROM {tagSetSql} - INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group}) + INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(t.languageId, -1)) ) AS tagSet2 LEFT OUTER JOIN cmsTagRelationship r ON (tagSet2.id = r.tagId AND r.nodeId = {contentId} AND r.propertyTypeID = {propertyTypeId}) WHERE r.tagId IS NULL"; - Database.Execute(sql3); + Database.Execute(sql2); } /// + // only invoked from tests public void Remove(int contentId, int propertyTypeId, IEnumerable tags) { var tagSetSql = GetTagSet(tags); + var group = SqlSyntax.GetQuotedColumnName("group"); - var deleteSql = string.Concat("DELETE FROM cmsTagRelationship WHERE nodeId = ", - contentId, - " AND propertyTypeId = ", - propertyTypeId, - " AND tagId IN ", - "(SELECT id FROM cmsTags INNER JOIN ", - tagSetSql, - " ON (TagSet.Tag = cmsTags.Tag and TagSet." + SqlSyntax.GetQuotedColumnName("group") + @" = cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @"))"); + var deleteSql = $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( + SELECT id FROM cmsTags INNER JOIN {tagSetSql} ON ( + tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1) + ) + )"; Database.Execute(deleteSql); } @@ -207,13 +199,6 @@ WHERE r.tagId IS NULL"; // private string GetTagSet(IEnumerable tags) { - string EscapeSqlString(string s) - { - // why were we escaping @ symbols? - //return NPocoDatabaseExtensions.EscapeAtSymbols(s.Replace("'", "''")); - return s.Replace("'", "''"); - } - var sql = new StringBuilder(); var group = SqlSyntax.GetQuotedColumnName("group"); var first = true; @@ -226,11 +211,17 @@ WHERE r.tagId IS NULL"; else sql.Append(" UNION "); sql.Append("SELECT N'"); - sql.Append(EscapeSqlString(tag.Text)); + sql.Append(SqlSyntax.EscapeString(tag.Text)); sql.Append("' AS tag, '"); - sql.Append(EscapeSqlString(tag.Group)); + sql.Append(SqlSyntax.EscapeString(tag.Group)); sql.Append("' AS "); sql.Append(group); + sql.Append(" , "); + if (tag.LanguageId.HasValue) + sql.Append(tag.LanguageId); + else + sql.Append("NULL"); + sql.Append(" AS languageId"); } sql.Append(") AS tagSet"); @@ -244,14 +235,17 @@ WHERE r.tagId IS NULL"; public bool Equals(ITag x, ITag y) { return ReferenceEquals(x, y) // takes care of both being null - || x != null && y != null && x.Text == y.Text && x.Group == y.Group; + || x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId; } public int GetHashCode(ITag obj) { unchecked { - return (obj.Text.GetHashCode() * 397) ^ obj.Group.GetHashCode(); + var h = obj.Text.GetHashCode(); + h = h * 397 ^ obj.Group.GetHashCode(); + h = h * 397 ^ (obj.LanguageId?.GetHashCode() ?? 0); + return h; } } } @@ -264,118 +258,126 @@ WHERE r.tagId IS NULL"; // consider caching implications // add lookups for parentId or path (ie get content in tag group, that are descendants of x) + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class TaggedEntityDto + { + public int NodeId { get; set; } + public string PropertyTypeAlias { get; set; } + public int PropertyTypeId { get; set; } + public int TagId { get; set; } + public string TagText { get; set; } + public string TagGroup { get; set; } + public int? TagLanguage { get; set; } + } + // ReSharper restore UnusedAutoPropertyAccessor.Local + + /// public TaggedEntity GetTaggedEntityByKey(Guid key) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + + sql = sql .Where(dto => dto.UniqueId == key); - return CreateTaggedEntityCollection(Database.Fetch(sql)).FirstOrDefault(); + return Map(Database.Fetch(sql)).FirstOrDefault(); } + /// public TaggedEntity GetTaggedEntityById(int id) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + + sql = sql .Where(dto => dto.NodeId == id); - return CreateTaggedEntityCollection(Database.Fetch(sql)).FirstOrDefault(); + return Map(Database.Fetch(sql)).FirstOrDefault(); } - public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string tagGroup) + /// + public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.Group == tagGroup); + var sql = GetTaggedEntitiesSql(objectType, culture); - if (objectType != TaggableObjectTypes.All) - { - var nodeObjectType = GetNodeObjectType(objectType); - sql = sql - .Where(dto => dto.NodeObjectType == nodeObjectType); - } + sql = sql + .Where(x => x.Group == group); - return CreateTaggedEntityCollection( - Database.Fetch(sql)); + return Map(Database.Fetch(sql)); } - public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null) { - var sql = Sql() - .Select("cmsTagRelationship.nodeId, cmsPropertyType.Alias, cmsPropertyType.id as propertyTypeId, cmsTags.tag, cmsTags.id as tagId, cmsTags." + SqlSyntax.GetQuotedColumnName("group")) - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + var sql = GetTaggedEntitiesSql(objectType, culture); + + sql = sql .Where(dto => dto.Text == tag); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); + + return Map(Database.Fetch(sql)); + } + + private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string culture) + { + var sql = Sql() + .Select(x => Alias(x.NodeId, "NodeId")) + .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), x => Alias(x.Id, "PropertyTypeId")) + .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .InnerJoin().On((rel, prop) => rel.PropertyTypeId == prop.Id) + .InnerJoin().On((content, node) => content.NodeId == node.NodeId); + + if (culture == null) + { + sql = sql + .Where(dto => dto.LanguageId == null); + } + else if (culture != "*") + { + sql = sql + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id) + .Where(x => x.IsoCode == culture); + } + if (objectType != TaggableObjectTypes.All) { var nodeObjectType = GetNodeObjectType(objectType); - sql = sql - .Where(dto => dto.NodeObjectType == nodeObjectType); + sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType); } - if (tagGroup.IsNullOrWhiteSpace() == false) - { - sql = sql.Where(dto => dto.Group == tagGroup); - } - - return CreateTaggedEntityCollection( - Database.Fetch(sql)); + return sql; } - private IEnumerable CreateTaggedEntityCollection(IEnumerable dbResult) + private static IEnumerable Map(IEnumerable dtos) { - foreach (var node in dbResult.GroupBy(x => (int)x.nodeId)) + return dtos.GroupBy(x => x.NodeId).Select(dtosForNode => { - var properties = new List(); - foreach (var propertyType in node.GroupBy(x => new { id = (int)x.propertyTypeId, alias = (string)x.Alias })) + var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty => { - var tags = propertyType.Select(x => new Tag((int)x.tagId, (string)x.group, (string)x.tag)); - properties.Add(new TaggedProperty(propertyType.Key.id, propertyType.Key.alias, tags)); - } - yield return new TaggedEntity(node.Key, properties); - } + string propertyTypeAlias = null; + var tags = dtosForProperty.Select(dto => + { + propertyTypeAlias = dto.PropertyTypeAlias; + return new Tag(dto.TagId, dto.TagGroup, dto.TagText, dto.TagLanguage); + }).ToList(); + return new TaggedProperty(dtosForProperty.Key, propertyTypeAlias, tags); + }).ToList(); + + return new TaggedEntity(dtosForNode.Key, taggedProperties); + }).ToList(); } - public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null) + /// + public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(true); + var sql = GetTagsSql(culture, true); - sql = ApplyRelationshipJoinToTagsQuery(sql); + AddTagsSqlWhere(sql, culture); if (objectType != TaggableObjectTypes.All) { @@ -384,116 +386,126 @@ WHERE r.tagId IS NULL"; .Where(dto => dto.NodeObjectType == nodeObjectType); } - sql = ApplyGroupFilterToTagsQuery(sql, group); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); - sql = ApplyGroupByToTagsQuery(sql); + sql = sql + .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForEntity(int contentId, string group = null) + /// + public IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); + var sql = GetTagsSql(culture); - sql = ApplyRelationshipJoinToTagsQuery(sql); + AddTagsSqlWhere(sql, culture); sql = sql .Where(dto => dto.NodeId == contentId); - sql = ApplyGroupFilterToTagsQuery(sql, group); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForEntity(Guid contentId, string group = null) + /// + public IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); + var sql = GetTagsSql(culture); - sql = ApplyRelationshipJoinToTagsQuery(sql); + AddTagsSqlWhere(sql, culture); sql = sql .Where(dto => dto.UniqueId == contentId); - sql = ApplyGroupFilterToTagsQuery(sql, group); + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null) + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); - - sql = ApplyRelationshipJoinToTagsQuery(sql); + var sql = GetTagsSql(culture); sql = sql - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) - .Where(dto => dto.NodeId == contentId) - .Where(dto => dto.Alias == propertyTypeAlias); + .InnerJoin().On((prop, rel) => prop.Id == rel.PropertyTypeId) + .Where(x => x.NodeId == contentId) + .Where(x => x.Alias == propertyTypeAlias); - sql = ApplyGroupFilterToTagsQuery(sql, group); + AddTagsSqlWhere(sql, culture); + + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null) + /// + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null) { - var sql = GetTagsQuerySelect(); - - sql = ApplyRelationshipJoinToTagsQuery(sql); + var sql = GetTagsSql(culture); sql = sql - .InnerJoin() - .On(left => left.Id, right => right.PropertyTypeId) + .InnerJoin().On((prop, rel) => prop.Id == rel.PropertyTypeId) .Where(dto => dto.UniqueId == contentId) .Where(dto => dto.Alias == propertyTypeAlias); - sql = ApplyGroupFilterToTagsQuery(sql, group); + AddTagsSqlWhere(sql, culture); + + if (group.IsNullOrWhiteSpace() == false) + sql = sql + .Where(dto => dto.Group == group); return ExecuteTagsQuery(sql); } - private Sql GetTagsQuerySelect(bool withGrouping = false) + private Sql GetTagsSql(string culture, bool withGrouping = false) { - var sql = Sql(); + var sql = Sql() + .Select(); if (withGrouping) - { - sql = sql.Select("cmsTags.id, cmsTags.tag, cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @", Count(*) NodeCount"); - } - else - { - sql = sql.Select("DISTINCT cmsTags.*"); - } + sql = sql + .AndSelectCount("NodeCount"); - return sql; - } - - private Sql ApplyRelationshipJoinToTagsQuery(Sql sql) - { - return sql + sql = sql .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId); - } + .InnerJoin().On((rel, tag) => tag.Id == rel.TagId) + .InnerJoin().On((content, rel) => content.NodeId == rel.NodeId) + .InnerJoin().On((node, content) => node.NodeId == content.NodeId); - private Sql ApplyGroupFilterToTagsQuery(Sql sql, string group) - { - if (group.IsNullOrWhiteSpace() == false) + if (culture != null && culture != "*") { - sql = sql.Where(dto => dto.Group == group); + sql = sql + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id); } return sql; } - private Sql ApplyGroupByToTagsQuery(Sql sql) + private Sql AddTagsSqlWhere(Sql sql, string culture) { - return sql.GroupBy("cmsTags.id", "cmsTags.tag", "cmsTags." + SqlSyntax.GetQuotedColumnName("group") + @""); + if (culture == null) + { + sql = sql + .Where(dto => dto.LanguageId == null); + } + else if (culture != "*") + { + sql = sql + .Where(x => x.IsoCode == culture); + } + + return sql; } private IEnumerable ExecuteTagsQuery(Sql sql) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs index 83876db599..19ef303ebe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using LightInject; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; @@ -30,13 +29,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ViewHelper _viewHelper; private readonly MasterPageHelper _masterPageHelper; - public TemplateRepository(IScopeAccessor scopeAccessor, CacheHelper cache, ILogger logger, ITemplatesSection templateConfig, - [Inject(Constants.Composing.FileSystems.MasterpageFileSystem)] IFileSystem masterpageFileSystem, - [Inject(Constants.Composing.FileSystems.ViewFileSystem)] IFileSystem viewFileSystem) + public TemplateRepository(IScopeAccessor scopeAccessor, CacheHelper cache, ILogger logger, ITemplatesSection templateConfig, IFileSystems fileSystems) : base(scopeAccessor, cache, logger) { - _masterpagesFileSystem = masterpageFileSystem; - _viewsFileSystem = viewFileSystem; + _masterpagesFileSystem = fileSystems.MasterPagesFileSystem; + _viewsFileSystem = fileSystems.MvcViewsFileSystem; _templateConfig = templateConfig; _viewHelper = new ViewHelper(_viewsFileSystem); _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); @@ -152,7 +149,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //Save to db var template = (Template)entity; template.AddingEntity(); - + var dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, template.Id); //Create the (base) node data - umbracoNode diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index b14c7659a3..17fe79a55b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -423,6 +423,7 @@ ORDER BY colName"; { "DELETE FROM umbracoUser2UserGroup WHERE userId = @id", "DELETE FROM umbracoUser2NodeNotify WHERE userId = @id", + "DELETE FROM umbracoUserStartNode WHERE userId = @id", "DELETE FROM umbracoUser WHERE id = @id", "DELETE FROM umbracoExternalLogin WHERE id = @id" }; diff --git a/src/Umbraco.Core/Persistence/SqlContext.cs b/src/Umbraco.Core/Persistence/SqlContext.cs index feb92e0849..6f9f91114c 100644 --- a/src/Umbraco.Core/Persistence/SqlContext.cs +++ b/src/Umbraco.Core/Persistence/SqlContext.cs @@ -12,6 +12,8 @@ namespace Umbraco.Core.Persistence /// public class SqlContext : ISqlContext { + private readonly Lazy _mappers; + /// /// Initializes a new instance of the class. /// @@ -20,24 +22,20 @@ namespace Umbraco.Core.Persistence /// The database type. /// The mappers. public SqlContext(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, IMapperCollection mappers = null) - { - // for tests - Mappers = mappers ?? new Mappers.MapperCollection(Enumerable.Empty()); - - SqlSyntax = sqlSyntax ?? throw new ArgumentNullException(nameof(sqlSyntax)); - PocoDataFactory = pocoDataFactory ?? throw new ArgumentNullException(nameof(pocoDataFactory)); - DatabaseType = databaseType ?? throw new ArgumentNullException(nameof(databaseType)); - Templates = new SqlTemplates(this); - } - - // fixme - internal SqlContext() + : this(sqlSyntax, databaseType, pocoDataFactory, new Lazy(() => mappers ?? new Mappers.MapperCollection(Enumerable.Empty()))) { } - internal void Initialize(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, IMapperCollection mappers = null) + /// + /// Initializes a new instance of the class. + /// + /// The sql syntax provider. + /// The Poco data factory. + /// The database type. + /// The mappers. + public SqlContext(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, Lazy mappers) { // for tests - Mappers = mappers ?? new Mappers.MapperCollection(Enumerable.Empty()); + _mappers = mappers; SqlSyntax = sqlSyntax ?? throw new ArgumentNullException(nameof(sqlSyntax)); PocoDataFactory = pocoDataFactory ?? throw new ArgumentNullException(nameof(pocoDataFactory)); @@ -46,10 +44,10 @@ namespace Umbraco.Core.Persistence } /// - public ISqlSyntaxProvider SqlSyntax { get; private set; } + public ISqlSyntaxProvider SqlSyntax { get; } /// - public DatabaseType DatabaseType { get; private set; } + public DatabaseType DatabaseType { get; } /// public Sql Sql() => NPoco.Sql.BuilderFor((ISqlContext) this); @@ -61,12 +59,12 @@ namespace Umbraco.Core.Persistence public IQuery Query() => new Query(this); /// - public SqlTemplates Templates { get; private set; } + public SqlTemplates Templates { get; } /// - public IPocoDataFactory PocoDataFactory { get; private set; } + public IPocoDataFactory PocoDataFactory { get; } /// - public IMapperCollection Mappers { get; private set; } + public IMapperCollection Mappers => _mappers.Value; } } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index d69786fbfc..5cf9fa3af8 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -11,7 +11,6 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// /// Represents an SqlSyntaxProvider for MySql /// - [SqlSyntaxProvider(Constants.DbProviderNames.MySql)] public class MySqlSyntaxProvider : SqlSyntaxProviderBase { private readonly ILogger _logger; @@ -334,7 +333,7 @@ ORDER BY TABLE_NAME, INDEX_NAME", switch (systemMethod) { case SystemMethods.NewGuid: - return null; // NOT SUPPORTED! + return null; // NOT SUPPORTED! case SystemMethods.CurrentDateTime: return "CURRENT_TIMESTAMP"; } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 75fc9c0b69..8f39e36fb9 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -4,14 +4,12 @@ using System.Linq; using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax { /// /// Represents an SqlSyntaxProvider for Sql Ce /// - [SqlSyntaxProvider(Constants.DbProviderNames.SqlCe)] public class SqlCeSyntaxProvider : MicrosoftSqlSyntaxProviderBase { public override Sql SelectTop(Sql sql, int top) diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 90a2215e3d..8b8550b694 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data.Common; using System.Linq; using NPoco; +using Umbraco.Core.Logging; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Scoping; @@ -11,29 +12,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// /// Represents an SqlSyntaxProvider for Sql Server. /// - [SqlSyntaxProvider(Constants.DbProviderNames.SqlServer)] public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase { - // IUmbracoDatabaseFactory to be lazily injected - public SqlServerSyntaxProvider(Lazy lazyScopeProvider) - { - _serverVersion = new Lazy(() => - { - var scopeProvider = lazyScopeProvider.Value; - if (scopeProvider == null) - throw new InvalidOperationException("Failed to determine Sql Server version (no scope provider)."); - using (var scope = scopeProvider.CreateScope()) - { - var version = DetermineVersion(scope.Database); - scope.Complete(); - return version; - } - }); - } - - private readonly Lazy _serverVersion; - - internal ServerVersionInfo ServerVersion => _serverVersion.Value; + internal ServerVersionInfo ServerVersion { get; private set; } internal enum VersionName { @@ -62,19 +43,31 @@ namespace Umbraco.Core.Persistence.SqlSyntax internal class ServerVersionInfo { - public string Edition { get; set; } - public string InstanceName { get; set; } - public string ProductVersion { get; set; } - public VersionName ProductVersionName { get; private set; } - public EngineEdition EngineEdition { get; set; } - public bool IsAzure => EngineEdition == EngineEdition.Azure; - public string MachineName { get; set; } - public string ProductLevel { get; set; } - - public void Initialize() + public ServerVersionInfo() { - ProductVersionName = MapProductVersion(ProductVersion); + ProductVersionName = VersionName.Unknown; + EngineEdition = EngineEdition.Unknown; } + + public ServerVersionInfo(string edition, string instanceName, string productVersion, EngineEdition engineEdition, string machineName, string productLevel) + { + Edition = edition; + InstanceName = instanceName; + ProductVersion = productVersion; + ProductVersionName = MapProductVersion(ProductVersion); + EngineEdition = engineEdition; + MachineName = machineName; + ProductLevel = productLevel; + } + + public string Edition { get; } + public string InstanceName { get; } + public string ProductVersion { get; } + public VersionName ProductVersionName { get; } + public EngineEdition EngineEdition { get; } + public bool IsAzure => EngineEdition == EngineEdition.Azure; + public string MachineName { get; } + public string ProductLevel { get; } } private static VersionName MapProductVersion(string productVersion) @@ -105,8 +98,14 @@ namespace Umbraco.Core.Persistence.SqlSyntax } } - private static ServerVersionInfo DetermineVersion(IUmbracoDatabase database) + internal ServerVersionInfo GetSetVersion(string connectionString, string providerName, ILogger logger) { + var factory = DbProviderFactories.GetFactory(providerName); + var connection = factory.CreateConnection(); + + if (connection == null) + throw new InvalidOperationException($"Could not create a connection for provider \"{providerName}\"."); + // Edition: "Express Edition", "Windows Azure SQL Database..." // EngineEdition: 1/Desktop 2/Standard 3/Enterprise 4/Express 5/Azure // ProductLevel: RTM, SPx, CTP... @@ -123,44 +122,30 @@ namespace Umbraco.Core.Persistence.SqlSyntax SERVERPROPERTY('ResourceLastUpdateDateTime') ResourceLastUpdateDateTime, SERVERPROPERTY('ProductLevel') ProductLevel;"; - try - { - var version = database.Fetch(sql).First(); - version.Initialize(); - return version; - } - catch (Exception e) - { - // can't ignore, really - throw new Exception("Failed to determine Sql Server version (see inner exception).", e); - } - } - - internal static VersionName GetVersionName(string connectionString, string providerName) - { - var factory = DbProviderFactories.GetFactory(providerName); - var connection = factory.CreateConnection(); - - if (connection == null) - throw new InvalidOperationException($"Could not create a connection for provider \"{providerName}\"."); - connection.ConnectionString = connectionString; + ServerVersionInfo version; using (connection) { try { connection.Open(); var command = connection.CreateCommand(); - command.CommandText = "SELECT SERVERPROPERTY('ProductVersion');"; - var productVersion = command.ExecuteScalar().ToString(); + command.CommandText = sql; + using (var reader = command.ExecuteReader()) + { + reader.Read(); + version = new ServerVersionInfo(reader.GetString(0), reader.GetString(2), reader.GetString(3), (EngineEdition) reader.GetInt32(5), reader.GetString(7), reader.GetString(9)); + } connection.Close(); - return MapProductVersion(productVersion); } - catch + catch (Exception e) { - return VersionName.Unknown; + logger.Error(e, "Failed to detected SqlServer version."); + version = new ServerVersionInfo(); // all unknown } } + + return ServerVersion = version; } /// diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderAttribute.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderAttribute.cs deleted file mode 100644 index 191ee86bac..0000000000 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Umbraco.Core.Persistence.SqlSyntax -{ - /// - /// Attribute for implementations of an ISqlSyntaxProvider - /// - [AttributeUsage(AttributeTargets.Class)] - public class SqlSyntaxProviderAttribute : Attribute - { - public SqlSyntaxProviderAttribute(string providerName) - { - ProviderName = providerName; - } - - /// - /// Gets or sets the ProviderName that corresponds to the sql syntax in a provider. - /// - public string ProviderName { get; set; } - } -} diff --git a/src/Umbraco.Core/Persistence/SqlTemplate.cs b/src/Umbraco.Core/Persistence/SqlTemplate.cs index 7304f45e7f..e81da20f41 100644 --- a/src/Umbraco.Core/Persistence/SqlTemplate.cs +++ b/src/Umbraco.Core/Persistence/SqlTemplate.cs @@ -95,9 +95,10 @@ namespace Umbraco.Core.Persistence return new Sql(_sqlContext, isBuilt, _sql, args); } - internal void WriteToConsole() + internal string ToText() { - new Sql(_sqlContext, _sql, _args.Values.ToArray()).WriteToConsole(); + var sql = new Sql(_sqlContext, _sql, _args.Values.ToArray()); + return sql.ToText(); } /// diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 64e4c0adca..fdf8061c8e 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -67,6 +67,24 @@ namespace Umbraco.Core.Persistence /// public ISqlContext SqlContext { get; } + #region Temp + + // work around NPoco issue https://github.com/schotime/NPoco/issues/517 while we wait for the fix + public override DbCommand CreateCommand(DbConnection connection, CommandType commandType, string sql, params object[] args) + { + var command = base.CreateCommand(connection, commandType, sql, args); + + if (!DatabaseType.IsSqlCe()) return command; + + foreach (DbParameter parameter in command.Parameters) + if (parameter.Value == DBNull.Value) + parameter.DbType = DbType.String; + + return command; + } + + #endregion + #region Testing, Debugging and Troubleshooting private bool _enableCount; @@ -228,24 +246,13 @@ namespace Umbraco.Core.Persistence private string CommandToString(string sql, object[] args) { - var sb = new StringBuilder(); + var text = new StringBuilder(); #if DEBUG_DATABASES - sb.Append(InstanceId); - sb.Append(": "); + text.Append(InstanceId); + text.Append(": "); #endif - sb.Append(sql); - if (args.Length > 0) - sb.Append(" --"); - var i = 0; - foreach (var arg in args) - { - sb.Append(" @"); - sb.Append(i++); - sb.Append(":"); - sb.Append(arg); - } - - return sb.ToString(); + NPocoSqlExtensions.ToText(sql, args, text); + return text.ToString(); } protected override void OnExecutedCommand(DbCommand cmd) diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs index 1e67993895..c9a509fe94 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs @@ -1,14 +1,11 @@ using System; -using System.Collections.Generic; using System.Configuration; using System.Data.Common; -using System.Linq; using System.Threading; using NPoco; using NPoco.FluentMappings; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; -using Umbraco.Core.Migrations.Install; using Umbraco.Core.Persistence.FaultHandling; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.SqlSyntax; @@ -27,10 +24,8 @@ namespace Umbraco.Core.Persistence /// internal class UmbracoDatabaseFactory : DisposableObject, IUmbracoDatabaseFactory { - private readonly ISqlSyntaxProvider[] _sqlSyntaxProviders; - private readonly IMapperCollection _mappers; + private readonly Lazy _mappers; private readonly ILogger _logger; - private readonly SqlContext _sqlContext = new SqlContext(); private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); private DatabaseFactory _npocoDatabaseFactory; @@ -51,24 +46,20 @@ namespace Umbraco.Core.Persistence /// /// Initializes a new instance of the . /// - /// Used by LightInject. - public UmbracoDatabaseFactory(IEnumerable sqlSyntaxProviders, ILogger logger, IMapperCollection mappers) - : this(Constants.System.UmbracoConnectionName, sqlSyntaxProviders, logger, mappers) - { - if (Configured == false) - DatabaseBuilder.GiveLegacyAChance(this, logger); - } + /// Used by core runtime. + public UmbracoDatabaseFactory(ILogger logger, Lazy mappers) + : this(Constants.System.UmbracoConnectionName, logger, mappers) + { } /// /// Initializes a new instance of the . /// /// Used by the other ctor and in tests. - public UmbracoDatabaseFactory(string connectionStringName, IEnumerable sqlSyntaxProviders, ILogger logger, IMapperCollection mappers) + public UmbracoDatabaseFactory(string connectionStringName, ILogger logger, Lazy mappers) { if (string.IsNullOrWhiteSpace(connectionStringName)) throw new ArgumentNullOrEmptyException(nameof(connectionStringName)); _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); - _sqlSyntaxProviders = sqlSyntaxProviders?.ToArray() ?? throw new ArgumentNullException(nameof(sqlSyntaxProviders)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); var settings = ConfigurationManager.ConnectionStrings[connectionStringName]; @@ -92,10 +83,9 @@ namespace Umbraco.Core.Persistence /// Initializes a new instance of the . /// /// Used in tests. - public UmbracoDatabaseFactory(string connectionString, string providerName, IEnumerable sqlSyntaxProviders, ILogger logger, IMapperCollection mappers) + public UmbracoDatabaseFactory(string connectionString, string providerName, ILogger logger, Lazy mappers) { _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); - _sqlSyntaxProviders = sqlSyntaxProviders?.ToArray() ?? throw new ArgumentNullException(nameof(sqlSyntaxProviders)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(providerName)) @@ -139,7 +129,7 @@ namespace Umbraco.Core.Persistence if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") || !Enum.TryParse(setting.Substring("SqlServer.".Length), out var versionName, true)) { - versionName = SqlServerSyntaxProvider.GetVersionName(_connectionString, _providerName); + versionName = ((SqlServerSyntaxProvider) _sqlSyntax).GetSetVersion(_connectionString, _providerName, _logger).ProductVersionName; } else { @@ -165,7 +155,7 @@ namespace Umbraco.Core.Persistence } /// - public ISqlContext SqlContext => _sqlContext; + public ISqlContext SqlContext { get; private set; } /// public void ConfigureForUpgrade() @@ -218,9 +208,7 @@ namespace Umbraco.Core.Persistence if (_npocoDatabaseFactory == null) throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance."); - // can initialize now because it is the UmbracoDatabaseFactory that determines - // the sql syntax, poco data factory, and database type - _sqlContext.Initialize(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); + SqlContext = new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); _logger.Debug("Configured."); Configured = true; @@ -245,17 +233,17 @@ namespace Umbraco.Core.Persistence // gets the sql syntax provider that corresponds, from attribute private ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) { - var name = providerName.ToLowerInvariant(); - var provider = _sqlSyntaxProviders.FirstOrDefault(x => - x.GetType() - .FirstAttribute() - .ProviderName.ToLowerInvariant() - .Equals(name)); - if (provider != null) return provider; - throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); - - // previously we'd try to return SqlServerSyntaxProvider by default but this is bad - //provider = _syntaxProviders.FirstOrDefault(x => x.GetType() == typeof(SqlServerSyntaxProvider)); + switch (providerName) + { + case Constants.DbProviderNames.MySql: + return new MySqlSyntaxProvider(_logger); + case Constants.DbProviderNames.SqlCe: + return new SqlCeSyntaxProvider(); + case Constants.DbProviderNames.SqlServer: + return new SqlServerSyntaxProvider(); + default: + throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); + } } // ensures that the database is configured, else throws @@ -277,7 +265,7 @@ namespace Umbraco.Core.Persistence // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance private UmbracoDatabase CreateDatabaseInstance() { - return new UmbracoDatabase(_connectionString, _sqlContext, _dbProviderFactory, _logger, _connectionRetryPolicy, _commandRetryPolicy); + return new UmbracoDatabase(_connectionString, SqlContext, _dbProviderFactory, _logger, _connectionRetryPolicy, _commandRetryPolicy); } protected override void DisposeResources() 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/DataEditorCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataEditorCollectionBuilder.cs index 2a53142a1c..c0c0a3651e 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorCollectionBuilder.cs @@ -1,14 +1,9 @@ -using LightInject; -using Umbraco.Core.Composing; +using Umbraco.Core.Composing; namespace Umbraco.Core.PropertyEditors { public class DataEditorCollectionBuilder : LazyCollectionBuilderBase { - public DataEditorCollectionBuilder(IServiceContainer container) - : base(container) - { } - protected override DataEditorCollectionBuilder This => this; } } 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.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/ManifestValueValidatorCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs index d616ecf715..8f7c68c813 100644 --- a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs @@ -1,14 +1,9 @@ -using LightInject; -using Umbraco.Core.Composing; +using Umbraco.Core.Composing; namespace Umbraco.Core.PropertyEditors { internal class ManifestValueValidatorCollectionBuilder : LazyCollectionBuilderBase { - public ManifestValueValidatorCollectionBuilder(IServiceContainer container) - : base(container) - { } - protected override ManifestValueValidatorCollectionBuilder This => this; } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs index 8f01bcab5a..3b2a9dd4e2 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs @@ -9,7 +9,7 @@ /// Determines whether an editor supports tags. /// public static bool IsTagsEditor(this IDataEditor editor) - => editor?.GetType().GetCustomAttribute(false) != null; + => editor.GetTagAttribute() != null; /// /// Gets the tags configuration attribute of an editor. diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs index 5c5a8c16c8..a64fe8c43a 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs @@ -1,14 +1,9 @@ -using LightInject; -using Umbraco.Core.Composing; +using Umbraco.Core.Composing; namespace Umbraco.Core.PropertyEditors { public class PropertyValueConverterCollectionBuilder : OrderedCollectionBuilderBase { - public PropertyValueConverterCollectionBuilder(IServiceContainer container) - : base(container) - { } - protected override PropertyValueConverterCollectionBuilder This => this; } } 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/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index 2131764ad6..29f6de0271 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration.Grid; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; @@ -19,9 +20,13 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter(typeof(JsonValueConverter))] //this shadows the JsonValueConverter public class GridValueConverter : JsonValueConverter { - public GridValueConverter(PropertyEditorCollection propertyEditors) + private readonly IGridConfig _config; + + public GridValueConverter(PropertyEditorCollection propertyEditors, IGridConfig config) : base(propertyEditors) - { } + { + _config = config; + } public override bool IsConverter(PublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Grid); @@ -46,15 +51,6 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters //so we have the grid json... we need to merge in the grid's configuration values with the values // we've saved in the database so that when the front end gets this value, it is up-to-date. - //TODO: Change all singleton access to use ctor injection in v8!!! - //TODO: That would mean that property value converters would need to be request lifespan, hrm.... - var gridConfig = UmbracoConfig.For.GridConfig( - Current.ProfilingLogger.Logger, - Current.ApplicationCache.RuntimeCache, - new DirectoryInfo(IOHelper.MapPath(SystemDirectories.AppPlugins)), - new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Config)), - Current.RuntimeState.Debug); - var sections = GetArray(obj, "sections"); foreach (var section in sections.Cast()) { @@ -74,7 +70,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters if (alias.IsNullOrWhiteSpace() == false) { //find the alias in config - var found = gridConfig.EditorsConfig.Editors.FirstOrDefault(x => x.Alias == alias); + var found = _config.EditorsConfig.Editors.FirstOrDefault(x => x.Alias == alias); if (found != null) { //add/replace the editor value with the one from config diff --git a/src/Umbraco.Core/ReflectionUtilities.cs b/src/Umbraco.Core/ReflectionUtilities.cs index 870cb9ec13..622d81f5f2 100644 --- a/src/Umbraco.Core/ReflectionUtilities.cs +++ b/src/Umbraco.Core/ReflectionUtilities.cs @@ -295,7 +295,7 @@ namespace Umbraco.Core /// Occurs when the constructor does not exist and is true. /// Occurs when is not a Func or when /// is specified and does not match the function's returned type. - public static TLambda EmitConstuctor(bool mustExist = true, Type declaring = null) + public static TLambda EmitConstructor(bool mustExist = true, Type declaring = null) { var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true); diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs old mode 100755 new mode 100644 index cf2712974d..cb6a1cb031 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; -using System.Configuration; +using System.Diagnostics; +using System.Linq; using System.Reflection; using System.Threading; using System.Web; -using LightInject; using Umbraco.Core.Cache; using Umbraco.Core.Components; using Umbraco.Core.Composing; -using Umbraco.Core.Composing.CompositionRoots; using Umbraco.Core.Configuration; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; @@ -16,11 +15,9 @@ using Umbraco.Core.Logging; using Umbraco.Core.Logging.Serilog; using Umbraco.Core.Migrations.Upgrade; using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Mappers; -using Umbraco.Core.Persistence.SqlSyntax; -using Umbraco.Core.Scoping; using Umbraco.Core.Services.Implement; +using Umbraco.Core.Sync; namespace Umbraco.Core.Runtime { @@ -31,44 +28,38 @@ namespace Umbraco.Core.Runtime /// should be possible to use this runtime in console apps. public class CoreRuntime : IRuntime { - private BootLoader _bootLoader; + private ComponentCollection _components; + private IFactory _factory; private RuntimeState _state; /// - /// Initializes a new instance of the class. + /// Gets the logger. /// - public CoreRuntime() - { } + protected ILogger Logger { get; private set; } + + /// + /// Gets the profiler. + /// + protected IProfiler Profiler { get; private set; } + + /// + /// Gets the profiling logger. + /// + protected IProfilingLogger ProfilingLogger { get; private set; } + + /// + public IRuntimeState State => _state; /// - public virtual void Boot(ServiceContainer container) + public virtual IFactory Boot(IRegister register) { - container.ConfigureUmbracoCore(); // also sets Current.Container + // create and register the essential services + // ie the bare minimum required to boot - // 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); - - // prepare essential stuff - - var path = GetApplicationRootPath(); - if (string.IsNullOrWhiteSpace(path) == false) - IOHelper.SetRootDirectory(path); - - _state = (RuntimeState) container.GetInstance(); - _state.Level = RuntimeLevel.Boot; - - Logger = container.GetInstance(); - Profiler = container.GetInstance(); - ProfilingLogger = container.GetInstance(); + // loggers + var logger = Logger = GetLogger(); + var profiler = Profiler = GetProfiler(); + var profilingLogger = ProfilingLogger = new ProfilingLogger(logger, profiler); // the boot loader boots using a container scope, so anything that is PerScope will // be disposed after the boot loader has booted, and anything else will remain. @@ -79,56 +70,126 @@ namespace Umbraco.Core.Runtime // are NOT disposed - which is not a big deal as long as they remain lightweight // objects. - using (var bootTimer = ProfilingLogger.TraceDuration( + using (var timer = profilingLogger.TraceDuration( $"Booting Umbraco {UmbracoVersion.SemanticVersion.ToSemanticString()} on {NetworkHelper.MachineName}.", "Booted.", "Boot failed.")) { - // throws if not full-trust - new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted).Demand(); + logger.Debug("Runtime: {Runtime}", GetType().FullName); - try - { - Logger.Debug("Runtime: {Runtime}", GetType().FullName); + // application environment + ConfigureUnhandledException(); + ConfigureAssemblyResolve(); + ConfigureApplicationRootPath(); - AquireMainDom(container); - DetermineRuntimeLevel(container); - var componentTypes = ResolveComponentTypes(); - _bootLoader = new BootLoader(container); - _bootLoader.Boot(componentTypes, _state.Level); - } - catch (Exception e) - { - _state.Level = RuntimeLevel.BootFailed; - var bfe = e as BootFailedException ?? new BootFailedException("Boot failed.", e); - _state.BootFailedException = bfe; - bootTimer.Fail(exception: bfe); // be sure to log the exception - even if we repeat ourselves - - // throwing here can cause w3wp to hard-crash and we want to avoid it. - // instead, we're logging the exception and setting level to BootFailed. - // various parts of Umbraco such as UmbracoModule and UmbracoDefaultOwinStartup - // understand this and will nullify themselves, while UmbracoModule will - // throw a BootFailedException for every requests. - } + Boot(register, timer); } - //fixme - // after Umbraco has started there is a scope in "context" and that context is - // going to stay there and never get destroyed nor reused, so we have to ensure that - // everything is cleared - //var sa = container.GetInstance(); - //sa.Scope?.Dispose(); + return _factory; } /// - /// Gets a logger. + /// Boots the runtime within a timer. /// - protected virtual ILogger GetLogger() + protected virtual IFactory Boot(IRegister register, DisposableTimer timer) { - return SerilogLogger.CreateWithDefaultConfiguration(); + Composition composition = null; + + try + { + // throws if not full-trust + new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted).Demand(); + + // application caches + var appCaches = GetAppCaches(); + var runtimeCache = appCaches.RuntimeCache; + + // database factory + var databaseFactory = GetDatabaseFactory(); + + // configs + var configs = GetConfigs(); + + // type loader + var localTempStorage = configs.Global().LocalTempStorageLocation; + var typeLoader = new TypeLoader(runtimeCache, localTempStorage, ProfilingLogger); + + // runtime state + // beware! must use '() => _factory.GetInstance()' and NOT '_factory.GetInstance' + // as the second one captures the current value (null) and therefore fails + _state = new RuntimeState(Logger, + configs.Settings(), configs.Global(), + new Lazy(() => _factory.GetInstance()), + new Lazy(() => _factory.GetInstance())) + { + Level = RuntimeLevel.Boot + }; + + // main dom + var mainDom = new MainDom(Logger); + + // create the composition + composition = new Composition(register, typeLoader, ProfilingLogger, _state, configs); + composition.RegisterEssentials(Logger, Profiler, ProfilingLogger, mainDom, appCaches, databaseFactory, typeLoader, _state); + + // register runtime-level services + // there should be none, really - this is here "just in case" + Compose(composition); + + // acquire the main domain, determine our runtime level + AcquireMainDom(mainDom); + DetermineRuntimeLevel(databaseFactory, ProfilingLogger); + + // get composers, and compose + var composerTypes = ResolveComposerTypes(typeLoader); + composition.WithCollectionBuilder(); + var composers = new Composers(composition, composerTypes, ProfilingLogger); + composers.Compose(); + + // create the factory + _factory = Current.Factory = composition.CreateFactory(); + + // create & initialize the components + _components = _factory.GetInstance(); + _components.Initialize(); + } + catch (Exception e) + { + var bfe = e as BootFailedException ?? new BootFailedException("Boot failed.", e); + + if (_state != null) + { + _state.Level = RuntimeLevel.BootFailed; + _state.BootFailedException = bfe; + } + + timer.Fail(exception: bfe); // be sure to log the exception - even if we repeat ourselves + + // if something goes wrong above, we may end up with no factory + // meaning nothing can get the runtime state, etc - so let's try + // to make sure we have a factory + if (_factory == null) + { + try + { + _factory = Current.Factory = composition?.CreateFactory(); + } + catch { /* yea */ } + } + + Debugger.Break(); + + // throwing here can cause w3wp to hard-crash and we want to avoid it. + // instead, we're logging the exception and setting level to BootFailed. + // various parts of Umbraco such as UmbracoModule and UmbracoDefaultOwinStartup + // understand this and will nullify themselves, while UmbracoModule will + // throw a BootFailedException for every requests. + } + + return _factory; } - protected virtual void ConfigureUnhandledException(ILogger logger) + protected virtual void ConfigureUnhandledException() { //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 @@ -141,32 +202,37 @@ namespace Umbraco.Core.Runtime var msg = "Unhandled exception in AppDomain"; if (isTerminating) msg += " (terminating)"; msg += "."; - logger.Error(exception, msg); + Logger.Error(exception, msg); }; } - protected virtual void ConfigureAssemblyResolve(ILogger logger) + protected virtual void ConfigureAssemblyResolve() { // 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 + // do NOT just replace this with 'return Assembly', as it will cause an infinite loop -> stack overflow 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) + protected virtual void ConfigureApplicationRootPath() { - using (var timer = ProfilingLogger.DebugDuration("Acquiring MainDom.", "Aquired.")) + var path = GetApplicationRootPath(); + if (string.IsNullOrWhiteSpace(path) == false) + IOHelper.SetRootDirectory(path); + } + + private void AcquireMainDom(MainDom mainDom) + { + using (var timer = ProfilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) { try { - var mainDom = container.GetInstance(); mainDom.Acquire(); } catch @@ -178,38 +244,38 @@ namespace Umbraco.Core.Runtime } // internal for tests - internal void DetermineRuntimeLevel(IServiceFactory container) + internal void DetermineRuntimeLevel(IUmbracoDatabaseFactory databaseFactory, IProfilingLogger profilingLogger) { - using (var timer = ProfilingLogger.DebugDuration("Determining runtime level.", "Determined.")) + using (var timer = profilingLogger.DebugDuration("Determining runtime level.", "Determined.")) { try { - var dbfactory = container.GetInstance(); - SetRuntimeStateLevel(dbfactory, Logger); + _state.DetermineRuntimeLevel(databaseFactory, profilingLogger); - Logger.Debug("Runtime level: {RuntimeLevel}", _state.Level); + profilingLogger.Debug("Runtime level: {RuntimeLevel}", _state.Level); if (_state.Level == RuntimeLevel.Upgrade) { - Logger.Debug("Configure database factory for upgrades."); - dbfactory.ConfigureForUpgrade(); + profilingLogger.Debug("Configure database factory for upgrades."); + databaseFactory.ConfigureForUpgrade(); } } catch { + _state.Level = RuntimeLevel.BootFailed; timer.Fail(); throw; } } } - private IEnumerable ResolveComponentTypes() + private IEnumerable ResolveComposerTypes(TypeLoader typeLoader) { - using (var timer = ProfilingLogger.TraceDuration("Resolving component types.", "Resolved.")) + using (var timer = ProfilingLogger.TraceDuration("Resolving composer types.", "Resolved.")) { try { - return GetComponentTypes(); + return GetComposerTypes(typeLoader); } catch { @@ -222,208 +288,76 @@ namespace Umbraco.Core.Runtime /// public virtual void Terminate() { - using (ProfilingLogger.DebugDuration("Terminating Umbraco.", "Terminated.")) - { - _bootLoader?.Terminate(); - } + _components.Terminate(); } /// /// Composes the runtime. /// - public virtual void Compose(ServiceContainer container) + public virtual void Compose(Composition composition) { - // compose the very essential things that are needed to bootstrap, before anything else, - // and only these things - the rest should be composed in runtime components - - // register basic things - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - - container.RegisterFrom(); - - // register caches - // need the deep clone runtime cache profiver to ensure entities are cached properly, ie - // are cloned in and cloned out - no request-based cache here since no web-based context, - // will be overriden later or - container.RegisterSingleton(_ => new CacheHelper( - new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()), - new StaticCacheProvider(), - NullCacheProvider.Instance, - new IsolatedRuntimeCache(type => new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider())))); - container.RegisterSingleton(f => f.GetInstance().RuntimeCache); - - // register the plugin manager - container.RegisterSingleton(f => new TypeLoader(f.GetInstance(), f.GetInstance(), f.GetInstance())); - - // register syntax providers - required by database factory - container.Register("MySqlSyntaxProvider"); - container.Register("SqlCeSyntaxProvider"); - container.Register("SqlServerSyntaxProvider"); - - // register persistence mappers - required by database factory so needs to be done here - // means the only place the collection can be modified is in a runtime - afterwards it - // has been frozen and it is too late - var mapperCollectionBuilder = container.RegisterCollectionBuilder(); - ComposeMapperCollection(mapperCollectionBuilder); - - // register database factory - required to check for migrations - // will be initialized with syntax providers and a logger, and will try to configure - // from the default connection string name, if possible, else will remain non-configured - // until properly configured (eg when installing) - container.RegisterSingleton(); - container.RegisterSingleton(f => f.GetInstance().SqlContext); - - // register the scope provider - container.RegisterSingleton(); // implements both IScopeProvider and IScopeAccessor - container.RegisterSingleton(f => f.GetInstance()); - container.RegisterSingleton(f => f.GetInstance()); - - // register MainDom - container.RegisterSingleton(); + // nothing } - protected virtual void ComposeMapperCollection(MapperCollectionBuilder builder) - { - builder.AddCore(); - } - - private void SetRuntimeStateLevel(IUmbracoDatabaseFactory databaseFactory, ILogger logger) - { - var localVersion = UmbracoVersion.LocalVersion; // the local, files, version - var codeVersion = _state.SemanticVersion; // the executing code version - var connect = false; - - // we don't know yet - _state.Level = RuntimeLevel.Unknown; - - if (localVersion == null) - { - // there is no local version, we are not installed - logger.Debug("No local version, need to install Umbraco."); - _state.Level = RuntimeLevel.Install; - } - else if (localVersion < codeVersion) - { - // there *is* a local version, but it does not match the code version - // need to upgrade - logger.Debug("Local version '{LocalVersion}' < code version '{CodeVersion}', need to upgrade Umbraco.", localVersion, codeVersion); - _state.Level = RuntimeLevel.Upgrade; - } - else if (localVersion > codeVersion) - { - logger.Warn("Local version '{LocalVersion}' > code version '{CodeVersion}', downgrading is not supported.", localVersion, codeVersion); - _state.Level = RuntimeLevel.BootFailed; - - // in fact, this is bad enough that we want to throw - throw new BootFailedException($"Local version \"{localVersion}\" > code version \"{codeVersion}\", downgrading is not supported."); - } - else if (databaseFactory.Configured == false) - { - // local version *does* match code version, but the database is not configured - // install (again? this is a weird situation...) - logger.Debug("Database is not configured, need to install Umbraco."); - _state.Level = RuntimeLevel.Install; - } - - // install? not going to test anything else - if (_state.Level == RuntimeLevel.Install) - return; - - // else, keep going, - // anything other than install wants a database - see if we can connect - // (since this is an already existing database, assume localdb is ready) - for (var i = 0; i < 5; i++) - { - connect = databaseFactory.CanConnect; - if (connect) break; - logger.Debug("Could not immediately connect to database, trying again."); - Thread.Sleep(1000); - } - - if (connect == false) - { - // cannot connect to configured database, this is bad, fail - logger.Debug("Could not connect to database."); - _state.Level = RuntimeLevel.BootFailed; - - // in fact, this is bad enough that we want to throw - throw new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); - } - - // if we already know we want to upgrade, - // still run EnsureUmbracoUpgradeState to get the states - // (v7 will just get a null state, that's ok) - - // else - // look for a matching migration entry - bypassing services entirely - they are not 'up' yet - // fixme - in a LB scenario, ensure that the DB gets upgraded only once! - bool noUpgrade; - try - { - noUpgrade = EnsureUmbracoUpgradeState(databaseFactory, logger); - } - catch (Exception e) - { - // can connect to the database but cannot check the upgrade state... oops - logger.Warn(e, "Could not check the upgrade state."); - throw new BootFailedException("Could not check the upgrade state.", e); - } - - if (noUpgrade) - { - // the database version matches the code & files version, all clear, can run - _state.Level = RuntimeLevel.Run; - return; - } - - // the db version does not match... but we do have a migration table - // so, at least one valid table, so we quite probably are installed & need to upgrade - - // although the files version matches the code version, the database version does not - // which means the local files have been upgraded but not the database - need to upgrade - logger.Debug("Has not reached the final upgrade step, need to upgrade Umbraco."); - _state.Level = RuntimeLevel.Upgrade; - } - - protected virtual bool EnsureUmbracoUpgradeState(IUmbracoDatabaseFactory databaseFactory, ILogger logger) - { - var umbracoPlan = new UmbracoPlan(); - var stateValueKey = Upgrader.GetStateValueKey(umbracoPlan); - - // no scope, no service - just directly accessing the database - using (var database = databaseFactory.CreateDatabase()) - { - _state.CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey); - _state.FinalMigrationState = umbracoPlan.FinalState; - } - - logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", _state.FinalMigrationState, _state.CurrentMigrationState ?? ""); - - return _state.CurrentMigrationState == _state.FinalMigrationState; - } - - #region Locals - - protected ILogger Logger { get; private set; } - - protected IProfiler Profiler { get; private set; } - - protected ProfilingLogger ProfilingLogger { get; private set; } - - #endregion - #region Getters // getters can be implemented by runtimes inheriting from CoreRuntime - // fixme - inject! no Current! - protected virtual IEnumerable GetComponentTypes() => Current.TypeLoader.GetTypes(); + /// + /// Gets all composer types. + /// + protected virtual IEnumerable GetComposerTypes(TypeLoader typeLoader) + => typeLoader.GetTypes(); + + /// + /// Gets a logger. + /// + protected virtual ILogger GetLogger() + => SerilogLogger.CreateWithDefaultConfiguration(); + + /// + /// Gets a profiler. + /// + protected virtual IProfiler GetProfiler() + => new LogProfiler(Logger); + + /// + /// Gets the application caches. + /// + protected virtual CacheHelper GetAppCaches() + { + // need the deep clone runtime cache provider to ensure entities are cached properly, ie + // are cloned in and cloned out - no request-based cache here since no web-based context, + // is overriden by the web runtime + + return new CacheHelper( + new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()), + new StaticCacheProvider(), + NullCacheProvider.Instance, + new IsolatedRuntimeCache(type => new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); + } // by default, returns null, meaning that Umbraco should auto-detect the application root path. // override and return the absolute path to the Umbraco site/solution, if needed - protected virtual string GetApplicationRootPath() => null; + protected virtual string GetApplicationRootPath() + => null; + + /// + /// Gets the database factory. + /// + /// This is strictly internal, for tests only. + protected internal virtual IUmbracoDatabaseFactory GetDatabaseFactory() + => new UmbracoDatabaseFactory(Logger, new Lazy(() => _factory.GetInstance())); + + /// + /// Gets the configurations. + /// + protected virtual Configs GetConfigs() + { + var configs = new Configs(); + configs.AddCoreConfigs(); + return configs; + } #endregion } diff --git a/src/Umbraco.Core/Runtime/CoreRuntimeComponent.cs b/src/Umbraco.Core/Runtime/CoreRuntimeComponent.cs index 90d3ece254..b9efdd6432 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntimeComponent.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntimeComponent.cs @@ -1,133 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.IO; +using System.Collections.Generic; using AutoMapper; -using LightInject; -using Umbraco.Core.Cache; using Umbraco.Core.Components; -using Umbraco.Core.Composing; -using Umbraco.Core.Composing.CompositionRoots; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; -using Umbraco.Core.IO.MediaPathSchemes; -using Umbraco.Core.Logging; -using Umbraco.Core.Manifest; -using Umbraco.Core.Migrations; -using Umbraco.Core.Migrations.Install; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Persistence; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.PropertyEditors.Validators; -using Umbraco.Core.Scoping; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Core.Sync; -using Umbraco.Core._Legacy.PackageActions; -using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidator; namespace Umbraco.Core.Runtime { - public class CoreRuntimeComponent : UmbracoComponentBase, IRuntimeComponent + public class CoreRuntimeComponent : IComponent { - public override void Compose(Composition composition) + private readonly IEnumerable _mapperProfiles; + + public CoreRuntimeComponent(IEnumerable mapperProfiles) { - base.Compose(composition); - - // register from roots - composition.Container.RegisterFrom(); - composition.Container.RegisterFrom(); - composition.Container.RegisterFrom(); - - // register database builder - // *not* a singleton, don't want to keep it around - composition.Container.Register(); - - // register filesystems - composition.Container.RegisterSingleton(); - composition.Container.RegisterSingleton(factory => factory.GetInstance().MediaFileSystem); - composition.Container.RegisterSingleton(factory => factory.GetInstance().ScriptsFileSystem, Constants.Composing.FileSystems.ScriptFileSystem); - composition.Container.RegisterSingleton(factory => factory.GetInstance().PartialViewsFileSystem, Constants.Composing.FileSystems.PartialViewFileSystem); - composition.Container.RegisterSingleton(factory => factory.GetInstance().MacroPartialsFileSystem, Constants.Composing.FileSystems.PartialViewMacroFileSystem); - composition.Container.RegisterSingleton(factory => factory.GetInstance().StylesheetsFileSystem, Constants.Composing.FileSystems.StylesheetFileSystem); - composition.Container.RegisterSingleton(factory => factory.GetInstance().MasterPagesFileSystem, Constants.Composing.FileSystems.MasterpageFileSystem); - composition.Container.RegisterSingleton(factory => factory.GetInstance().MvcViewsFileSystem, Constants.Composing.FileSystems.ViewFileSystem); - - // register manifest parser, will be injected in collection builders where needed - composition.Container.RegisterSingleton(); - - // register our predefined validators - composition.Container.RegisterCollectionBuilder() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add(); - - // properties and parameters derive from data editors - composition.Container.RegisterCollectionBuilder() - .Add(factory => factory.GetInstance().GetDataEditors()); - composition.Container.RegisterSingleton(); - composition.Container.RegisterSingleton(); - - // register a server registrar, by default it's the db registrar - composition.Container.RegisterSingleton(f => - { - if ("true".InvariantEquals(ConfigurationManager.AppSettings["umbracoDisableElectionForSingleServer"])) - return new SingleServerRegistrar(f.GetInstance()); - return new DatabaseServerRegistrar( - new Lazy(f.GetInstance), - new DatabaseServerRegistrarOptions()); - }); - - // by default we'll use the database server messenger with default options (no callbacks), - // this will be overridden by either the legacy thing or the db thing in the corresponding - // components in the web project - fixme - should obsolete the legacy thing - composition.Container.RegisterSingleton(factory - => new DatabaseServerMessenger( - factory.GetInstance(), - factory.GetInstance(), - factory.GetInstance(), - factory.GetInstance(), - factory.GetInstance(), - true, new DatabaseServerMessengerOptions())); - - composition.Container.RegisterCollectionBuilder() - .Add(factory => factory.GetInstance().GetCacheRefreshers()); - - composition.Container.RegisterCollectionBuilder() - .Add(f => f.GetInstance().GetPackageActions()); - - composition.Container.RegisterCollectionBuilder() - .Append(factory => factory.GetInstance().GetTypes()); - - composition.Container.Register(new PerContainerLifetime()); - - composition.Container.RegisterSingleton(factory - => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetInstance()))); - - composition.Container.RegisterCollectionBuilder() - .Append(); - - composition.Container.RegisterCollectionBuilder() - .Add(factory => factory.GetInstance().GetTypes()); - - composition.Container.RegisterSingleton(); - - // by default, register a noop factory - composition.Container.RegisterSingleton(); - - composition.Container.RegisterSingleton(); + _mapperProfiles = mapperProfiles; } - internal void Initialize(IEnumerable mapperProfiles) + public void Initialize() { // mapper profiles have been registered & are created by the container Mapper.Initialize(configuration => { - foreach (var profile in mapperProfiles) + foreach (var profile in _mapperProfiles) configuration.AddProfile(profile); }); @@ -139,5 +31,8 @@ namespace Umbraco.Core.Runtime IOHelper.EnsurePathExists(SystemDirectories.MvcViews + "/Partials"); IOHelper.EnsurePathExists(SystemDirectories.MvcViews + "/MacroPartials"); } + + public void Terminate() + { } } } diff --git a/src/Umbraco.Core/Runtime/CoreRuntimeComposer.cs b/src/Umbraco.Core/Runtime/CoreRuntimeComposer.cs new file mode 100644 index 0000000000..3d959f5263 --- /dev/null +++ b/src/Umbraco.Core/Runtime/CoreRuntimeComposer.cs @@ -0,0 +1,121 @@ +using System; +using System.Configuration; +using Umbraco.Core.Cache; +using Umbraco.Core.Components; +using Umbraco.Core.Composing; +using Umbraco.Core.Composing.Composers; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; +using Umbraco.Core.Manifest; +using Umbraco.Core.Migrations; +using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.Validators; +using Umbraco.Core.Scoping; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Core.Sync; +using Umbraco.Core._Legacy.PackageActions; +using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidator; + +namespace Umbraco.Core.Runtime +{ + public class CoreRuntimeComposer : ComponentComposer, IRuntimeComposer + { + public override void Compose(Composition composition) + { + base.Compose(composition); + + // composers + composition + .ComposeConfiguration() + .ComposeRepositories() + .ComposeServices() + .ComposeCoreMappingProfiles() + .ComposeFileSystems(); + + // register persistence mappers - required by database factory so needs to be done here + // means the only place the collection can be modified is in a runtime - afterwards it + // has been frozen and it is too late + composition.WithCollectionBuilder().AddCoreMappers(); + + // register the scope provider + composition.RegisterUnique(); // implements both IScopeProvider and IScopeAccessor + composition.RegisterUnique(f => f.GetInstance()); + composition.RegisterUnique(f => f.GetInstance()); + + // register database builder + // *not* a singleton, don't want to keep it around + composition.Register(); + + // register manifest parser, will be injected in collection builders where needed + composition.RegisterUnique(); + + // register our predefined validators + composition.WithCollectionBuilder() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(); + + // properties and parameters derive from data editors + composition.WithCollectionBuilder() + .Add(() => composition.TypeLoader.GetDataEditors()); + composition.RegisterUnique(); + composition.RegisterUnique(); + + // register a server registrar, by default it's the db registrar + composition.RegisterUnique(f => + { + if ("true".InvariantEquals(ConfigurationManager.AppSettings["umbracoDisableElectionForSingleServer"])) + return new SingleServerRegistrar(f.GetInstance()); + return new DatabaseServerRegistrar( + new Lazy(f.GetInstance), + new DatabaseServerRegistrarOptions()); + }); + + // by default we'll use the database server messenger with default options (no callbacks), + // this will be overridden by either the legacy thing or the db thing in the corresponding + // components in the web project - fixme - should obsolete the legacy thing + composition.RegisterUnique(factory + => new DatabaseServerMessenger( + factory.GetInstance(), + factory.GetInstance(), + factory.GetInstance(), + factory.GetInstance(), + factory.GetInstance(), + true, new DatabaseServerMessengerOptions())); + + composition.WithCollectionBuilder() + .Add(() => composition.TypeLoader.GetCacheRefreshers()); + + composition.WithCollectionBuilder() + .Add(() => composition.TypeLoader.GetPackageActions()); + + composition.WithCollectionBuilder() + .Append(composition.TypeLoader.GetTypes()); + + composition.RegisterUnique(); + + composition.RegisterUnique(factory + => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetInstance()))); + + composition.WithCollectionBuilder() + .Append(); + + composition.WithCollectionBuilder() + .Add(() => composition.TypeLoader.GetTypes()); + + composition.RegisterUnique(factory => new MigrationBuilder(factory)); + + // by default, register a noop factory + composition.RegisterUnique(); + } + } +} diff --git a/src/Umbraco.Core/RuntimeState.cs b/src/Umbraco.Core/RuntimeState.cs index 4f6f56531b..df2ee44a7d 100644 --- a/src/Umbraco.Core/RuntimeState.cs +++ b/src/Umbraco.Core/RuntimeState.cs @@ -7,6 +7,9 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; +using Umbraco.Core.Migrations.Upgrade; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services.Implement; using Umbraco.Core.Sync; namespace Umbraco.Core @@ -17,76 +20,64 @@ namespace Umbraco.Core internal class RuntimeState : IRuntimeState { private readonly ILogger _logger; - private readonly Lazy _serverRegistrar; - private readonly Lazy _mainDom; private readonly IUmbracoSettingsSection _settings; private readonly IGlobalSettings _globalSettings; private readonly HashSet _applicationUrls = new HashSet(); - private RuntimeLevel _level; + private readonly Lazy _mainDom; + private readonly Lazy _serverRegistrar; + private RuntimeLevel _level = RuntimeLevel.Unknown; /// /// Initializes a new instance of the class. /// - /// A logger. - /// A (lazy) server registrar. - /// A (lazy) MainDom. - public RuntimeState(ILogger logger, Lazy serverRegistrar, Lazy mainDom, IUmbracoSettingsSection settings, IGlobalSettings globalSettings) + public RuntimeState(ILogger logger, IUmbracoSettingsSection settings, IGlobalSettings globalSettings, + Lazy mainDom, Lazy serverRegistrar) { _logger = logger; - _serverRegistrar = serverRegistrar; - _mainDom = mainDom; _settings = settings; _globalSettings = globalSettings; + _mainDom = mainDom; + _serverRegistrar = serverRegistrar; } + /// + /// Gets the server registrar. + /// + /// + /// This is NOT exposed in the interface. + /// private IServerRegistrar ServerRegistrar => _serverRegistrar.Value; /// /// Gets the application MainDom. /// - /// This is NOT exposed in the interface as MainDom is internal. - public MainDom MainDom => _mainDom.Value; + /// + /// This is NOT exposed in the interface as MainDom is internal. + /// + public IMainDom MainDom => _mainDom.Value; - /// - /// Gets the version of the executing code. - /// + /// public Version Version => UmbracoVersion.Current; - /// - /// Gets the version comment of the executing code. - /// + /// public string VersionComment => UmbracoVersion.Comment; - /// - /// Gets the semantic version of the executing code. - /// + /// public SemVersion SemanticVersion => UmbracoVersion.SemanticVersion; - /// - /// Gets a value indicating whether the application is running in debug mode. - /// + /// public bool Debug { get; } = GlobalSettings.DebugMode; - /// - /// Gets a value indicating whether the runtime is the current main domain. - /// + /// public bool IsMainDom => MainDom.IsMainDom; - /// - /// Get the server's current role. - /// + /// public ServerRole ServerRole => ServerRegistrar.GetCurrentServerRole(); - /// - /// Gets the Umbraco application url. - /// - /// This is eg "http://www.example.com". + /// public Uri ApplicationUrl { get; private set; } - /// - /// Gets the Umbraco application virtual path. - /// - /// This is either "/" or eg "/virtual". + /// public string ApplicationVirtualPath { get; } = HttpRuntime.AppDomainAppVirtualPath; /// @@ -95,9 +86,7 @@ namespace Umbraco.Core /// public string FinalMigrationState { get; internal set; } - /// - /// Gets the runtime level of execution. - /// + /// public RuntimeLevel Level { get => _level; @@ -137,9 +126,123 @@ namespace Umbraco.Core return _runLevel.WaitHandle.WaitOne(timeout); } - /// - /// Gets the exception that caused the boot to fail. - /// + /// public BootFailedException BootFailedException { get; internal set; } + + /// + /// Determines the runtime level. + /// + public void DetermineRuntimeLevel(IUmbracoDatabaseFactory databaseFactory, ILogger logger) + { + var localVersion = UmbracoVersion.LocalVersion; // the local, files, version + var codeVersion = SemanticVersion; // the executing code version + var connect = false; + + if (localVersion == null) + { + // there is no local version, we are not installed + logger.Debug("No local version, need to install Umbraco."); + Level = RuntimeLevel.Install; + return; + } + + if (localVersion < codeVersion) + { + // there *is* a local version, but it does not match the code version + // need to upgrade + logger.Debug("Local version '{LocalVersion}' < code version '{CodeVersion}', need to upgrade Umbraco.", localVersion, codeVersion); + Level = RuntimeLevel.Upgrade; + } + else if (localVersion > codeVersion) + { + logger.Warn("Local version '{LocalVersion}' > code version '{CodeVersion}', downgrading is not supported.", localVersion, codeVersion); + + // in fact, this is bad enough that we want to throw + throw new BootFailedException($"Local version \"{localVersion}\" > code version \"{codeVersion}\", downgrading is not supported."); + } + else if (databaseFactory.Configured == false) + { + // local version *does* match code version, but the database is not configured + // install (again? this is a weird situation...) + logger.Debug("Database is not configured, need to install Umbraco."); + Level = RuntimeLevel.Install; + return; + } + + // else, keep going, + // anything other than install wants a database - see if we can connect + // (since this is an already existing database, assume localdb is ready) + for (var i = 0; i < 5; i++) + { + connect = databaseFactory.CanConnect; + if (connect) break; + logger.Debug("Could not immediately connect to database, trying again."); + Thread.Sleep(1000); + } + + if (connect == false) + { + // cannot connect to configured database, this is bad, fail + logger.Debug("Could not connect to database."); + + // in fact, this is bad enough that we want to throw + throw new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); + } + + // if we already know we want to upgrade, + // still run EnsureUmbracoUpgradeState to get the states + // (v7 will just get a null state, that's ok) + + // else + // look for a matching migration entry - bypassing services entirely - they are not 'up' yet + // fixme - in a LB scenario, ensure that the DB gets upgraded only once! + bool noUpgrade; + try + { + noUpgrade = EnsureUmbracoUpgradeState(databaseFactory, logger); + } + catch (Exception e) + { + // can connect to the database but cannot check the upgrade state... oops + logger.Warn(e, "Could not check the upgrade state."); + throw new BootFailedException("Could not check the upgrade state.", e); + } + + // if we already know we want to upgrade, exit here + if (Level == RuntimeLevel.Upgrade) + return; + + if (noUpgrade) + { + // the database version matches the code & files version, all clear, can run + Level = RuntimeLevel.Run; + return; + } + + // the db version does not match... but we do have a migration table + // so, at least one valid table, so we quite probably are installed & need to upgrade + + // although the files version matches the code version, the database version does not + // which means the local files have been upgraded but not the database - need to upgrade + logger.Debug("Has not reached the final upgrade step, need to upgrade Umbraco."); + Level = RuntimeLevel.Upgrade; + } + + protected virtual bool EnsureUmbracoUpgradeState(IUmbracoDatabaseFactory databaseFactory, ILogger logger) + { + var upgrader = new UmbracoUpgrader(); + var stateValueKey = upgrader.StateValueKey; + + // no scope, no service - just directly accessing the database + using (var database = databaseFactory.CreateDatabase()) + { + CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey); + FinalMigrationState = upgrader.Plan.FinalState; + } + + logger.Debug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", CurrentMigrationState, FinalMigrationState ?? ""); + + return CurrentMigrationState == FinalMigrationState; + } } } diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index adc5482e68..b8d4d7b430 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -1,6 +1,7 @@ using System; using System.Data; using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.IO; @@ -491,7 +492,7 @@ namespace Umbraco.Core.Scoping // caching config // true if Umbraco.CoreDebug.LogUncompletedScope appSetting is set to "true" private static bool LogUncompletedScopes => (_logUncompletedScopes - ?? (_logUncompletedScopes = UmbracoConfig.For.CoreDebug().LogUncompletedScopes)).Value; + ?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value; /// public void ReadLock(params int[] lockIds) diff --git a/src/Umbraco.Core/Scoping/ScopeContext.cs b/src/Umbraco.Core/Scoping/ScopeContext.cs index dd26fda85e..4ba1999474 100644 --- a/src/Umbraco.Core/Scoping/ScopeContext.cs +++ b/src/Umbraco.Core/Scoping/ScopeContext.cs @@ -7,27 +7,32 @@ namespace Umbraco.Core.Scoping internal class ScopeContext : IScopeContext, IInstanceIdentifiable { private Dictionary _enlisted; - private bool _exiting; public void ScopeExit(bool completed) { if (_enlisted == null) return; - _exiting = true; + // fixme - can we create infinite loops? + // fixme - what about nested events? will they just be plainly ignored = really bad? List exceptions = null; - foreach (var enlisted in _enlisted.Values.OrderBy(x => x.Priority)) + List orderedEnlisted; + while ((orderedEnlisted = _enlisted.Values.OrderBy(x => x.Priority).ToList()).Count > 0) { - try + _enlisted.Clear(); + foreach (var enlisted in orderedEnlisted) { - enlisted.Execute(completed); - } - catch (Exception e) - { - if (exceptions == null) - exceptions = new List(); - exceptions.Add(e); + try + { + enlisted.Execute(completed); + } + catch (Exception e) + { + if (exceptions == null) + exceptions = new List(); + exceptions.Add(e); + } } } @@ -74,9 +79,6 @@ namespace Umbraco.Core.Scoping public T Enlist(string key, Func creator, Action action = null, int priority = 100) { - if (_exiting) - throw new InvalidOperationException("Cannot enlist now, context is exiting."); - var enlistedObjects = _enlisted ?? (_enlisted = new Dictionary()); if (enlistedObjects.TryGetValue(key, out var enlisted)) diff --git a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs index ca01330212..ff595a5d45 100644 --- a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs +++ b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Web; using System.Web.Hosting; using System.Web.Security; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Services; @@ -87,11 +88,11 @@ namespace Umbraco.Core.Security /// public static MembershipProvider GetUsersMembershipProvider() { - if (Membership.Providers[UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider] == null) + if (Membership.Providers[Current.Configs.Settings().Providers.DefaultBackOfficeUserProvider] == null) { - throw new InvalidOperationException("No membership provider found with name " + UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider); + throw new InvalidOperationException("No membership provider found with name " + Current.Configs.Settings().Providers.DefaultBackOfficeUserProvider); } - return Membership.Providers[UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider]; + return Membership.Providers[Current.Configs.Settings().Providers.DefaultBackOfficeUserProvider]; } /// diff --git a/src/Umbraco.Core/Serialization/CaseInsensitiveDictionaryConverter.cs b/src/Umbraco.Core/Serialization/CaseInsensitiveDictionaryConverter.cs new file mode 100644 index 0000000000..a92d562a52 --- /dev/null +++ b/src/Umbraco.Core/Serialization/CaseInsensitiveDictionaryConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Newtonsoft.Json.Converters; + +namespace Umbraco.Core.Serialization +{ + /// + /// Marks dictionaries so they are deserialized as case-insensitive. + /// + /// + /// [JsonConverter(typeof(CaseInsensitiveDictionaryConverter{PropertyData[]}))] + /// public Dictionary{string, PropertyData[]} PropertyData {{ get; set; }} + /// + public class CaseInsensitiveDictionaryConverter : CustomCreationConverter + { + public override bool CanWrite => false; + + public override bool CanRead => true; + + public override bool CanConvert(Type objectType) => typeof(IDictionary).IsAssignableFrom(objectType); + + public override IDictionary Create(Type objectType) => new Dictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs index 5b64584dc6..38d5471bb7 100644 --- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -52,7 +52,7 @@ namespace Umbraco.Core.Services xml.Add(new XAttribute("writerName", content.GetWriterProfile(userService)?.Name ?? "??")); xml.Add(new XAttribute("writerID", content.WriterId)); - xml.Add(new XAttribute("template", content.Template?.Id.ToString(CultureInfo.InvariantCulture) ?? "0")); + xml.Add(new XAttribute("template", content.TemplateId?.ToString(CultureInfo.InvariantCulture) ?? "")); xml.Add(new XAttribute("isPublished", content.Published)); @@ -337,7 +337,8 @@ namespace Umbraco.Core.Services new XElement("Thumbnail", contentType.Thumbnail), new XElement("Description", contentType.Description), new XElement("AllowAtRoot", contentType.AllowedAsRoot.ToString()), - new XElement("IsListView", contentType.IsContainer.ToString())); + new XElement("IsListView", contentType.IsContainer.ToString()), + new XElement("IsElement", contentType.IsElement.ToString())); var masterContentType = contentType.ContentTypeComposition.FirstOrDefault(x => x.Id == contentType.ParentId); if(masterContentType != null) diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 22138a5e8c..7915bbe24b 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -176,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); diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 926afcf0a9..3937d5bf40 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Services { @@ -227,25 +228,25 @@ namespace Umbraco.Core.Services /// Gets children of an entity. /// IEnumerable GetPagedChildren(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + IQuery filter = null, Ordering ordering = null); /// /// Gets descendants of an entity. /// IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); + IQuery filter = null, Ordering ordering = null); /// /// Gets descendants of entities. /// IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); + IQuery filter = null, Ordering ordering = null); /// - /// Gets descendants of root. + /// Gets descendants of root. fixme: Do we really need this? why not just pass in -1 /// IEnumerable GetPagedDescendants(UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "", bool includeTrashed = true); + IQuery filter = null, Ordering ordering = null, bool includeTrashed = true); /// /// Gets the object type of an entity. diff --git a/src/Umbraco.Core/Services/IMemberGroupService.cs b/src/Umbraco.Core/Services/IMemberGroupService.cs index 934bf1a480..6a554aad31 100644 --- a/src/Umbraco.Core/Services/IMemberGroupService.cs +++ b/src/Umbraco.Core/Services/IMemberGroupService.cs @@ -7,6 +7,7 @@ namespace Umbraco.Core.Services { IEnumerable GetAll(); IMemberGroup GetById(int id); + IEnumerable GetByIds(IEnumerable ids); IMemberGroup GetByName(string name); void Save(IMemberGroup memberGroup, bool raiseEvents = true); void Delete(IMemberGroup memberGroup); diff --git a/src/Umbraco.Core/Services/IRedirectUrlService.cs b/src/Umbraco.Core/Services/IRedirectUrlService.cs index 62e59e910c..3bd9b6f2cf 100644 --- a/src/Umbraco.Core/Services/IRedirectUrlService.cs +++ b/src/Umbraco.Core/Services/IRedirectUrlService.cs @@ -14,8 +14,9 @@ namespace Umbraco.Core.Services /// /// The Umbraco url route. /// The content unique key. + /// The culture. /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo. - void Register(string url, Guid contentKey); + void Register(string url, Guid contentKey, string culture = null); /// /// Deletes all redirect urls for a given content. diff --git a/src/Umbraco.Core/Services/ITagService.cs b/src/Umbraco.Core/Services/ITagService.cs index 63b7ce31a7..6be18624cb 100644 --- a/src/Umbraco.Core/Services/ITagService.cs +++ b/src/Umbraco.Core/Services/ITagService.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Services { /// - /// Tag service to query for tags in the tags db table. The tags returned are only relavent for published content & saved media or members + /// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & saved media or members /// /// /// If there is unpublished content with tags, those tags will not be contained. @@ -15,135 +15,84 @@ namespace Umbraco.Core.Services /// public interface ITagService : IService { - + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityById(int id); + + /// + /// Gets a tagged entity. + /// TaggedEntity GetTaggedEntityByKey(Guid key); /// - /// Gets tagged Content by a specific 'Tag Group'. + /// Gets all documents tagged with any tag in the specified group. /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedContentByTagGroup(string tagGroup); + IEnumerable GetTaggedContentByTagGroup(string group, string culture = null); /// - /// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'. + /// Gets all documents tagged with the specified tag. /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedContentByTag(string tag, string tagGroup = null); + IEnumerable GetTaggedContentByTag(string tag, string group = null, string culture = null); /// - /// Gets tagged Media by a specific 'Tag Group'. + /// Gets all media tagged with any tag in the specified group. /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMediaByTagGroup(string tagGroup); + IEnumerable GetTaggedMediaByTagGroup(string group, string culture = null); /// - /// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'. + /// Gets all media tagged with the specified tag. /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMediaByTag(string tag, string tagGroup = null); + IEnumerable GetTaggedMediaByTag(string tag, string group = null, string culture = null); /// - /// Gets tagged Members by a specific 'Tag Group'. + /// Gets all members tagged with any tag in the specified group. /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMembersByTagGroup(string tagGroup); + IEnumerable GetTaggedMembersByTagGroup(string group, string culture = null); /// - /// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'. + /// Gets all members tagged with the specified tag. /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTaggedMembersByTag(string tag, string tagGroup = null); + IEnumerable GetTaggedMembersByTag(string tag, string group = null, string culture = null); /// - /// Gets every tag stored in the database + /// Gets all tags. /// - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllTags(string tagGroup = null); + IEnumerable GetAllTags(string group = null, string culture = null); /// - /// Gets all tags for content items + /// Gets all document tags. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllContentTags(string tagGroup = null); + IEnumerable GetAllContentTags(string group = null, string culture = null); /// - /// Gets all tags for media items + /// Gets all media tags. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllMediaTags(string tagGroup = null); + IEnumerable GetAllMediaTags(string group = null, string culture = null); /// - /// Gets all tags for member items + /// Gets all member tags. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetAllMemberTags(string tagGroup = null); + IEnumerable GetAllMemberTags(string group = null, string culture = null); /// - /// Gets all tags attached to a property by entity id + /// Gets all tags attached to an entity via a property. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null); + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null); /// - /// Gets all tags attached to an entity (content, media or member) by entity id + /// Gets all tags attached to an entity. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForEntity(int contentId, string tagGroup = null); + IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null); /// - /// Gets all tags attached to a property by entity id + /// Gets all tags attached to an entity via a property. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string tagGroup = null); + IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null); /// - /// Gets all tags attached to an entity (content, media or member) by entity id + /// Gets all tags attached to an entity. /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - IEnumerable GetTagsForEntity(Guid contentId, string tagGroup = null); + IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null); } } diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index cb2d90aa63..a926ce32aa 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -90,16 +90,6 @@ namespace Umbraco.Core.Services string[] userGroups = null, string filter = null); - /// - /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method - /// - /// - /// This method exists so that Umbraco developers can use one entry point to create/update users if they choose to. - /// - /// The user to save the password for - /// The password to save - void SavePassword(IUser user, string password); - /// /// Deletes or disables a User /// diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 0412ecb409..9943893473 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -19,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; @@ -27,13 +27,13 @@ namespace Umbraco.Core.Services.Implement private readonly IContentTypeRepository _contentTypeRepository; private readonly IDocumentBlueprintRepository _documentBlueprintRepository; private readonly ILanguageRepository _languageRepository; - private readonly MediaFileSystem _mediaFileSystem; + private readonly IMediaFileSystem _mediaFileSystem; private IQuery _queryNotTrashed; #region Constructors public ContentService(IScopeProvider provider, ILogger logger, - IEventMessagesFactory eventMessagesFactory, MediaFileSystem mediaFileSystem, + IEventMessagesFactory eventMessagesFactory, IMediaFileSystem mediaFileSystem, IDocumentRepository documentRepository, IEntityRepository entityRepository, IAuditRepository auditRepository, IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository, ILanguageRepository languageRepository) : base(provider, logger, eventMessagesFactory) @@ -1552,9 +1552,7 @@ namespace Umbraco.Core.Services.Implement var args = new DeleteEventArgs(c, false); // raise event & get flagged files scope.Events.Dispatch(Deleted, this, args, nameof(Deleted)); - // fixme not going to work, do it differently - _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files - (file, e) => Logger.Error(e, "An error occurred while deleting file attached to nodes: {File}", file)); + // media files deleted by QueuingEventDispatcher } const int pageSize = 500; @@ -2710,11 +2708,6 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); - if (string.IsNullOrWhiteSpace(content.Name)) - { - throw new ArgumentException("Cannot save content blueprint with empty name."); - } - if (content.HasIdentity == false) { content.CreatorId = userId; @@ -2740,6 +2733,8 @@ namespace Umbraco.Core.Services.Implement } } + private static readonly string[] ArrayOfOneNullString = { null }; + public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0) { if (blueprint == null) throw new ArgumentNullException(nameof(blueprint)); @@ -2751,8 +2746,23 @@ namespace Umbraco.Core.Services.Implement content.CreatorId = userId; content.WriterId = userId; - foreach (var property in blueprint.Properties) - content.SetValue(property.Alias, property.GetValue()); //fixme doesn't take into account variants + var now = DateTime.Now; + var cultures = blueprint.CultureInfos.Any() ? blueprint.CultureInfos.Select(x=>x.Key) : ArrayOfOneNullString; + foreach (var culture in cultures) + { + foreach (var property in blueprint.Properties) + { + content.SetValue(property.Alias, property.GetValue(culture), culture); + } + + content.Name = blueprint.Name; + if (!string.IsNullOrEmpty(culture)) + { + content.SetCultureInfo(culture, blueprint.GetCultureName(culture), now); + } + } + + return content; } diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index b74abc03f7..be4f719bb1 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -739,7 +739,7 @@ namespace Umbraco.Core.Services.Implement try { - var container = new EntityContainer(Constants.ObjectTypes.DocumentType) + var container = new EntityContainer(ContainedObjectType) { Name = name, ParentId = parentId, diff --git a/src/Umbraco.Core/Services/Implement/EntityService.cs b/src/Umbraco.Core/Services/Implement/EntityService.cs index 45c229214a..4a3db29940 100644 --- a/src/Umbraco.Core/Services/Implement/EntityService.cs +++ b/src/Umbraco.Core/Services/Implement/EntityService.cs @@ -415,20 +415,19 @@ namespace Umbraco.Core.Services.Implement /// public IEnumerable GetPagedChildren(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "") + IQuery filter = null, Ordering ordering = null) { using (ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.ParentId == id && x.Trashed == false); - var filterQuery = string.IsNullOrWhiteSpace(filter) ? null : Query().Where(x => x.Name.Contains(filter)); - return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering); } } /// public IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") + IQuery filter = null, Ordering ordering = null) { using (ScopeProvider.CreateScope(autoComplete: true)) { @@ -448,14 +447,13 @@ namespace Umbraco.Core.Services.Implement query.Where(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar)); } - var filterQuery = string.IsNullOrWhiteSpace(filter) ? null : Query().Where(x => x.Name.Contains(filter)); - return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering); } } /// public IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") + IQuery filter = null, Ordering ordering = null) { totalRecords = 0; @@ -492,14 +490,13 @@ namespace Umbraco.Core.Services.Implement query.WhereAny(clauses); } - var filterQuery = string.IsNullOrWhiteSpace(filter) ? null : Query().Where(x => x.Name.Contains(filter)); - return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering); } } /// public IEnumerable GetPagedDescendants(UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "", bool includeTrashed = true) + IQuery filter = null, Ordering ordering = null, bool includeTrashed = true) { using (ScopeProvider.CreateScope(autoComplete: true)) { @@ -507,8 +504,7 @@ namespace Umbraco.Core.Services.Implement if (includeTrashed == false) query.Where(x => x.Trashed == false); - var filterQuery = string.IsNullOrWhiteSpace(filter) ? null : Query().Where(x => x.Name.Contains(filter)); - return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering); } } diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 1d04462836..f8c6badb37 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -26,11 +26,11 @@ namespace Umbraco.Core.Services.Implement private readonly IAuditRepository _auditRepository; private readonly IEntityRepository _entityRepository; - private readonly MediaFileSystem _mediaFileSystem; + private readonly IMediaFileSystem _mediaFileSystem; #region Constructors - public MediaService(IScopeProvider provider, MediaFileSystem mediaFileSystem, ILogger logger, IEventMessagesFactory eventMessagesFactory, + public MediaService(IScopeProvider provider, IMediaFileSystem mediaFileSystem, ILogger logger, IEventMessagesFactory eventMessagesFactory, IMediaRepository mediaRepository, IAuditRepository auditRepository, IMediaTypeRepository mediaTypeRepository, IEntityRepository entityRepository) : base(provider, logger, eventMessagesFactory) @@ -757,8 +757,7 @@ namespace Umbraco.Core.Services.Implement var args = new DeleteEventArgs(c, false); // raise event & get flagged files scope.Events.Dispatch(Deleted, this, args); - _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files - (file, e) => Logger.Error(e, "An error occurred while deleting file attached to nodes: {File}", file)); + // media files deleted by QueuingEventDispatcher } const int pageSize = 500; diff --git a/src/Umbraco.Core/Services/Implement/MemberGroupService.cs b/src/Umbraco.Core/Services/Implement/MemberGroupService.cs index e38c1530db..15b3101744 100644 --- a/src/Umbraco.Core/Services/Implement/MemberGroupService.cs +++ b/src/Umbraco.Core/Services/Implement/MemberGroupService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -60,6 +61,19 @@ namespace Umbraco.Core.Services.Implement } } + public IEnumerable GetByIds(IEnumerable ids) + { + if (ids == null || ids.Any() == false) + { + return new IMemberGroup[0]; + } + + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _memberGroupRepository.GetMany(ids.ToArray()); + } + } + public IMemberGroup GetById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) diff --git a/src/Umbraco.Core/Services/Implement/MemberService.cs b/src/Umbraco.Core/Services/Implement/MemberService.cs index 5a644cfec1..2f8c2f9a79 100644 --- a/src/Umbraco.Core/Services/Implement/MemberService.cs +++ b/src/Umbraco.Core/Services/Implement/MemberService.cs @@ -28,14 +28,14 @@ namespace Umbraco.Core.Services.Implement private readonly IAuditRepository _auditRepository; private readonly IMemberGroupService _memberGroupService; - private readonly MediaFileSystem _mediaFileSystem; + private readonly IMediaFileSystem _mediaFileSystem; //only for unit tests! internal MembershipProviderBase MembershipProvider { get; set; } #region Constructor - public MemberService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IMemberGroupService memberGroupService, MediaFileSystem mediaFileSystem, + public MemberService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IMemberGroupService memberGroupService, IMediaFileSystem mediaFileSystem, IMemberRepository memberRepository, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, IAuditRepository auditRepository) : base(provider, logger, eventMessagesFactory) { @@ -929,10 +929,7 @@ namespace Umbraco.Core.Services.Implement args.CanCancel = false; scope.Events.Dispatch(Deleted, this, args); - // fixme - this is MOOT because the event will not trigger immediately - // it's been refactored already (think it's the dispatcher that deals with it?) - _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files - (file, e) => Logger.Error(e, "An error occurred while deleting file attached to nodes: {File}", file)); + // media files deleted by QueuingEventDispatcher } #endregion @@ -1033,7 +1030,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 +1047,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 +1063,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 +1079,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/Implement/PackagingService.cs b/src/Umbraco.Core/Services/Implement/PackagingService.cs index fff865e097..99ee7a098b 100644 --- a/src/Umbraco.Core/Services/Implement/PackagingService.cs +++ b/src/Umbraco.Core/Services/Implement/PackagingService.cs @@ -66,7 +66,7 @@ namespace Umbraco.Core.Services.Implement IEntityService entityService, IUserService userService, IScopeProvider scopeProvider, - IEnumerable urlSegmentProviders, + UrlSegmentProviderCollection urlSegmentProviders, IAuditRepository auditRepository, IContentTypeRepository contentTypeRepository, PropertyEditorCollection propertyEditors) { @@ -578,6 +578,10 @@ namespace Umbraco.Core.Services.Implement if (isListView != null) contentType.IsContainer = isListView.Value.InvariantEquals("true"); + var isElement = infoElement.Element("IsElement"); + if (isElement != null) + contentType.IsElement = isElement.Value.InvariantEquals("true"); + //Name of the master corresponds to the parent and we need to ensure that the Parent Id is set var masterElement = infoElement.Element("Master"); if (masterElement != null) diff --git a/src/Umbraco.Core/Services/Implement/RedirectUrlService.cs b/src/Umbraco.Core/Services/Implement/RedirectUrlService.cs index e9703bd85c..80816961fc 100644 --- a/src/Umbraco.Core/Services/Implement/RedirectUrlService.cs +++ b/src/Umbraco.Core/Services/Implement/RedirectUrlService.cs @@ -19,15 +19,15 @@ namespace Umbraco.Core.Services.Implement _redirectUrlRepository = redirectUrlRepository; } - public void Register(string url, Guid contentKey) + public void Register(string url, Guid contentKey, string culture = null) { using (var scope = ScopeProvider.CreateScope()) { - var redir = _redirectUrlRepository.Get(url, contentKey); + var redir = _redirectUrlRepository.Get(url, contentKey, culture); if (redir != null) redir.CreateDateUtc = DateTime.UtcNow; else - redir = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey }; + redir = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey, Culture = culture}; _redirectUrlRepository.Save(redir); scope.Complete(); } diff --git a/src/Umbraco.Core/Services/Implement/TagService.cs b/src/Umbraco.Core/Services/Implement/TagService.cs index b2395502dc..e888258067 100644 --- a/src/Umbraco.Core/Services/Implement/TagService.cs +++ b/src/Umbraco.Core/Services/Implement/TagService.cs @@ -25,230 +25,147 @@ namespace Umbraco.Core.Services.Implement _tagRepository = tagRepository; } + /// public TaggedEntity GetTaggedEntityById(int id) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { return _tagRepository.GetTaggedEntityById(id); } } + /// public TaggedEntity GetTaggedEntityByKey(Guid key) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { return _tagRepository.GetTaggedEntityByKey(key); } } - /// - /// Gets tagged Content by a specific 'Tag Group'. - /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedContentByTagGroup(string tagGroup) + /// + public IEnumerable GetTaggedContentByTagGroup(string group, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, tagGroup); + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, group, culture); } } - /// - /// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'. - /// - /// The contains the Id and Tags of the Content, not the actual Content item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedContentByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedContentByTag(string tag, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, tagGroup); + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, group, culture); } } - /// - /// Gets tagged Media by a specific 'Tag Group'. - /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMediaByTagGroup(string tagGroup) + /// + public IEnumerable GetTaggedMediaByTagGroup(string group, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, tagGroup); + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, group, culture); } } - /// - /// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'. - /// - /// The contains the Id and Tags of the Media, not the actual Media item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMediaByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedMediaByTag(string tag, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, tagGroup); + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, group, culture); } } - /// - /// Gets tagged Members by a specific 'Tag Group'. - /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMembersByTagGroup(string tagGroup) + /// + public IEnumerable GetTaggedMembersByTagGroup(string group, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, tagGroup); + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, group, culture); } } - /// - /// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'. - /// - /// The contains the Id and Tags of the Member, not the actual Member item. - /// Tag - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTaggedMembersByTag(string tag, string tagGroup = null) + /// + public IEnumerable GetTaggedMembersByTag(string tag, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, tagGroup); + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, group, culture); } } - /// - /// Gets every tag stored in the database - /// - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllTags(string tagGroup = null) + /// + public IEnumerable GetAllTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, group, culture); } } - /// - /// Gets all tags for content items - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllContentTags(string tagGroup = null) + /// + public IEnumerable GetAllContentTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, group, culture); } } - /// - /// Gets all tags for media items - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllMediaTags(string tagGroup = null) + /// + public IEnumerable GetAllMediaTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, group, culture); } } - /// - /// Gets all tags for member items - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetAllMemberTags(string tagGroup = null) + /// + public IEnumerable GetAllMemberTags(string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, tagGroup); + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, group, culture); } } - /// - /// Gets all tags attached to a property by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null) + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup); + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } } - /// - /// Gets all tags attached to an entity (content, media or member) by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForEntity(int contentId, string tagGroup = null) + /// + public IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntity(contentId, tagGroup); + return _tagRepository.GetTagsForEntity(contentId, group, culture); } } - /// - /// Gets all tags attached to a property by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Property type alias - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string tagGroup = null) + /// + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, tagGroup); + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } } - /// - /// Gets all tags attached to an entity (content, media or member) by entity id - /// - /// Use the optional tagGroup parameter to limit the - /// result to a specific 'Tag Group'. - /// The content item id to get tags for - /// Optional name of the 'Tag Group' - /// An enumerable list of - public IEnumerable GetTagsForEntity(Guid contentId, string tagGroup = null) + /// + public IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (ScopeProvider.CreateScope(autoComplete: true)) { - return _tagRepository.GetTagsForEntity(contentId, tagGroup); + return _tagRepository.GetTagsForEntity(contentId, group, culture); } } } diff --git a/src/Umbraco.Core/Services/Implement/UserService.cs b/src/Umbraco.Core/Services/Implement/UserService.cs index 44358caa84..188c6feb04 100644 --- a/src/Umbraco.Core/Services/Implement/UserService.cs +++ b/src/Umbraco.Core/Services/Implement/UserService.cs @@ -227,30 +227,6 @@ namespace Umbraco.Core.Services.Implement Save(membershipUser); } - [Obsolete("ASP.NET Identity APIs like the BackOfficeUserManager should be used to manage passwords, this will not work with correct security practices because you would need the existing password")] - [EditorBrowsable(EditorBrowsableState.Never)] - public void SavePassword(IUser user, string password) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - var provider = MembershipProviderExtensions.GetUsersMembershipProvider(); - - if (provider.IsUmbracoMembershipProvider() == false) - throw new NotSupportedException("When using a non-Umbraco membership provider you must change the user password by using the MembershipProvider.ChangePassword method"); - - provider.ChangePassword(user.Username, "", password); - - //go re-fetch the member and update the properties that may have changed - var result = GetByUsername(user.Username); - if (result != null) - { - //should never be null but it could have been deleted by another thread. - user.RawPasswordValue = result.RawPasswordValue; - user.LastPasswordChangeDate = result.LastPasswordChangeDate; - user.UpdateDate = result.UpdateDate; - } - } - /// /// Deletes or disables a User /// 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/PublicAccessServiceExtensions.cs b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs index 12db4daf40..b0dc979ebf 100644 --- a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs +++ b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Web.Security; +using Umbraco.Core.Models; namespace Umbraco.Core.Services { @@ -41,7 +42,7 @@ namespace Umbraco.Core.Services return hasChange; } - public static bool HasAccess(this IPublicAccessService publicAccessService, int documentId, IContentService contentService, IEnumerable currentMemberRoles) + public static bool HasAccess(this IPublicAccessService publicAccessService, int documentId, IContentService contentService, string username, IEnumerable currentMemberRoles) { var content = contentService.GetById(documentId); if (content == null) return true; @@ -49,8 +50,7 @@ namespace Umbraco.Core.Services var entry = publicAccessService.GetEntryForContent(content); if (entry == null) return true; - return entry.Rules.Any(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType - && currentMemberRoles.Contains(x.RuleValue)); + return HasAccess(entry, username, currentMemberRoles); } public static bool HasAccess(this IPublicAccessService publicAccessService, string path, MembershipUser member, RoleProvider roleProvider) @@ -77,8 +77,15 @@ namespace Umbraco.Core.Services var roles = rolesCallback(username); - return entry.Rules.Any(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType - && roles.Contains(x.RuleValue)); + return HasAccess(entry, username, roles); + } + + private static bool HasAccess(PublicAccessEntry entry, string username, IEnumerable roles) + { + return entry.Rules.Any(x => + (x.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType && username.Equals(x.RuleValue, StringComparison.OrdinalIgnoreCase)) + || (x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType && roles.Contains(x.RuleValue)) + ); } } } diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index f90b1d8d64..731d3a58c6 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -38,7 +38,6 @@ namespace Umbraco.Core.Services /// /// Initializes a new instance of the class with lazy services. /// - /// Used by IoC. Note that LightInject will favor lazy args when picking a constructor. public ServiceContext(Lazy publicAccessService, Lazy domainService, Lazy auditService, Lazy localizedTextService, Lazy tagService, Lazy contentService, Lazy userService, Lazy memberService, Lazy mediaService, Lazy contentTypeService, Lazy mediaTypeService, Lazy dataTypeService, Lazy fileService, Lazy localizationService, Lazy packagingService, Lazy serverRegistrationService, Lazy entityService, Lazy relationService, Lazy treeService, Lazy sectionService, Lazy macroService, Lazy memberTypeService, Lazy memberGroupService, Lazy notificationService, Lazy externalLoginService, Lazy redirectUrlService, Lazy consentService) { _publicAccessService = publicAccessService; @@ -71,10 +70,13 @@ namespace Umbraco.Core.Services } /// - /// Initializes a new instance of the class with services. + /// Creates a partial service context with only some services (for tests). /// - /// Used in tests. All items are optional and remain null if not specified. - public ServiceContext(IContentService contentService = null, + /// + /// Using a true constructor for this confuses DI containers. + /// + public static ServiceContext CreatePartial( + IContentService contentService = null, IMediaService mediaService = null, IContentTypeService contentTypeService = null, IMediaTypeService mediaTypeService = null, @@ -102,40 +104,43 @@ namespace Umbraco.Core.Services IRedirectUrlService redirectUrlService = null, IConsentService consentService = null) { - if (serverRegistrationService != null) _serverRegistrationService = new Lazy(() => serverRegistrationService); - if (externalLoginService != null) _externalLoginService = new Lazy(() => externalLoginService); - if (auditService != null) _auditService = new Lazy(() => auditService); - if (localizedTextService != null) _localizedTextService = new Lazy(() => localizedTextService); - if (tagService != null) _tagService = new Lazy(() => tagService); - if (contentService != null) _contentService = new Lazy(() => contentService); - if (mediaService != null) _mediaService = new Lazy(() => mediaService); - if (contentTypeService != null) _contentTypeService = new Lazy(() => contentTypeService); - if (mediaTypeService != null) _mediaTypeService = new Lazy(() => mediaTypeService); - if (dataTypeService != null) _dataTypeService = new Lazy(() => dataTypeService); - if (fileService != null) _fileService = new Lazy(() => fileService); - if (localizationService != null) _localizationService = new Lazy(() => localizationService); - if (packagingService != null) _packagingService = new Lazy(() => packagingService); - if (entityService != null) _entityService = new Lazy(() => entityService); - if (relationService != null) _relationService = new Lazy(() => relationService); - if (sectionService != null) _sectionService = new Lazy(() => sectionService); - if (memberGroupService != null) _memberGroupService = new Lazy(() => memberGroupService); - if (memberTypeService != null) _memberTypeService = new Lazy(() => memberTypeService); - if (treeService != null) _treeService = new Lazy(() => treeService); - if (memberService != null) _memberService = new Lazy(() => memberService); - if (userService != null) _userService = new Lazy(() => userService); - if (notificationService != null) _notificationService = new Lazy(() => notificationService); - if (domainService != null) _domainService = new Lazy(() => domainService); - if (macroService != null) _macroService = new Lazy(() => macroService); - if (publicAccessService != null) _publicAccessService = new Lazy(() => publicAccessService); - if (redirectUrlService != null) _redirectUrlService = new Lazy(() => redirectUrlService); - if (consentService != null) _consentService = new Lazy(() => consentService); + Lazy Lazy(T service) => service == null ? null : new Lazy(() => service); + + return new ServiceContext( + Lazy(publicAccessService), + Lazy(domainService), + Lazy(auditService), + Lazy(localizedTextService), + Lazy(tagService), + Lazy(contentService), + Lazy(userService), + Lazy(memberService), + Lazy(mediaService), + Lazy(contentTypeService), + Lazy(mediaTypeService), + Lazy(dataTypeService), + Lazy(fileService), + Lazy(localizationService), + Lazy(packagingService), + Lazy(serverRegistrationService), + Lazy(entityService), + Lazy(relationService), + Lazy(treeService), + Lazy(sectionService), + Lazy(macroService), + Lazy(memberTypeService), + Lazy(memberGroupService), + Lazy(notificationService), + Lazy(externalLoginService), + Lazy(redirectUrlService), + Lazy(consentService)); } - + /// /// Gets the /// public IPublicAccessService PublicAccessService => _publicAccessService.Value; - + /// /// Gets the /// diff --git a/src/Umbraco.Core/SimpleMainDom.cs b/src/Umbraco.Core/SimpleMainDom.cs new file mode 100644 index 0000000000..87cc7bcff1 --- /dev/null +++ b/src/Umbraco.Core/SimpleMainDom.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core +{ + /// + /// Provides a simple implementation of . + /// + public class SimpleMainDom : IMainDom + { + private readonly object _locko = new object(); + private readonly List> _callbacks = new List>(); + private bool _isStopping; + + /// + public bool IsMainDom { get; private set; } = true; + + /// + public bool Register(Action release, int weight = 100) + => Register(null, release, weight); + + /// + public bool Register(Action install, Action release, int weight = 100) + { + lock (_locko) + { + if (_isStopping) return false; + install?.Invoke(); + if (release != null) + _callbacks.Add(new KeyValuePair(weight, release)); + return true; + } + } + + public void Stop() + { + lock (_locko) + { + if (_isStopping) return; + if (IsMainDom == false) return; // probably not needed + _isStopping = true; + } + + try + { + foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) + { + callback(); // no timeout on callbacks + } + } + finally + { + // in any case... + IsMainDom = false; + } + } + } +} diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 461b1d3c1e..90fc472e30 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -187,7 +187,6 @@ namespace Umbraco.Core outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement; return new string(outputArray); } - private static readonly char[] CleanForXssChars = "*?(){}[];:%<>/\\|&'\"".ToCharArray(); /// @@ -540,7 +539,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); } /// @@ -1115,7 +1114,7 @@ namespace Umbraco.Core /// Checks UmbracoSettings.ForceSafeAliases to determine whether it should filter the text. public static string ToSafeAliasWithForcingCheck(this string alias) { - return UmbracoConfig.For.UmbracoSettings().Content.ForceSafeAliases ? alias.ToSafeAlias() : alias; + return Current.Configs.Settings().Content.ForceSafeAliases ? alias.ToSafeAlias() : alias; } /// @@ -1127,7 +1126,7 @@ namespace Umbraco.Core /// Checks UmbracoSettings.ForceSafeAliases to determine whether it should filter the text. public static string ToSafeAliasWithForcingCheck(this string alias, string culture) { - return UmbracoConfig.For.UmbracoSettings().Content.ForceSafeAliases ? alias.ToSafeAlias(culture) : alias; + return Current.Configs.Settings().Content.ForceSafeAliases ? alias.ToSafeAlias(culture) : alias; } // the new methods to get a url segment diff --git a/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs b/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs index a9b8234e14..5183c28e15 100644 --- a/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs +++ b/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs @@ -1,14 +1,9 @@ -using LightInject; -using Umbraco.Core.Composing; +using Umbraco.Core.Composing; namespace Umbraco.Core.Strings { public class UrlSegmentProviderCollectionBuilder : OrderedCollectionBuilderBase { - public UrlSegmentProviderCollectionBuilder(IServiceContainer container) - : base(container) - { } - protected override UrlSegmentProviderCollectionBuilder This => this; } } diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 20f9276ba1..5cfcb501e5 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -33,7 +33,7 @@ namespace Umbraco.Core.Sync private readonly IRuntimeState _runtime; private readonly ManualResetEvent _syncIdle; private readonly object _locko = new object(); - private readonly ProfilingLogger _profilingLogger; + private readonly IProfilingLogger _profilingLogger; private readonly ISqlContext _sqlContext; private readonly Lazy _distCacheFilePath; private int _lastId = -1; @@ -46,7 +46,7 @@ namespace Umbraco.Core.Sync public DatabaseServerMessengerOptions Options { get; } public DatabaseServerMessenger( - IRuntimeState runtime, IScopeProvider scopeProvider, ISqlContext sqlContext, ProfilingLogger proflog, IGlobalSettings globalSettings, + IRuntimeState runtime, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, bool distributedEnabled, DatabaseServerMessengerOptions options) : base(distributedEnabled) { @@ -54,7 +54,7 @@ namespace Umbraco.Core.Sync _sqlContext = sqlContext; _runtime = runtime; _profilingLogger = proflog ?? throw new ArgumentNullException(nameof(proflog)); - Logger = proflog.Logger; + Logger = proflog; Options = options ?? throw new ArgumentNullException(nameof(options)); _lastPruned = _lastSync = DateTime.UtcNow; _syncIdle = new ManualResetEvent(true); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 23b90aaf3c..7d65a46f49 100755 --- 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 @@ -111,7 +111,6 @@ - @@ -155,35 +154,49 @@ - + + + - - - - - - - - + + + + + + + + + + + + - - + + - - - - + + + + + + + + + + - - + + + + @@ -192,10 +205,12 @@ + + - - + + @@ -203,7 +218,7 @@ - + @@ -245,7 +260,6 @@ - @@ -304,7 +318,6 @@ - @@ -313,18 +326,27 @@ + + + + + + + + + @@ -333,9 +355,12 @@ - + + + + @@ -352,6 +377,7 @@ + @@ -363,6 +389,8 @@ + + @@ -378,7 +406,7 @@ - + @@ -439,6 +467,7 @@ + @@ -447,6 +476,7 @@ + @@ -561,7 +591,7 @@ - + @@ -578,7 +608,6 @@ - @@ -1213,7 +1242,6 @@ - @@ -1272,6 +1300,7 @@ + @@ -1305,6 +1334,7 @@ + @@ -1415,6 +1445,7 @@ + diff --git a/src/Umbraco.Core/_Legacy/PackageActions/PackageActionCollectionBuilder.cs b/src/Umbraco.Core/_Legacy/PackageActions/PackageActionCollectionBuilder.cs index 42ab3ec7c2..2f73a2b489 100644 --- a/src/Umbraco.Core/_Legacy/PackageActions/PackageActionCollectionBuilder.cs +++ b/src/Umbraco.Core/_Legacy/PackageActions/PackageActionCollectionBuilder.cs @@ -1,14 +1,9 @@ -using LightInject; -using Umbraco.Core.Composing; +using Umbraco.Core.Composing; namespace Umbraco.Core._Legacy.PackageActions { internal class PackageActionCollectionBuilder : LazyCollectionBuilderBase { - public PackageActionCollectionBuilder(IServiceContainer container) - : base(container) - { } - protected override PackageActionCollectionBuilder This => this; } } 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/ConfigIndexCriteria.cs b/src/Umbraco.Examine/Config/ConfigIndexCriteria.cs deleted file mode 100644 index de2a5ced36..0000000000 --- a/src/Umbraco.Examine/Config/ConfigIndexCriteria.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Examine; - -namespace Umbraco.Examine.Config -{ - /// - /// a data structure for storing indexing/searching instructions based on config based indexers - /// - public class ConfigIndexCriteria - { - /// - /// Constructor - /// - /// - /// - /// - /// - /// - public ConfigIndexCriteria(IEnumerable standardFields, IEnumerable userFields, IEnumerable includeNodeTypes, IEnumerable excludeNodeTypes, int? parentNodeId) - { - UserFields = userFields.ToList(); - StandardFields = standardFields.ToList(); - IncludeItemTypes = includeNodeTypes; - ExcludeItemTypes = excludeNodeTypes; - ParentNodeId = parentNodeId; - } - - public IEnumerable StandardFields { get; internal set; } - public IEnumerable UserFields { get; internal set; } - - public IEnumerable IncludeItemTypes { get; internal set; } - public IEnumerable ExcludeItemTypes { get; internal set; } - public int? ParentNodeId { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Examine/Config/ConfigIndexField.cs b/src/Umbraco.Examine/Config/ConfigIndexField.cs deleted file mode 100644 index ec9cbf797e..0000000000 --- a/src/Umbraco.Examine/Config/ConfigIndexField.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Configuration; - -namespace Umbraco.Examine.Config -{ - /// - /// A configuration item representing a field to index - /// - public sealed class ConfigIndexField : ConfigurationElement - { - [ConfigurationProperty("Name", IsRequired = true)] - public string Name - { - get => (string)this["Name"]; - set => this["Name"] = value; - } - - [ConfigurationProperty("EnableSorting", IsRequired = false)] - public bool EnableSorting - { - get => (bool)this["EnableSorting"]; - set => this["EnableSorting"] = value; - } - - [ConfigurationProperty("Type", IsRequired = false, DefaultValue = "String")] - public string Type - { - get => (string)this["Type"]; - set => this["Type"] = value; - } - - public bool Equals(ConfigIndexField other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return string.Equals(Name, other.Name); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((ConfigIndexField)obj); - } - - public override int GetHashCode() - { - return Name.GetHashCode(); - } - - public static bool operator ==(ConfigIndexField left, ConfigIndexField right) - { - return Equals(left, right); - } - - public static bool operator !=(ConfigIndexField left, ConfigIndexField right) - { - return !Equals(left, right); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Examine/Config/IndexFieldCollectionExtensions.cs b/src/Umbraco.Examine/Config/IndexFieldCollectionExtensions.cs deleted file mode 100644 index eea117ce05..0000000000 --- a/src/Umbraco.Examine/Config/IndexFieldCollectionExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace Umbraco.Examine.Config -{ - public static class IndexFieldCollectionExtensions - { - public static List ToList(this IndexFieldCollection indexes) - { - List fields = new List(); - foreach (ConfigIndexField field in indexes) - fields.Add(field); - return fields; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Examine/Config/IndexSet.cs b/src/Umbraco.Examine/Config/IndexSet.cs deleted file mode 100644 index 33b67412f2..0000000000 --- a/src/Umbraco.Examine/Config/IndexSet.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System.Configuration; -using System.IO; -using System.Web; -using System.Web.Hosting; - -namespace Umbraco.Examine.Config -{ - public sealed class IndexSet : ConfigurationElement - { - - [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. - /// - [ConfigurationProperty("IndexParentId", IsRequired = false, IsKey = false)] - public int? IndexParentId - { - get - { - if (this["IndexParentId"] == null) - return null; - - return (int)this["IndexParentId"]; - } - } - - /// - /// The collection of node types to index, if not specified, all node types will be indexed (apart from the ones specified in the ExcludeNodeTypes collection). - /// - [ConfigurationCollection(typeof(IndexFieldCollection))] - [ConfigurationProperty("IncludeNodeTypes", IsDefaultCollection = false, IsRequired = false)] - public IndexFieldCollection IncludeNodeTypes => (IndexFieldCollection)base["IncludeNodeTypes"]; - - /// - /// The collection of node types to not index. If specified, these node types will not be indexed. - /// - [ConfigurationCollection(typeof(IndexFieldCollection))] - [ConfigurationProperty("ExcludeNodeTypes", IsDefaultCollection = false, IsRequired = false)] - public IndexFieldCollection ExcludeNodeTypes => (IndexFieldCollection)base["ExcludeNodeTypes"]; - - /// - /// A collection of user defined umbraco fields to index - /// - /// - /// If this property is not specified, or if it's an empty collection, the default user fields will be all user fields defined in Umbraco - /// - [ConfigurationCollection(typeof(IndexFieldCollection))] - [ConfigurationProperty("IndexUserFields", IsDefaultCollection = false, IsRequired = false)] - public IndexFieldCollection IndexUserFields => (IndexFieldCollection)base["IndexUserFields"]; - - /// - /// The fields umbraco values that will be indexed. i.e. id, nodeTypeAlias, writer, etc... - /// - /// - /// If this is not specified, or if it's an empty collection, the default optins will be specified: - /// - id - /// - version - /// - parentID - /// - level - /// - writerID - /// - creatorID - /// - nodeType - /// - template - /// - sortOrder - /// - createDate - /// - updateDate - /// - nodeName - /// - urlName - /// - writerName - /// - creatorName - /// - nodeTypeAlias - /// - path - /// - [ConfigurationCollection(typeof(IndexFieldCollection))] - [ConfigurationProperty("IndexAttributeFields", IsDefaultCollection = false, IsRequired = false)] - public IndexFieldCollection IndexAttributeFields => (IndexFieldCollection)base["IndexAttributeFields"]; - } -} diff --git a/src/Umbraco.Examine/Config/IndexSetCollection.cs b/src/Umbraco.Examine/Config/IndexSetCollection.cs deleted file mode 100644 index 5ba2382563..0000000000 --- a/src/Umbraco.Examine/Config/IndexSetCollection.cs +++ /dev/null @@ -1,73 +0,0 @@ -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); - } - } - - } - - - public sealed class IndexSetCollection : ConfigurationElementCollection - { - #region Overridden methods to define collection - protected override ConfigurationElement CreateNewElement() - { - return new IndexSet(); - } - protected override object GetElementKey(ConfigurationElement element) - { - return ((IndexSet)element).SetName; - } - public override ConfigurationElementCollectionType CollectionType => ConfigurationElementCollectionType.BasicMap; - protected override string ElementName => "IndexSet"; - - #endregion - - /// - /// Default property for accessing Image Sets - /// - /// - /// - public new IndexSet this[string setName] => (IndexSet)this.BaseGet(setName); - } -} \ No newline at end of file diff --git a/src/Umbraco.Examine/Config/IndexSets.cs b/src/Umbraco.Examine/Config/IndexSets.cs deleted file mode 100644 index c6ad1476c3..0000000000 --- a/src/Umbraco.Examine/Config/IndexSets.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Configuration; - -namespace Umbraco.Examine.Config -{ - public sealed class IndexSets : ConfigurationSection - { - #region Singleton definition - - private IndexSets() { } - - public static IndexSets Instance { get; } = ConfigurationManager.GetSection(SectionName) as IndexSets; - - #endregion - - private const string SectionName = "ExamineLuceneIndexSets"; - - [ConfigurationCollection(typeof(IndexSetCollection))] - [ConfigurationProperty("", IsDefaultCollection = true, IsRequired = true)] - public IndexSetCollection Sets => (IndexSetCollection)base[""]; - } -} diff --git a/src/Umbraco.Examine/ContentIndexPopulator.cs b/src/Umbraco.Examine/ContentIndexPopulator.cs new file mode 100644 index 0000000000..51b9de4a0b --- /dev/null +++ b/src/Umbraco.Examine/ContentIndexPopulator.cs @@ -0,0 +1,100 @@ +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; + } + + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) return; + + 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..d8e698cdad --- /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 ? "y" : "n"}}, //Always add invariant published value + {"id", new object[] {c.Id}}, + {UmbracoExamineIndex.NodeKeyFieldName, 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}}, + {"templateID", new object[] {c.TemplateId ?? 0}}, + {UmbracoContentIndex.VariesByCultureFieldName, new object[] {"n"}}, + }; + + if (isVariant) + { + values[UmbracoContentIndex.VariesByCultureFieldName] = new object[] { "y" }; + + 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) ? "y" : "n").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..9555566c53 --- /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("y")) + 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("y")) + { + //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("y")) + { + //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..4fe6c359d7 --- /dev/null +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Examine; +using Examine.LuceneEngine.Providers; +using Lucene.Net.Analysis; +using Lucene.Net.Index; +using Lucene.Net.QueryParsers; +using Lucene.Net.Search; +using Lucene.Net.Store; +using Umbraco.Core; +using Version = Lucene.Net.Util.Version; +using Umbraco.Core.Logging; + +namespace Umbraco.Examine +{ + /// + /// Extension methods for the LuceneIndex + /// + public static class ExamineExtensions + { + /// + /// Matches a culture iso name suffix + /// + /// + /// myFieldName_en-us will match the "en-us" + /// + internal static readonly Regex CultureIsoCodeFieldNameMatchExpression = new Regex("^([_\\w]+)_([a-z]{2}-[a-z0-9]{2,4})$", RegexOptions.Compiled); + + /// + /// Returns all index fields that are culture specific (suffixed) + /// + /// + /// + /// + public static IEnumerable GetCultureFields(this IUmbracoIndex index, string culture) + { + var allFields = index.GetFields(); + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var field in allFields) + { + var match = CultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && match.Groups.Count == 3 && culture.InvariantEquals(match.Groups[2].Value)) + yield return field; + } + } + + internal static bool TryParseLuceneQuery(string query) + { + //TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet, for now we'll + // also do this rudimentary check + if (!query.Contains(":")) + return false; + + try + { + //This will pass with a plain old string without any fields, need to figure out a way to have it properly parse + var parsed = new QueryParser(Version.LUCENE_30, "nodeName", new KeywordAnalyzer()).Parse(query); + return true; + } + catch (ParseException) + { + return false; + } + catch (Exception) + { + return false; + } + } + + /// + /// Forcibly unlocks all lucene based indexes + /// + /// + /// This is not thread safe, use with care + /// + internal static void UnlockLuceneIndexes(this IExamineManager examineManager, ILogger logger) + { + foreach (var luceneIndexer in examineManager.Indexes.OfType()) + { + //We now need to disable waiting for indexing for Examine so that the appdomain is shutdown immediately and doesn't wait for pending + //indexing operations. We used to wait for indexing operations to complete but this can cause more problems than that is worth because + //that could end up halting shutdown for a very long time causing overlapping appdomains and many other problems. + luceneIndexer.WaitForIndexQueueOnShutdown = false; + + //we should check if the index is locked ... it shouldn't be! We are using simple fs lock now and we are also ensuring that + //the indexes are not operational unless MainDom is true + var dir = luceneIndexer.GetLuceneDirectory(); + if (IndexWriter.IsLocked(dir)) + { + logger.Info(typeof(ExamineExtensions), "Forcing index {IndexerName} to be unlocked since it was left in a locked state", luceneIndexer.Name); + IndexWriter.Unlock(dir); + } + } + } + + /// + /// 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/IIndexCreator.cs b/src/Umbraco.Examine/IIndexCreator.cs new file mode 100644 index 0000000000..3b8f683990 --- /dev/null +++ b/src/Umbraco.Examine/IIndexCreator.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Examine; + +namespace Umbraco.Examine +{ + /// + /// Creates 's + /// + public interface IIndexCreator + { + IEnumerable Create(); + } +} diff --git a/src/Umbraco.Examine/IIndexDiagnostics.cs b/src/Umbraco.Examine/IIndexDiagnostics.cs new file mode 100644 index 0000000000..29d530c2d0 --- /dev/null +++ b/src/Umbraco.Examine/IIndexDiagnostics.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +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..97a1216fae --- /dev/null +++ b/src/Umbraco.Examine/IIndexPopulator.cs @@ -0,0 +1,20 @@ +using Examine; + +namespace Umbraco.Examine +{ + public interface IIndexPopulator + { + /// + /// If this index is registered with this populator + /// + /// + /// + bool IsRegistered(IIndex index); + + /// + /// 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/IUmbracoIndex.cs b/src/Umbraco.Examine/IUmbracoIndex.cs new file mode 100644 index 0000000000..9461434fff --- /dev/null +++ b/src/Umbraco.Examine/IUmbracoIndex.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Examine; + +namespace Umbraco.Examine +{ + /// + /// A Marker interface for defining an Umbraco indexer + /// + public interface IUmbracoIndex : 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; } + + /// + /// Returns a list of all indexed fields + /// + /// + IEnumerable GetFields(); + } +} diff --git a/src/Umbraco.Examine/IValueSetBuilder.cs b/src/Umbraco.Examine/IValueSetBuilder.cs new file mode 100644 index 0000000000..1c4890f404 --- /dev/null +++ b/src/Umbraco.Examine/IValueSetBuilder.cs @@ -0,0 +1,21 @@ +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 + { + /// + /// Creates a collection of to be indexed based on a collection of + /// + /// + /// + IEnumerable GetValueSets(params T[] content); + } + +} diff --git a/src/Umbraco.Examine/IndexPopulator.cs b/src/Umbraco.Examine/IndexPopulator.cs new file mode 100644 index 0000000000..f9d4d85dc8 --- /dev/null +++ b/src/Umbraco.Examine/IndexPopulator.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; +using Examine; +using Umbraco.Core.Collections; + +namespace Umbraco.Examine +{ + /// + /// An that is automatically associated to any index of type + /// + /// + public abstract class IndexPopulator : IndexPopulator where TIndex : IIndex + { + public override bool IsRegistered(IIndex index) + { + if (base.IsRegistered(index)) return true; + return index is TIndex; + } + } + + public abstract class IndexPopulator : IIndexPopulator + { + private readonly ConcurrentHashSet _registeredIndexes = new ConcurrentHashSet(); + + public virtual bool IsRegistered(IIndex index) + { + return _registeredIndexes.Contains(index.Name); + } + + /// + /// Registers an index for this populator + /// + /// + public void RegisterIndex(string indexName) + { + _registeredIndexes.Add(indexName); + } + + public void Populate(params IIndex[] indexes) + { + PopulateIndexes(indexes.Where(IsRegistered).ToList()); + } + + protected abstract void PopulateIndexes(IReadOnlyList indexes); + } +} diff --git a/src/Umbraco.Examine/IndexRebuilder.cs b/src/Umbraco.Examine/IndexRebuilder.cs new file mode 100644 index 0000000000..43c309b9c5 --- /dev/null +++ b/src/Umbraco.Examine/IndexRebuilder.cs @@ -0,0 +1,57 @@ +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(IIndex index) + { + return _populators.Any(x => x.IsRegistered(index)); + } + + 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(); + + if (indexes.Length == 0) return; + + 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/LuceneIndexCreator.cs b/src/Umbraco.Examine/LuceneIndexCreator.cs new file mode 100644 index 0000000000..0e83b37dc5 --- /dev/null +++ b/src/Umbraco.Examine/LuceneIndexCreator.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.IO; +using Examine; +using Examine.LuceneEngine.Directories; +using Lucene.Net.Store; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.IO; + +namespace Umbraco.Examine +{ + /// + /// + /// Abstract class for creating Lucene based Indexes + /// + public abstract class LuceneIndexCreator : IIndexCreator + { + public abstract IEnumerable Create(); + + /// + /// Creates a file system based Lucene with the correct locking guidelines for Umbraco + /// + /// + /// The folder name to store the index (single word, not a fully qualified folder) (i.e. Internal) + /// + /// + public virtual Lucene.Net.Store.Directory CreateFileSystemLuceneDirectory(string folderName) + { + + var dirInfo = new DirectoryInfo(Path.Combine(IOHelper.MapPath(SystemDirectories.Data), "TEMP", "ExamineIndexes", folderName)); + if (!dirInfo.Exists) + System.IO.Directory.CreateDirectory(dirInfo.FullName); + + //check if there's a configured directory factory, if so create it and use that to create the lucene dir + var configuredDirectoryFactory = ConfigurationManager.AppSettings["Umbraco.Examine.LuceneDirectoryFactory"]; + if (!configuredDirectoryFactory.IsNullOrWhiteSpace()) + { + //this should be a fully qualified type + var factoryType = TypeFinder.GetTypeByName(configuredDirectoryFactory); + if (factoryType == null) throw new NullReferenceException("No directory type found for value: " + configuredDirectoryFactory); + var directoryFactory = (IDirectoryFactory)Activator.CreateInstance(factoryType); + return directoryFactory.CreateDirectory(dirInfo); + } + + //no dir factory, just create a normal fs directory + + var luceneDir = new SimpleFSDirectory(dirInfo); + + //we want to tell examine to use a different fs lock instead of the default NativeFSFileLock which could cause problems if the appdomain + //terminates and in some rare cases would only allow unlocking of the file if IIS is forcefully terminated. Instead we'll rely on the simplefslock + //which simply checks the existence of the lock file + // The full syntax of this is: new NoPrefixSimpleFsLockFactory(dirInfo) + // however, we are setting the DefaultLockFactory in startup so we'll use that instead since it can be managed globally. + luceneDir.SetLockFactory(DirectoryFactory.DefaultLockFactory(dirInfo)); + return luceneDir; + + + } + } +} diff --git a/src/Umbraco.Examine/MediaIndexPopulator.cs b/src/Umbraco.Examine/MediaIndexPopulator.cs new file mode 100644 index 0000000000..6dadcbe4b3 --- /dev/null +++ b/src/Umbraco.Examine/MediaIndexPopulator.cs @@ -0,0 +1,74 @@ +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; + } + + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) return; + + 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..2676093eeb --- /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, + UrlSegmentProviderCollection 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}}, + {UmbracoExamineIndex.NodeKeyFieldName, 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..e20dab91ca --- /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; + } + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) return; + + 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..d9f0b7806d --- /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}}, + {UmbracoExamineIndex.NodeKeyFieldName, 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/Properties/AssemblyInfo.cs b/src/Umbraco.Examine/Properties/AssemblyInfo.cs index 6713111968..5c42a236f4 100644 --- a/src/Umbraco.Examine/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Examine/Properties/AssemblyInfo.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; // Umbraco Cms [assembly: InternalsVisibleTo("Umbraco.Tests")] +[assembly: InternalsVisibleTo("Umbraco.Web")] // code analysis // IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it diff --git a/src/Umbraco.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..1320f3b0d2 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -48,30 +48,46 @@ - + - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + Properties\SolutionInfo.cs + diff --git a/src/Umbraco.Examine/UmbracoContentIndex.cs b/src/Umbraco.Examine/UmbracoContentIndex.cs new file mode 100644 index 0000000000..a9e2c72cb6 --- /dev/null +++ b/src/Umbraco.Examine/UmbracoContentIndex.cs @@ -0,0 +1,146 @@ +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 Lucene.Net.Analysis; +using Lucene.Net.Store; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +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 + + /// + /// Create an index at runtime + /// + /// + /// + /// + /// + /// + /// + /// + /// + public UmbracoContentIndex( + string name, + Directory luceneDirectory, + FieldDefinitionCollection fieldDefinitions, + Analyzer defaultAnalyzer, + IProfilingLogger profilingLogger, + ILocalizationService languageService, + IContentValueSetValidator validator, + IReadOnlyDictionary indexValueTypes = null) + : base(name, luceneDirectory, fieldDefinitions, 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 + + /// + /// 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. + + base.PerformDeleteFromIndex(group.Select(x => x.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(IEnumerable itemIds, Action onComplete) + { + var idsAsList = itemIds.ToList(); + foreach (var nodeId in idsAsList) + { + //find all descendants based on path + var descendantPath = $@"\-1\,*{nodeId}\,*"; + var rawQuery = $"{IndexPathFieldName}:{descendantPath}"; + var searcher = GetSearcher(); + var c = searcher.CreateQuery(); + var filtered = c.NativeQuery(rawQuery); + var results = filtered.Execute(); + + ProfilingLogger.Debug(GetType(), "DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); + + //need to queue a delete item for each one found + QueueIndexOperation(results.Select(r => new IndexOperation(new ValueSet(r.Id), IndexOperationType.Delete))); + } + + base.PerformDeleteFromIndex(idsAsList, onComplete); + } + + } +} 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..f33b7587e0 100644 --- a/src/Umbraco.Examine/UmbracoExamineExtensions.cs +++ b/src/Umbraco.Examine/UmbracoExamineExtensions.cs @@ -1,10 +1,6 @@ -using System; -using System.ComponentModel; -using System.Web; -using Examine.LuceneEngine.SearchCriteria; -using Examine.SearchCriteria; +using Examine.LuceneEngine.Search; +using Examine.Search; using Umbraco.Core; -using Umbraco.Examine.Config; namespace Umbraco.Examine { @@ -76,17 +72,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/UmbracoExamineIndex.cs b/src/Umbraco.Examine/UmbracoExamineIndex.cs new file mode 100644 index 0000000000..24952050da --- /dev/null +++ b/src/Umbraco.Examine/UmbracoExamineIndex.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Examine.LuceneEngine.Providers; +using Lucene.Net.Analysis; +using Lucene.Net.Documents; +using Lucene.Net.Index; +using Umbraco.Core; +using Examine; +using Examine.LuceneEngine; +using Lucene.Net.Store; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +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 UmbracoExamineIndex : LuceneIndex, IUmbracoIndex, IIndexDiagnostics + { + // note + // wrapping all operations that end up calling base.SafelyProcessQueueItems in a safe call + // context because they will fork a thread/task/whatever which should *not* capture our + // call context (and the database it can contain)! ideally we should be able to override + // SafelyProcessQueueItems but that's not possible in the current version of Examine. + + /// + /// Used to store the path of a content object + /// + 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 = SpecialFieldPrefix + "Raw_"; + + /// + /// Create a new + /// + /// + /// + /// + /// + /// + /// + /// + protected UmbracoExamineIndex( + string name, + Directory luceneDirectory, + FieldDefinitionCollection fieldDefinitions, + Analyzer defaultAnalyzer, + IProfilingLogger profilingLogger, + IValueSetValidator validator = null, + IReadOnlyDictionary indexValueTypes = null) + : base(name, luceneDirectory, fieldDefinitions, 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); + } + + private readonly bool _configBased = false; + + protected IProfilingLogger ProfilingLogger { get; } + + /// + /// When set to true Umbraco will keep the index in sync with Umbraco data automatically + /// + public bool EnableDefaultEventHandler { get; set; } = true; + + public bool PublishedValuesOnly { get; protected set; } = false; + + /// + public IEnumerable GetFields() + { + //we know this is a LuceneSearcher + var searcher = (LuceneSearcher) GetSearcher(); + return searcher.GetAllIndexedFields(); + } + + /// + /// override to check if we can actually initialize. + /// + /// + /// This check is required since the base examine lib will try to rebuild on startup + /// + protected override void PerformDeleteFromIndex(IEnumerable itemIds, Action onComplete) + { + if (CanInitialize()) + { + using (new SafeCallContext()) + { + base.PerformDeleteFromIndex(itemIds, onComplete); + } + } + } + + /// + /// 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; + } + + /// + /// overridden for logging + /// + /// + protected override void OnIndexingError(IndexingErrorEventArgs ex) + { + ProfilingLogger.Error(GetType(), ex.InnerException, ex.Message); + base.OnIndexingError(ex); + } + + /// + /// 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)).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(), + Field.Store.YES, + Field.Index.NO, //don't index this field, we never want to search by it + Field.TermVector.NO)); + } + } + + base.OnDocumentWriting(docArgs); + } + + /// + /// Overridden for logging. + /// + protected override void AddDocument(Document doc, ValueSet valueSet, IndexWriter writer) + { + ProfilingLogger.Debug(GetType(), + "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) + { + base.OnTransformingIndexValues(e); + + //ensure special __Path field + var path = e.ValueSet.GetValue("path"); + if (path != null) + { + e.ValueSet.Set(IndexPathFieldName, path); + } + + //icon + if (e.ValueSet.Values.TryGetValue("icon", out var icon) && e.ValueSet.Values.ContainsKey(IconFieldName) == false) + { + e.ValueSet.Values[IconFieldName] = icon; + } + } + + #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..fed5b9bae7 --- /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, + ["LuceneDirectory"] = _index.GetLuceneDirectory().GetType().Name, + [nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler, + [nameof(UmbracoExamineIndex.LuceneIndexFolder)] = + _index.LuceneIndexFolder == null + ? string.Empty + : _index.LuceneIndexFolder.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'), + [nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly, + //There's too much info here + //[nameof(UmbracoExamineIndexer.FieldDefinitionCollection)] = _index.FieldDefinitionCollection, + }; + + 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/UmbracoExamineIndexer.cs b/src/Umbraco.Examine/UmbracoExamineIndexer.cs deleted file mode 100644 index a4c1fb4336..0000000000 --- a/src/Umbraco.Examine/UmbracoExamineIndexer.cs +++ /dev/null @@ -1,435 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using Examine.LuceneEngine.Providers; -using Lucene.Net.Analysis; -using Lucene.Net.Documents; -using Lucene.Net.Index; -using Umbraco.Core; -using Examine; -using Examine.LuceneEngine; -using Examine.LuceneEngine.Indexing; -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 - { - // note - // wrapping all operations that end up calling base.SafelyProcessQueueItems in a safe call - // context because they will fork a thread/task/whatever which should *not* capture our - // call context (and the database it can contain)! ideally we should be able to override - // SafelyProcessQueueItems but that's not possible in the current version of 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"; - /// - /// The prefix added to a field when it is duplicated in order to store the original raw value. - /// - public const string RawFieldPrefix = "__Raw_"; - - /// - /// Constructor for config provider based indexes - /// - [EditorBrowsable(EditorBrowsableState.Never)] - protected UmbracoExamineIndexer() - : 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( - string name, - IEnumerable fieldDefinitions, - Directory luceneDirectory, - Analyzer defaultAnalyzer, - ProfilingLogger profilingLogger, - IValueSetValidator validator = null, - IReadOnlyDictionary> indexValueTypes = null) - : base(name, fieldDefinitions, luceneDirectory, defaultAnalyzer, validator, indexValueTypes) - { - ProfilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); - } - - private readonly bool _configBased = false; - - /// - /// A type that defines the type of index for each Umbraco field (non user defined fields) - /// 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 = - { - new FieldDefinition("parentID", FieldDefinitionTypes.Integer), - new FieldDefinition("level", FieldDefinitionTypes.Integer), - new FieldDefinition("writerID", FieldDefinitionTypes.Integer), - new FieldDefinition("creatorID", FieldDefinitionTypes.Integer), - new FieldDefinition("sortOrder", FieldDefinitionTypes.Integer), - new FieldDefinition("template", FieldDefinitionTypes.Integer), - - new FieldDefinition("createDate", FieldDefinitionTypes.DateTime), - new FieldDefinition("updateDate", FieldDefinitionTypes.DateTime), - - new FieldDefinition("key", FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("version", FieldDefinitionTypes.Raw), - new FieldDefinition("nodeType", FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("template", FieldDefinitionTypes.Raw), - new FieldDefinition("urlName", FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("path", FieldDefinitionTypes.Raw), - - new FieldDefinition(IndexPathFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(IconFieldName, FieldDefinitionTypes.Raw) - }; - - protected ProfilingLogger ProfilingLogger { get; } - - /// - /// Overridden to ensure that the umbraco system field definitions are in place - /// - /// - /// - /// - protected override FieldValueTypeCollection CreateFieldValueTypes(Directory x, IReadOnlyDictionary> indexValueTypesFactory = null) - { - foreach (var field in UmbracoIndexFields) - { - FieldDefinitionCollection.TryAdd(field.Name, field); - } - - return base.CreateFieldValueTypes(x, indexValueTypesFactory); - } - - /// - /// When set to true Umbraco will keep the index in sync with Umbraco data automatically - /// - public bool EnableDefaultEventHandler { get; set; } = true; - - /// - /// the supported indexable types - /// - protected abstract IEnumerable SupportedTypes { get; } - - protected ConfigIndexCriteria ConfigIndexCriteria { get; private set; } - - /// - /// The index set name which references an Examine - /// - public string IndexSetName { get; private set; } - - #region Initialize - - - /// - /// Setup the properties for the indexer from the provider settings - /// - /// - /// - /// - /// This is ONLY used for configuration based indexes - /// - public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) - { - ProfilingLogger.Logger.Debug(GetType(), "{IndexerName} indexer initializing", name); - - if (config["enableDefaultEventHandler"] != null && bool.TryParse(config["enableDefaultEventHandler"], out var enabled)) - { - EnableDefaultEventHandler = enabled; - } - - //Need to check if the index set or IndexerData is specified... - 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 setNameByConvension = name.Remove(name.LastIndexOf("Indexer")) + "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) - { - //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; - } - } - - if (!found) - throw new ArgumentNullException("indexSet on LuceneExamineIndexer provider has not been set in configuration and/or the IndexerData property has not been explicitly set"); - - } - else if (config["indexSet"] != null) - { - //if an index set is specified, ensure it exists and initialize the indexer based on the set - - if (IndexSets.Instance.Sets[config["indexSet"]] == null) - { - throw new ArgumentException("The indexSet specified for the LuceneExamineIndexer provider does not exist"); - } - else - { - IndexSetName = config["indexSet"]; - - 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) - { - 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); - } - } - } - - /// - /// 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; - } - - /// - /// Reindexes all supported types - /// - protected override void PerformIndexRebuild() - { - foreach (var t in SupportedTypes) - { - IndexAll(t); - } - } - - /// - /// overridden for logging - /// - /// - protected override void OnIndexingError(IndexingErrorEventArgs ex) - { - ProfilingLogger.Logger.Error(GetType(), ex.InnerException, ex.Message); - base.OnIndexingError(ex); - } - - /// - /// This ensures that the special __Raw_ fields are indexed - /// - /// - protected override void OnDocumentWriting(DocumentWritingEventArgs docArgs) - { - var d = docArgs.Document; - - foreach (var f in docArgs.ValueSet.Values.Where(x => x.Key.StartsWith(RawFieldPrefix))) - { - if (f.Value.Count > 0) - { - d.Add(new Field( - f.Key, - f.Value[0].ToString(), - Field.Store.YES, - Field.Index.NO, //don't index this field, we never want to search by it - Field.TermVector.NO)); - } - } - - 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) - { - ProfilingLogger.Logger.Debug(GetType(), - "AddDocument {DocumentId} with type {DocumentItemType}", - item.ValueSet.Id, - item.ValueSet.ItemType); - base.AddDocument(doc, item, writer); - } - - protected override void OnTransformingIndexValues(IndexingItemEventArgs e) - { - base.OnTransformingIndexValues(e); - - //ensure special __Path field - var path = e.IndexItem.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() }; - } - } - } - } - - //icon - if (e.IndexItem.ValueSet.Values.TryGetValue("icon", out var icon) && e.IndexItem.ValueSet.Values.ContainsKey(IconFieldName) == false) - { - e.IndexItem.ValueSet.Values[IconFieldName] = icon; - } - } - - private ConfigIndexCriteria CreateFieldDefinitionsFromConfig(IndexSet indexSet) - { - return new ConfigIndexCriteria( - indexSet.IndexAttributeFields.Cast().Select(x => new FieldDefinition(x.Name, x.Type)).ToArray(), - indexSet.IndexUserFields.Cast().Select(x => new FieldDefinition(x.Name, x.Type)).ToArray(), - indexSet.IncludeNodeTypes.ToList().Select(x => x.Name).ToArray(), - indexSet.ExcludeNodeTypes.ToList().Select(x => x.Name).ToArray(), - indexSet.IndexParentId); - } - } -} diff --git a/src/Umbraco.Examine/UmbracoExamineSearcher.cs b/src/Umbraco.Examine/UmbracoExamineSearcher.cs deleted file mode 100644 index 4b5ac50af7..0000000000 --- a/src/Umbraco.Examine/UmbracoExamineSearcher.cs +++ /dev/null @@ -1,156 +0,0 @@ -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; - - -namespace Umbraco.Examine -{ - /// - /// An Examine searcher which uses Lucene.Net as the - /// - public class UmbracoExamineSearcher : LuceneSearcher - { - private readonly bool _configBased = false; - - /// - /// Default constructor for config based construction - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public UmbracoExamineSearcher() - { - _configBased = true; - } - - /// - /// 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/UmbracoFieldDefinitionCollection.cs b/src/Umbraco.Examine/UmbracoFieldDefinitionCollection.cs new file mode 100644 index 0000000000..1e7b51aa14 --- /dev/null +++ b/src/Umbraco.Examine/UmbracoFieldDefinitionCollection.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Examine; +using Umbraco.Core; + +namespace Umbraco.Examine +{ + /// + /// Custom allowing dynamic creation of + /// + public class UmbracoFieldDefinitionCollection : FieldDefinitionCollection + { + + public UmbracoFieldDefinitionCollection() + : base(UmbracoIndexFieldDefinitions) + { + } + + /// + /// A type that defines the type of index for each Umbraco field (non user defined fields) + /// Alot of standard umbraco fields shouldn't be tokenized or even indexed, just stored into lucene + /// for retreival after searching. + /// + public static readonly FieldDefinition[] UmbracoIndexFieldDefinitions = + { + new FieldDefinition("parentID", FieldDefinitionTypes.Integer), + new FieldDefinition("level", FieldDefinitionTypes.Integer), + new FieldDefinition("writerID", FieldDefinitionTypes.Integer), + new FieldDefinition("creatorID", FieldDefinitionTypes.Integer), + new FieldDefinition("sortOrder", FieldDefinitionTypes.Integer), + new FieldDefinition("template", FieldDefinitionTypes.Integer), + + new FieldDefinition("createDate", FieldDefinitionTypes.DateTime), + new FieldDefinition("updateDate", FieldDefinitionTypes.DateTime), + + new FieldDefinition(UmbracoExamineIndex.NodeKeyFieldName, FieldDefinitionTypes.InvariantCultureIgnoreCase), + new FieldDefinition("version", FieldDefinitionTypes.Raw), + new FieldDefinition("nodeType", FieldDefinitionTypes.InvariantCultureIgnoreCase), + new FieldDefinition("template", FieldDefinitionTypes.Raw), + new FieldDefinition("urlName", FieldDefinitionTypes.InvariantCultureIgnoreCase), + new FieldDefinition("path", FieldDefinitionTypes.Raw), + + new FieldDefinition("email", FieldDefinitionTypes.EmailAddress), + + new FieldDefinition(UmbracoExamineIndex.PublishedFieldName, FieldDefinitionTypes.Raw), + new FieldDefinition(UmbracoExamineIndex.IndexPathFieldName, FieldDefinitionTypes.Raw), + new FieldDefinition(UmbracoExamineIndex.IconFieldName, FieldDefinitionTypes.Raw), + new FieldDefinition(UmbracoContentIndex.VariesByCultureFieldName, FieldDefinitionTypes.Raw), + }; + + + /// + /// Overridden to dynamically add field definitions for culture variations + /// + /// + /// + /// + /// + /// We need to do this so that we don't have to maintain a huge static list of all field names and their definitions + /// otherwise we'd have to dynamically add/remove definitions anytime languages are added/removed, etc... + /// For example, we have things like `nodeName` and `__Published` which are also used for culture fields like `nodeName_en-us` + /// and we don't want to have a full static list of all of these definitions when we can just define the one definition and then + /// dynamically apply that to culture specific fields. + /// + /// There is a caveat to this however, when a field definition is found for a non-culture field we will create and store a new field + /// definition for that culture so that the next time it needs to be looked up and used we are not allocating more objects. This does mean + /// however that if a language is deleted, the field definitions for that language will still exist in memory. This isn't going to cause any + /// problems and the mem will be cleared on next site restart but it's worth pointing out. + /// + public override bool TryGetValue(string fieldName, out FieldDefinition fieldDefinition) + { + if (base.TryGetValue(fieldName, out fieldDefinition)) + return true; + + //before we use regex to match do some faster simple matching since this is going to execute quite a lot + if (!fieldName.Contains("_") || !fieldName.Contains("-")) + return false; + + var match = ExamineExtensions.CultureIsoCodeFieldNameMatchExpression.Match(fieldName); + if (match.Success && match.Groups.Count == 3) + { + var nonCultureFieldName = match.Groups[1].Value; + //check if there's a definition for this and if so return the field definition for the culture field based on the non-culture field + if (base.TryGetValue(nonCultureFieldName, out var existingFieldDefinition)) + { + //now add a new field def + fieldDefinition = GetOrAdd(fieldName, s => new FieldDefinition(s, existingFieldDefinition.Type)); + return true; + } + } + return false; + } + + + } +} diff --git a/src/Umbraco.Examine/UmbracoMemberIndex.cs b/src/Umbraco.Examine/UmbracoMemberIndex.cs new file mode 100644 index 0000000000..fbf8a1cc0f --- /dev/null +++ b/src/Umbraco.Examine/UmbracoMemberIndex.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Examine; +using Examine.LuceneEngine; +using Lucene.Net.Analysis; +using Umbraco.Core.Logging; +using Directory = Lucene.Net.Store.Directory; + +namespace Umbraco.Examine +{ + + /// + /// Custom indexer for members + /// + public class UmbracoMemberIndex : UmbracoExamineIndex + { + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + /// + /// + public UmbracoMemberIndex( + string name, + FieldDefinitionCollection fieldDefinitions, + Directory luceneDirectory, + Analyzer analyzer, + IProfilingLogger profilingLogger, + IValueSetValidator validator = null) : + base(name, luceneDirectory, fieldDefinitions, analyzer, profilingLogger, validator) + { + } + + } +} 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/BulkInsertBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs index 0505974304..ee2e75cfad 100644 --- a/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs @@ -4,18 +4,12 @@ using System.Data.SqlServerCe; using System.IO; using System.Linq; using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Horology; -using BenchmarkDotNet.Jobs; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Mappers; -using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; using Umbraco.Tests.Benchmarks.Config; using Umbraco.Tests.TestHelpers; @@ -34,13 +28,11 @@ namespace Umbraco.Tests.Benchmarks { IScopeProvider f = null; var l = new Lazy(() => f); - var p = new SqlServerSyntaxProvider(l); var factory = new UmbracoDatabaseFactory( "server=.\\SQLExpress;database=YOURDB;user id=YOURUSER;password=YOURPASS", Constants.DatabaseProviders.SqlServer, - new [] { p }, logger, - new MapperCollection(Enumerable.Empty())); + new Lazy(() => new MapperCollection(Enumerable.Empty()))); return factory.CreateDatabase(); } @@ -49,9 +41,8 @@ namespace Umbraco.Tests.Benchmarks var f = new UmbracoDatabaseFactory( cstr, Constants.DatabaseProviders.SqlCe, - new[] { new SqlCeSyntaxProvider() }, logger, - new MapperCollection(Enumerable.Empty())); + new Lazy(() => new MapperCollection(Enumerable.Empty()))); return f.CreateDatabase(); } diff --git a/src/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs new file mode 100644 index 0000000000..ce55f6890d --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs @@ -0,0 +1,48 @@ +using System; +using BenchmarkDotNet.Attributes; +using Umbraco.Core; +using Umbraco.Tests.Benchmarks.Config; + +namespace Umbraco.Tests.Benchmarks +{ + [QuickRunWithMemoryDiagnoserConfig] + public class CombineGuidBenchmarks + { + private static readonly Guid _a = Guid.NewGuid(); + private static readonly Guid _b = Guid.NewGuid(); + + [Benchmark] + public byte[] CombineUtils() => GuidUtils.Combine(_a, _b).ToByteArray(); + + [Benchmark] + public byte[] CombineLoop() => Combine(_a, _b); + + private static byte[] Combine(Guid guid1, Guid guid2) + { + var bytes1 = guid1.ToByteArray(); + var bytes2 = guid2.ToByteArray(); + var bytes = new byte[bytes1.Length]; + for (var i = 0; i < bytes1.Length; i++) + { + bytes[i] = (byte)(bytes1[i] ^ bytes2[i]); + } + + return bytes; + } + } + + // Nov 8 2018 + //BenchmarkDotNet=v0.11.2, OS=Windows 10.0.17763.55 (1809/October2018Update/Redstone5) + //Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores + // [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0 + // Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0 + + //IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1 + //WarmupCount=3 + + // Method | Mean | Error | StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + //------------- |---------:|----------:|----------:|------------:|------------:|------------:|--------------------:| + // CombineUtils | 33.34 ns | 8.086 ns | 0.4432 ns | 0.0133 | - | - | 28 B | + // CombineLoop | 55.03 ns | 11.311 ns | 0.6200 ns | 0.0395 | - | - | 84 B | +} + diff --git a/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs index 5588e13d12..8d15613791 100644 --- a/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs @@ -11,7 +11,13 @@ using Umbraco.Core; namespace Umbraco.Tests.Benchmarks { - [Config(typeof(Config))] + // some conclusions + // - ActivatorCreateInstance is slow + // - it's faster to get+invoke the ctor + // - emitting the ctor is unless if invoked only 1 + + //[Config(typeof(Config))] + [MemoryDiagnoser] public class CtorInvokeBenchmarks { private class Config : ManualConfig @@ -25,7 +31,7 @@ namespace Umbraco.Tests.Benchmarks // see benchmarkdotnet FAQ Add(Job.Default .WithLaunchCount(1) // benchmark process will be launched only once - .WithIterationTime(TimeInterval.FromMilliseconds(400)) + .WithIterationTime(TimeInterval.FromMilliseconds(400)) .WithWarmupCount(3) .WithIterationCount(6)); } @@ -144,7 +150,7 @@ namespace Umbraco.Tests.Benchmarks // however, unfortunately, the generated "compiled to delegate" code cannot access private stuff :( - _emittedCtor = ReflectionUtilities.EmitConstuctor>(); + _emittedCtor = ReflectionUtilities.EmitConstructor>(); } public IFoo IlCtor(IFoo foo) @@ -158,6 +164,28 @@ namespace Umbraco.Tests.Benchmarks var foo = new Foo(_foo); } + [Benchmark] + public void EmitCtor() + { + var ctor = ReflectionUtilities.EmitConstructor>(); + var foo = ctor(_foo); + } + + [Benchmark] + public void ActivatorCreateInstance() + { + var foo = Activator.CreateInstance(typeof(Foo), _foo); + } + + [Benchmark] + public void GetAndInvokeCtor() + { + var ctorArgTypes = new[] { typeof(IFoo) }; + var type = typeof(Foo); + var ctorInfo = type.GetConstructor(ctorArgTypes); + var foo = ctorInfo.Invoke(new object[] { _foo }); + } + [Benchmark] public void InvokeCtor() { 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/HexStringBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/HexStringBenchmarks.cs new file mode 100644 index 0000000000..e29a5a24f3 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/HexStringBenchmarks.cs @@ -0,0 +1,69 @@ +using System; +using System.Text; +using BenchmarkDotNet.Attributes; +using Umbraco.Core; +using Umbraco.Tests.Benchmarks.Config; + +namespace Umbraco.Tests.Benchmarks +{ + [QuickRunConfig] + public class HexStringBenchmarks + { + private byte[] _buffer; + + [Params(8, 16, 32, 64, 128, 256)] + public int Count { get; set; } + + [GlobalSetup] + public void Setup() + { + this._buffer = new byte[this.Count]; + var random = new Random(); + random.NextBytes(this._buffer); + } + + [Benchmark(Baseline = true)] + public string ToHexStringBuilder() + { + var sb = new StringBuilder(this._buffer.Length * 2); + for (var i = 0; i < this._buffer.Length; i++) + { + sb.Append(this._buffer[i].ToString("X2")); + } + + return sb.ToString(); + } + + [Benchmark] + public string ToHexStringEncoder() => HexEncoder.Encode(this._buffer); + } + + // Nov 8 2018 + //BenchmarkDotNet=v0.11.2, OS=Windows 10.0.17763.55 (1809/October2018Update/Redstone5) + //Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores + // [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0 + // Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0 + + //IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1 + //WarmupCount=3 + + // Method | Count | Mean | Error | StdDev | Ratio | + //------------------- |------ |-------------:|-------------:|-----------:|------:| + // ToHexStringBuilder | 8 | 786.49 ns | 319.92 ns | 17.536 ns | 1.00 | + // ToHexStringEncoder | 8 | 64.19 ns | 30.21 ns | 1.656 ns | 0.08 | + // | | | | | | + // ToHexStringBuilder | 16 | 1,442.43 ns | 503.00 ns | 27.571 ns | 1.00 | + // ToHexStringEncoder | 16 | 133.46 ns | 177.55 ns | 9.732 ns | 0.09 | + // | | | | | | + // ToHexStringBuilder | 32 | 2,869.23 ns | 924.35 ns | 50.667 ns | 1.00 | + // ToHexStringEncoder | 32 | 181.03 ns | 96.64 ns | 5.297 ns | 0.06 | + // | | | | | | + // ToHexStringBuilder | 64 | 5,775.33 ns | 2,825.42 ns | 154.871 ns | 1.00 | + // ToHexStringEncoder | 64 | 331.16 ns | 125.63 ns | 6.886 ns | 0.06 | + // | | | | | | + // ToHexStringBuilder | 128 | 11,662.35 ns | 4,908.03 ns | 269.026 ns | 1.00 | + // ToHexStringEncoder | 128 | 633.78 ns | 57.56 ns | 3.155 ns | 0.05 | + // | | | | | | + // ToHexStringBuilder | 256 | 22,960.11 ns | 14,111.47 ns | 773.497 ns | 1.00 | + // ToHexStringEncoder | 256 | 1,224.76 ns | 547.27 ns | 29.998 ns | 0.05 | +} diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 99bb768842..233da0d14b 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -46,10 +46,13 @@ + + + @@ -87,8 +90,8 @@ - 0.11.2 + 0.11.3 - + \ No newline at end of file diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index f76f6b73b6..73f9656f56 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -67,7 +67,7 @@ - + diff --git a/src/Umbraco.Tests/Cache/DistributedCache/DistributedCacheTests.cs b/src/Umbraco.Tests/Cache/DistributedCache/DistributedCacheTests.cs index dc67bb532f..68b666632c 100644 --- a/src/Umbraco.Tests/Cache/DistributedCache/DistributedCacheTests.cs +++ b/src/Umbraco.Tests/Cache/DistributedCache/DistributedCacheTests.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; -using LightInject; +using Moq; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Components; using Umbraco.Core.Composing; +using Umbraco.Core.Logging; using Umbraco.Core.Sync; +using Umbraco.Tests.Components; namespace Umbraco.Tests.Cache.DistributedCache { @@ -20,15 +24,18 @@ namespace Umbraco.Tests.Cache.DistributedCache [SetUp] public void Setup() { - var container = new ServiceContainer(); - container.ConfigureUmbracoCore(); + var register = RegisterFactory.Create(); - container.Register(_ => new TestServerRegistrar()); - container.Register(_ => new TestServerMessenger(), new PerContainerLifetime()); + var composition = new Composition(register, new TypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run)); - container.RegisterCollectionBuilder() + composition.RegisterUnique(_ => new TestServerRegistrar()); + composition.RegisterUnique(_ => new TestServerMessenger()); + + composition.WithCollectionBuilder() .Add(); + Current.Factory = composition.CreateFactory(); + _distributedCache = new Umbraco.Web.Cache.DistributedCache(); } diff --git a/src/Umbraco.Tests/Cache/CacheRefresherComponentTests.cs b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs similarity index 86% rename from src/Umbraco.Tests/Cache/CacheRefresherComponentTests.cs rename to src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs index 0616b4098f..50b27da89f 100644 --- a/src/Umbraco.Tests/Cache/CacheRefresherComponentTests.cs +++ b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs @@ -1,22 +1,34 @@ using System; -using System.Collections.Generic; using System.Linq; +using System.Threading; +using Moq; using NUnit.Framework; +using Umbraco.Core.Components; using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; -using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; using Umbraco.Web.Cache; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; namespace Umbraco.Tests.Cache { [TestFixture] [UmbracoTest(WithApplication = true)] - public class CacheRefresherEventHandlerTests : UmbracoTestBase + public class DistributedCacheBinderTests : UmbracoTestBase { + protected override void Compose(Composition composition) + { + base.Compose(composition); + // refreshers.HandleEvents wants a UmbracoContext + // which wants these + composition.RegisterUnique(_ => Mock.Of()); + composition.WithCollectionBuilder(); + } + [Test] public void Can_Find_All_Event_Handlers() { @@ -114,7 +126,7 @@ namespace Umbraco.Tests.Cache var ok = true; foreach (var definition in definitions) { - var found = CacheRefresherComponent.FindHandler(definition); + var found = DistributedCacheBinder.FindHandler(definition); if (found == null) { Console.WriteLine("Couldn't find method for " + definition.EventName + " on " + definition.Sender.GetType()); @@ -123,5 +135,30 @@ namespace Umbraco.Tests.Cache } Assert.IsTrue(ok, "see log for details"); } + + [Test] + public void CanHandleEvent() + { + // refreshers.HandleEvents wants a UmbracoContext + // which wants an HttpContext, which we build using a SimpleWorkerRequest + // which requires these to be non-null + var domain = Thread.GetDomain(); + if (domain.GetData(".appPath") == null) + domain.SetData(".appPath", ""); + if (domain.GetData(".appVPath") == null) + domain.SetData(".appVPath", ""); + + // create some event definitions + var definitions = new IEventDefinition[] + { + // works because that event definition maps to an empty handler + new EventDefinition>(null, Current.Services.ContentTypeService, new SaveEventArgs(Enumerable.Empty()), "Saved"), + + }; + + // just assert it does not throw + var refreshers = new DistributedCacheBinder(null, null); + refreshers.HandleEvents(definitions); + } } } diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs index 7f2edac876..28e753cadc 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs @@ -4,6 +4,8 @@ using Moq; using NUnit.Framework; using Umbraco.Core.Cache; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; using Umbraco.Tests.Testing.Objects.Accessors; @@ -48,16 +50,14 @@ namespace Umbraco.Tests.Cache.PublishedCache "; } - public override void SetUp() + protected override void Initialize() { - base.SetUp(); + base.Initialize(); _httpContextFactory = new FakeHttpContextFactory("~/Home"); - var umbracoSettings = SettingsForTests.GenerateMockUmbracoSettings(); - var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); - SettingsForTests.ConfigureSettings(umbracoSettings); - SettingsForTests.ConfigureSettings(globalSettings); + var umbracoSettings = Factory.GetInstance(); + var globalSettings = Factory.GetInstance(); _xml = new XmlDocument(); _xml.LoadXml(GetXml()); diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs index 64194ebb47..5e75f47304 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Xml; @@ -7,13 +6,13 @@ using Examine; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Strings; using Umbraco.Tests.TestHelpers; using Umbraco.Web.PublishedCache.XmlPublishedCache; using Umbraco.Tests.Testing; using Current = Umbraco.Web.Composing.Current; -using LightInject; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Tests.PublishedContent; @@ -30,7 +29,7 @@ namespace Umbraco.Tests.Cache.PublishedCache { base.Compose(); - Container.GetInstance() + Composition.WithCollectionBuilder() .Clear() .Append(); } @@ -111,7 +110,6 @@ namespace Umbraco.Tests.Cache.PublishedCache } [TestCase("id")] - [TestCase("nodeId")] [TestCase("__NodeId")] public void DictionaryDocument_Id_Keys(string key) { @@ -128,7 +126,6 @@ namespace Umbraco.Tests.Cache.PublishedCache } [TestCase("nodeName")] - [TestCase("__nodeName")] public void DictionaryDocument_NodeName_Keys(string key) { var dicDoc = GetDictionaryDocument(nodeNameKey: key); @@ -204,12 +201,12 @@ namespace Umbraco.Tests.Cache.PublishedCache {"creatorName", "Shannon"} }; - var result = new SearchResult("1234", 1, 1, () => fields.ToDictionary(x => x.Key, x => new List { x.Value })); + var result = new SearchResult("1234", 1, () => fields.ToDictionary(x => x.Key, x => new List { x.Value })); var store = new PublishedMediaCache(new XmlStore((XmlDocument)null, null, null, null), ServiceContext.MediaService, ServiceContext.UserService, new StaticCacheProvider(), ContentTypesCache); var doc = store.CreateFromCacheValues(store.ConvertFromSearchResult(result)); - DoAssert(doc, 1234, key, 0, 0, "/media/test.jpg", "Image", 23, "Shannon", "Shannon", 0, 0, "-1,1234", DateTime.Parse("2012-07-17T10:34:09"), DateTime.Parse("2012-07-16T10:34:09"), 2); + DoAssert(doc, 1234, key, templateIdVal: null, 0, "/media/test.jpg", "Image", 23, "Shannon", "Shannon", 0, 0, "-1,1234", DateTime.Parse("2012-07-17T10:34:09"), DateTime.Parse("2012-07-16T10:34:09"), 2); Assert.AreEqual(null, doc.Parent); } @@ -225,7 +222,7 @@ namespace Umbraco.Tests.Cache.PublishedCache var cache = new PublishedMediaCache(new XmlStore((XmlDocument)null, null, null, null), ServiceContext.MediaService, ServiceContext.UserService, new StaticCacheProvider(), ContentTypesCache); var doc = cache.CreateFromCacheValues(cache.ConvertFromXPathNavigator(navigator, true)); - DoAssert(doc, 2000, key, 0, 2, "image1", "Image", 23, "Shannon", "Shannon", 33, 33, "-1,2000", DateTime.Parse("2012-06-12T14:13:17"), DateTime.Parse("2012-07-20T18:50:43"), 1); + DoAssert(doc, 2000, key, templateIdVal: null, 2, "image1", "Image", 23, "Shannon", "Shannon", 33, 33, "-1,2000", DateTime.Parse("2012-06-12T14:13:17"), DateTime.Parse("2012-07-20T18:50:43"), 1); Assert.AreEqual(null, doc.Parent); Assert.AreEqual(2, doc.Children.Count()); Assert.AreEqual(2001, doc.Children.ElementAt(0).Id); @@ -339,7 +336,7 @@ namespace Umbraco.Tests.Cache.PublishedCache DictionaryPublishedContent dicDoc, int idVal = 1234, Guid keyVal = default(Guid), - int templateIdVal = 0, + int? templateIdVal = null, int sortOrderVal = 44, string urlNameVal = "testing", string nodeTypeAliasVal = "myType", @@ -370,7 +367,7 @@ namespace Umbraco.Tests.Cache.PublishedCache IPublishedContent doc, int idVal = 1234, Guid keyVal = default(Guid), - int templateIdVal = 0, + int? templateIdVal = null, int sortOrderVal = 44, string urlNameVal = "testing", string nodeTypeAliasVal = "myType", @@ -404,9 +401,6 @@ namespace Umbraco.Tests.Cache.PublishedCache Assert.AreEqual(createDateVal.Value, doc.CreateDate); Assert.AreEqual(updateDateVal.Value, doc.UpdateDate); Assert.AreEqual(levelVal, doc.Level); - } - - } } diff --git a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs b/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs index 46dae8bcfd..f40ca3f500 100644 --- a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs +++ b/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using NUnit.Framework; using Umbraco.Core; @@ -13,16 +14,16 @@ namespace Umbraco.Tests.Clr [Test] public void EmitCtorEmits() { - var ctor1 = ReflectionUtilities.EmitConstuctor>(); + var ctor1 = ReflectionUtilities.EmitConstructor>(); Assert.IsInstanceOf(ctor1()); - var ctor2 = ReflectionUtilities.EmitConstuctor>(declaring: typeof(Class1)); + var ctor2 = ReflectionUtilities.EmitConstructor>(declaring: typeof(Class1)); Assert.IsInstanceOf(ctor2()); - var ctor3 = ReflectionUtilities.EmitConstuctor>(); + var ctor3 = ReflectionUtilities.EmitConstructor>(); Assert.IsInstanceOf(ctor3(42)); - var ctor4 = ReflectionUtilities.EmitConstuctor>(declaring: typeof(Class3)); + var ctor4 = ReflectionUtilities.EmitConstructor>(declaring: typeof(Class3)); Assert.IsInstanceOf(ctor4(42)); } @@ -43,14 +44,14 @@ namespace Umbraco.Tests.Clr [Test] public void EmitCtorEmitsPrivateCtor() { - var ctor = ReflectionUtilities.EmitConstuctor>(); + var ctor = ReflectionUtilities.EmitConstructor>(); Assert.IsInstanceOf(ctor("foo")); } [Test] public void EmitCtorThrowsIfNotFound() { - Assert.Throws(() => ReflectionUtilities.EmitConstuctor>()); + Assert.Throws(() => ReflectionUtilities.EmitConstructor>()); } [Test] @@ -63,7 +64,7 @@ namespace Umbraco.Tests.Clr [Test] public void EmitCtorReturnsNull() { - Assert.IsNull(ReflectionUtilities.EmitConstuctor>(false)); + Assert.IsNull(ReflectionUtilities.EmitConstructor>(false)); } [Test] @@ -553,6 +554,22 @@ namespace Umbraco.Tests.Clr // fixme - missing tests specifying 'returned' on method, property + [Test] + public void DeconstructAnonymousType() + { + var o = new { a = 1, b = "hello" }; + + var getters = new Dictionary>(); + foreach (var prop in o.GetType().GetProperties()) + getters[prop.Name] = ReflectionUtilities.EmitMethodUnsafe>(prop.GetMethod); + + Assert.AreEqual(2, getters.Count); + Assert.IsTrue(getters.ContainsKey("a")); + Assert.IsTrue(getters.ContainsKey("b")); + Assert.AreEqual(1, getters["a"](o)); + Assert.AreEqual("hello", getters["b"](o)); + } + #region IL Code // these functions can be examined in eg DotPeek to understand IL works diff --git a/src/Umbraco.Tests/Components/ComponentTests.cs b/src/Umbraco.Tests/Components/ComponentTests.cs index 995350f80e..a04636f919 100644 --- a/src/Umbraco.Tests/Components/ComponentTests.cs +++ b/src/Umbraco.Tests/Components/ComponentTests.cs @@ -1,17 +1,16 @@ using System; using System.Collections.Generic; using System.Linq; -using LightInject; using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Components; +using Umbraco.Core.Composing; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Scoping; -using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.Components { @@ -19,309 +18,426 @@ namespace Umbraco.Tests.Components public class ComponentTests { private static readonly List Composed = new List(); - private static readonly List Initialized = new List(); + private static readonly List Initialized = new List(); + private static readonly List Terminated = new List(); - private static IServiceContainer MockContainer(Action> setup = null) + private static IFactory MockFactory(Action> setup = null) { // fixme use IUmbracoDatabaseFactory vs UmbracoDatabaseFactory, clean it all up! - var testObjects = new TestObjects(null); + var mock = new Mock(); + var logger = Mock.Of(); - var s = testObjects.GetDefaultSqlSyntaxProviders(logger); - var f = new UmbracoDatabaseFactory(s, logger, new MapperCollection(Enumerable.Empty())); - var fs = new FileSystems(logger); + var f = new UmbracoDatabaseFactory(logger, new Lazy(() => new MapperCollection(Enumerable.Empty()))); + var fs = new FileSystems(mock.Object, logger); var p = new ScopeProvider(f, fs, logger); - var mock = new Mock(); mock.Setup(x => x.GetInstance(typeof (ILogger))).Returns(logger); - mock.Setup(x => x.GetInstance(typeof (ProfilingLogger))).Returns(new ProfilingLogger(Mock.Of(), Mock.Of())); + mock.Setup(x => x.GetInstance(typeof (IProfilingLogger))).Returns(new ProfilingLogger(Mock.Of(), Mock.Of())); mock.Setup(x => x.GetInstance(typeof (IUmbracoDatabaseFactory))).Returns(f); mock.Setup(x => x.GetInstance(typeof (IScopeProvider))).Returns(p); + setup?.Invoke(mock); return mock.Object; } + private static IRegister MockRegister() + { + return Mock.Of(); + } + + private static TypeLoader MockTypeLoader() + { + return new TypeLoader(); + } + + public static IRuntimeState MockRuntimeState(RuntimeLevel level) + { + var runtimeState = Mock.Of(); + Mock.Get(runtimeState).Setup(x => x.Level).Returns(level); + return runtimeState; + } + [Test] public void Boot1A() { - var container = MockContainer(); + var register = MockRegister(); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); - var loader = new BootLoader(container); + var types = TypeArray(); + var composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); // 2 is Core and requires 4 // 3 is User - goes away with RuntimeLevel.Unknown // => reorder components accordingly - loader.Boot(TypeArray(), RuntimeLevel.Unknown); - AssertTypeArray(TypeArray(), Composed); + composers.Compose(); + AssertTypeArray(TypeArray(), Composed); + + var factory = MockFactory(m => + { + m.Setup(x => x.TryGetInstance(It.Is(t => t == typeof(ISomeResource)))).Returns(() => new SomeResource()); + m.Setup(x => x.GetInstance(It.IsAny())).Returns((type) => + { + if (type == typeof(Composer1)) return new Composer1(); + if (type == typeof(Composer5)) return new Composer5(); + if (type == typeof(Component5)) return new Component5(new SomeResource()); + if (type == typeof(IProfilingLogger)) return new ProfilingLogger(Mock.Of(), Mock.Of()); + throw new NotSupportedException(type.FullName); + }); + }); + + var builder = composition.WithCollectionBuilder(); + builder.RegisterWith(register); + var components = builder.CreateCollection(factory); + + Assert.IsEmpty(components); + components.Initialize(); + Assert.IsEmpty(Initialized); + components.Terminate(); + Assert.IsEmpty(Terminated); } [Test] public void Boot1B() { - var container = MockContainer(); + var register = MockRegister(); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); - var loader = new BootLoader(container); + var types = TypeArray(); + var components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); // 2 is Core and requires 4 // 3 is User - stays with RuntimeLevel.Run // => reorder components accordingly - loader.Boot(TypeArray(), RuntimeLevel.Run); - AssertTypeArray(TypeArray(), Composed); + components.Compose(); + AssertTypeArray(TypeArray(), Composed); } [Test] public void Boot2() { - var container = MockContainer(); + var register = MockRegister(); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); - var loader = new BootLoader(container); + var types = TypeArray(); + var components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); // 21 is required by 20 // => reorder components accordingly - loader.Boot(TypeArray(), RuntimeLevel.Unknown); - AssertTypeArray(TypeArray(), Composed); + components.Compose(); + AssertTypeArray(TypeArray(), Composed); } [Test] public void Boot3() { - var container = MockContainer(); + var register = MockRegister(); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); - var loader = new BootLoader(container); + var types = TypeArray(); + var components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); // i23 requires 22 // 24, 25 implement i23 // 25 required by i23 // => reorder components accordingly - loader.Boot(TypeArray(), RuntimeLevel.Unknown); - AssertTypeArray(TypeArray(), Composed); + components.Compose(); + AssertTypeArray(TypeArray(), Composed); } [Test] public void BrokenRequire() { - var container = MockContainer(); + var register = MockRegister(); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); - var thing = new BootLoader(container); + var types = TypeArray(); + var components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); try { // 2 is Core and requires 4 // 4 is missing // => throw - thing.Boot(TypeArray < Component1, Component2, Component3>(), RuntimeLevel.Unknown); + components.Compose(); Assert.Fail("Expected exception."); } catch (Exception e) { - Assert.AreEqual("Broken component dependency: Umbraco.Tests.Components.ComponentTests+Component2 -> Umbraco.Tests.Components.ComponentTests+Component4.", e.Message); + Assert.AreEqual("Broken composer dependency: Umbraco.Tests.Components.ComponentTests+Composer2 -> Umbraco.Tests.Components.ComponentTests+Composer4.", e.Message); } } [Test] public void BrokenRequired() { - var container = MockContainer(); + var register = MockRegister(); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); - var thing = new BootLoader(container); + var types = TypeArray(); + var components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); // 2 is Core and requires 4 // 13 is required by 1 // 1 is missing // => reorder components accordingly - thing.Boot(TypeArray(), RuntimeLevel.Unknown); - AssertTypeArray(TypeArray(), Composed); + components.Compose(); + AssertTypeArray(TypeArray(), Composed); } [Test] public void Initialize() { - var container = MockContainer(m => + Composed.Clear(); + Initialized.Clear(); + Terminated.Clear(); + + var register = MockRegister(); + var factory = MockFactory(m => { m.Setup(x => x.TryGetInstance(It.Is(t => t == typeof (ISomeResource)))).Returns(() => new SomeResource()); + m.Setup(x => x.GetInstance(It.IsAny())).Returns((type) => + { + if (type == typeof(Composer1)) return new Composer1(); + if (type == typeof(Composer5)) return new Composer5(); + if (type == typeof(Component5)) return new Component5(new SomeResource()); + if (type == typeof(IProfilingLogger)) return new ProfilingLogger(Mock.Of(), Mock.Of()); + throw new NotSupportedException(type.FullName); + }); }); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); - var thing = new BootLoader(container); - Composed.Clear(); - thing.Boot(new[] { typeof(Component1), typeof(Component5) }, RuntimeLevel.Unknown); - Assert.AreEqual(2, Composed.Count); - Assert.AreEqual(typeof(Component1), Composed[0]); - Assert.AreEqual(typeof(Component5), Composed[1]); - Assert.AreEqual(1, Initialized.Count); - Assert.AreEqual("Umbraco.Tests.Components.ComponentTests+SomeResource", Initialized[0]); + var types = new[] { typeof(Composer1), typeof(Composer5) }; + var composers = new Composers(composition, types, Mock.Of()); + + Assert.IsEmpty(Composed); + composers.Compose(); + AssertTypeArray(TypeArray(), Composed); + + var builder = composition.WithCollectionBuilder(); + builder.RegisterWith(register); + var components = builder.CreateCollection(factory); + + Assert.IsEmpty(Initialized); + components.Initialize(); + AssertTypeArray(TypeArray(), Initialized); + + Assert.IsEmpty(Terminated); + components.Terminate(); + AssertTypeArray(TypeArray(), Terminated); } [Test] public void Requires1() { - var container = MockContainer(); + var register = MockRegister(); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); - var thing = new BootLoader(container); + var types = new[] { typeof(Composer6), typeof(Composer7), typeof(Composer8) }; + var components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); - thing.Boot(new[] { typeof(Component6), typeof(Component7), typeof(Component8) }, RuntimeLevel.Unknown); + components.Compose(); Assert.AreEqual(2, Composed.Count); - Assert.AreEqual(typeof(Component6), Composed[0]); - Assert.AreEqual(typeof(Component8), Composed[1]); + Assert.AreEqual(typeof(Composer6), Composed[0]); + Assert.AreEqual(typeof(Composer8), Composed[1]); } [Test] public void Requires2A() { - var container = MockContainer(); + var register = MockRegister(); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); - var thing = new BootLoader(container); + var types = new[] { typeof(Composer9), typeof(Composer2), typeof(Composer4) }; + var components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); - thing.Boot(new[] { typeof(Component9), typeof(Component2), typeof(Component4) }, RuntimeLevel.Unknown); + components.Compose(); Assert.AreEqual(2, Composed.Count); - Assert.AreEqual(typeof(Component4), Composed[0]); - Assert.AreEqual(typeof(Component2), Composed[1]); + Assert.AreEqual(typeof(Composer4), Composed[0]); + Assert.AreEqual(typeof(Composer2), Composed[1]); //Assert.AreEqual(typeof(Component9), Composed[2]); -- goes away with RuntimeLevel.Unknown } [Test] public void Requires2B() { - var container = MockContainer(); + var register = MockRegister(); + var factory = MockFactory(); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); - var thing = new BootLoader(container); + var types = new[] { typeof(Composer9), typeof(Composer2), typeof(Composer4) }; + var composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); - thing.Boot(new[] { typeof(Component9), typeof(Component2), typeof(Component4) }, RuntimeLevel.Run); + composers.Compose(); + var builder = composition.WithCollectionBuilder(); + builder.RegisterWith(register); + var components = builder.CreateCollection(factory); Assert.AreEqual(3, Composed.Count); - Assert.AreEqual(typeof(Component4), Composed[0]); - Assert.AreEqual(typeof(Component2), Composed[1]); - Assert.AreEqual(typeof(Component9), Composed[2]); + Assert.AreEqual(typeof(Composer4), Composed[0]); + Assert.AreEqual(typeof(Composer2), Composed[1]); + Assert.AreEqual(typeof(Composer9), Composed[2]); } [Test] public void WeakDependencies() { - var container = MockContainer(); + var register = MockRegister(); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); - var thing = new BootLoader(container); + var types = new[] { typeof(Composer10) }; + var components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); - thing.Boot(new[] { typeof(Component10) }, RuntimeLevel.Unknown); + components.Compose(); Assert.AreEqual(1, Composed.Count); - Assert.AreEqual(typeof(Component10), Composed[0]); + Assert.AreEqual(typeof(Composer10), Composed[0]); - thing = new BootLoader(container); + types = new[] { typeof(Composer11) }; + components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); - Assert.Throws(() => thing.Boot(new[] { typeof(Component11) }, RuntimeLevel.Unknown)); + Assert.Throws(() => components.Compose()); - thing = new BootLoader(container); + types = new[] { typeof(Composer2) }; + components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); - Assert.Throws(() => thing.Boot(new[] { typeof(Component2) }, RuntimeLevel.Unknown)); + Assert.Throws(() => components.Compose()); - thing = new BootLoader(container); + types = new[] { typeof(Composer12) }; + components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); - thing.Boot(new[] { typeof(Component12) }, RuntimeLevel.Unknown); + components.Compose(); Assert.AreEqual(1, Composed.Count); - Assert.AreEqual(typeof(Component12), Composed[0]); + Assert.AreEqual(typeof(Composer12), Composed[0]); } [Test] public void DisableMissing() { - var container = MockContainer(); + var register = MockRegister(); + var composition = new Composition(register, MockTypeLoader(), Mock.Of(), MockRuntimeState(RuntimeLevel.Unknown)); - var thing = new BootLoader(container); + var types = new[] { typeof(Composer6), typeof(Composer8) }; // 8 disables 7 which is not in the list + var components = new Core.Components.Composers(composition, types, Mock.Of()); Composed.Clear(); - thing.Boot(new[] { typeof(Component6), typeof(Component8) }, RuntimeLevel.Unknown); // 8 disables 7 which is not in the list + components.Compose(); Assert.AreEqual(2, Composed.Count); - Assert.AreEqual(typeof(Component6), Composed[0]); - Assert.AreEqual(typeof(Component8), Composed[1]); + Assert.AreEqual(typeof(Composer6), Composed[0]); + Assert.AreEqual(typeof(Composer8), Composed[1]); } #region Components - public class TestComponentBase : UmbracoComponentBase + public class TestComposerBase : IComposer { - public override void Compose(Composition composition) + public virtual void Compose(Composition composition) { - base.Compose(composition); Composed.Add(GetType()); } } - public class Component1 : TestComponentBase + public class Composer1 : TestComposerBase { } - [RequireComponent(typeof(Component4))] - public class Component2 : TestComponentBase, IUmbracoCoreComponent + [ComposeAfter(typeof(Composer4))] + public class Composer2 : TestComposerBase, ICoreComposer { } - public class Component3 : TestComponentBase, IUmbracoUserComponent + public class Composer3 : TestComposerBase, IUserComposer { } - public class Component4 : TestComponentBase + public class Composer4 : TestComposerBase { } - public class Component5 : TestComponentBase + public class Composer5 : TestComposerBase { - public void Initialize(ISomeResource resource) + public override void Compose(Composition composition) { - Initialized.Add(resource.GetType().FullName); + base.Compose(composition); + composition.Components().Append(); } } - [DisableComponent] - public class Component6 : TestComponentBase + public class TestComponentBase : IComponent + { + public virtual void Initialize() + { + Initialized.Add(GetType()); + } + + public virtual void Terminate() + { + Terminated.Add(GetType()); + } + } + + public class Component5 : TestComponentBase + { + private readonly ISomeResource _resource; + + public Component5(ISomeResource resource) + { + _resource = resource; + } + } + + [Disable] + public class Composer6 : TestComposerBase { } - public class Component7 : TestComponentBase + public class Composer7 : TestComposerBase { } - [DisableComponent(typeof(Component7))] - [EnableComponent(typeof(Component6))] - public class Component8 : TestComponentBase + [Disable(typeof(Composer7))] + [Enable(typeof(Composer6))] + public class Composer8 : TestComposerBase { } - public interface ITestComponent : IUmbracoUserComponent + public interface ITestComposer : IUserComposer { } - public class Component9 : TestComponentBase, ITestComponent + public class Composer9 : TestComposerBase, ITestComposer { } - [RequireComponent(typeof(ITestComponent))] - public class Component10 : TestComponentBase + [ComposeAfter(typeof(ITestComposer))] + public class Composer10 : TestComposerBase { } - [RequireComponent(typeof(ITestComponent), false)] - public class Component11 : TestComponentBase + [ComposeAfter(typeof(ITestComposer), false)] + public class Composer11 : TestComposerBase { } - [RequireComponent(typeof(Component4), true)] - public class Component12 : TestComponentBase, IUmbracoCoreComponent + [ComposeAfter(typeof(Composer4), true)] + public class Composer12 : TestComposerBase, ICoreComposer { } - [RequiredComponent(typeof(Component1))] - public class Component13 : TestComponentBase + [ComposeBefore(typeof(Composer1))] + public class Composer13 : TestComposerBase { } public interface ISomeResource { } public class SomeResource : ISomeResource { } - public class Component20 : TestComponentBase + public class Composer20 : TestComposerBase { } - [RequiredComponent(typeof(Component20))] - public class Component21 : TestComponentBase + [ComposeBefore(typeof(Composer20))] + public class Composer21 : TestComposerBase { } - public class Component22 : TestComponentBase + public class Composer22 : TestComposerBase { } - [RequireComponent(typeof(Component22))] - public interface IComponent23 : IUmbracoComponent + [ComposeAfter(typeof(Composer22))] + public interface IComposer23 : IComposer { } - public class Component24 : TestComponentBase, IComponent23 + public class Composer24 : TestComposerBase, IComposer23 { } // should insert itself between 22 and anything i23 - [RequiredComponent(typeof(IComponent23))] + [ComposeBefore(typeof(IComposer23))] //[RequireComponent(typeof(Component22))] - not needed, implement i23 - public class Component25 : TestComponentBase, IComponent23 + public class Composer25 : TestComposerBase, IComposer23 { } #endregion diff --git a/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs index 87b0cd5173..4c262fbf82 100644 --- a/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs +++ b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs @@ -1,39 +1,40 @@ using System; using System.Collections.Generic; using System.Linq; -using LightInject; +using Moq; using NUnit.Framework; using Umbraco.Core.Composing; +using Umbraco.Core; +using Umbraco.Core.Components; +using Umbraco.Core.Logging; +using Umbraco.Tests.Components; namespace Umbraco.Tests.Composing { [TestFixture] public class CollectionBuildersTests { - private ServiceContainer _container; + private Composition _composition; [SetUp] public void Setup() { Current.Reset(); - _container = new ServiceContainer(); - _container.ConfigureUmbracoCore(); + var register = RegisterFactory.Create(); + _composition = new Composition(register, new TypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run)); } [TearDown] public void TearDown() { Current.Reset(); - - _container.Dispose(); - _container = null; } [Test] public void ContainsTypes() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append(); @@ -42,14 +43,15 @@ namespace Umbraco.Tests.Composing Assert.IsFalse(builder.Has()); //Assert.IsFalse(col.ContainsType()); // does not compile - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); AssertCollection(col, typeof(Resolved1), typeof(Resolved2)); } [Test] public void CanClearBuilderBeforeCollectionIsCreated() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append(); @@ -57,18 +59,20 @@ namespace Umbraco.Tests.Composing Assert.IsFalse(builder.Has()); Assert.IsFalse(builder.Has()); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); AssertCollection(col); } [Test] public void CannotClearBuilderOnceCollectionIsCreated() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append(); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); Assert.Throws(() => builder.Clear()); } @@ -76,7 +80,7 @@ namespace Umbraco.Tests.Composing [Test] public void CanAppendToBuilder() { - var builder = _container.RegisterCollectionBuilder(); + var builder = _composition.WithCollectionBuilder(); builder.Append(); builder.Append(); @@ -84,16 +88,18 @@ namespace Umbraco.Tests.Composing Assert.IsTrue(builder.Has()); Assert.IsFalse(builder.Has()); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); AssertCollection(col, typeof(Resolved1), typeof(Resolved2)); } [Test] public void CannotAppendToBuilderOnceCollectionIsCreated() { - var builder = _container.RegisterCollectionBuilder(); + var builder = _composition.WithCollectionBuilder(); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); Assert.Throws(() => builder.Append() @@ -103,18 +109,21 @@ namespace Umbraco.Tests.Composing [Test] public void CanAppendDuplicateToBuilderAndDeDuplicate() { - var builder = _container.RegisterCollectionBuilder(); + var builder = _composition.WithCollectionBuilder(); builder.Append(); builder.Append(); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + + var col = builder.CreateCollection(factory); AssertCollection(col, typeof(Resolved1)); } [Test] public void CannotAppendInvalidTypeToBUilder() { - var builder = _container.RegisterCollectionBuilder(); + var builder = _composition.WithCollectionBuilder(); + //builder.Append(); // does not compile Assert.Throws(() => builder.Append(new[] { typeof (Resolved4) }) // throws @@ -124,7 +133,7 @@ namespace Umbraco.Tests.Composing [Test] public void CanRemoveFromBuilder() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append() .Remove(); @@ -133,30 +142,33 @@ namespace Umbraco.Tests.Composing Assert.IsFalse(builder.Has()); Assert.IsFalse(builder.Has()); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); AssertCollection(col, typeof(Resolved1)); } [Test] public void CanRemoveMissingFromBuilder() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append() .Remove(); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); AssertCollection(col, typeof(Resolved1), typeof(Resolved2)); } [Test] public void CannotRemoveFromBuilderOnceCollectionIsCreated() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append(); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); Assert.Throws(() => builder.Remove() // throws ); @@ -165,7 +177,7 @@ namespace Umbraco.Tests.Composing [Test] public void CanInsertIntoBuilder() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append() .Insert(); @@ -174,18 +186,20 @@ namespace Umbraco.Tests.Composing Assert.IsTrue(builder.Has()); Assert.IsTrue(builder.Has()); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); AssertCollection(col, typeof(Resolved3), typeof(Resolved1), typeof(Resolved2)); } [Test] public void CannotInsertIntoBuilderOnceCollectionIsCreated() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append(); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); Assert.Throws(() => builder.Insert() // throws ); @@ -194,29 +208,31 @@ namespace Umbraco.Tests.Composing [Test] public void CanInsertDuplicateIntoBuilderAndDeDuplicate() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append() .Insert(); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); AssertCollection(col, typeof(Resolved2), typeof(Resolved1)); } [Test] public void CanInsertIntoEmptyBuilder() { - var builder = _container.RegisterCollectionBuilder(); + var builder = _composition.WithCollectionBuilder(); builder.Insert(); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); AssertCollection(col, typeof(Resolved2)); } [Test] public void CannotInsertIntoBuilderAtWrongIndex() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append(); @@ -232,7 +248,7 @@ namespace Umbraco.Tests.Composing [Test] public void CanInsertIntoBuilderBefore() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append() .InsertBefore(); @@ -241,18 +257,20 @@ namespace Umbraco.Tests.Composing Assert.IsTrue(builder.Has()); Assert.IsTrue(builder.Has()); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); AssertCollection(col, typeof(Resolved1), typeof(Resolved3), typeof(Resolved2)); } [Test] public void CannotInsertIntoBuilderBeforeOnceCollectionIsCreated() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append(); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); Assert.Throws(() => builder.InsertBefore() ); @@ -261,19 +279,20 @@ namespace Umbraco.Tests.Composing [Test] public void CanInsertDuplicateIntoBuilderBeforeAndDeDuplicate() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Append() .InsertBefore(); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); AssertCollection(col, typeof(Resolved2), typeof(Resolved1)); } [Test] public void CannotInsertIntoBuilderBeforeMissing() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append(); Assert.Throws(() => @@ -284,7 +303,7 @@ namespace Umbraco.Tests.Composing [Test] public void ScopeBuilderCreatesScopedCollection() { - _container.RegisterCollectionBuilder() + _composition.WithCollectionBuilder() .Append() .Append(); @@ -292,19 +311,25 @@ namespace Umbraco.Tests.Composing // but the container manages the scope, so to test the scope // the collection must come from the container - var col1 = _container.GetInstance(); - AssertCollection(col1, typeof(Resolved1), typeof(Resolved2)); + var factory = _composition.CreateFactory(); - var col2 = _container.GetInstance(); - AssertCollection(col2, typeof(Resolved1), typeof(Resolved2)); + using (factory.BeginScope()) + { + var col1 = factory.GetInstance(); + AssertCollection(col1, typeof(Resolved1), typeof(Resolved2)); + + var col2 = factory.GetInstance(); + AssertCollection(col2, typeof(Resolved1), typeof(Resolved2)); + + AssertSameCollection(col1, col2); + } - AssertSameCollection(col1, col2); } [Test] public void TransientBuilderCreatesTransientCollection() { - _container.RegisterCollectionBuilder() + _composition.WithCollectionBuilder() .Append() .Append(); @@ -312,10 +337,12 @@ namespace Umbraco.Tests.Composing // but the container manages the scope, so to test the scope // the collection must come from the container - var col1 = _container.GetInstance(); + var factory = _composition.CreateFactory(); + + var col1 = factory.GetInstance(); AssertCollection(col1, typeof(Resolved1), typeof(Resolved2)); - var col2 = _container.GetInstance(); + var col2 = factory.GetInstance(); AssertCollection(col1, typeof(Resolved1), typeof(Resolved2)); AssertNotSameCollection(col1, col2); @@ -324,19 +351,20 @@ namespace Umbraco.Tests.Composing [Test] public void BuilderRespectsTypesOrder() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Append() .Insert() .InsertBefore(); - var col1 = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col1 = builder.CreateCollection(factory); AssertCollection(col1, typeof(Resolved1), typeof(Resolved2), typeof(Resolved3)); } [Test] public void ScopeBuilderRespectsContainerScope() { - _container.RegisterCollectionBuilder() + _composition.WithCollectionBuilder() .Append() .Append(); @@ -344,34 +372,40 @@ namespace Umbraco.Tests.Composing // but the container manages the scope, so to test the scope // the collection must come from the container - var scope1 = _container.BeginScope(); + TestCollection col1A, col1B; + + var factory = _composition.CreateFactory(); + + using (factory.BeginScope()) + { + col1A = factory.GetInstance(); + col1B = factory.GetInstance(); + } - var col1A = _container.GetInstance(); AssertCollection(col1A, typeof(Resolved1), typeof(Resolved2)); - var col1B = _container.GetInstance(); AssertCollection(col1B, typeof(Resolved1), typeof(Resolved2)); - AssertSameCollection(col1A, col1B); - _container.ScopeManagerProvider.GetScopeManager(_container).CurrentScope.Dispose(); - var scope2 = _container.BeginScope(); + TestCollection col2; + + using (factory.BeginScope()) + { + col2 = factory.GetInstance(); + } - var col2 = _container.GetInstance(); AssertCollection(col2, typeof(Resolved1), typeof(Resolved2)); - AssertNotSameCollection(col1A, col2); - - _container.ScopeManagerProvider.GetScopeManager(_container).CurrentScope.Dispose(); } [Test] public void WeightedBuilderCreatesWeightedCollection() { - var builder = _container.RegisterCollectionBuilder() + var builder = _composition.WithCollectionBuilder() .Add() .Add(); - var col = builder.CreateCollection(); + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); AssertCollection(col, typeof(Resolved2), typeof(Resolved1)); } @@ -432,44 +466,28 @@ namespace Umbraco.Tests.Composing // ReSharper disable once ClassNeverInstantiated.Local private class TestCollectionBuilder : OrderedCollectionBuilderBase { - public TestCollectionBuilder(IServiceContainer container) - : base(container) - { } - protected override TestCollectionBuilder This => this; } // ReSharper disable once ClassNeverInstantiated.Local private class TestCollectionBuilderTransient : OrderedCollectionBuilderBase { - public TestCollectionBuilderTransient(IServiceContainer container) - : base(container) - { } - protected override TestCollectionBuilderTransient This => this; - protected override ILifetime CollectionLifetime => null; // transient + protected override Lifetime CollectionLifetime => Lifetime.Transient; // transient } // ReSharper disable once ClassNeverInstantiated.Local private class TestCollectionBuilderScope : OrderedCollectionBuilderBase { - public TestCollectionBuilderScope(IServiceContainer container) - : base(container) - { } - protected override TestCollectionBuilderScope This => this; - protected override ILifetime CollectionLifetime => new PerScopeLifetime(); + protected override Lifetime CollectionLifetime => Lifetime.Scope; } // ReSharper disable once ClassNeverInstantiated.Local private class TestCollectionBuilderWeighted : WeightedCollectionBuilderBase { - public TestCollectionBuilderWeighted(IServiceContainer container) - : base(container) - { } - protected override TestCollectionBuilderWeighted This => this; } diff --git a/src/Umbraco.Tests/Composing/ComposingTestBase.cs b/src/Umbraco.Tests/Composing/ComposingTestBase.cs index be595885e7..48850afd97 100644 --- a/src/Umbraco.Tests/Composing/ComposingTestBase.cs +++ b/src/Umbraco.Tests/Composing/ComposingTestBase.cs @@ -4,6 +4,7 @@ using Moq; using NUnit.Framework; using Umbraco.Core.Cache; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Tests.TestHelpers; @@ -13,14 +14,14 @@ namespace Umbraco.Tests.Composing { protected TypeLoader TypeLoader { get; private set; } - protected ProfilingLogger ProfilingLogger { get; private set; } + protected IProfilingLogger ProfilingLogger { get; private set; } [SetUp] public void Initialize() { ProfilingLogger = new ProfilingLogger(Mock.Of(), Mock.Of()); - TypeLoader = new TypeLoader(NullCacheProvider.Instance, SettingsForTests.GenerateMockGlobalSettings(), ProfilingLogger, detectChanges: false) + TypeLoader = new TypeLoader(NullCacheProvider.Instance, LocalTempStorage.Default, ProfilingLogger, detectChanges: false) { AssembliesToScan = AssembliesToScan }; diff --git a/src/Umbraco.Tests/Composing/ContainerConformingTests.cs b/src/Umbraco.Tests/Composing/ContainerConformingTests.cs new file mode 100644 index 0000000000..21ea961636 --- /dev/null +++ b/src/Umbraco.Tests/Composing/ContainerConformingTests.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Composing; + +namespace Umbraco.Tests.Composing +{ + [TestFixture] + public class ContainerConformingTests + { + // tests that a container conforms + + private IRegister GetRegister() => RegisterFactory.Create(); + + [Test] + public void CanRegisterAndGet() + { + var register = GetRegister(); + + register.Register(); + + var factory = register.CreateFactory(); + + var thing = factory.GetInstance(); + Assert.IsNotNull(thing); + Assert.IsInstanceOf(thing); + } + + [Test] + public void CanRegisterAndGetLazy() + { + var register = GetRegister(); + + register.Register(); + + var factory = register.CreateFactory(); + + var lazyThing = factory.GetInstance>(); + Assert.IsNotNull(lazyThing); + Assert.IsInstanceOf>(lazyThing); + var thing = lazyThing.Value; + Assert.IsNotNull(thing); + Assert.IsInstanceOf(thing); + } + + [Test] + public void CannotRegistedAndGetBase() + { + var register = GetRegister(); + + register.Register(); + + var factory = register.CreateFactory(); + + Assert.IsNull(factory.TryGetInstance()); + } + + [Test] + public void CannotRegisterAndGetInterface() + { + var register = GetRegister(); + + register.Register(); + + var factory = register.CreateFactory(); + + Assert.IsNull(factory.TryGetInstance()); + } + + [Test] + public void CanRegisterAndGetAllBase() + { + var register = GetRegister(); + + register.Register(); + + var factory = register.CreateFactory(); + + var things = factory.GetAllInstances(); + Assert.AreEqual(1, things.Count()); + + // lightInject: would be zero with option EnableVariance set to false + } + + [Test] + public void CanRegisterAndGetAllInterface() + { + var register = GetRegister(); + + register.Register(); + + var factory = register.CreateFactory(); + + var things = factory.GetAllInstances(); + Assert.AreEqual(1, things.Count()); + + // lightInject: would be zero with option EnableVariance set to false + } + + [Test] + public void CanRegisterBaseAndGet() + { + var register = GetRegister(); + + register.Register(); + + var factory = register.CreateFactory(); + + var thing = factory.GetInstance(); + Assert.IsNotNull(thing); + Assert.IsInstanceOf(thing); + } + + [Test] + public void CanRegisterInterfaceAndGet() + { + var register = GetRegister(); + + register.Register(); + + var factory = register.CreateFactory(); + + var thing = factory.GetInstance(); + Assert.IsNotNull(thing); + Assert.IsInstanceOf(thing); + } + + [Test] + public void NonSingletonServiceIsNotUnique() + { + var register = GetRegister(); + + register.Register(); + register.Register(); + + var factory = register.CreateFactory(); + + var things = factory.GetInstance>(); + Assert.AreEqual(2, things.Count()); + + Assert.IsNull(factory.TryGetInstance()); + } + + [Test] + public void SingletonServiceIsUnique() // fixme - but what is LightInject actually doing + { + var register = GetRegister(); + + // fixme + // LightInject is 'unique' per serviceType+serviceName + // but that's not how all containers work + // and we should not rely on it + // if we need unique, use RegisterUnique + + // for Core services that ppl may want to redefine in components, + // it is important to be able to have a unique, singleton implementation, + // and to redefine it - how it's done at container's level depends + // on each container + + // redefine the service + register.Register(Lifetime.Singleton); + register.Register(Lifetime.Singleton); + + var factory = register.CreateFactory(); + + var things = factory.GetInstance>(); + Assert.AreEqual(1, things.Count()); + + var thing = factory.GetInstance(); + Assert.IsInstanceOf(thing); + } + + [Test] + public void SingletonImplementationIsNotUnique() + { + var register = GetRegister(); + + // define two implementations + register.Register(Lifetime.Singleton); + register.Register(Lifetime.Singleton); + + var factory = register.CreateFactory(); + + var things = factory.GetInstance>(); + Assert.AreEqual(2, things.Count()); + + Assert.IsNull(factory.TryGetInstance()); + } + + [Test] + public void ActualInstanceIsNotUnique() + { + var register = GetRegister(); + + // define two instances + register.Register(typeof(Thing1), new Thing1()); + register.Register(typeof(Thing1), new Thing2()); + + var factory = register.CreateFactory(); + + var things = factory.GetInstance>(); + //Assert.AreEqual(2, things.Count()); + Assert.AreEqual(1, things.Count()); // well, yes they are unique? + + Assert.IsNull(factory.TryGetInstance()); + } + + [Test] + public void InterfaceInstanceIsNotUnique() + { + var register = GetRegister(); + + // define two instances + register.Register(typeof(IThing), new Thing1()); + register.Register(typeof(IThing), new Thing2()); + + var factory = register.CreateFactory(); + + var things = factory.GetInstance>(); + //Assert.AreEqual(2, things.Count()); + Assert.AreEqual(1, things.Count()); // well, yes they are unique? + + //Assert.IsNull(factory.TryGetInstance()); + Assert.IsNotNull(factory.TryGetInstance()); // well, what? + } + + [Test] + public void CanInjectEnumerableOfBase() + { + var register = GetRegister(); + + register.Register(); + register.Register(); + register.Register(); + + var factory = register.CreateFactory(); + + var needThings = factory.GetInstance(); + Assert.AreEqual(2, needThings.Things.Count()); + } + + [Test] + public void CanGetEnumerableOfBase() + { + var register = GetRegister(); + + register.Register(); + register.Register(); + + var factory = register.CreateFactory(); + + var things = factory.GetInstance>(); + Assert.AreEqual(2, things. Count()); + } + + [Test] + public void CanGetEmptyEnumerableOfBase() + { + var register = GetRegister(); + var factory = register.CreateFactory(); + + var things = factory.GetInstance>(); + Assert.AreEqual(0, things.Count()); + } + + [Test] + public void CanGetEmptyAllInstancesOfBase() + { + var register = GetRegister(); + var factory = register.CreateFactory(); + + var things = factory.GetAllInstances(); + Assert.AreEqual(0, things.Count()); + } + + [Test] + public void CanTryGetEnumerableOfBase() + { + var register = GetRegister(); + + register.Register(); + register.Register(); + + var factory = register.CreateFactory(); + + var things = factory.TryGetInstance>(); + Assert.AreEqual(2, things.Count()); + } + + [Test] + public void CanRegisterSingletonInterface() + { + var register = GetRegister(); + register.Register(Lifetime.Singleton); + var factory = register.CreateFactory(); + var s1 = factory.GetInstance(); + var s2 = factory.GetInstance(); + Assert.AreSame(s1, s2); + } + + [Test] + public void CanRegisterSingletonClass() + { + var register = GetRegister(); + register.Register(Lifetime.Singleton); + var factory = register.CreateFactory(); + var s1 = factory.GetInstance(); + var s2 = factory.GetInstance(); + Assert.AreSame(s1, s2); + } + + [Test] + public void CanReRegisterSingletonInterface() + { + var register = GetRegister(); + register.Register(Lifetime.Singleton); + register.Register(Lifetime.Singleton); + var factory = register.CreateFactory(); + var s = factory.GetInstance(); + Assert.IsInstanceOf(s); + } + + [Test] + public void CanRegisterSingletonWithCreate() + { + var register = GetRegister(); + register.Register(c => c.CreateInstance(new Thing1()), Lifetime.Singleton); + var factory = register.CreateFactory(); + var s1 = factory.GetInstance(); + var s2 = factory.GetInstance(); + Assert.AreSame(s1, s2); + } + + public interface IThing { } + + public abstract class ThingBase : IThing { } + public class Thing1 : ThingBase { } + public class Thing2 : ThingBase { } + + public class Thing3 : ThingBase + { + public Thing3(Thing1 thing) { } + } + + public class NeedThings + { + public NeedThings(IEnumerable things) + { + Things = things; + } + + public IEnumerable Things { get; } + } + } +} diff --git a/src/Umbraco.Tests/Composing/LazyCollectionBuilderTests.cs b/src/Umbraco.Tests/Composing/LazyCollectionBuilderTests.cs index 7a39186fea..cbabae1a83 100644 --- a/src/Umbraco.Tests/Composing/LazyCollectionBuilderTests.cs +++ b/src/Umbraco.Tests/Composing/LazyCollectionBuilderTests.cs @@ -1,10 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; -using LightInject; +using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Components; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Tests.Components; namespace Umbraco.Tests.Composing { @@ -23,6 +28,11 @@ namespace Umbraco.Tests.Composing Current.Reset(); } + private IRegister CreateRegister() + { + return RegisterFactory.Create(); + } + // note // lazy collection builder does not throw on duplicate, just uses distinct types // so we don't have a test for duplicates as we had with resolvers in v7 @@ -30,22 +40,24 @@ namespace Umbraco.Tests.Composing [Test] public void LazyCollectionBuilderHandlesTypes() { - var container = new ServiceContainer(); - container.ConfigureUmbracoCore(); + var container = CreateRegister(); + var composition = new Composition(container, new TypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run)); - container.RegisterCollectionBuilder() + composition.WithCollectionBuilder() .Add() .Add() .Add() .Add(); - var values = container.GetInstance(); + var factory = composition.CreateFactory(); + + var values = factory.GetInstance(); Assert.AreEqual(3, values.Count()); Assert.IsTrue(values.Select(x => x.GetType()) .ContainsAll(new[] { typeof(TransientObject1), typeof(TransientObject2), typeof(TransientObject3) })); - var other = container.GetInstance(); + var other = factory.GetInstance(); Assert.AreNotSame(values, other); // transient var o1 = other.FirstOrDefault(x => x is TransientObject1); Assert.IsFalse(values.Contains(o1)); // transient @@ -54,21 +66,23 @@ namespace Umbraco.Tests.Composing [Test] public void LazyCollectionBuilderHandlesProducers() { - var container = new ServiceContainer(); - container.ConfigureUmbracoCore(); + var container = CreateRegister(); + var composition = new Composition(container, new TypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run)); - container.RegisterCollectionBuilder() + composition.WithCollectionBuilder() .Add(() => new[] { typeof(TransientObject3), typeof(TransientObject2) }) .Add(() => new[] { typeof(TransientObject3), typeof(TransientObject2) }) .Add(() => new[] { typeof(TransientObject1) }); - var values = container.GetInstance(); + var factory = composition.CreateFactory(); + + var values = factory.GetInstance(); Assert.AreEqual(3, values.Count()); Assert.IsTrue(values.Select(x => x.GetType()) .ContainsAll(new[] { typeof(TransientObject1), typeof(TransientObject2), typeof(TransientObject3) })); - var other = container.GetInstance(); + var other = factory.GetInstance(); Assert.AreNotSame(values, other); // transient var o1 = other.FirstOrDefault(x => x is TransientObject1); Assert.IsFalse(values.Contains(o1)); // transient @@ -77,22 +91,24 @@ namespace Umbraco.Tests.Composing [Test] public void LazyCollectionBuilderHandlesTypesAndProducers() { - var container = new ServiceContainer(); - container.ConfigureUmbracoCore(); + var container = CreateRegister(); + var composition = new Composition(container, new TypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run)); - container.RegisterCollectionBuilder() + composition.WithCollectionBuilder() .Add() .Add() .Add() .Add(() => new[] { typeof(TransientObject1) }); - var values = container.GetInstance(); + var factory = composition.CreateFactory(); + + var values = factory.GetInstance(); Assert.AreEqual(3, values.Count()); Assert.IsTrue(values.Select(x => x.GetType()) .ContainsAll(new[] { typeof(TransientObject1), typeof(TransientObject2), typeof(TransientObject3) })); - var other = container.GetInstance(); + var other = factory.GetInstance(); Assert.AreNotSame(values, other); // transient var o1 = other.FirstOrDefault(x => x is TransientObject1); Assert.IsFalse(values.Contains(o1)); // transient @@ -101,10 +117,10 @@ namespace Umbraco.Tests.Composing [Test] public void LazyCollectionBuilderThrowsOnIllegalTypes() { - var container = new ServiceContainer(); - container.ConfigureUmbracoCore(); + var container = CreateRegister(); + var composition = new Composition(container, new TypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run)); - container.RegisterCollectionBuilder() + composition.WithCollectionBuilder() .Add() // illegal, does not implement the interface! @@ -115,23 +131,25 @@ namespace Umbraco.Tests.Composing Assert.Throws(() => { - // but throws here when trying to register the types - var values = container.GetInstance(); + // but throws here when trying to register the types, right before creating the factory + var factory = composition.CreateFactory(); }); } [Test] public void LazyCollectionBuilderCanExcludeTypes() { - var container = new ServiceContainer(); - container.ConfigureUmbracoCore(); + var container = CreateRegister(); + var composition = new Composition(container, new TypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run)); - container.RegisterCollectionBuilder() + composition.WithCollectionBuilder() .Add() .Add(() => new[] { typeof(TransientObject3), typeof(TransientObject2), typeof(TransientObject1) }) .Exclude(); - var values = container.GetInstance(); + var factory = composition.CreateFactory(); + + var values = factory.GetInstance(); Assert.AreEqual(2, values.Count()); Assert.IsFalse(values.Select(x => x.GetType()) @@ -139,7 +157,7 @@ namespace Umbraco.Tests.Composing Assert.IsTrue(values.Select(x => x.GetType()) .ContainsAll(new[] { typeof(TransientObject1), typeof(TransientObject2) })); - var other = container.GetInstance(); + var other = factory.GetInstance(); Assert.AreNotSame(values, other); // transient var o1 = other.FirstOrDefault(x => x is TransientObject1); Assert.IsFalse(values.Contains(o1)); // transient @@ -165,13 +183,9 @@ namespace Umbraco.Tests.Composing // ReSharper disable once ClassNeverInstantiated.Local private class TestCollectionBuilder : LazyCollectionBuilderBase { - public TestCollectionBuilder(IServiceContainer container) - : base(container) - { } - protected override TestCollectionBuilder This => this; - protected override ILifetime CollectionLifetime => null; // transient + protected override Lifetime CollectionLifetime => Lifetime.Transient; // transient } // ReSharper disable once ClassNeverInstantiated.Local diff --git a/src/Umbraco.Tests/Composing/LightInjectValidation.cs b/src/Umbraco.Tests/Composing/LightInjectValidation.cs new file mode 100644 index 0000000000..75062e613c --- /dev/null +++ b/src/Umbraco.Tests/Composing/LightInjectValidation.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LightInject; +using System.Collections.Concurrent; +using System.Collections.ObjectModel; +using System.Reflection; +using ServiceMap = System.Collections.Generic.Dictionary>; + +/********************************************************************************* + The MIT License (MIT) + + Copyright (c) 2017 bernhard.richter@gmail.com + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +****************************************************************************** + LightInject.Validation version 1.0.1 + http://www.lightinject.net/ + http://twitter.com/bernhardrichter +******************************************************************************/ + +namespace Umbraco.Tests.Composing +{ + public static class LightInjectValidation + { + private static readonly ConcurrentDictionary LifeSpans = new ConcurrentDictionary(); + + private const string NotDisposeMessageServiceType = + @"The service {0} is being injected as a constructor argument into {1} implements IDisposable, " + + "but is registered without a lifetime (transient). LightInject will not be able to dispose the instance represented by {0}. " + + "If the intent was to manually control the instantiation and destruction, inject Func<{0}> instead. " + + "Otherwise register `{0}` with a lifetime (PerContainer, PerRequest or PerScope)."; + + private const string NotDisposeMessageImplementingType = + @"The service {0} represented by {1} is being injected as a constructor argument into {2} implements IDisposable, " + + "but is registered without a lifetime (transient). LightInject will not be able to dispose the instance represented by {0}. " + + "If the intent was to manually control the instantiation and destruction, inject Func<{0}> instead. " + + "Otherwise register `{0}` with a lifetime (PerContainer, PerRequest or PerScope)."; + + + private const string MissingDeferredDependency = + @"The injected '{0}' does not contain a registration for the underlying type '{1}'. " + + "Ensure that '{1}' is registered so that the service can be resolved by '{0}'"; + + /* + The service 'NameSpace.IBar' that is being injected into 'NameSpace.Foo' is registered with +with a 'Transient' lifetime while the 'NameSpace.Foo' is registered with the 'PerScope' lifetime. +Ensure that 'NameSpace.IBar' is registered with a lifetime that is equal to or has a longer lifetime than the 'PerScope' lifetime. + */ + private const string CaptiveDependency = + @"The service '{0}' that is being injected into {1} is registered with " + + "a '{2}' lifetime while the {1} is registered with the '{3}' lifetime. " + + "Ensure that '{0}' is registered with a lifetime that is equal to or has a longer lifetime than the '{3}' lifetime. " + + "Alternatively ensure that `{1}` is registered with a lifetime that is equal to or " + + "has a shorter lifetime than `{2}` lifetime."; + + private const string MissingDependency = + "Class: 'NameSpace.Foo', Parameter 'NameSpace.IBar bar' -> The injected service NameSpace IBar is not registered." + ; + + + static LightInjectValidation() + { + LifeSpans.TryAdd(typeof(PerRequestLifeTime), 10); + LifeSpans.TryAdd(typeof(PerScopeLifetime), 20); + LifeSpans.TryAdd(typeof(PerContainerLifetime), 30); + } + + public static IEnumerable Validate(this ServiceContainer container) + { + var serviceMap = container.AvailableServices.GroupBy(sr => sr.ServiceType).ToDictionary(gr => gr.Key, + gr => gr.ToDictionary(sr => sr.ServiceName, sr => sr, StringComparer.OrdinalIgnoreCase)); + + var verifyableServices = container.AvailableServices.Where(sr => sr.ImplementingType != null); + + return verifyableServices.SelectMany(sr => + ValidateConstructor(serviceMap, sr, container.ConstructorSelector.Execute(sr.ImplementingType))); + } + + private static IReadOnlyCollection ValidateConstructor(ServiceMap serviceMap, + ServiceRegistration serviceRegistration, ConstructorInfo constructorInfo) + { + var result = new Collection(); + + foreach (var parameter in constructorInfo.GetParameters()) + { + var validationTarget = new ValidationTarget(serviceRegistration, parameter); + Validate(validationTarget, serviceMap, result); + } + return result; + } + + private static void Validate(ValidationTarget validationTarget, ServiceMap serviceMap, ICollection result) + { + var registration = GetServiceRegistration(serviceMap, validationTarget); + if (registration == null) + { + if (validationTarget.ServiceType.IsFunc() || validationTarget.ServiceType.IsLazy()) + { + var serviceType = validationTarget.ServiceType.GenericTypeArguments[0]; + var underlyingvalidationTarget = validationTarget.WithServiceDescription(serviceType, string.Empty); + registration = GetServiceRegistration(serviceMap, underlyingvalidationTarget); + + if (registration != null) + { + return; + } + + if (serviceMap.ContainsAmbiguousRegistrationFor(serviceType)) + { + result.Add(new ValidationResult("", ValidationSeverity.Ambiguous, underlyingvalidationTarget)); + } + else + { + string message = string.Format(MissingDeferredDependency, validationTarget.ServiceType, underlyingvalidationTarget.ServiceType); + result.Add(new ValidationResult(message, ValidationSeverity.MissingDependency, underlyingvalidationTarget)); + } + } + else if (validationTarget.ServiceType.IsGenericType && validationTarget.ServiceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + var serviceType = validationTarget.ServiceType.GenericTypeArguments[0]; + var underlyingvalidationTarget = validationTarget.WithServiceDescription(serviceType, string.Empty); + var registrations = GetServiceRegistrations(serviceMap, underlyingvalidationTarget); + if (registrations.Any()) return; + + // strict: there has to be at least 1 + string message = string.Format(MissingDeferredDependency, validationTarget.ServiceType, underlyingvalidationTarget.ServiceType); + result.Add(new ValidationResult(message, ValidationSeverity.MissingDependency, underlyingvalidationTarget)); + } + else + { + if (serviceMap.ContainsAmbiguousRegistrationFor(validationTarget.ServiceType)) + { + result.Add(new ValidationResult("", ValidationSeverity.Ambiguous, validationTarget)); + } + else + { + result.Add(new ValidationResult("", ValidationSeverity.MissingDependency, validationTarget)); + } + } + } + else + { + ValidateDisposable(validationTarget, result, registration); + ValidateLifetime(validationTarget, registration, result); + } + } + + private static void ValidateDisposable(ValidationTarget validationTarget, ICollection result, + ServiceRegistration registration) + { + if (registration.ServiceType.Implements()) + { + var message = string.Format(NotDisposeMessageServiceType, registration.ServiceType, + validationTarget.DeclaringService.ImplementingType); + result.Add(new ValidationResult(message, ValidationSeverity.NotDisposed, validationTarget)); + } + + else if (registration.ImplementingType != null && registration.ImplementingType.Implements()) + { + var message = string.Format(NotDisposeMessageImplementingType, registration.ImplementingType, + registration.ServiceType, + validationTarget.DeclaringService.ImplementingType); + result.Add(new ValidationResult(message, ValidationSeverity.NotDisposed, validationTarget)); + } + } + + + private static void ValidateLifetime(ValidationTarget validationTarget, ServiceRegistration dependencyRegistration, ICollection result) + { + if (GetLifespan(validationTarget.DeclaringService.Lifetime) > GetLifespan(dependencyRegistration.Lifetime)) + { + var message = string.Format(CaptiveDependency, dependencyRegistration.ServiceType, + validationTarget.DeclaringService.ServiceType, GetLifetimeName(dependencyRegistration.Lifetime), + GetLifetimeName(validationTarget.DeclaringService.Lifetime)); + result.Add(new ValidationResult(message, ValidationSeverity.Captive, validationTarget)); + } + } + + public static void SetLifespan(int lifeSpan) where TLifetime : ILifetime + { + LifeSpans.TryAdd(typeof(TLifetime), lifeSpan); + } + + private static IEnumerable GetServiceRegistrations(ServiceMap serviceMap, ValidationTarget validationTarget) + { + return serviceMap.Where(x => validationTarget.ServiceType.IsAssignableFrom(x.Key)).SelectMany(x => x.Value.Values); + } + + private static ServiceRegistration GetServiceRegistration(ServiceMap serviceMap, ValidationTarget validationTarget) + { + if (!serviceMap.TryGetValue(validationTarget.ServiceType, out var registrations)) + { + return null; + } + + if (registrations.TryGetValue(string.Empty, out var registration)) + { + return registration; + } + + if (registrations.Count == 1) + { + return registrations.Values.First(); + } + + if (registrations.TryGetValue(validationTarget.ServiceName, out registration)) + { + return registration; + } + + return null; + } + + private static string GetLifetimeName(ILifetime lifetime) + { + if (lifetime == null) + { + return "Transient"; + } + return lifetime.GetType().Name; + } + + private static int GetLifespan(ILifetime lifetime) + { + if (lifetime == null) + { + return 0; + } + if (LifeSpans.TryGetValue(lifetime.GetType(), out var lifespan)) + { + return lifespan; + } + return 0; + } + } + + + public class ValidationTarget + { + public ServiceRegistration DeclaringService { get; } + public ParameterInfo Parameter { get; } + public Type ServiceType { get; } + public string ServiceName { get; } + + + public ValidationTarget(ServiceRegistration declaringRegistration, ParameterInfo parameter) : this(declaringRegistration, parameter, parameter.ParameterType, string.Empty) + { + } + + + public ValidationTarget(ServiceRegistration declaringService, ParameterInfo parameter, Type serviceType, string serviceName) + { + DeclaringService = declaringService; + Parameter = parameter; + ServiceType = serviceType; + ServiceName = serviceName; + + + if (serviceType.GetTypeInfo().IsGenericType && serviceType.GetTypeInfo().ContainsGenericParameters) + { + ServiceType = serviceType.GetGenericTypeDefinition(); + } + + } + + public ValidationTarget WithServiceDescription(Type serviceType, string serviceName) + { + return new ValidationTarget(DeclaringService, Parameter, serviceType, serviceName); + } + + } + + + + + + public class ValidationResult + { + public ValidationResult(string message, ValidationSeverity severity, ValidationTarget validationTarget) + { + Message = message; + Severity = severity; + ValidationTarget = validationTarget; + } + + public string Message { get; } + + public ValidationSeverity Severity { get; } + public ValidationTarget ValidationTarget { get; } + } + + public enum ValidationSeverity + { + NoIssues, + Captive, + NotDisposed, + MissingDependency, + Ambiguous + } + + internal static class TypeExtensions + { + public static bool Implements(this Type type) + { + return type.GetTypeInfo().ImplementedInterfaces.Contains(typeof(TBaseType)); + } + + public static bool IsFunc(this Type type) + { + var typeInfo = type.GetTypeInfo(); + return typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Func<>); + } + + public static bool IsLazy(this Type type) + { + var typeInfo = type.GetTypeInfo(); + return typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Lazy<>); + } + } + + internal static class ServiceMapExtensions + { + public static bool ContainsAmbiguousRegistrationFor(this ServiceMap serviceMap, Type serviceType) + { + if (!serviceMap.TryGetValue(serviceType, out var registrations)) + { + return false; + } + return registrations.Count > 1; + } + } +} diff --git a/src/Umbraco.Tests/Composing/PackageActionCollectionTests.cs b/src/Umbraco.Tests/Composing/PackageActionCollectionTests.cs index e2145f557a..d100713102 100644 --- a/src/Umbraco.Tests/Composing/PackageActionCollectionTests.cs +++ b/src/Umbraco.Tests/Composing/PackageActionCollectionTests.cs @@ -1,10 +1,14 @@ using System; using System.Linq; using System.Xml; -using LightInject; +using Moq; using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Components; using Umbraco.Core.Composing; +using Umbraco.Core.Logging; using Umbraco.Core._Legacy.PackageActions; +using Umbraco.Tests.Components; namespace Umbraco.Tests.Composing { @@ -14,18 +18,21 @@ namespace Umbraco.Tests.Composing [Test] public void PackageActionCollectionBuilderWorks() { - var container = new ServiceContainer(); - container.ConfigureUmbracoCore(); + var container = RegisterFactory.Create(); + + var composition = new Composition(container, new TypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run)); - container.RegisterCollectionBuilder() + composition.WithCollectionBuilder() .Add(() => TypeLoader.GetPackageActions()); + Current.Factory = composition.CreateFactory(); + var actions = Current.PackageActions; Assert.AreEqual(2, actions.Count()); // order is unspecified, but both must be there - bool hasAction1 = actions.ElementAt(0) is PackageAction1 || actions.ElementAt(1) is PackageAction1; - bool hasAction2 = actions.ElementAt(0) is PackageAction2 || actions.ElementAt(1) is PackageAction2; + var hasAction1 = actions.ElementAt(0) is PackageAction1 || actions.ElementAt(1) is PackageAction1; + var hasAction2 = actions.ElementAt(0) is PackageAction2 || actions.ElementAt(1) is PackageAction2; Assert.IsTrue(hasAction1); Assert.IsTrue(hasAction2); } diff --git a/src/Umbraco.Tests/Composing/TypeFinderTests.cs b/src/Umbraco.Tests/Composing/TypeFinderTests.cs index 955f6f94c8..2b9474310b 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 }; } @@ -93,7 +93,7 @@ namespace Umbraco.Tests.Composing Assert.AreEqual(21, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] } - private static ProfilingLogger GetTestProfilingLogger() + private static IProfilingLogger GetTestProfilingLogger() { var logger = new DebugDiagnosticsLogger(); var profiler = new TestProfiler(); diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 07625db9bf..5148c7eb1b 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -9,6 +9,7 @@ using umbraco; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.PropertyEditors; @@ -22,11 +23,12 @@ namespace Umbraco.Tests.Composing public class TypeLoaderTests { private TypeLoader _typeLoader; + [SetUp] public void Initialize() { // this ensures it's reset - _typeLoader = new TypeLoader(NullCacheProvider.Instance, SettingsForTests.GenerateMockGlobalSettings(), new ProfilingLogger(Mock.Of(), Mock.Of())); + _typeLoader = new TypeLoader(NullCacheProvider.Instance, LocalTempStorage.Default, new ProfilingLogger(Mock.Of(), Mock.Of())); foreach (var file in Directory.GetFiles(IOHelper.MapPath("~/App_Data/TEMP/TypesCache"))) File.Delete(file); @@ -53,16 +55,21 @@ namespace Umbraco.Tests.Composing public void TearDown() { _typeLoader = null; + + + // cleanup + var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; + var tlDir = Path.Combine(assDir.FullName, "TypeLoader"); + if (!Directory.Exists(tlDir)) + return; + Directory.Delete(tlDir, true); } private DirectoryInfo PrepareFolder() { var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; - var dir = Directory.CreateDirectory(Path.Combine(assDir.FullName, "TypeLoader", Guid.NewGuid().ToString("N"))); - foreach (var f in dir.GetFiles()) - { - f.Delete(); - } + var tlDir = Path.Combine(assDir.FullName, "TypeLoader"); + var dir = Directory.CreateDirectory(Path.Combine(tlDir, Guid.NewGuid().ToString("N"))); return dir; } 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/Configurations/GlobalSettingsTests.cs b/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs index ebba8bc1cc..8587a7b194 100644 --- a/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs +++ b/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs @@ -2,6 +2,8 @@ using System.Web.Routing; using Moq; using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Tests.TestHelpers; @@ -45,15 +47,13 @@ namespace Umbraco.Tests.Configurations [TestCase("~/some-wacky/nestedPath", "/MyVirtualDir/NestedVDir/", "some-wacky-nestedpath")] public void Umbraco_Mvc_Area(string path, string rootPath, string outcome) { - var globalSettingsMock = Mock.Get(TestObjects.GetGlobalSettings()); //this will modify the IGlobalSettings instance stored in the container + var globalSettingsMock = Mock.Get(Factory.GetInstance()); //this will modify the IGlobalSettings instance stored in the container globalSettingsMock.Setup(x => x.Path).Returns(IOHelper.ResolveUrl(path)); - SettingsForTests.ConfigureSettings(globalSettingsMock.Object); SystemDirectories.Root = rootPath; - Assert.AreEqual(outcome, UmbracoConfig.For.GlobalSettings().GetUmbracoMvcArea()); + Assert.AreEqual(outcome, Current.Configs.Global().GetUmbracoMvcArea()); } - [TestCase("/umbraco/umbraco.aspx")] [TestCase("/umbraco/editContent.aspx")] [TestCase("/install/default.aspx")] [TestCase("/install/")] @@ -93,10 +93,9 @@ namespace Umbraco.Tests.Configurations public void Is_Reserved_By_Route(string url, bool shouldMatch) { //reset the app config, we only want to test routes not the hard coded paths - var globalSettingsMock = Mock.Get(TestObjects.GetGlobalSettings()); //this will modify the IGlobalSettings instance stored in the container + var globalSettingsMock = Mock.Get(Factory.GetInstance()); //this will modify the IGlobalSettings instance stored in the container globalSettingsMock.Setup(x => x.ReservedPaths).Returns(""); globalSettingsMock.Setup(x => x.ReservedUrls).Returns(""); - SettingsForTests.ConfigureSettings(globalSettingsMock.Object); var routes = new RouteCollection(); diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs index f1ac463305..962d6d13a9 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs @@ -143,7 +143,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public void PreviewBadge() { - Assert.AreEqual(SettingsSection.Content.PreviewBadge, @"In Preview Mode - click to end"); + Assert.AreEqual(SettingsSection.Content.PreviewBadge, @"In Preview Mode - click to end"); } [Test] public void ResolveUrlsFromTextString() diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index a436dad9f5..4c64485503 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -77,7 +77,7 @@ In Preview Mode - click to end + In Preview Mode - click to end ]]> - - + + - - + + - - @@ -112,30 +132,38 @@ - @@ -148,9 +176,7 @@ -
-				{{model.result.queryExpression}}
-						
+
{{model.result.queryExpression}}
@@ -167,6 +193,7 @@ type="button" button-style="link" label-key="general_close" + shortcut="esc" action="vm.close()"> @@ -183,4 +210,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js index 6b8462b583..e179e0acb3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js @@ -123,11 +123,14 @@ oldProperty.isObject = true; } - // create new property object used in the diff table + // diff requires a string + property.value = property.value ? property.value : ""; + oldProperty.value = oldProperty.value ? oldProperty.value : ""; + var diffProperty = { "alias": property.alias, "label": property.label, - "diff": (property.value || oldProperty.value) ? JsDiff.diffWords(property.value, oldProperty.value) : "", + "diff": JsDiff.diffWords(property.value, oldProperty.value), "isObject": (property.isObject || oldProperty.isObject) ? true : false }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html index f7ab78b7a0..d7ba57c1af 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html @@ -63,7 +63,7 @@ {{property.label}} - + {{part.value}} {{part.value}} @@ -86,6 +86,7 @@ type="button" button-style="link" label-key="general_close" + shortcut="esc" action="vm.close()"> - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html index 8ca1993dcc..2e88bf709c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html @@ -38,6 +38,7 @@ type="button" button-style="link" label-key="general_close" + shortcut="esc" action="vm.close()"> - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html index d6e3996287..5b946976d7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html @@ -87,6 +87,7 @@ type="button" button-style="link" label-key="general_close" + shortcut="esc" action="vm.close()"> diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html index e97d80648b..e2ae1ab524 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html @@ -84,6 +84,7 @@ type="button" button-style="link" label-key="general_close" + shortcut="esc" action="vm.close()"> - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.html index bc6c8b5761..e39d693b47 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.html @@ -74,6 +74,7 @@ type="button" button-style="link" label-key="general_close" + shortcut="esc" action="vm.close()"> - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js deleted file mode 100644 index 827b2ad4e0..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ /dev/null @@ -1,518 +0,0 @@ -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", - function ($scope, $q, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService, contentResource, mediaResource, memberResource) { - - var tree = null; - var dialogOptions = $scope.model; - $scope.treeReady = false; - $scope.dialogTreeEventHandler = $({}); - $scope.section = dialogOptions.section; - $scope.treeAlias = dialogOptions.treeAlias; - $scope.multiPicker = dialogOptions.multiPicker; - $scope.hideHeader = (typeof dialogOptions.hideHeader) === "boolean" ? dialogOptions.hideHeader : true; - // if you need to load a not initialized tree set this value to false - default is true - $scope.onlyInitialized = dialogOptions.onlyInitialized; - $scope.searchInfo = { - searchFromId: dialogOptions.startNodeId, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - } - - $scope.model.selection = []; - - //Used for toggling an empty-state message - //Some trees can have no items (dictionary & forms email templates) - $scope.hasItems = true; - $scope.emptyStateMessage = dialogOptions.emptyStateMessage; - var node = dialogOptions.currentNode; - - //This is called from ng-init - //it turns out it is called from the angular html : / Have a look at views/common / overlays / contentpicker / contentpicker.html you'll see ng-init. - //this is probably an anti pattern IMO and shouldn't be used - $scope.init = function (contentType) { - - if (contentType === "content") { - $scope.entityType = "Document"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectContent"); - } - } else if (contentType === "member") { - $scope.entityType = "Member"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMember"); - } - } else if (contentType === "media") { - $scope.entityType = "Media"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); - } - } - } - - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - // Allow the entity type to be passed in but defaults to Document for backwards compatibility. - $scope.entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; - - - //min / max values - if (dialogOptions.minNumber) { - dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); - } - if (dialogOptions.maxNumber) { - dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); - } - - if (dialogOptions.section === "member") { - $scope.entityType = "Member"; - } - else if (dialogOptions.section === "media") { - $scope.entityType = "Media"; - } - - // Search and listviews is only working for content, media and member section - var searchableSections = ["content", "media", "member"]; - - $scope.enableSearh = searchableSections.indexOf($scope.section) !== -1; - - //if a alternative startnode is used, we need to check if it is a container - if ($scope.enableSearh && dialogOptions.startNodeId && dialogOptions.startNodeId !== -1 && dialogOptions.startNodeId !== "-1") { - entityResource.getById(dialogOptions.startNodeId, $scope.entityType).then(function(node) { - if (node.metaData.IsContainer) { - openMiniListView(node); - } - initTree(); - }); - } - else { - initTree(); - } - - //Configures filtering - if (dialogOptions.filter) { - - dialogOptions.filterExclude = false; - dialogOptions.filterAdvanced = false; - - //used advanced filtering - if (angular.isFunction(dialogOptions.filter)) { - dialogOptions.filterAdvanced = true; - } - else if (angular.isObject(dialogOptions.filter)) { - dialogOptions.filterAdvanced = true; - } - else { - if (dialogOptions.filter.startsWith("!")) { - dialogOptions.filterExclude = true; - dialogOptions.filterTypes = dialogOptions.filter.substring(1); - } else { - dialogOptions.filterExclude = false; - dialogOptions.filterTypes = dialogOptions.filter; - } - - //used advanced filtering - if (dialogOptions.filter.startsWith("{")) { - dialogOptions.filterAdvanced = true; - //convert to object - dialogOptions.filter = angular.fromJson(dialogOptions.filter); - } - } - - $scope.filter = { - filterAdvanced: dialogOptions.filterAdvanced, - filterExclude: dialogOptions.filterExclude, - filter: dialogOptions.filterTypes - }; - } - - function initTree() { - //create the custom query string param for this tree - $scope.customTreeParams = dialogOptions.startNodeId ? "startNodeId=" + dialogOptions.startNodeId : ""; - $scope.customTreeParams += dialogOptions.customTreeParams ? "&" + dialogOptions.customTreeParams : ""; - $scope.treeReady = true; - } - - function nodeExpandedHandler(ev, args) { - - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - - if (angular.isArray(args.children)) { - - //iterate children - _.each(args.children, function (child) { - - //now we need to look in the already selected search results and - // toggle the check boxes for those ones that are listed - var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; - }); - if (exists) { - child.selected = true; - } - }); - - //check filter - performFiltering(args.children); - } - } - - //gets the tree object when it loads - function treeLoadedHandler(ev, args) { - //args.tree contains children (args.tree.root.children) - $scope.hasItems = args.tree.root.children.length > 0; - - tree = args.tree; - - var nodeHasPath = typeof node !== "undefined" && typeof node.path !== "undefined"; - var startNodeNotDefined = typeof dialogOptions.startNodeId === "undefined" || dialogOptions.startNodeId === "" || dialogOptions.startNodeId === "-1"; - if (startNodeNotDefined && nodeHasPath) { - $scope.dialogTreeEventHandler.syncTree({ path: node.path, activate: false }); - } - - } - - //wires up selection - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if (args.node.metaData.isSearchResult) { - //check if the item selected was a search result from a list view - - //unselect - select(args.node.name, args.node.id); - - //remove it from the list view children - var listView = args.node.parent(); - listView.children = _.reject(listView.children, function (child) { - return child.id == args.node.id; - }); - - //remove it from the custom tracked search result list - $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { - return i.id == args.node.id; - }); - } - else { - eventsService.emit("dialogs.treePickerController.select", args); - - if (args.node.filtered) { - return; - } - - //This is a tree node, so we don't have an entity to pass in, it will need to be looked up - //from the server in this method. - if ($scope.model.select) { - $scope.model.select(args.node) - } else { - select(args.node.name, args.node.id); - //toggle checked state - args.node.selected = args.node.selected === true ? false : true; - } - - } - } - - /** Method used for selecting a node */ - function select(text, id, entity) { - //if we get the root, we just return a constructed entity, no need for server data - if (id < 0) { - - var rootNode = { - alias: null, - icon: "icon-folder", - id: id, - name: text - }; - - if ($scope.multiPicker) { - if (entity) { - multiSelectItem(entity); - } else { - multiSelectItem(rootNode); - } - } - else { - $scope.model.selection.push(rootNode); - $scope.model.submit($scope.model); - } - } - else { - - if ($scope.multiPicker) { - - if (entity) { - multiSelectItem(entity); - } else { - //otherwise we have to get it from the server - entityResource.getById(id, $scope.entityType).then(function (ent) { - multiSelectItem(ent); - }); - } - - } - - else { - - $scope.hideSearch(); - - //if an entity has been passed in, use it - if (entity) { - $scope.model.selection.push(entity); - $scope.model.submit($scope.model); - } else { - //otherwise we have to get it from the server - entityResource.getById(id, $scope.entityType).then(function (ent) { - $scope.model.selection.push(ent); - $scope.model.submit($scope.model); - }); - } - } - } - } - - function multiSelectItem(item) { - - var found = false; - var foundIndex = 0; - - if ($scope.model.selection.length > 0) { - for (var i = 0; $scope.model.selection.length > i; i++) { - var selectedItem = $scope.model.selection[i]; - if (selectedItem.id === item.id) { - found = true; - foundIndex = i; - } - } - } - - if (found) { - $scope.model.selection.splice(foundIndex, 1); - } else { - $scope.model.selection.push(item); - } - - } - - function performFiltering(nodes) { - - if (!dialogOptions.filter) { - return; - } - - //remove any list view search nodes from being filtered since these are special nodes that always must - // be allowed to be clicked on - nodes = _.filter(nodes, function (n) { - return !angular.isObject(n.metaData.listViewNode); - }); - - if (dialogOptions.filterAdvanced) { - - //filter either based on a method or an object - var filtered = angular.isFunction(dialogOptions.filter) - ? _.filter(nodes, dialogOptions.filter) - : _.where(nodes, dialogOptions.filter); - - angular.forEach(filtered, function (value, key) { - value.filtered = true; - if (dialogOptions.filterCssClass) { - if (!value.cssClasses) { - value.cssClasses = []; - } - value.cssClasses.push(dialogOptions.filterCssClass); - } - }); - } else { - var a = dialogOptions.filterTypes.toLowerCase().replace(/\s/g, '').split(','); - angular.forEach(nodes, function (value, key) { - - var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; - - if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { - value.filtered = true; - - if (dialogOptions.filterCssClass) { - if (!value.cssClasses) { - value.cssClasses = []; - } - value.cssClasses.push(dialogOptions.filterCssClass); - } - } - }); - } - } - - $scope.multiSubmit = function (result) { - entityResource.getByIds(result, $scope.entityType).then(function (ents) { - $scope.submit(ents); - }); - }; - - /** method to select a search result */ - $scope.selectResult = function (evt, result) { - - if (result.filtered) { - return; - } - - result.selected = result.selected === true ? false : true; - - //since result = an entity, we'll pass it in so we don't have to go back to the server - select(result.name, result.id, result); - - //add/remove to our custom tracked list of selected search results - if (result.selected) { - $scope.searchInfo.selectedSearchResults.push(result); - } - else { - $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { - return i.id == result.id; - }); - } - - //ensure the tree node in the tree is checked/unchecked if it already exists there - if (tree) { - var found = treeService.getDescendantNode(tree.root, result.id); - if (found) { - found.selected = result.selected; - } - } - - }; - - $scope.hideSearch = function () { - - //Traverse the entire displayed tree and update each node to sync with the selected search results - if (tree) { - - //we need to ensure that any currently displayed nodes that get selected - // from the search get updated to have a check box! - function checkChildren(children) { - _.each(children, function (child) { - //check if the id is in the selection, if so ensure it's flagged as selected - var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; - }); - //if the curr node exists in selected search results, ensure it's checked - if (exists) { - child.selected = true; - } - //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result - else if (child.metaData.isSearchResult) { - //if this tree node is under a list view it means that the node was added - // to the tree dynamically under the list view that was searched, so we actually want to remove - // it all together from the tree - var listView = child.parent(); - listView.children = _.reject(listView.children, function (c) { - return c.id == child.id; - }); - } - - //check if the current node is a list view and if so, check if there's any new results - // that need to be added as child nodes to it based on search results selected - if (child.metaData.isContainer) { - - child.cssClasses = _.reject(child.cssClasses, function (c) { - return c === 'tree-node-slide-up-hide-active'; - }); - - var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { - return i.parentId == child.id; - }); - _.each(listViewResults, function (item) { - var childExists = _.find(child.children, function (c) { - return c.id == item.id; - }); - if (!childExists) { - var parent = child; - child.children.unshift({ - id: item.id, - name: item.name, - cssClass: "icon umb-tree-icon sprTree " + item.icon, - level: child.level + 1, - metaData: { - isSearchResult: true - }, - hasChildren: false, - parent: function () { - return parent; - } - }); - } - }); - } - - //recurse - if (child.children && child.children.length > 0) { - checkChildren(child.children); - } - }); - } - checkChildren(tree.root.children); - } - - - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = dialogOptions.startNodeId; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - $scope.onSearchResults = function (results) { - - //filter all items - this will mark an item as filtered - performFiltering(results); - - //now actually remove all filtered items so they are not even displayed - results = _.filter(results, function (item) { - return !item.filtered; - }); - - $scope.searchInfo.results = results; - - //sync with the curr selected results - _.each($scope.searchInfo.results, function (result) { - var exists = _.find($scope.model.selection, function (selectedId) { - return result.id == selectedId; - }); - if (exists) { - result.selected = true; - } - }); - - $scope.searchInfo.showSearch = true; - }; - - $scope.dialogTreeEventHandler.bind("treeLoaded", treeLoadedHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeLoaded", treeLoadedHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - - $scope.selectListViewNode = function (node) { - select(node.name, node.id); - //toggle checked state - node.selected = node.selected === true ? false : true; - }; - - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; - - function openMiniListView(node) { - $scope.miniListView = node; - } - - }); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index c5167ba964..2ce49880d7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -97,14 +97,7 @@ - - @@ -161,7 +154,7 @@
- +
@@ -199,7 +192,7 @@
- +
@@ -230,7 +223,7 @@
- +
@@ -268,4 +261,4 @@
- \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html index 9d1da590ab..c5b4f69cef 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html @@ -19,7 +19,7 @@
+ on-init="onTreeInit()">
@@ -44,6 +44,9 @@ + +
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html index 3810630fa9..054681d7f1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html @@ -16,17 +16,20 @@ add-ellipsis={{defaultButton.addEllipsis}}>
- + - + + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html index 68d4adef5a..03813c8518 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html @@ -11,6 +11,7 @@ {{vm.buttonLabel}} + @@ -18,6 +19,7 @@ {{vm.buttonLabel}} + @@ -25,6 +27,7 @@ {{vm.buttonLabel}} + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle.html index bc5c114bb6..c8039448fd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle.html @@ -1,4 +1,4 @@ - - +
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html index 56c5be6fb2..d13474d221 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html @@ -24,7 +24,7 @@ title="{{ngModel}}" focus-when="{{!locked}}" umb-select-when="{{!locked}}" - on-blur="lock()" /> + ng-blur="lock()" /> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index 2591789a36..360c6b2bb7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -24,7 +24,7 @@ - .{{item.extension}} + .{{item.extension}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html index aebc559a26..317b0f9684 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html @@ -63,10 +63,10 @@
+ ng-class="{'umb-table-row--selected':child.selected, 'not-allowed':!child.allowed}">
-   +  
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html index 9d2997625e..c99655b7fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html index f205dffa57..dea532b7ea 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html @@ -1,4 +1,5 @@ -
+
+
0 && userData.startContentIds.indexOf(-1) == -1; }); - var node = $scope.currentNode; + $scope.source = _.clone($scope.currentNode); function treeLoadedHandler(args) { - if (node && node.path) { - $scope.dialogTreeApi.syncTree({ path: node.path, activate: false }); + if ($scope.source && $scope.source.path) { + $scope.dialogTreeApi.syncTree({ path: $scope.source.path, activate: false }); } } @@ -107,7 +107,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", $scope.busy = true; $scope.error = false; - contentResource.copy({ parentId: $scope.target.id, id: node.id, relateToOriginal: $scope.relateToOriginal, recursive: $scope.recursive }) + contentResource.copy({ parentId: $scope.target.id, id: $scope.source.id, relateToOriginal: $scope.relateToOriginal, recursive: $scope.recursive }) .then(function (path) { $scope.error = false; $scope.success = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index 6b1021b663..9fbf342435 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -42,14 +42,21 @@ function contentCreateController($scope, } function createOrSelectBlueprintIfAny(docType) { - var blueprintIds = _.keys(docType.blueprints || {}); + // map the blueprints into a collection that's sortable in the view + var blueprints = _.map(_.pairs(docType.blueprints || {}), function (pair) { + return { + id: pair[0], + name: pair[1] + }; + }); $scope.docType = docType; - if (blueprintIds.length) { + if (blueprints.length) { if (blueprintConfig.skipSelect) { - createFromBlueprint(blueprintIds[0]); + createFromBlueprint(blueprints[0].id); } else { $scope.selectContentType = false; $scope.selectBlueprint = true; + $scope.selectableBlueprints = blueprints; } } else { createBlank(docType); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js index d4be18cf05..92e02d0d14 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js @@ -34,22 +34,25 @@ function ContentDeleteController($scope, $timeout, contentResource, treeService, toggleDeleting(false); if (rootNode) { - $timeout(function () { - //ensure the recycle bin has child nodes now - var recycleBin = treeService.getDescendantNode(rootNode, -20); - if (recycleBin) { - //TODO: This seems to return a rejection and we end up with "Possibly unhanded rejection" - treeService.syncTree({ node: recycleBin, path: treeService.getPath(recycleBin), forceReload: true }); + //ensure the recycle bin has child nodes now + var recycleBin = treeService.getDescendantNode(rootNode, -20); + if (recycleBin) { + recycleBin.hasChildren = true; + //reload the recycle bin if it's already expanded so the deleted item is shown + if (recycleBin.expanded) { + treeService.loadNodeChildren({ node: recycleBin, section: "content" }); } - }, 500); + } } - + //if the current edited item is the same one as we're deleting, we need to navigate elsewhere if (editorState.current && editorState.current.id == $scope.currentNode.id) { //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent var location = "/content"; - if ($scope.currentNode.parentId.toString() !== "-1") + if ($scope.currentNode.parentId.toString() === "-20") + location = "/content/content/recyclebin"; + else if ($scope.currentNode.parentId.toString() !== "-1") location = "/content/content/edit/" + $scope.currentNode.parentId; $location.path(location); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js index a593bbad24..5dceff2571 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js @@ -22,11 +22,11 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; }); - var node = $scope.currentNode; + $scope.source = _.clone($scope.currentNode); function treeLoadedHandler(args) { - if (node && node.path) { - $scope.dialogTreeApi.syncTree({ path: node.path, activate: false }); + if ($scope.source && $scope.source.path) { + $scope.dialogTreeApi.syncTree({ path: $scope.source.path, activate: false }); } } @@ -84,7 +84,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", $scope.busy = true; $scope.error = false; - contentResource.move({ parentId: $scope.target.id, id: node.id }) + contentResource.move({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { $scope.error = false; $scope.success = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.notify.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.notify.controller.js index 4c446b11e0..9f59fba0ea 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.notify.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.notify.controller.js @@ -3,7 +3,8 @@ $scope, contentResource, navigationService, - angularHelper) { + angularHelper, + localizationService) { var vm = this; var currentForm; vm.notifyOptions = []; @@ -11,7 +12,8 @@ vm.cancel = cancel; vm.message = { name: $scope.currentNode.name - };; + }; + vm.labels = {}; function onInit() { vm.loading = true; contentResource.getNotifySettingsById($scope.currentNode.id).then(function (options) { @@ -19,6 +21,9 @@ vm.loading = false; vm.notifyOptions = options; }); + localizationService.localize("notifications_editNotifications", [$scope.currentNode.name]).then(function(value) { + vm.labels.headline = value; + }); } function cancel() { navigationService.hideMenu(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js new file mode 100644 index 0000000000..8d80f308ab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -0,0 +1,255 @@ +(function () { + "use strict"; + + function ContentProtectController($scope, $q, contentResource, memberResource, memberGroupResource, navigationService, localizationService, editorService) { + + var vm = this; + var id = $scope.currentNode.id; + + vm.loading = false; + vm.buttonState = "init"; + + vm.isValid = isValid; + vm.next = next; + vm.save = save; + vm.close = close; + vm.toggle = toggle; + vm.pickLoginPage = pickLoginPage; + vm.pickErrorPage = pickErrorPage; + vm.pickGroup = pickGroup; + vm.removeGroup = removeGroup; + vm.pickMember = pickMember; + vm.removeMember = removeMember; + vm.removeProtection = removeProtection; + vm.removeProtectionConfirm = removeProtectionConfirm; + + vm.type = null; + vm.step = null; + + function onInit() { + vm.loading = true; + + // get the current public access protection + contentResource.getPublicAccess(id).then(function (publicAccess) { + vm.loading = false; + + // init the current settings for public access (if any) + vm.loginPage = publicAccess.loginPage; + vm.errorPage = publicAccess.errorPage; + vm.groups = publicAccess.groups || []; + vm.members = publicAccess.members || []; + vm.canRemove = true; + + if (vm.members.length) { + vm.type = "member"; + next(); + } + else if (vm.groups.length) { + vm.type = "group"; + next(); + } + else { + vm.canRemove = false; + } + }); + } + + function next() { + if (vm.type === "group") { + vm.loading = true; + // get all existing member groups for lookup upon selection + // NOTE: if/when member groups support infinite editing, we can't rely on using a cached lookup list of valid groups anymore + memberGroupResource.getGroups().then(function (groups) { + vm.step = vm.type; + vm.allGroups = groups; + vm.hasGroups = groups.length > 0; + vm.loading = false; + }); + } + else { + vm.step = vm.type; + } + } + + function isValid() { + if (!vm.type) { + return false; + } + if (!vm.protectForm.$valid) { + return false; + } + if (!vm.loginPage || !vm.errorPage) { + return false; + } + if (vm.type === "group") { + return vm.groups && vm.groups.length > 0; + } + if (vm.type === "member") { + return vm.members && vm.members.length > 0; + } + return true; + } + + function save() { + vm.buttonState = "busy"; + var groups = _.map(vm.groups, function (group) { return group.name; }); + var usernames = _.map(vm.members, function (member) { return member.username; }); + contentResource.updatePublicAccess(id, groups, usernames, vm.loginPage.id, vm.errorPage.id).then( + function () { + localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { + vm.success = { + message: value + }; + }); + navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); + }, function (error) { + vm.error = error; + vm.buttonState = "error"; + } + ); + } + + function close() { + // ensure that we haven't set a locked state on the dialog before closing it + navigationService.allowHideDialog(true); + navigationService.hideDialog(); + } + + function toggle(group) { + group.selected = !group.selected; + } + + function pickGroup() { + navigationService.allowHideDialog(false); + editorService.memberGroupPicker({ + multiPicker: true, + submit: function(model) { + var selectedGroupIds = model.selectedMemberGroups + ? model.selectedMemberGroups + : [model.selectedMemberGroup]; + _.each(selectedGroupIds, + function (groupId) { + // find the group in the lookup list and add it if it isn't already + var group = _.find(vm.allGroups, function(g) { return g.id === parseInt(groupId); }); + if (group && !_.find(vm.groups, function (g) { return g.id === group.id })) { + vm.groups.push(group); + } + }); + editorService.close(); + navigationService.allowHideDialog(true); + }, + close: function() { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); + } + + function removeGroup(group) { + vm.groups = _.reject(vm.groups, function(g) { return g.id === group.id }); + } + + function pickMember() { + navigationService.allowHideDialog(false); + // TODO: once editorService has a memberPicker method, use that instead + editorService.treePicker({ + multiPicker: true, + entityType: "Member", + section: "member", + treeAlias: "member", + filter: function (i) { + return i.metaData.isContainer; + }, + filterCssClass: "not-allowed", + submit: function (model) { + if (model.selection && model.selection.length) { + var promises = []; + // get the selected member usernames + _.each(model.selection, + function (member) { + // TODO: + // as-is we need to fetch all the picked members one at a time to get their usernames. + // when editorService has a memberPicker method, see if this can't be avoided - otherwise + // add a memberResource.getByKeys() method to do all this in one request + promises.push( + memberResource.getByKey(member.key).then(function(newMember) { + if (!_.find(vm.members, function (currentMember) { return currentMember.username === newMember.username })) { + vm.members.push(newMember); + } + }) + ); + }); + editorService.close(); + navigationService.allowHideDialog(true); + // wait for all the member lookups to complete + vm.loading = true; + $q.all(promises).then(function() { + vm.loading = false; + }); + } + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); + } + + function removeMember(member) { + vm.members = _.without(vm.members, member); + } + + function pickLoginPage() { + pickPage(vm.loginPage); + } + + function pickErrorPage() { + pickPage(vm.errorPage); + } + + function pickPage(page) { + navigationService.allowHideDialog(false); + editorService.contentPicker({ + submit: function (model) { + if (page === vm.loginPage) { + vm.loginPage = model.selection[0]; + } + else { + vm.errorPage = model.selection[0]; + } + editorService.close(); + navigationService.allowHideDialog(true); + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); + } + + function removeProtection() { + vm.removing = true; + } + + function removeProtectionConfirm() { + vm.buttonState = "busy"; + contentResource.removePublicAccess(id).then( + function () { + localizationService.localize("publicAccess_paIsRemoved", [$scope.currentNode.name]).then(function(value) { + vm.success = { + message: value + }; + }); + navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); + }, function (error) { + vm.error = error; + vm.buttonState = "error"; + } + ); + } + + onInit(); + } + + angular.module("umbraco").controller("Umbraco.Editors.Content.ProtectController", ContentProtectController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.restore.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.restore.controller.js index 53651a83df..24457f6980 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.restore.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.restore.controller.js @@ -1,62 +1,130 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController", - function ($scope, relationResource, contentResource, navigationService, appState, treeService, localizationService) { + function ($scope, relationResource, contentResource, entityResource, navigationService, appState, treeService, userService) { - var node = $scope.currentNode; + $scope.source = _.clone($scope.currentNode); - $scope.error = null; - $scope.success = false; + $scope.error = null; + $scope.loading = true; + $scope.moving = false; + $scope.success = false; - relationResource.getByChildId(node.id, "relateParentDocumentOnDelete").then(function (data) { + $scope.dialogTreeApi = {}; + $scope.searchInfo = { + showSearch: false, + results: [], + selectedSearchResults: [] + } + $scope.treeModel = { + hideHeader: false + } + userService.getCurrentUser().then(function (userData) { + $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; + }); - if (data.length == 0) { - $scope.success = false; - $scope.error = { - errorMsg: localizationService.localize('recycleBin_itemCannotBeRestored'), - data: { - Message: localizationService.localize('recycleBin_noRestoreRelation') - } - } + function nodeSelectHandler(args) { + + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); + } + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + + } + + function nodeExpandedHandler(args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + } + + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.results = []; + } + + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { event: evt, node: result }); + }; + + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; + + $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); + $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); + } + + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { node: node }); + }; + + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + + function openMiniListView(node) { + $scope.miniListView = node; + } + + relationResource.getByChildId($scope.source.id, "relateParentDocumentOnDelete").then(function (data) { + $scope.loading = false; + + if (!data.length) { + $scope.moving = true; return; } $scope.relation = data[0]; - if ($scope.relation.parentId == -1) { + if ($scope.relation.parentId === -1) { $scope.target = { id: -1, name: "Root" }; - } else { - contentResource.getById($scope.relation.parentId).then(function (data) { - $scope.target = data; + } else { + $scope.loading = true; - // make sure the target item isn't in the recycle bin - if($scope.target.path.indexOf("-20") !== -1) { - $scope.error = { - errorMsg: localizationService.localize('recycleBin_itemCannotBeRestored'), - data: { - Message: localizationService.localize('recycleBin_restoreUnderRecycled').then(function (value) { - value.replace('%0%', $scope.target.name); - }) - } - }; - $scope.success = false; - } + entityResource.getById($scope.relation.parentId, "Document").then(function (data) { + $scope.loading = false; + $scope.target = data; + // make sure the target item isn't in the recycle bin + if ($scope.target.path.indexOf("-20") !== -1) { + $scope.moving = true; + $scope.target = null; + } }, function (err) { - $scope.success = false; - $scope.error = err; + $scope.loading = false; + $scope.error = err; }); } }, function (err) { - $scope.success = false; - $scope.error = err; + $scope.loading = false; + $scope.error = err; }); $scope.restore = function () { - // this code was copied from `content.move.controller.js` - contentResource.move({ parentId: $scope.target.id, id: node.id }) + $scope.loading = true; + + // this code was copied from `content.move.controller.js` + contentResource.move({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { + $scope.loading = false; $scope.success = true; //first we need to remove the node that launched the dialog @@ -77,8 +145,13 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController" }); }, function (err) { - $scope.success = false; - $scope.error = err; + $scope.loading = false; + $scope.error = err; }); - }; + }; + + $scope.close = function () { + navigationService.hideDialog(); + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js index afab478fc4..a8f87ce2c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js @@ -63,6 +63,8 @@ vm.labels.permissionsSetForGroup = value; }); setViewSate("managePermissions"); + // hide dropdown + vm.groupsDropdownOpen = false; } function assignGroupPermissions(group) { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index 14ea54cf42..0ebe577ed8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -11,14 +11,17 @@
- {{currentNode.name}} was copied to + {{source.name}} was copied to + was copied to {{target.name}}

- Choose where to copy {{currentNode.name}} to in the tree structure below + Choose where to copy + {{source.name}} + to in the tree structure below

diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index e76d17caa8..94299f6a54 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -25,13 +25,13 @@ -
    +
      -
    • - +
    • + - {{value}} + {{blueprint.name}}
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/content/move.html b/src/Umbraco.Web.UI.Client/src/views/content/move.html index c713bd90e7..2c40a61d18 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/move.html @@ -11,14 +11,16 @@
      - {{currentNode.name}} was moved underneath {{target.name}} + {{source.name}} + was moved to + {{target.name}}
      - +

      Choose where to move - {{currentNode.name}} + {{source.name}} to in the tree structure below

      diff --git a/src/Umbraco.Web.UI.Client/src/views/content/notify.html b/src/Umbraco.Web.UI.Client/src/views/content/notify.html index cb12d92f08..8e2f860661 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/notify.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/notify.html @@ -13,12 +13,13 @@
- {{currentNode.name}} + {{currentNode.name}}
+
-
+
-
Set your notification for {{ currentNode.name }}
+
- diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js index a99da13811..8d21234aee 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js @@ -1,16 +1,17 @@ (function () { "use strict"; - + function SaveContentController($scope, localizationService) { var vm = this; vm.loading = true; vm.hasPristineVariants = false; + vm.isNew = true; vm.changeSelection = changeSelection; vm.dirtyVariantFilter = dirtyVariantFilter; vm.pristineVariantFilter = pristineVariantFilter; - + function changeSelection(variant) { var firstSelected = _.find(vm.variants, function (v) { return v.save; @@ -30,8 +31,28 @@ return !(dirtyVariantFilter(variant)); } - function onInit() { + function hasAnyData(variant) { + var result = variant.isDirty != null || (variant.name != null && variant.name.length > 0); + if(result) return true; + + for (var t=0; t < variant.tabs.length; t++){ + for (var p=0; p < variant.tabs[t].properties.length; p++){ + + var property = variant.tabs[t].properties[p]; + + if(property.culture == null) continue; + + result = result || (property.value != null && property.value.length > 0); + + if(result) return true; + } + } + + return result; + } + + function onInit() { vm.variants = $scope.model.variants; if(!$scope.model.title) { @@ -42,6 +63,13 @@ vm.hasPristineVariants = false; + _.each(vm.variants, + function (variant) { + if(variant.state !== "NotCreated"){ + vm.isNew = false; + } + }); + _.each(vm.variants, function (variant) { variant.compositeId = variant.language.culture + "_" + (variant.segment ? variant.segment : ""); @@ -51,6 +79,10 @@ if (!vm.hasPristineVariants) { vm.hasPristineVariants = pristineVariantFilter(variant); } + + if(vm.isNew && hasAnyData(variant)){ + variant.save = true; + } }); if (vm.variants.length !== 0) { @@ -88,5 +120,5 @@ } angular.module("umbraco").controller("Umbraco.Overlays.SaveContentController", SaveContentController); - + })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html index 6f4aef4e84..c4dcd5b767 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html @@ -1,72 +1,120 @@
-
-

-
+
-
-
- -
- -
- - -
- -
- -
-
{{saveVariantSelectorForm.saveVariantSelector.errorMsg}}
-
- -
-
{{notification.message}}
-
- -
-
- -
+
+
+

+
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+
{{saveVariantSelectorForm.saveVariantSelector.errorMsg}}
+
+ +
+
{{notification.message}}
+
+ +
+
+ +
+
+
-
-
-
-

+
+
+

-
-
-
- {{ variant.language.name }} - * -
+
-
- -
+
+ +
+ +
+ -
-
{{notification.message}}
+
+ +
+ +
+
{{saveVariantSelectorForm.saveVariantSelector.errorMsg}}
+
+ +
+
{{notification.message}}
+
+ +
+
+ + +
+
+
+ +
+
+

+
+ +
+
+
+ {{ variant.language.name }} + * +
+ +
+ +
+ +
+
{{notification.message}}
+
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html new file mode 100644 index 0000000000..ae4a15e8c1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -0,0 +1,186 @@ +
+ +
+ + +
+
+
{{vm.error.errorMsg}}
+
{{vm.error.data.message}}
+
+
+ + + + +
+

+ Choose how to restrict access to this page +

+ + +
+ + + +
+ +
+ + + +
+
+
+ +
+
+

Select the members that should have access to this page

+ + + + + Add + + +
+ +
+

You need to create a member group before you can use group based authentication

+
+ +
+

Select the groups that should have access to this page

+ + + + + Add + + +
+ +
+

Select the pages that contain login form and error messages

+ +
+
+ + + Add + + + +
+
+
+
+ + + Add + + + +
+
+
+
+
+ +
+

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

+
+ + + +
+ + +
+ + +
+
+ + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/restore.html b/src/Umbraco.Web.UI.Client/src/views/content/restore.html index e99e2eb251..ead6ed91ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/restore.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/restore.html @@ -1,26 +1,93 @@
-
- +
+ + + -

- Restore {{currentNode.name}} under {{target.name}}? -

+
+
+
{{error.errorMsg}}
+
{{error.data.Message}}
+
+
-
-
{{error.errorMsg}}
-
{{error.data.Message}}
-
+
+
+ {{source.name}} + was restored under + was moved underneath + {{target.name}} +
+ +
-
-

{{currentNode.name}} was moved underneath {{target.name}}

- -
+
- +

+ Restore {{source.name}} under {{target.name}}? +

+ +
+ +
+
+
+
Cannot automatically restore this item
+
There is no location where this item can be automatically restored. You can move the item manually using the tree below.
+
+
+ +
+ + + +
+ + + + +
+ + +
+
+ + + + +
+ +
- + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/rights.html b/src/Umbraco.Web.UI.Client/src/views/content/rights.html index afa932e606..35f3d34260 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/rights.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/rights.html @@ -27,20 +27,24 @@

diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js index cb8e091e50..3140af9d6c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js @@ -7,28 +7,47 @@ * The controller for the content editor */ function ContentBlueprintEditController($scope, $routeParams, contentResource) { - var excludedProps = ["_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template"]; + function getScaffold() { + return contentResource.getScaffold(-1, $routeParams.doctype) + .then(function (scaffold) { + return initialize(scaffold); + }); + } - function getScaffold() { - return contentResource.getScaffold(-1, $routeParams.doctype) - .then(function (scaffold) { - var lastTab = scaffold.tabs[scaffold.tabs.length - 1]; - lastTab.properties = _.filter(lastTab.properties, - function(p) { - return excludedProps.indexOf(p.alias) === -1; - }); - scaffold.allowPreview = false; - scaffold.allowedActions = ["A", "S", "C"]; + function getBlueprintById(id) { + return contentResource.getBlueprintById(id).then(function (blueprint) { + return initialize(blueprint); + }); + } - return scaffold; - }); - } + function initialize(content) { + if (content.apps && content.apps.length) { + var contentApp = _.find(content.apps, function (app) { + return app.alias === "umbContent"; + }); + content.apps = [contentApp]; + } + content.allowPreview = false; + content.allowedActions = ["A", "S", "C"]; + return content; + } + + $scope.contentId = $routeParams.id; + $scope.isNew = $routeParams.id === "-1"; + $scope.saveMethod = contentResource.saveBlueprint; + $scope.getMethod = getBlueprintById; + $scope.getScaffoldMethod = getScaffold; + + //load the default culture selected in the main tree if any + $scope.culture = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + + //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. + //This is so we can listen to changes on the cculture parameter since that will not cause a route change + // and then we can pass in the updated culture to the editor + $scope.$on('$routeUpdate', function (event, next) { + $scope.culture = next.params.cculture ? next.params.cculture : $routeParams.mculture; + }); - $scope.contentId = $routeParams.id; - $scope.isNew = $routeParams.id === "-1"; - $scope.saveMethod = contentResource.saveBlueprint; - $scope.getMethod = contentResource.getBlueprintById; - $scope.getScaffoldMethod = getScaffold; } angular.module("umbraco").controller("Umbraco.Editors.ContentBlueprint.EditController", ContentBlueprintEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html index 5d27978072..191dd4db5c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html @@ -4,6 +4,7 @@ get-method="getMethod" get-scaffold-method="getScaffoldMethod" is-new="isNew" - tree-alias="contentblueprints"> + tree-alias="contentblueprints" + culture="culture">
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html index 24b32815e5..837c24cb0c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/content/redirecturls.html @@ -54,9 +54,10 @@
-
Original URL
+
Culture
+
Original URL
-
Redirected To
+
Redirected To
@@ -64,8 +65,10 @@
- -
+
+ {{redirectUrl.culture ||'*'}} +
+ @@ -73,7 +76,7 @@
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html index 72cc3ddb06..0478e6ba3c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html @@ -19,7 +19,7 @@
-
+
@@ -65,7 +65,7 @@
- +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardintro.html deleted file mode 100644 index 5b4bc988c0..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardintro.html +++ /dev/null @@ -1,14 +0,0 @@ -

Start here

-

This section contains the tools to add advanced features to your Umbraco site

-

From here you can explore and install packages, create macros, add data types, and much more. Start by exploring the below links or videos.

- -

Find out more:

- - diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.controller.js new file mode 100644 index 0000000000..a5e4125742 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.controller.js @@ -0,0 +1,187 @@ +function ExamineManagementController($scope, umbRequestHelper, $http, $q, $timeout) { + + var vm = this; + + vm.indexerDetails = []; + vm.searcherDetails = []; + vm.loading = true; + vm.viewState = "list"; + vm.selectedIndex = null; + vm.selectedSearcher = null; + vm.searchResults = null; + + vm.showSearchResultDialog = showSearchResultDialog; + vm.showIndexInfo = showIndexInfo; + vm.showSearcherInfo = showSearcherInfo; + vm.search = search; + vm.toggle = toggle; + vm.rebuildIndex = rebuildIndex; + vm.setViewState = setViewState; + vm.nextSearchResultPage = nextSearchResultPage; + vm.prevSearchResultPage = prevSearchResultPage; + vm.goToPageSearchResultPage = goToPageSearchResultPage; + + vm.infoOverlay = null; + + function showSearchResultDialog(values) { + if (vm.searchResults) { + vm.searchResults.overlay = { + title: "Field values", + searchResultValues: values, + view: "views/dashboard/settings/examinemanagementresults.html", + close: function () { + vm.searchResults.overlay = null; + } + }; + } + } + + function nextSearchResultPage(pageNumber) { + search(vm.selectedIndex ? vm.selectedIndex : vm.selectedSearcher, null, pageNumber); + } + function prevSearchResultPage(pageNumber) { + search(vm.selectedIndex ? vm.selectedIndex : vm.selectedSearcher, null, pageNumber); + } + function goToPageSearchResultPage(pageNumber) { + search(vm.selectedIndex ? vm.selectedIndex : vm.selectedSearcher, null, pageNumber); + } + + function setViewState(state) { + vm.searchResults = null; + vm.viewState = state; + } + + function showIndexInfo(index) { + vm.selectedIndex = index; + setViewState("index-details"); + } + + function showSearcherInfo(searcher) { + vm.selectedSearcher = searcher; + setViewState("searcher-details"); + } + + function checkProcessing(index, checkActionName) { + umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", + checkActionName, + { indexName: index.name })), + 'Failed to check index processing') + .then(function(data) { + + if (data !== null && data !== "null") { + + //copy all resulting properties + for (var k in data) { + index[k] = data[k]; + } + index.isProcessing = false; + } else { + $timeout(() => { + //don't continue if we've tried 100 times + if (index.processingAttempts < 100) { + checkProcessing(index, checkActionName); + //add an attempt + index.processingAttempts++; + } else { + //we've exceeded 100 attempts, stop processing + index.isProcessing = false; + } + }, + 1000); + } + }); + } + + function search(searcher, e, pageNumber) { + + //deal with accepting pressing the enter key + if (e && e.keyCode !== 13) { + return; + } + + if (!searcher) { + throw "searcher parameter is required"; + } + + searcher.isProcessing = true; + + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", + "GetSearchResults", + { + searcherName: searcher.name, + query: encodeURIComponent(vm.searchText), + pageIndex: pageNumber ? (pageNumber - 1) : 0 + })), + 'Failed to search') + .then(searchResults => { + searcher.isProcessing = false; + vm.searchResults = searchResults + vm.searchResults.pageNumber = pageNumber ? pageNumber : 1; + //20 is page size + vm.searchResults.totalPages = Math.ceil(vm.searchResults.totalRecords / 20); + }); + } + + function toggle(provider, propName) { + if (provider[propName] !== undefined) { + provider[propName] = !provider[propName]; + } else { + provider[propName] = true; + } + } + + function rebuildIndex(index) { + if (confirm("This will cause the index to be rebuilt. " + + "Depending on how much content there is in your site this could take a while. " + + "It is not recommended to rebuild an index during times of high website traffic " + + "or when editors are editing content.")) { + + index.isProcessing = true; + index.processingAttempts = 0; + + umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", + "PostRebuildIndex", + { indexName: index.name })), + 'Failed to rebuild index') + .then(function() { + + //rebuilding has started, nothing is returned accept a 200 status code. + //lets poll to see if it is done. + $timeout(() => { checkProcessing(index, "PostCheckRebuildIndex"), 1000 }); + + }); + } + } + + function init() { + //go get the data + + //combine two promises and execute when they are both done + $q.all([ + + //get the indexer details + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetIndexerDetails")), + 'Failed to retrieve indexer details') + .then(function (data) { + vm.indexerDetails = data; + }), + + //get the searcher details + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetSearcherDetails")), + 'Failed to retrieve searcher details') + .then(data => { + vm.searcherDetails = data; + }) + ]) + .then(() => { vm.loading = false }); + } + + init(); +} + +angular.module("umbraco").controller("Umbraco.Dashboard.ExamineManagementController", ExamineManagementController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html index b5f1fd12ed..5f226771e9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html @@ -1,286 +1,411 @@ -
+
- - -

Examine Management

-
-
+
+ + +

Examine Management

+
+
+
-
+
-
+
+
-
+
-
-
Indexers
-
+
+
Indexers
+
-
-
-
-
Manage Examine's indexes
-
Allows you to view the details of each index and provides some tools for managing the indexes
-
- -
- -
- +
+
+
+
Manage Examine's indexes
+
Allows you to view the details of each index and provides some tools for managing the indexes
-
-
-
- {{indexer.name}} -
- - {{indexer.name}} - -
- The index cannot be read and will need to be rebuilt +
+ +
+ +
+ + -
-
- - -
-
- -
-
    -
  • - Index info & tools - -
    -
    -
    - -
    - -
    - -
    - The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation -
    -
    - - - - - - - - - - -
     
    Documents in index{{indexer.documentCount}}
    Fields in index{{indexer.fieldCount}}
    -
    -
  • -
  • - Node types - - - - - - - - - - - - - -
    Include node types{{indexer.indexCriteria.IncludeNodeTypes | json}}
    Exclude node types{{indexer.indexCriteria.ExcludeNodeTypes | json}}
    Parent node id{{indexer.indexCriteria.ParentNodeId}}
    -
  • -
  • - System fields - - - - - - - - - - - - - - - - -
     
    NameEnable sortingType
    {{field.Name}}{{field.EnableSorting}}{{field.Type}}
    -
  • -
  • - User fields - - - - - - - - - - - - - - - - -
     
    NameEnable sortingType
    {{field.Name}}{{field.EnableSorting}}{{field.Type}}
    -
  • -
  • - Provider properties - - - - - - -
     
    {{key}}{{val}}
    -
  • -
-
-
- -
-
-
+ +
+ +
+ +
+ +
+
Searchers
+
+ +
+
+
+
Configured Searchers
+
Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher)
+
+ +
+ +
+ +
+ + +
+
+
+
-
+
-
+ + + ← Back to overview + + -
-
Searchers
-
+
-
-
-
-
Search indexes
-
Allows you to search the indexes and view the searcher properties
+
+ +
+
{{ vm.selectedSearcher.name }}
-
+
-
- -
- -
-
- - {{searcher.name}} - + + +
+ +
+
Search
+
Search the index and view the results
- -
-
    -
  • - Search tools +
    -
    - Hide search results +
    -
    - -
    +
    +
    + - +
    -
    +
    -
    -
    +
    + +
    + +
    + +
    +
    + +
    + + + + ← Back to overview + + + + +
    + +
    + +
    +
    {{ vm.selectedIndex.name }}
    +
    + +
    + + + +
    + +
    +
    Health status
    +
    The health status of the index and if it can be read
    +
    + +
    + +
    + +
    + +
    + +
    +
    {{vm.selectedIndex.healthStatus}}
    +
    + The index cannot be read and will need to be rebuilt +
    + +
    +
    + +
    + +
    + + + +
    + +
    +
    Search
    +
    Search the index and view the results
    +
    + +
    + +
    + +
    +
    + + + - - - - - - - - - - - - - - - -
    ScoreIdValues
    {{result.Score}}{{result.Id}} - - {{key}}: - {{val}} - -
    +
    +
    + + + + + + + + + + + + + + + + +
    ScoreIdName
    {{result.score}}{{result.id}} + {{result.values['nodeName']}}  + + ({{result.fieldCount}} fields) + +
    + +
    + + +
    + +
    + +
    +
    +
    + +
    + +
    + +
    + + + +
    + +
    +
    Index info
    +
    Lists the properties of the index
    +
    + +
    + +
    + + + + + + + +
     
    {{key}}{{val}}
    + +
    + +
    + +
    + + + +
    + +
    +
    Tools
    +
    Tools to manage the index
    +
    + +
    + +
    + +
    +
    + + + + + +
    +
    + +
    + +
    + The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation +
    + +
    + +
    + This index cannot be rebuilt because it has no assigned IIndexPopulator
    -
  • -
  • - Provider properties - - - - - - -
     
    {{key}}{{val}}
    -
  • -
+
+ +
+
+
+
+
+
- -
- + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagementresults.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagementresults.html new file mode 100644 index 0000000000..1df737816f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagementresults.html @@ -0,0 +1,18 @@ +
+ + + + + + + + + + + + + + +
FieldValue
{{key}}{{val}}
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemgmt.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemgmt.controller.js deleted file mode 100644 index f4e78a9dc3..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemgmt.controller.js +++ /dev/null @@ -1,128 +0,0 @@ -function ExamineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeout) { - - $scope.indexerDetails = []; - $scope.searcherDetails = []; - $scope.loading = true; - - function checkProcessing(indexer, checkActionName) { - umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", - checkActionName, - { indexerName: indexer.name })), - 'Failed to check index processing') - .then(function(data) { - - if (data !== null && data !== "null") { - - //copy all resulting properties - for (var k in data) { - indexer[k] = data[k]; - } - indexer.isProcessing = false; - } else { - $timeout(function() { - //don't continue if we've tried 100 times - if (indexer.processingAttempts < 100) { - checkProcessing(indexer, checkActionName); - //add an attempt - indexer.processingAttempts++; - } else { - //we've exceeded 100 attempts, stop processing - indexer.isProcessing = false; - } - }, - 1000); - } - }); - } - - $scope.search = function(searcher, e) { - if (e && e.keyCode !== 13) { - return; - } - - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", - "GetSearchResults", - { - searcherName: searcher.name, - query: encodeURIComponent(searcher.searchText), - queryType: searcher.searchType - })), - 'Failed to search') - .then(function(searchResults) { - searcher.isSearching = true; - searcher.searchResults = searchResults; - }); - } - - $scope.toggle = function(provider, propName) { - if (provider[propName] !== undefined) { - provider[propName] = !provider[propName]; - } else { - provider[propName] = true; - } - } - - $scope.rebuildIndex = function(indexer) { - if (confirm("This will cause the index to be rebuilt. " + - "Depending on how much content there is in your site this could take a while. " + - "It is not recommended to rebuild an index during times of high website traffic " + - "or when editors are editing content.")) { - - indexer.isProcessing = true; - indexer.processingAttempts = 0; - - umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", - "PostRebuildIndex", - { indexerName: indexer.name })), - 'Failed to rebuild index') - .then(function() { - - //rebuilding has started, nothing is returned accept a 200 status code. - //lets poll to see if it is done. - $timeout(function() { - checkProcessing(indexer, "PostCheckRebuildIndex"); - }, - 1000); - - }); - } - } - - $scope.closeSearch = function(searcher) { - searcher.isSearching = true; - } - - //go get the data - - //combine two promises and execute when they are both done - $q.all([ - - //get the indexer details - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetIndexerDetails")), - 'Failed to retrieve indexer details') - .then(function(data) { - $scope.indexerDetails = data; - }), - - //get the searcher details - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetSearcherDetails")), - 'Failed to retrieve searcher details') - .then(function(data) { - $scope.searcherDetails = data; - for (var s in $scope.searcherDetails) { - $scope.searcherDetails[s].searchType = "text"; - } - }) - ]) - .then(function() { - //all init loading is complete - $scope.loading = false; - }); -} - -angular.module("umbraco").controller("Umbraco.Dashboard.ExamineMgmtController", ExamineMgmtController); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.controller.js index 4e684cb5bd..742a2caa29 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.controller.js @@ -3,6 +3,7 @@ angular.module("umbraco") function ($scope, dataTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { $scope.dialogTreeApi = {}; + $scope.source = _.clone($scope.currentNode); function nodeSelectHandler(args) { args.event.preventDefault(); @@ -22,7 +23,7 @@ angular.module("umbraco") $scope.busy = true; $scope.error = false; - dataTypeResource.move({ parentId: $scope.target.id, id: $scope.currentNode.id }) + dataTypeResource.move({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { $scope.error = false; $scope.success = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html index 38bf7c0581..2723dd305d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html @@ -4,7 +4,7 @@

- Select the folder to move {{currentNode.name}} to in the tree structure below + Select the folder to move {{source.name}} to in the tree structure below

@@ -20,7 +20,7 @@
- {{currentNode.name}} was moved underneath {{target.name}} + {{source.name}} was moved underneath {{target.name}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.controller.js index 385ff13b25..2039175819 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.controller.js @@ -3,6 +3,7 @@ angular.module("umbraco") function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { $scope.dialogTreeApi = {}; + $scope.source = _.clone($scope.currentNode); function nodeSelectHandler(args) { args.event.preventDefault(); @@ -22,7 +23,7 @@ angular.module("umbraco") $scope.busy = true; $scope.error = false; - contentTypeResource.copy({ parentId: $scope.target.id, id: $scope.currentNode.id }) + contentTypeResource.copy({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { $scope.error = false; $scope.success = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html index cbf08274fc..4b90c244e8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html @@ -4,7 +4,7 @@

- Select the folder to copy {{currentNode.name}} to in the tree structure below + Select the folder to copy {{source.name}} to in the tree structure below

@@ -20,7 +20,7 @@
- {{currentNode.name}} was copied underneath {{target.name}} + {{source.name}} was copied underneath {{target.name}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.controller.js index 7d7d1f1bf8..e63656cee3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.controller.js @@ -3,6 +3,7 @@ angular.module("umbraco") function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { $scope.dialogTreeApi = {}; + $scope.source = _.clone($scope.currentNode); function nodeSelectHandler(args) { args.event.preventDefault(); @@ -22,7 +23,7 @@ angular.module("umbraco") $scope.busy = true; $scope.error = false; - contentTypeResource.move({ parentId: $scope.target.id, id: $scope.currentNode.id }) + contentTypeResource.move({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { $scope.error = false; $scope.success = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html index aada5193ec..6b81c15bc5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html @@ -4,7 +4,7 @@

- Select the folder to move {{currentNode.name}} to in the tree structure below + Select the folder to move {{source.name}} to in the tree structure below

@@ -20,7 +20,7 @@
- {{currentNode.name}} was moved underneath {{target.name}} + {{source.name}} was moved underneath {{target.name}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js index 4a7a870618..317fe094ae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js @@ -25,6 +25,7 @@ vm.removeChild = removeChild; vm.toggleAllowAsRoot = toggleAllowAsRoot; vm.toggleAllowCultureVariants = toggleAllowCultureVariants; + vm.toggleIsElement = toggleIsElement; /* ---------- INIT ---------- */ @@ -84,25 +85,18 @@ $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); } - /** - * Toggle the $scope.model.allowAsRoot value to either true or false - */ - function toggleAllowAsRoot(){ - if($scope.model.allowAsRoot){ - $scope.model.allowAsRoot = false; - return; - } + // note: "safe toggling" here ie handling cases where the value is undefined, etc - $scope.model.allowAsRoot = true; + function toggleAllowAsRoot() { + $scope.model.allowAsRoot = $scope.model.allowAsRoot ? false : true; } function toggleAllowCultureVariants() { - if ($scope.model.allowCultureVariant) { - $scope.model.allowCultureVariant = false; - return; - } + $scope.model.allowCultureVariant = $scope.model.allowCultureVariant ? false : true; + } - $scope.model.allowCultureVariant = true; + function toggleIsElement() { + $scope.model.isElement = $scope.model.isElement ? false : true; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html index ec1e528f8c..0d74c655d7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html @@ -53,9 +53,25 @@ hotkey="alt+shift+v">
- +
- +
+ +
+
+ +
+ +
+ + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index 533337549b..4c9ef8dc9a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -39,7 +39,8 @@
+ checked="vm.language.isDefault" + disabled="vm.initIsDefault">
{{vm.labels.defaultLanguage}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/edit.html b/src/Umbraco.Web.UI.Client/src/views/media/edit.html index 2dfc8c967e..be1219f052 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/edit.html @@ -13,7 +13,8 @@ hide-icon="true" hide-description="true" hide-alias="true" - navigation="content.apps"> + navigation="content.apps" + on-select-navigation-item="appChanged(app)"> diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js index 974f2ad21b..e5e95e94df 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js @@ -30,6 +30,10 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer var recycleBin = treeService.getDescendantNode(rootNode, -21); if (recycleBin) { recycleBin.hasChildren = true; + //reload the recycle bin if it's already expanded so the deleted item is shown + if (recycleBin.expanded) { + treeService.loadNodeChildren({ node: recycleBin, section: "media" }); + } } } @@ -37,8 +41,10 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer if (editorState.current && editorState.current.id == $scope.currentNode.id) { //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent - var location = "/media"; - if ($scope.currentNode.parentId.toString() !== "-1") + var location = "/media"; + if ($scope.currentNode.parentId.toString() === "-21") + location = "/media/media/recyclebin"; + else if ($scope.currentNode.parentId.toString() !== "-1") location = "/media/media/edit/" + $scope.currentNode.parentId; $location.path(location); diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index cc63b5f9f0..19932887fd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -95,8 +95,11 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, function init() { - // set first app to active - $scope.content.apps[0].active = true; + if (!$scope.app) { + // set first app to active + $scope.content.apps[0].active = true; + $scope.app = $scope.content.apps[0]; + } // setup infinite mode if(infiniteMode) { @@ -207,6 +210,10 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, } }; + $scope.appChanged = function (app) { + $scope.app = app; + } + evts.push(eventsService.on("editors.mediaType.saved", function(name, args) { // if this media item uses the updated media type we need to reload the media item if(args && args.mediaType && args.mediaType.key === $scope.content.contentType.key) { diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js index c5ff80b7fb..163cee9088 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js @@ -3,8 +3,16 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", function ($scope, userService, eventsService, mediaResource, appState, treeService, navigationService) { $scope.dialogTreeApi = {}; - var node = $scope.currentNode; + $scope.source = _.clone($scope.currentNode); + $scope.busy = false; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + } $scope.treeModel = { hideHeader: false } @@ -13,8 +21,8 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", }); function treeLoadedHandler(args) { - if (node && node.path) { - $scope.dialogTreeApi.syncTree({ path: node.path, activate: false }); + if ($scope.source && $scope.source.path) { + $scope.dialogTreeApi.syncTree({ path: $scope.source.path, activate: false }); } } @@ -52,10 +60,28 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", $scope.close = function() { navigationService.hideDialog(); }; + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + } + + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler({ event: evt, node: result }); + }; + + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; $scope.move = function () { $scope.busy = true; - mediaResource.move({ parentId: $scope.target.id, id: node.id }) + mediaResource.move({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { $scope.busy = false; $scope.error = false; @@ -83,11 +109,11 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", $scope.error = err; }); }; - + // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { node: node }); + nodeSelectHandler({ node: node }); }; $scope.closeMiniListView = function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js index 4e8bfb9a5d..a64d6eed66 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js @@ -1,62 +1,74 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.RestoreController", function ($scope, relationResource, mediaResource, navigationService, appState, treeService, localizationService) { - var node = $scope.currentNode; + $scope.source = _.clone($scope.currentNode); $scope.error = null; $scope.success = false; + $scope.loading = true; - relationResource.getByChildId(node.id, "relateParentDocumentOnDelete").then(function (data) { + relationResource.getByChildId($scope.source.id, "relateParentDocumentOnDelete").then(function (data) { + $scope.loading = false; - if (data.length == 0) { - $scope.success = false; - $scope.error = { - errorMsg: localizationService.localize('recycleBin_itemCannotBeRestored'), - data: { - Message: localizationService.localize('recycleBin_noRestoreRelation') - } - } + if (!data.length) { + localizationService.localizeMany(["recycleBin_itemCannotBeRestored", "recycleBin_noRestoreRelation"]) + .then(function(values) { + $scope.success = false; + $scope.error = { + errorMsg: values[0], + data: { + Message: values[1] + } + } + }); return; } $scope.relation = data[0]; - if ($scope.relation.parentId == -1) { + if ($scope.relation.parentId === -1) { $scope.target = { id: -1, name: "Root" }; } else { + $scope.loading = true; mediaResource.getById($scope.relation.parentId).then(function (data) { + $scope.loading = false; $scope.target = data; - - // make sure the target item isn't in the recycle bin - if ($scope.target.path.indexOf("-20") !== -1) { - $scope.error = { - errorMsg: localizationService.localize('recycleBin_itemCannotBeRestored'), - data: { - Message: localizationService.localize('recycleBin_restoreUnderRecycled').then(function (value) { - value.replace('%0%', $scope.target.name); - }) - } - }; + // make sure the target item isn't in the recycle bin + if ($scope.target.path.indexOf("-21") !== -1) { + localizationService.localizeMany(["recycleBin_itemCannotBeRestored", "recycleBin_restoreUnderRecycled"]) + .then(function (values) { + $scope.success = false; + $scope.error = { + errorMsg: values[0], + data: { + Message: values[1].replace('%0%', $scope.target.name) + } + } + }); $scope.success = false; } }, function (err) { $scope.success = false; $scope.error = err; + $scope.loading = false; }); } }, function (err) { $scope.success = false; $scope.error = err; + $scope.loading = false; }); $scope.restore = function () { + $scope.loading = true; // this code was copied from `content.move.controller.js` - mediaResource.move({ parentId: $scope.target.id, id: node.id }) + mediaResource.move({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { + $scope.loading = false; $scope.success = true; //first we need to remove the node that launched the dialog @@ -79,6 +91,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.RestoreController", }, function (err) { $scope.success = false; $scope.error = err; + $scope.loading = false; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/media/move.html b/src/Umbraco.Web.UI.Client/src/views/media/move.html index 15d38a6130..6f80b5ab90 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/move.html @@ -11,30 +11,51 @@
- {{currentNode.name}} was moved underneath {{target.name}} + {{source.name}} + was moved to + {{target.name}}

Choose where to move - {{currentNode.name}} + {{source.name}} to in the tree structure below

- + + +
+ + + + +
+ - + enablelistviewexpand="true" + enablecheckboxes="true"> + +
-

- Restore {{currentNode.name}} under {{target.name}}? + + + +

+ Restore {{source.name}} under {{target.name}}?

-
-
{{error.errorMsg}}
-
{{error.data.Message}}
-
+
+
+
{{error.errorMsg}}
+
{{error.data.Message}}
+
+
-
-

{{currentNode.name}} was moved underneath {{target.name}}

- -
+
+
+ {{source.name}} was moved underneath {{target.name}} +
+ +
-