diff --git a/README.md b/README.md index 5988cc2a19..5fb4cb868b 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ If you're interested in making changes to Belle make sure to read the [Belle Rea **More than 177,000 sites trust Umbraco** -For the first time on the Microsoft platform a free user and developer friendly CMS that makes it quick and easy to create websites - or a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, out of the box. It's a developers dream and your users will love it too. +For the first time on the Microsoft platform, there is a free user and developer friendly CMS that makes it quick and easy to create websites - or a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, out of the box. It's a developer's dream and your users will love it too. -Used by more than 177,000 active websites including [http://daviscup.com](http://daviscup.com), [http://heinz.com](http://heinz.com), [http://peugeot.com](http://peugeot.com), [http://www.hersheys.com/](http://www.hersheys.com/) and **The Official ASP.NET and IIS.NET website from Microsoft** ([http://asp.net](http://asp.net) / [http://iis.net](http://iis.net)) you can be sure that the technology is proven, stable and scales. +Used by more than 177,000 active websites including [http://daviscup.com](http://daviscup.com), [http://heinz.com](http://heinz.com), [http://peugeot.com](http://peugeot.com), [http://www.hersheys.com/](http://www.hersheys.com/) and **The Official ASP.NET and IIS.NET website from Microsoft** ([http://asp.net](http://asp.net) / [http://iis.net](http://iis.net)), you can be sure that the technology is proven, stable and scales. -To view more examples please visit [http://umbraco.com/why-umbraco/#caseStudies](http://umbraco.com/why-umbraco/#caseStudies) +To view more examples, please visit [http://umbraco.com/why-umbraco/#caseStudies](http://umbraco.com/why-umbraco/#caseStudies) ## Downloading ## @@ -35,6 +35,6 @@ If you want to contribute back to Umbraco you should check out our [guide to con ## Found a bug? ## -Another way you can contribute to Umbraco is by providing issue reports, for information on how to submit an issue report refer to our [online guide for reporting issues](http://our.umbraco.org/contribute/report-an-issue-or-request-a-feature). +Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](http://our.umbraco.org/contribute/report-an-issue-or-request-a-feature). -To view existing issues please visit [http://issues.umbraco.org](http://issues.umbraco.org) +To view existing issues, please visit [http://issues.umbraco.org](http://issues.umbraco.org). diff --git a/build/Build.bat b/build/Build.bat index bd18f719ad..1167d9fcb6 100644 --- a/build/Build.bat +++ b/build/Build.bat @@ -21,14 +21,6 @@ ECHO Building Umbraco %version% ReplaceIISExpressPortNumber.exe ..\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj %release% -ECHO Installing the Microsoft.Bcl.Build package before anything else, otherwise you'd have to run build.cmd twice -SET nuGetFolder=%CD%\..\src\packages\ -..\src\.nuget\NuGet.exe sources Remove -Name MyGetUmbracoCore >NUL -..\src\.nuget\NuGet.exe sources Add -Name MyGetUmbracoCore -Source https://www.myget.org/F/umbracocore/api/v2/ >NUL -..\src\.nuget\NuGet.exe install ..\src\Umbraco.Web.UI\packages.config -OutputDirectory %nuGetFolder% -Verbosity quiet -..\src\.nuget\NuGet.exe install ..\src\umbraco.businesslogic\packages.config -OutputDirectory %nuGetFolder% -Verbosity quiet -..\src\.nuget\NuGet.exe install ..\src\Umbraco.Core\packages.config -OutputDirectory %nuGetFolder% -Verbosity quiet - ECHO Removing the belle build folder and bower_components folder to make sure everything is clean as a whistle RD ..\src\Umbraco.Web.UI.Client\build /Q /S RD ..\src\Umbraco.Web.UI.Client\bower_components /Q /S diff --git a/build/Build.proj b/build/Build.proj index e937df15bd..9413b49d15 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -175,8 +175,8 @@ diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat index 0d58204f65..6c11cc9fc5 100644 --- a/build/BuildBelle.bat +++ b/build/BuildBelle.bat @@ -1,4 +1,6 @@ @ECHO OFF +SETLOCAL + SET release=%1 ECHO Installing Npm NuGet Package @@ -11,12 +13,9 @@ ECHO Current folder: %CD% for /f "delims=" %%A in ('dir %nuGetFolder%node.js.* /b') do set "nodePath=%nuGetFolder%%%A\" for /f "delims=" %%A in ('dir %nuGetFolder%npm.js.* /b') do set "npmPath=%nuGetFolder%%%A\tools\" -ECHO Temporarily adding Npm and Node to path -SET oldPath=%PATH% - -path=%npmPath%;%nodePath%;%PATH% - -ECHO %path% +ECHO Adding Npm and Node to path +REM SETLOCAL is on, so changes to the path not persist to the actual user's path +PATH=%npmPath%;%nodePath%;%PATH% SET buildFolder=%CD% @@ -29,8 +28,5 @@ call npm install -g grunt-cli --quiet call npm install -g bower --quiet call grunt build --buildversion=%release% -ECHO Reset path to what it was before -path=%oldPath% - ECHO Move back to the build folder CD %buildFolder% \ No newline at end of file diff --git a/build/InstallGit.cmd b/build/InstallGit.cmd index 2bd6d7cc35..b6ba71df9b 100644 --- a/build/InstallGit.cmd +++ b/build/InstallGit.cmd @@ -1,5 +1,6 @@ @ECHO OFF -SET oldPath=%PATH% +SETLOCAL +REM SETLOCAL is on, so changes to the path not persist to the actual user's path git.exe 2> NUL if %ERRORLEVEL%==9009 GOTO :trydefaultpath @@ -12,8 +13,7 @@ if %ERRORLEVEL%==9009 GOTO :showerror GOTO :EOF :showerror -path=%oldPath% -ECHO Git is not in your path and could not be found in C:\Program Files (x86)\Git\cmd +ECHO Git is not in your path and could not be found in C:\Program Files (x86)\Git\cmd nor in C:\Program Files\Git\cmd set /p install=" Do you want to install Git through Chocolatey [y/n]? " %=% if %install%==y ( GOTO :installgit @@ -29,5 +29,4 @@ GOTO :EOF ECHO Installing Chocolatey first @powershell -NoProfile -ExecutionPolicy unrestricted -Command "iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))" && SET PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin ECHO Installing Git through Chocolatey -choco install git -path=C:\Program Files (x86)\Git\cmd;%path% +choco install git \ No newline at end of file diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 6852b04510..ae2c45e329 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -17,11 +17,10 @@ - - + @@ -31,13 +30,13 @@ - + - - + + - - + + @@ -72,35 +71,35 @@ - + - - + + - + - + - + - + - + - + - + - + - + - + - + - - + + \ No newline at end of file diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 61a8188804..46703a0c39 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -16,7 +16,8 @@ umbraco - + + @@ -32,6 +33,7 @@ + diff --git a/build/NuSpecs/tools/Dashboard.config.install.xdt b/build/NuSpecs/tools/Dashboard.config.install.xdt index 197f9c1b6f..a77632926c 100644 --- a/build/NuSpecs/tools/Dashboard.config.install.xdt +++ b/build/NuSpecs/tools/Dashboard.config.install.xdt @@ -77,4 +77,8 @@
+ +
+ +
\ No newline at end of file diff --git a/build/NuSpecs/tools/Readme.txt b/build/NuSpecs/tools/Readme.txt index 53d9c3c7da..b6b55c1c4f 100644 --- a/build/NuSpecs/tools/Readme.txt +++ b/build/NuSpecs/tools/Readme.txt @@ -10,6 +10,7 @@ Don't forget to build! + When upgrading your website using NuGet you should answer "No" to the questions to overwrite the Web.config file (and config files in the config folder). diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt index 894f5e5f93..e0d660a795 100644 --- a/build/NuSpecs/tools/ReadmeUpgrade.txt +++ b/build/NuSpecs/tools/ReadmeUpgrade.txt @@ -10,6 +10,7 @@ Don't forget to build! + We've done our best to transform your configuration files but in case something is not quite right: remember we backed up your files in App_Data\NuGetBackup so you can find the original files before they were transformed. diff --git a/build/NuSpecs/tools/Views.Web.config.install.xdt b/build/NuSpecs/tools/Views.Web.config.install.xdt index c34963f2b0..a5797232b8 100644 --- a/build/NuSpecs/tools/Views.Web.config.install.xdt +++ b/build/NuSpecs/tools/Views.Web.config.install.xdt @@ -17,6 +17,11 @@ + + + + + diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index cff81ba2d8..f8ec871700 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -7,8 +7,8 @@
-
- +
+
@@ -22,7 +22,7 @@ - + @@ -37,7 +37,7 @@ - + @@ -48,13 +48,15 @@ - + - + - + + + > @@ -63,9 +65,9 @@ - + + @@ -77,6 +79,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -84,10 +136,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -122,6 +278,8 @@ + + @@ -140,20 +298,27 @@ - - + + + + + + + - + + + @@ -165,6 +330,10 @@ + + + + @@ -175,7 +344,11 @@ - + + + + + @@ -188,16 +361,16 @@ - + - + - - + + @@ -205,19 +378,19 @@ - + - + - + - + - + \ No newline at end of file diff --git a/build/NuSpecs/tools/install.core.ps1 b/build/NuSpecs/tools/install.core.ps1 index c81fa419c9..32dfecbb00 100644 --- a/build/NuSpecs/tools/install.core.ps1 +++ b/build/NuSpecs/tools/install.core.ps1 @@ -1,18 +1,30 @@ -param($rootPath, $toolsPath, $package, $project) +param($installPath, $toolsPath, $package, $project) + +Write-Host "installPath:" "${installPath}" +Write-Host "toolsPath:" "${toolsPath}" + +Write-Host " " if ($project) { $dateTime = Get-Date -Format yyyyMMdd-HHmmss - $backupPath = Join-Path (Split-Path $project.FullName -Parent) "\App_Data\NuGetBackup\$dateTime" + + # Create paths and list them + $projectPath = (Get-Item $project.Properties.Item("FullPath").Value).FullName + Write-Host "projectPath:" "${projectPath}" + $backupPath = Join-Path $projectPath "App_Data\NuGetBackup\$dateTime" + Write-Host "backupPath:" "${backupPath}" $copyLogsPath = Join-Path $backupPath "CopyLogs" - $projectDestinationPath = Split-Path $project.FullName -Parent - + Write-Host "copyLogsPath:" "${copyLogsPath}" + $umbracoBinFolder = Join-Path $projectPath "bin" + Write-Host "umbracoBinFolder:" "${umbracoBinFolder}" + # Create backup folder and logs folder if it doesn't exist yet New-Item -ItemType Directory -Force -Path $backupPath New-Item -ItemType Directory -Force -Path $copyLogsPath # After backing up, remove all umbraco dlls from bin folder in case dll files are included in the VS project # See: http://issues.umbraco.org/issue/U4-4930 - $umbracoBinFolder = Join-Path $projectDestinationPath "bin" + if(Test-Path $umbracoBinFolder) { $umbracoBinBackupPath = Join-Path $backupPath "bin" @@ -20,7 +32,7 @@ if ($project) { robocopy $umbracoBinFolder $umbracoBinBackupPath /e /LOG:$copyLogsPath\UmbracoBinBackup.log - # Delete files Umbraco brings in + # Delete files Umbraco ships with if(Test-Path $umbracoBinFolder\businesslogic.dll) { Remove-Item $umbracoBinFolder\businesslogic.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\cms.dll) { Remove-Item $umbracoBinFolder\cms.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\controls.dll) { Remove-Item $umbracoBinFolder\controls.dll -Force -Confirm:$false } @@ -35,16 +47,18 @@ if ($project) { if(Test-Path $umbracoBinFolder\umbraco.DataLayer.dll) { Remove-Item $umbracoBinFolder\umbraco.DataLayer.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\umbraco.editorControls.dll) { Remove-Item $umbracoBinFolder\umbraco.editorControls.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\umbraco.MacroEngines.dll) { Remove-Item $umbracoBinFolder\umbraco.MacroEngines.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Umbraco.ModelsBuilder.dll) { Remove-Item $umbracoBinFolder\Umbraco.ModelsBuilder.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Umbraco.ModelsBuilder.AspNet.dll) { Remove-Item $umbracoBinFolder\Umbraco.ModelsBuilder.AspNet.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\umbraco.providers.dll) { Remove-Item $umbracoBinFolder\umbraco.providers.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Umbraco.Web.UI.dll) { Remove-Item $umbracoBinFolder\Umbraco.Web.UI.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\UmbracoExamine.dll) { Remove-Item $umbracoBinFolder\UmbracoExamine.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\UrlRewritingNet.UrlRewriter.dll) { Remove-Item $umbracoBinFolder\UrlRewritingNet.UrlRewriter.dll -Force -Confirm:$false } # Delete files Umbraco depends upon - $amd64Folder = Join-Path $projectDestinationPath "bin\amd64" + $amd64Folder = Join-Path $umbracoBinFolder "amd64" if(Test-Path $amd64Folder) { Remove-Item $amd64Folder -Force -Recurse -Confirm:$false } - $x86Folder = Join-Path $projectDestinationPath "bin\x86" - if(Test-Path $x86Folder) { Remove-Item $x86Folder -Force -Recurse -Confirm:$false } + $x86Folder = Join-Path $umbracoBinFolder "x86" + if(Test-Path $x86Folder) { Remove-Item $x86Folder -Force -Recurse -Confirm:$false } if(Test-Path $umbracoBinFolder\AutoMapper.dll) { Remove-Item $umbracoBinFolder\AutoMapper.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\AutoMapper.Net4.dll) { Remove-Item $umbracoBinFolder\AutoMapper.Net4.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\ClientDependency.Core.dll) { Remove-Item $umbracoBinFolder\ClientDependency.Core.dll -Force -Confirm:$false } @@ -58,6 +72,8 @@ if ($project) { if(Test-Path $umbracoBinFolder\Lucene.Net.dll) { Remove-Item $umbracoBinFolder\Lucene.Net.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Microsoft.AspNet.Identity.Core.dll) { Remove-Item $umbracoBinFolder\Microsoft.AspNet.Identity.Core.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Microsoft.AspNet.Identity.Owin.dll) { Remove-Item $umbracoBinFolder\Microsoft.AspNet.Identity.Owin.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.CodeAnalysis.CSharp.dll) { Remove-Item $umbracoBinFolder\Microsoft.CodeAnalysis.CSharp.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.CodeAnalysis.dll) { Remove-Item $umbracoBinFolder\Microsoft.CodeAnalysis.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Microsoft.Owin.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Microsoft.Owin.Host.SystemWeb.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.Host.SystemWeb.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Microsoft.Owin.Security.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.Security.dll -Force -Confirm:$false } @@ -71,6 +87,8 @@ if ($project) { if(Test-Path $umbracoBinFolder\Newtonsoft.Json.dll) { Remove-Item $umbracoBinFolder\Newtonsoft.Json.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Owin.dll) { Remove-Item $umbracoBinFolder\Owin.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Semver.dll) { Remove-Item $umbracoBinFolder\Semver.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\System.Collections.Immutable.dll) { Remove-Item $umbracoBinFolder\System.Collections.Immutable.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\System.Reflection.Metadata.dll) { Remove-Item $umbracoBinFolder\System.Reflection.Metadata.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Net.Http.Extensions.dll) { Remove-Item $umbracoBinFolder\System.Net.Http.Extensions.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Net.Http.Formatting.dll) { Remove-Item $umbracoBinFolder\System.Net.Http.Formatting.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Net.Http.Primitives.dll) { Remove-Item $umbracoBinFolder\System.Net.Http.Primitives.dll -Force -Confirm:$false } @@ -81,6 +99,6 @@ if ($project) { if(Test-Path $umbracoBinFolder\System.Web.Razor.dll) { Remove-Item $umbracoBinFolder\System.Web.Razor.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Web.WebPages.dll) { Remove-Item $umbracoBinFolder\System.Web.WebPages.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Web.WebPages.Deployment.dll) { Remove-Item $umbracoBinFolder\System.Web.WebPages.Deployment.dll -Force -Confirm:$false } - if(Test-Path $umbracoBinFolder\System.Web.WebPages.Razor.dll) { Remove-Item $umbracoBinFolder\System.Web.WebPages.Razor.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\System.Web.WebPages.Razor.dll) { Remove-Item $umbracoBinFolder\System.Web.WebPages.Razor.dll -Force -Confirm:$false } } } \ No newline at end of file diff --git a/build/NuSpecs/tools/install.ps1 b/build/NuSpecs/tools/install.ps1 index 49b49f6cfe..de7a6cc16e 100644 --- a/build/NuSpecs/tools/install.ps1 +++ b/build/NuSpecs/tools/install.ps1 @@ -1,21 +1,33 @@ -param($rootPath, $toolsPath, $package, $project) +param($installPath, $toolsPath, $package, $project) + +Write-Host "installPath:" "${installPath}" +Write-Host "toolsPath:" "${toolsPath}" + +Write-Host " " if ($project) { $dateTime = Get-Date -Format yyyyMMdd-HHmmss - $backupPath = Join-Path (Split-Path $project.FullName -Parent) "\App_Data\NuGetBackup\$dateTime" + + # Create paths and list them + $projectPath = (Get-Item $project.Properties.Item("FullPath").Value).FullName + Write-Host "projectPath:" "${projectPath}" + $backupPath = Join-Path $projectPath "App_Data\NuGetBackup\$dateTime" + Write-Host "backupPath:" "${backupPath}" $copyLogsPath = Join-Path $backupPath "CopyLogs" - $projectDestinationPath = Split-Path $project.FullName -Parent + Write-Host "copyLogsPath:" "${copyLogsPath}" + $webConfigSource = Join-Path $projectPath "Web.config" + Write-Host "webConfigSource:" "${webConfigSource}" + $configFolder = Join-Path $projectPath "Config" + Write-Host "configFolder:" "${configFolder}" # Create backup folder and logs folder if it doesn't exist yet New-Item -ItemType Directory -Force -Path $backupPath New-Item -ItemType Directory -Force -Path $copyLogsPath # Create a backup of original web.config - $webConfigSource = Join-Path $projectDestinationPath "Web.config" Copy-Item $webConfigSource $backupPath -Force - # Backup config files folder - $configFolder = Join-Path $projectDestinationPath "Config" + # Backup config files folder if(Test-Path $configFolder) { $umbracoBackupPath = Join-Path $backupPath "Config" New-Item -ItemType Directory -Force -Path $umbracoBackupPath @@ -24,32 +36,24 @@ if ($project) { } # Copy umbraco and umbraco_files from package to project folder - # This is only done when these folders already exist because we - # only want to do this for upgrades - $umbracoFolder = Join-Path $projectDestinationPath "Umbraco" - if(Test-Path $umbracoFolder) { - $umbracoFolderSource = Join-Path $rootPath "UmbracoFiles\Umbraco" - - $umbracoBackupPath = Join-Path $backupPath "Umbraco" - New-Item -ItemType Directory -Force -Path $umbracoBackupPath - - robocopy $umbracoFolder $umbracoBackupPath /e /LOG:$copyLogsPath\UmbracoBackup.log - robocopy $umbracoFolderSource $umbracoFolder /is /it /e /xf UI.xml /LOG:$copyLogsPath\UmbracoCopy.log - } + $umbracoFolder = Join-Path $projectPath "Umbraco" + New-Item -ItemType Directory -Force -Path $umbracoFolder + $umbracoFolderSource = Join-Path $installPath "UmbracoFiles\Umbraco" + $umbracoBackupPath = Join-Path $backupPath "Umbraco" + New-Item -ItemType Directory -Force -Path $umbracoBackupPath + robocopy $umbracoFolder $umbracoBackupPath /e /LOG:$copyLogsPath\UmbracoBackup.log + robocopy $umbracoFolderSource $umbracoFolder /is /it /e /xf UI.xml /LOG:$copyLogsPath\UmbracoCopy.log - $umbracoClientFolder = Join-Path $projectDestinationPath "Umbraco_Client" - if(Test-Path $umbracoClientFolder) { - $umbracoClientFolderSource = Join-Path $rootPath "UmbracoFiles\Umbraco_Client" - - $umbracoClientBackupPath = Join-Path $backupPath "Umbraco_Client" - New-Item -ItemType Directory -Force -Path $umbracoClientBackupPath - - robocopy $umbracoClientFolder $umbracoClientBackupPath /e /LOG:$copyLogsPath\UmbracoClientBackup.log - robocopy $umbracoClientFolderSource $umbracoClientFolder /is /it /e /LOG:$copyLogsPath\UmbracoClientCopy.log - } + $umbracoClientFolder = Join-Path $projectPath "Umbraco_Client" + New-Item -ItemType Directory -Force -Path $umbracoClientFolder + $umbracoClientFolderSource = Join-Path $installPath "UmbracoFiles\Umbraco_Client" + $umbracoClientBackupPath = Join-Path $backupPath "Umbraco_Client" + New-Item -ItemType Directory -Force -Path $umbracoClientBackupPath + robocopy $umbracoClientFolder $umbracoClientBackupPath /e /LOG:$copyLogsPath\UmbracoClientBackup.log + robocopy $umbracoClientFolderSource $umbracoClientFolder /is /it /e /LOG:$copyLogsPath\UmbracoClientCopy.log $copyWebconfig = $true - $destinationWebConfig = Join-Path $projectDestinationPath "Web.config" + $destinationWebConfig = Join-Path $projectPath "Web.config" if(Test-Path $destinationWebConfig) { @@ -71,11 +75,11 @@ if ($project) { if($copyWebconfig -eq $true) { - $packageWebConfigSource = Join-Path $rootPath "UmbracoFiles\Web.config" + $packageWebConfigSource = Join-Path $installPath "UmbracoFiles\Web.config" Copy-Item $packageWebConfigSource $destinationWebConfig -Force } - $installFolder = Join-Path $projectDestinationPath "Install" + $installFolder = Join-Path $projectPath "Install" if(Test-Path $installFolder) { Remove-Item $installFolder -Force -Recurse -Confirm:$false } diff --git a/build/NuSpecs/tools/trees.config.install.xdt b/build/NuSpecs/tools/trees.config.install.xdt index f5a807b4bf..580c619547 100644 --- a/build/NuSpecs/tools/trees.config.install.xdt +++ b/build/NuSpecs/tools/trees.config.install.xdt @@ -95,19 +95,19 @@ xdt:Transform="SetAttributes()" /> - - - diff --git a/build/NuSpecs/tools/uninstall.core.ps1 b/build/NuSpecs/tools/uninstall.core.ps1 new file mode 100644 index 0000000000..3daa6a1ba7 --- /dev/null +++ b/build/NuSpecs/tools/uninstall.core.ps1 @@ -0,0 +1,48 @@ +param($installPath, $toolsPath, $package, $project) + +Write-Host "installPath:" "${installPath}" +Write-Host "toolsPath:" "${toolsPath}" + +Write-Host " " + +if ($project) { + + # Create paths and list them + $projectPath = (Get-Item $project.Properties.Item("FullPath").Value).FullName + Write-Host "projectPath:" "${projectPath}" + $backupPath = Join-Path $projectPath "App_Data\NuGetBackup" + Write-Host "backupPath:" "${backupPath}" + $umbracoBinFolder = Join-Path $projectPath "bin" + Write-Host "umbracoBinFolder:" "${umbracoBinFolder}" + + # Remove backups + Write-Host "removing backups:" "${backupPath}" + if(Test-Path $backupPath) { Remove-Item -Recurse -Force $backupPath -Confirm:$false } + + # Delete files Umbraco ships with + + Write-Host "removing dlls:" "${umbracoBinFolder}" + if(Test-Path $umbracoBinFolder\businesslogic.dll) { Remove-Item $umbracoBinFolder\businesslogic.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\cms.dll) { Remove-Item $umbracoBinFolder\cms.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\controls.dll) { Remove-Item $umbracoBinFolder\controls.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\interfaces.dll) { Remove-Item $umbracoBinFolder\interfaces.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\log4net.dll) { Remove-Item $umbracoBinFolder\log4net.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.ApplicationBlocks.Data.dll) { Remove-Item $umbracoBinFolder\Microsoft.ApplicationBlocks.Data.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\SQLCE4Umbraco.dll) { Remove-Item $umbracoBinFolder\SQLCE4Umbraco.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\System.Data.SqlServerCe.dll) { Remove-Item $umbracoBinFolder\System.Data.SqlServerCe.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\System.Data.SqlServerCe.Entity.dll) { Remove-Item $umbracoBinFolder\System.Data.SqlServerCe.Entity.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\TidyNet.dll) { Remove-Item $umbracoBinFolder\TidyNet.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\umbraco.dll) { Remove-Item $umbracoBinFolder\umbraco.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Umbraco.Core.dll) { Remove-Item $umbracoBinFolder\Umbraco.Core.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\umbraco.DataLayer.dll) { Remove-Item $umbracoBinFolder\umbraco.DataLayer.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\umbraco.editorControls.dll) { Remove-Item $umbracoBinFolder\umbraco.editorControls.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\umbraco.MacroEngines.dll) { Remove-Item $umbracoBinFolder\umbraco.MacroEngines.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\umbraco.providers.dll) { Remove-Item $umbracoBinFolder\umbraco.providers.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Umbraco.Web.UI.dll) { Remove-Item $umbracoBinFolder\Umbraco.Web.UI.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\UmbracoExamine.dll) { Remove-Item $umbracoBinFolder\UmbracoExamine.dll -Force -Confirm:$false } + + $amd64Folder = Join-Path $umbracoBinFolder "amd64" + if(Test-Path $amd64Folder) { Remove-Item $amd64Folder -Force -Recurse -Confirm:$false } + $x86Folder = Join-Path $umbracoBinFolder "x86" + if(Test-Path $x86Folder) { Remove-Item $x86Folder -Force -Recurse -Confirm:$false } +} \ No newline at end of file diff --git a/build/NuSpecs/tools/uninstall.ps1 b/build/NuSpecs/tools/uninstall.ps1 new file mode 100644 index 0000000000..4f7dd35384 --- /dev/null +++ b/build/NuSpecs/tools/uninstall.ps1 @@ -0,0 +1,39 @@ +param($installPath, $toolsPath, $package, $project) + +Write-Host "installPath:" "${installPath}" +Write-Host "toolsPath:" "${toolsPath}" + +Write-Host " " + +if ($project) { + + # Create paths and list them + $projectPath = (Get-Item $project.Properties.Item("FullPath").Value).FullName + Write-Host "projectPath:" "${projectPath}" + $backupPath = Join-Path $projectPath "App_Data\NuGetBackup" + Write-Host "backupPath:" "${backupPath}" + $appBrowsers = Join-Path $projectPath "App_Browsers" + Write-Host "appBrowsers:" "${appBrowsers}" + $appData = Join-Path $projectPath "App_Data" + Write-Host "appData:" "${appData}" + + # Remove backups + Write-Host "removing backups:" "${backupPath}" + if(Test-Path $backupPath) { Remove-Item -Recurse -Force $backupPath -Confirm:$false } + + # Remove app_data files + Write-Host "removing app_data files:" "${appData}" + if(Test-Path $appData\packages) { Remove-Item $appData\packages -Recurse -Force -Confirm:$false } + + Write-Host "removing app_browsers:" "${appBrowsers}" + if(Test-Path $appBrowsers\Form.browser) { Remove-Item $appBrowsers\Form.browser -Force -Confirm:$false } + if(Test-Path $appBrowsers\w3cvalidator.browser) { Remove-Item $appBrowsers\w3cvalidator.browser -Force -Confirm:$false } + + # Remove umbraco and umbraco_files + $umbracoFolder = Join-Path $projectPath "Umbraco" + Write-Host "removing umbraco folder:" "${umbracoFolder}" + if(Test-Path $umbracoFolder) { Remove-Item $umbracoFolder -Recurse -Force -Confirm:$false } + $umbracoClientFolder = Join-Path $projectPath "Umbraco_Client" + Write-Host "removing umbraco client folder:" "${umbracoClientFolder}" + if(Test-Path $umbracoClientFolder) { Remove-Item $umbracoClientFolder -Recurse -Force -Confirm:$false } +} diff --git a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj index ceb8381b48..216fa74a67 100644 --- a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj +++ b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj @@ -47,13 +47,15 @@ - - True - ..\packages\SqlServerCE.4.0.0.0\lib\System.Data.SqlServerCe.dll + + ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.dll + False + False - - True - ..\packages\SqlServerCE.4.0.0.0\lib\System.Data.SqlServerCe.Entity.dll + + ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.Entity.dll + False + False diff --git a/src/SQLCE4Umbraco/packages.config b/src/SQLCE4Umbraco/packages.config index 5d2d5789e7..e2435f3e8b 100644 --- a/src/SQLCE4Umbraco/packages.config +++ b/src/SQLCE4Umbraco/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 28b314750a..2e29693e9a 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -2,7 +2,7 @@ using System.Resources; [assembly: AssemblyCompany("Umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2015")] +[assembly: AssemblyCopyright("Copyright © Umbraco 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.4.0")] -[assembly: AssemblyInformationalVersion("7.4.0-beta")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.4.1")] +[assembly: AssemblyInformationalVersion("7.4.1")] \ No newline at end of file diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index 0e497531f1..db905be43f 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -356,7 +356,8 @@ namespace Umbraco.Core //clear the cache if (ApplicationCache != null) { - ApplicationCache.ClearAllCache(); + ApplicationCache.RuntimeCache.ClearAllCache(); + ApplicationCache.IsolatedRuntimeCache.ClearAllCaches(); } //reset all resolvers ResolverCollection.ResetAll(); diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 88d570beff..0c1a202b66 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -1,8 +1,9 @@ using System; +using System.ComponentModel; +using Umbraco.Core.CodeAnnotations; namespace Umbraco.Core.Cache { - /// /// Constants storing cache keys used in caching /// @@ -12,52 +13,78 @@ namespace Umbraco.Core.Cache public const string ApplicationsCacheKey = "ApplicationCache"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string UserTypeCacheKey = "UserTypeCache"; + [Obsolete("This is no longer used and will be removed from the codebase in the future - it is referenced but no cache is stored against this key")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string ContentItemCacheKey = "contentItem"; + [UmbracoWillObsolete("This cache key is only used for the legacy 'library' caching, remove in v8")] public const string MediaCacheKey = "UL_GetMedia"; public const string MacroXsltCacheKey = "macroXslt_"; + + [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string MacroCacheKey = "UmbracoMacroCache"; + public const string MacroHtmlCacheKey = "macroHtml_"; public const string MacroControlCacheKey = "macroControl_"; public const string MacroHtmlDateAddedCacheKey = "macroHtml_DateAdded_"; public const string MacroControlDateAddedCacheKey = "macroControl_DateAdded_"; + [UmbracoWillObsolete("This cache key is only used for legacy 'library' member caching, remove in v8")] public const string MemberLibraryCacheKey = "UL_GetMember"; + + [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string MemberBusinessLogicCacheKey = "MemberCacheItem_"; - + + [UmbracoWillObsolete("This cache key is only used for legacy template business logic caching, remove in v8")] public const string TemplateFrontEndCacheKey = "template"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string TemplateBusinessLogicCacheKey = "UmbracoTemplateCache"; + [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string UserContextCacheKey = "UmbracoUserContext"; + public const string UserContextTimeoutCacheKey = "UmbracoUserContextTimeout"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string UserCacheKey = "UmbracoUser"; public const string UserPermissionsCacheKey = "UmbracoUserPermissions"; + [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string ContentTypeCacheKey = "UmbracoContentType"; + [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string ContentTypePropertiesCacheKey = "ContentType_PropertyTypes_Content:"; + [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string PropertyTypeCacheKey = "UmbracoPropertyTypeCache"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string LanguageCacheKey = "UmbracoLanguageCache"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string DomainCacheKey = "UmbracoDomainList"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string StylesheetCacheKey = "UmbracoStylesheet"; + [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string StylesheetPropertyCacheKey = "UmbracoStylesheetProperty"; + [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string DataTypeCacheKey = "UmbracoDataTypeDefinition"; public const string DataTypePreValuesCacheKey = "UmbracoPreVal"; diff --git a/src/Umbraco.Core/Cache/CacheRefresherBase.cs b/src/Umbraco.Core/Cache/CacheRefresherBase.cs index 2931805b08..60ad69b6fc 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherBase.cs @@ -2,6 +2,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Sync; using umbraco.interfaces; +using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache { @@ -63,5 +64,15 @@ namespace Umbraco.Core.Cache { OnCacheUpdated(Instance, new CacheRefresherEventArgs(id, MessageType.RefreshById)); } + + /// + /// Clears the cache for all repository entities of this type + /// + /// + internal void ClearAllIsolatedCacheByEntityType() + where TEntity : class, IAggregateRoot + { + ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.ClearCache(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DeepCloneRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs similarity index 93% rename from src/Umbraco.Core/Persistence/Repositories/DeepCloneRuntimeCacheProvider.cs rename to src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs index 0b5b42660d..0ae721943d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DeepCloneRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs @@ -2,20 +2,27 @@ using System; using System.Collections.Generic; using System.Linq; using System.Web.Caching; -using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; -namespace Umbraco.Core.Persistence.Repositories +namespace Umbraco.Core.Cache { + /// + /// Interface describing this cache provider as a wrapper for another + /// + internal interface IRuntimeCacheProviderWrapper + { + IRuntimeCacheProvider InnerProvider { get; } + } + /// /// A wrapper for any IRuntimeCacheProvider that ensures that all inserts and returns /// are a deep cloned copy of the item when the item is IDeepCloneable and that tracks changes are /// reset if the object is TracksChangesEntityBase /// - internal class DeepCloneRuntimeCacheProvider : IRuntimeCacheProvider + internal class DeepCloneRuntimeCacheProvider : IRuntimeCacheProvider, IRuntimeCacheProviderWrapper { - internal IRuntimeCacheProvider InnerProvider { get; private set; } + public IRuntimeCacheProvider InnerProvider { get; private set; } public DeepCloneRuntimeCacheProvider(IRuntimeCacheProvider innerProvider) { diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs new file mode 100644 index 0000000000..1f51fc3ccc --- /dev/null +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// The default cache policy for retrieving a single entity + /// + /// + /// + /// + /// This cache policy uses sliding expiration and caches instances for 5 minutes. However if allow zero count is true, then we use the + /// default policy with no expiry. + /// + internal class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase + where TEntity : class, IAggregateRoot + { + private readonly RepositoryCachePolicyOptions _options; + + public DefaultRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) + : base(cache) + { + if (options == null) throw new ArgumentNullException("options"); + _options = options; + } + + protected string GetCacheIdKey(object id) + { + if (id == null) throw new ArgumentNullException("id"); + + return string.Format("{0}{1}", GetCacheTypeKey(), id); + } + + protected string GetCacheTypeKey() + { + return string.Format("uRepo_{0}_", typeof(TEntity).Name); + } + + public override void CreateOrUpdate(TEntity entity, Action persistMethod) + { + if (entity == null) throw new ArgumentNullException("entity"); + if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + + try + { + persistMethod(entity); + + //set the disposal action + SetCacheAction(() => + { + //just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) + { + Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity, + timeout: TimeSpan.FromMinutes(5), + isSliding: true); + } + + //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + + } + catch + { + //set the disposal action + SetCacheAction(() => + { + //if an exception is thrown we need to remove the entry from cache, this is ONLY a work around because of the way + // that we cache entities: http://issues.umbraco.org/issue/U4-4259 + Cache.ClearCacheItem(GetCacheIdKey(entity.Id)); + + //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + + throw; + } + } + + public override void Remove(TEntity entity, Action persistMethod) + { + if (entity == null) throw new ArgumentNullException("entity"); + if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + + try + { + persistMethod(entity); + } + finally + { + //set the disposal action + var cacheKey = GetCacheIdKey(entity.Id); + SetCacheAction(() => + { + Cache.ClearCacheItem(cacheKey); + //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + } + } + + public override TEntity Get(TId id, Func getFromRepo) + { + if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); + + var cacheKey = GetCacheIdKey(id); + var fromCache = Cache.GetCacheItem(cacheKey); + if (fromCache != null) + return fromCache; + + var entity = getFromRepo(id); + + //set the disposal action + SetCacheAction(cacheKey, entity); + + return entity; + } + + public override TEntity Get(TId id) + { + var cacheKey = GetCacheIdKey(id); + return Cache.GetCacheItem(cacheKey); + } + + public override bool Exists(TId id, Func getFromRepo) + { + if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); + + var cacheKey = GetCacheIdKey(id); + var fromCache = Cache.GetCacheItem(cacheKey); + return fromCache != null || getFromRepo(id); + } + + public override TEntity[] GetAll(TId[] ids, Func> getFromRepo) + { + if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); + + if (ids.Any()) + { + var entities = ids.Select(Get).ToArray(); + if (ids.Length.Equals(entities.Length) && entities.Any(x => x == null) == false) + return entities; + } + else + { + var allEntities = GetAllFromCache(); + if (allEntities.Any()) + { + if (_options.GetAllCacheValidateCount) + { + //Get count of all entities of current type (TEntity) to ensure cached result is correct + var totalCount = _options.PerformCount(); + if (allEntities.Length == totalCount) + return allEntities; + } + else + { + return allEntities; + } + } + else if (_options.GetAllCacheAllowZeroCount) + { + //if the repository allows caching a zero count, then check the zero count cache + if (HasZeroCountCache()) + { + //there is a zero count cache so return an empty list + return new TEntity[] {}; + } + } + } + + //we need to do the lookup from the repo + var entityCollection = getFromRepo(ids) + //ensure we don't include any null refs in the returned collection! + .WhereNotNull() + .ToArray(); + + //set the disposal action + SetCacheAction(ids, entityCollection); + + return entityCollection; + } + + /// + /// Looks up the zero count cache, must return null if it doesn't exist + /// + /// + protected bool HasZeroCountCache() + { + var zeroCount = Cache.GetCacheItem(GetCacheTypeKey()); + return (zeroCount != null && zeroCount.Any() == false); + } + + /// + /// Performs the lookup for all entities of this type from the cache + /// + /// + protected TEntity[] GetAllFromCache() + { + var allEntities = Cache.GetCacheItemsByKeySearch(GetCacheTypeKey()) + .WhereNotNull() + .ToArray(); + return allEntities.Any() ? allEntities : new TEntity[] {}; + } + + /// + /// Sets the action to execute on disposal for a single entity + /// + /// + /// + protected virtual void SetCacheAction(string cacheKey, TEntity entity) + { + if (entity == null) return; + + SetCacheAction(() => + { + //just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) + { + Cache.InsertCacheItem(cacheKey, () => entity, + timeout: TimeSpan.FromMinutes(5), + isSliding: true); + } + }); + } + + /// + /// Sets the action to execute on disposal for an entity collection + /// + /// + /// + protected virtual void SetCacheAction(TId[] ids, TEntity[] entityCollection) + { + SetCacheAction(() => + { + //This option cannot execute if we are looking up specific Ids + if (ids.Any() == false && entityCollection.Length == 0 && _options.GetAllCacheAllowZeroCount) + { + //there was nothing returned but we want to cache a zero count result so add an TEntity[] to the cache + // to signify that there is a zero count cache + //NOTE: Don't set expiry/sliding for a zero count + Cache.InsertCacheItem(GetCacheTypeKey(), () => new TEntity[] {}); + } + else + { + //This is the default behavior, we'll individually cache each item so that if/when these items are resolved + // by id, they are returned from the already existing cache. + foreach (var entity in entityCollection.WhereNotNull()) + { + var localCopy = entity; + //just to be safe, we cannot cache an item without an identity + if (localCopy.HasIdentity) + { + Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => localCopy, + timeout: TimeSpan.FromMinutes(5), + isSliding: true); + } + } + } + }); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..5c02e41a48 --- /dev/null +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs @@ -0,0 +1,27 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// Creates cache policies + /// + /// + /// + internal class DefaultRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory + where TEntity : class, IAggregateRoot + { + private readonly IRuntimeCacheProvider _runtimeCache; + private readonly RepositoryCachePolicyOptions _options; + + public DefaultRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, RepositoryCachePolicyOptions options) + { + _runtimeCache = runtimeCache; + _options = options; + } + + public virtual IRepositoryCachePolicy CreatePolicy() + { + return new DefaultRepositoryCachePolicy(_runtimeCache, _options); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs new file mode 100644 index 0000000000..9b37d1861f --- /dev/null +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Collections; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// A caching policy that caches an entire dataset as a single collection + /// + /// + /// + internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase + where TEntity : class, IAggregateRoot + { + private readonly Func _getEntityId; + private readonly Func> _getAllFromRepo; + private readonly bool _expires; + + public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, Func getEntityId, Func> getAllFromRepo, bool expires) + : base(cache) + { + _getEntityId = getEntityId; + _getAllFromRepo = getAllFromRepo; + _expires = expires; + } + + private bool? _hasZeroCountCache; + + + protected string GetCacheTypeKey() + { + return string.Format("uRepo_{0}_", typeof(TEntity).Name); + } + + public override void CreateOrUpdate(TEntity entity, Action persistMethod) + { + if (entity == null) throw new ArgumentNullException("entity"); + if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + + try + { + persistMethod(entity); + + //set the disposal action + SetCacheAction(() => + { + //Clear all + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + } + catch + { + //set the disposal action + SetCacheAction(() => + { + //Clear all + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + throw; + } + } + + public override void Remove(TEntity entity, Action persistMethod) + { + if (entity == null) throw new ArgumentNullException("entity"); + if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + + try + { + persistMethod(entity); + } + finally + { + //set the disposal action + SetCacheAction(() => + { + //Clear all + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + } + } + + public override TEntity Get(TId id, Func getFromRepo) + { + //Force get all with cache + var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); + + //we don't have anything in cache (this should never happen), just return from the repo + if (found == null) return getFromRepo(id); + var entity = found.FirstOrDefault(x => _getEntityId(x).Equals(id)); + if (entity == null) return null; + + //We must ensure to deep clone each one out manually since the deep clone list only clones one way + return (TEntity)entity.DeepClone(); + } + + public override TEntity Get(TId id) + { + //Force get all with cache + var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); + + //we don't have anything in cache (this should never happen), just return null + if (found == null) return null; + var entity = found.FirstOrDefault(x => _getEntityId(x).Equals(id)); + if (entity == null) return null; + + //We must ensure to deep clone each one out manually since the deep clone list only clones one way + return (TEntity)entity.DeepClone(); + } + + public override bool Exists(TId id, Func getFromRepo) + { + //Force get all with cache + var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); + + //we don't have anything in cache (this should never happen), just return from the repo + return found == null + ? getFromRepo(id) + : found.Any(x => _getEntityId(x).Equals(id)); + } + + public override TEntity[] GetAll(TId[] ids, Func> getFromRepo) + { + //process getting all including setting the cache callback + var result = PerformGetAll(getFromRepo); + + //now that the base result has been calculated, they will all be cached. + // Now we can just filter by ids if they have been supplied + + return (ids.Any() + ? result.Where(x => ids.Contains(_getEntityId(x))).ToArray() + : result) + //We must ensure to deep clone each one out manually since the deep clone list only clones one way + .Select(x => (TEntity)x.DeepClone()) + .ToArray(); + } + + private TEntity[] PerformGetAll(Func> getFromRepo) + { + var allEntities = GetAllFromCache(); + if (allEntities.Any()) + { + return allEntities; + } + + //check the zero count cache + if (HasZeroCountCache()) + { + //there is a zero count cache so return an empty list + return new TEntity[] { }; + } + + //we need to do the lookup from the repo + var entityCollection = getFromRepo(new TId[] { }) + //ensure we don't include any null refs in the returned collection! + .WhereNotNull() + .ToArray(); + + //set the disposal action + SetCacheAction(entityCollection); + + return entityCollection; + } + + /// + /// For this type of caching policy, we don't cache individual items + /// + /// + /// + protected void SetCacheAction(string cacheKey, TEntity entity) + { + //No-op + } + + /// + /// Sets the action to execute on disposal for an entity collection + /// + /// + protected void SetCacheAction(TEntity[] entityCollection) + { + //set the disposal action + SetCacheAction(() => + { + //We want to cache the result as a single collection + + if (_expires) + { + Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection), + timeout: TimeSpan.FromMinutes(5), + isSliding: true); + } + else + { + Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection)); + } + }); + } + + /// + /// Looks up the zero count cache, must return null if it doesn't exist + /// + /// + protected bool HasZeroCountCache() + { + if (_hasZeroCountCache.HasValue) + return _hasZeroCountCache.Value; + + _hasZeroCountCache = Cache.GetCacheItem>(GetCacheTypeKey()) != null; + return _hasZeroCountCache.Value; + } + + /// + /// This policy will cache the full data set as a single collection + /// + /// + protected TEntity[] GetAllFromCache() + { + var found = Cache.GetCacheItem>(GetCacheTypeKey()); + + //This method will get called before checking for zero count cache, so we'll just set the flag here + _hasZeroCountCache = found != null; + + return found == null ? new TEntity[] { } : found.WhereNotNull().ToArray(); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..e4addcf355 --- /dev/null +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// Creates cache policies + /// + /// + /// + internal class FullDataSetRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory + where TEntity : class, IAggregateRoot + { + private readonly IRuntimeCacheProvider _runtimeCache; + private readonly Func _getEntityId; + private readonly Func> _getAllFromRepo; + private readonly bool _expires; + + public FullDataSetRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, Func getEntityId, Func> getAllFromRepo, bool expires) + { + _runtimeCache = runtimeCache; + _getEntityId = getEntityId; + _getAllFromRepo = getAllFromRepo; + _expires = expires; + } + + public virtual IRepositoryCachePolicy CreatePolicy() + { + return new FullDataSetRepositoryCachePolicy(_runtimeCache, _getEntityId, _getAllFromRepo, _expires); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs new file mode 100644 index 0000000000..215487c3be --- /dev/null +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + internal interface IRepositoryCachePolicy : IDisposable + where TEntity : class, IAggregateRoot + { + TEntity Get(TId id, Func getFromRepo); + TEntity Get(TId id); + bool Exists(TId id, Func getFromRepo); + + void CreateOrUpdate(TEntity entity, Action persistMethod); + void Remove(TEntity entity, Action persistMethod); + TEntity[] GetAll(TId[] ids, Func> getFromRepo); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..2d69704b63 --- /dev/null +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + internal interface IRepositoryCachePolicyFactory where TEntity : class, IAggregateRoot + { + IRepositoryCachePolicy CreatePolicy(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/IsolatedRuntimeCache.cs b/src/Umbraco.Core/Cache/IsolatedRuntimeCache.cs new file mode 100644 index 0000000000..da20f7eb73 --- /dev/null +++ b/src/Umbraco.Core/Cache/IsolatedRuntimeCache.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Concurrent; + +namespace Umbraco.Core.Cache +{ + /// + /// Used to get/create/manipulate isolated runtime cache + /// + /// + /// This is useful for repository level caches to ensure that cache lookups by key are fast so + /// that the repository doesn't need to search through all keys on a global scale. + /// + public class IsolatedRuntimeCache + { + internal Func CacheFactory { get; set; } + + /// + /// Constructor that allows specifying a factory for the type of runtime isolated cache to create + /// + /// + public IsolatedRuntimeCache(Func cacheFactory) + { + CacheFactory = cacheFactory; + } + + private readonly ConcurrentDictionary _isolatedCache = new ConcurrentDictionary(); + + /// + /// Returns an isolated runtime cache for a given type + /// + /// + /// + public IRuntimeCacheProvider GetOrCreateCache() + { + return _isolatedCache.GetOrAdd(typeof(T), type => CacheFactory(type)); + } + + /// + /// Returns an isolated runtime cache for a given type + /// + /// + public IRuntimeCacheProvider GetOrCreateCache(Type type) + { + return _isolatedCache.GetOrAdd(type, t => CacheFactory(t)); + } + + /// + /// Tries to get a cache by the type specified + /// + /// + /// + public Attempt GetCache() + { + IRuntimeCacheProvider cache; + if (_isolatedCache.TryGetValue(typeof(T), out cache)) + { + return Attempt.Succeed(cache); + } + return Attempt.Fail(); + } + + /// + /// Clears all values inside this isolated runtime cache + /// + /// + /// + public void ClearCache() + { + IRuntimeCacheProvider cache; + if (_isolatedCache.TryGetValue(typeof(T), out cache)) + { + cache.ClearAllCache(); + } + } + + /// + /// Clears all of the isolated caches + /// + public void ClearAllCaches() + { + foreach (var key in _isolatedCache.Keys) + { + IRuntimeCacheProvider cache; + if (_isolatedCache.TryRemove(key, out cache)) + { + cache.ClearAllCache(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..b24838bc3b --- /dev/null +++ b/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs @@ -0,0 +1,27 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// Creates cache policies + /// + /// + /// + internal class OnlySingleItemsRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory + where TEntity : class, IAggregateRoot + { + private readonly IRuntimeCacheProvider _runtimeCache; + private readonly RepositoryCachePolicyOptions _options; + + public OnlySingleItemsRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, RepositoryCachePolicyOptions options) + { + _runtimeCache = runtimeCache; + _options = options; + } + + public virtual IRepositoryCachePolicy CreatePolicy() + { + return new SingleItemsOnlyRepositoryCachePolicy(_runtimeCache, _options); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs new file mode 100644 index 0000000000..b939cd14e6 --- /dev/null +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + internal abstract class RepositoryCachePolicyBase : DisposableObject, IRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + private Action _action; + + protected RepositoryCachePolicyBase(IRuntimeCacheProvider cache) + { + if (cache == null) throw new ArgumentNullException("cache"); + + Cache = cache; + } + + protected IRuntimeCacheProvider Cache { get; private set; } + + /// + /// The disposal performs the caching + /// + protected override void DisposeResources() + { + if (_action != null) + { + _action(); + } + } + + /// + /// Sets the action to execute on disposal + /// + /// + protected void SetCacheAction(Action action) + { + _action = action; + } + + public abstract TEntity Get(TId id, Func getFromRepo); + public abstract TEntity Get(TId id); + public abstract bool Exists(TId id, Func getFromRepo); + public abstract void CreateOrUpdate(TEntity entity, Action persistMethod); + public abstract void Remove(TEntity entity, Action persistMethod); + public abstract TEntity[] GetAll(TId[] ids, Func> getFromRepo); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheOptions.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs similarity index 57% rename from src/Umbraco.Core/Persistence/Repositories/RepositoryCacheOptions.cs rename to src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs index 9ac8aa6abd..e8c6ac02b0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheOptions.cs +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs @@ -1,17 +1,34 @@ -namespace Umbraco.Core.Persistence.Repositories +using System; + +namespace Umbraco.Core.Cache { - internal class RepositoryCacheOptions + internal class RepositoryCachePolicyOptions { /// - /// Constructor sets defaults + /// Ctor - sets GetAllCacheValidateCount = true /// - public RepositoryCacheOptions() + public RepositoryCachePolicyOptions(Func performCount) { + PerformCount = performCount; GetAllCacheValidateCount = true; GetAllCacheAllowZeroCount = false; - GetAllCacheThresholdLimit = 100; } + /// + /// Ctor - sets GetAllCacheValidateCount = false + /// + public RepositoryCachePolicyOptions() + { + PerformCount = null; + GetAllCacheValidateCount = false; + GetAllCacheAllowZeroCount = false; + } + + /// + /// Callback required to get count for GetAllCacheValidateCount + /// + public Func PerformCount { get; private set; } + /// /// True/false as to validate the total item count when all items are returned from cache, the default is true but this /// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the normal @@ -21,16 +38,11 @@ namespace Umbraco.Core.Persistence.Repositories /// setting this to return false will improve performance of GetAll cache with no params but should only be used /// for specific circumstances /// - public bool GetAllCacheValidateCount { get; set; } + public bool GetAllCacheValidateCount { get; private set; } /// /// True if the GetAll method will cache that there are zero results so that the db is not hit when there are no results found /// public bool GetAllCacheAllowZeroCount { get; set; } - - /// - /// The threshold entity count for which the GetAll method will cache entities - /// - public int GetAllCacheThresholdLimit { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs new file mode 100644 index 0000000000..28ac4ee2d1 --- /dev/null +++ b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs @@ -0,0 +1,24 @@ +using System.Linq; +using Umbraco.Core.Collections; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// A caching policy that ignores all caches for GetAll - it will only cache calls for individual items + /// + /// + /// + internal class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + public SingleItemsOnlyRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) : base(cache, options) + { + } + + protected override void SetCacheAction(TId[] ids, TEntity[] entityCollection) + { + //no-op + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/CacheHelper.cs b/src/Umbraco.Core/CacheHelper.cs index 51cf37aa23..0dc5f5b00f 100644 --- a/src/Umbraco.Core/CacheHelper.cs +++ b/src/Umbraco.Core/CacheHelper.cs @@ -1,6 +1,8 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -11,20 +13,15 @@ using Umbraco.Core.Logging; namespace Umbraco.Core { - /// /// Class that is exposed by the ApplicationContext for application wide caching purposes /// public class CacheHelper { - private readonly bool _enableCache; - private readonly ICacheProvider _requestCache; - private readonly ICacheProvider _nullRequestCache = new NullCacheProvider(); - private readonly ICacheProvider _staticCache; - private readonly ICacheProvider _nullStaticCache = new NullCacheProvider(); - private readonly IRuntimeCacheProvider _httpCache; - private readonly IRuntimeCacheProvider _nullHttpCache = new NullCacheProvider(); - + private static readonly ICacheProvider NullRequestCache = new NullCacheProvider(); + private static readonly ICacheProvider NullStaticCache = new NullCacheProvider(); + private static readonly IRuntimeCacheProvider NullRuntimeCache = new NullCacheProvider(); + /// /// Creates a cache helper with disabled caches /// @@ -34,7 +31,7 @@ namespace Umbraco.Core /// public static CacheHelper CreateDisabledCacheHelper() { - return new CacheHelper(null, null, null, false); + return new CacheHelper(NullRuntimeCache, NullStaticCache, NullRequestCache, new IsolatedRuntimeCache(t => NullRuntimeCache)); } /// @@ -44,7 +41,8 @@ namespace Umbraco.Core : this( new HttpRuntimeCacheProvider(HttpRuntime.Cache), new StaticCacheProvider(), - new HttpRequestCacheProvider()) + new HttpRequestCacheProvider(), + new IsolatedRuntimeCache(t => new ObjectCacheRuntimeCacheProvider())) { } @@ -56,93 +54,75 @@ namespace Umbraco.Core : this( new HttpRuntimeCacheProvider(cache), new StaticCacheProvider(), - new HttpRequestCacheProvider()) + new HttpRequestCacheProvider(), + new IsolatedRuntimeCache(t => new ObjectCacheRuntimeCacheProvider())) { } - /// - /// Initializes a new instance based on the provided providers - /// - /// - /// - /// + [Obsolete("Use the constructor the specifies all dependencies")] + [EditorBrowsable(EditorBrowsableState.Never)] public CacheHelper( IRuntimeCacheProvider httpCacheProvider, ICacheProvider staticCacheProvider, ICacheProvider requestCacheProvider) - : this(httpCacheProvider, staticCacheProvider, requestCacheProvider, true) + : this(httpCacheProvider, staticCacheProvider, requestCacheProvider, new IsolatedRuntimeCache(t => new ObjectCacheRuntimeCacheProvider())) { } - /// - /// Private ctor used for creating a disabled cache helper - /// - /// - /// - /// - /// - private CacheHelper( + /// + /// Initializes a new instance based on the provided providers + /// + /// + /// + /// + /// + public CacheHelper( IRuntimeCacheProvider httpCacheProvider, ICacheProvider staticCacheProvider, - ICacheProvider requestCacheProvider, - bool enableCache) + ICacheProvider requestCacheProvider, + IsolatedRuntimeCache isolatedCacheManager) { - if (enableCache) - { - _httpCache = httpCacheProvider; - _staticCache = staticCacheProvider; - _requestCache = requestCacheProvider; - } - else - { - _httpCache = null; - _staticCache = null; - _requestCache = null; - } - - _enableCache = enableCache; + 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; } /// /// Returns the current Request cache /// - public ICacheProvider RequestCache - { - get { return _enableCache ? _requestCache : _nullRequestCache; } - } - - /// - /// Returns the current Runtime cache - /// - public ICacheProvider StaticCache - { - get { return _enableCache ? _staticCache : _nullStaticCache; } - } - - /// - /// Returns the current Runtime cache - /// - public IRuntimeCacheProvider RuntimeCache - { - get { return _enableCache ? _httpCache : _nullHttpCache; } - } + public ICacheProvider RequestCache { get; internal set; } - #region Legacy Runtime/Http Cache accessors + /// + /// Returns the current Runtime cache + /// + public ICacheProvider StaticCache { get; internal set; } + + /// + /// Returns the current Runtime cache + /// + public IRuntimeCacheProvider RuntimeCache { get; internal set; } + + /// + /// Returns the current Isolated Runtime cache manager + /// + public IsolatedRuntimeCache IsolatedRuntimeCache { get; internal set; } + + #region Legacy Runtime/Http Cache accessors /// /// Clears the item in umbraco's runtime cache /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void ClearAllCache() { - if (_enableCache == false) - { - _nullHttpCache.ClearAllCache(); - } - else - { - _httpCache.ClearAllCache(); - } + RuntimeCache.ClearAllCache(); + IsolatedRuntimeCache.ClearAllCaches(); } /// @@ -150,16 +130,10 @@ namespace Umbraco.Core /// /// Key [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void ClearCacheItem(string key) { - if (_enableCache == false) - { - _nullHttpCache.ClearCacheItem(key); - } - else - { - _httpCache.ClearCacheItem(key); - } + RuntimeCache.ClearCacheItem(key); } @@ -171,30 +145,17 @@ namespace Umbraco.Core [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] public void ClearCacheObjectTypes(string typeName) { - if (_enableCache == false) - { - _nullHttpCache.ClearCacheObjectTypes(typeName); - } - else - { - _httpCache.ClearCacheObjectTypes(typeName); - } + RuntimeCache.ClearCacheObjectTypes(typeName); } /// /// Clears all objects in the System.Web.Cache with the System.Type specified /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void ClearCacheObjectTypes() { - if (_enableCache == false) - { - _nullHttpCache.ClearCacheObjectTypes(); - } - else - { - _httpCache.ClearCacheObjectTypes(); - } + RuntimeCache.ClearCacheObjectTypes(); } /// @@ -202,16 +163,10 @@ namespace Umbraco.Core /// /// The start of the key [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void ClearCacheByKeySearch(string keyStartsWith) { - if (_enableCache == false) - { - _nullHttpCache.ClearCacheByKeySearch(keyStartsWith); - } - else - { - _httpCache.ClearCacheByKeySearch(keyStartsWith); - } + RuntimeCache.ClearCacheByKeySearch(keyStartsWith); } /// @@ -219,29 +174,17 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void ClearCacheByKeyExpression(string regexString) { - if (_enableCache == false) - { - _nullHttpCache.ClearCacheByKeyExpression(regexString); - } - else - { - _httpCache.ClearCacheByKeyExpression(regexString); - } + RuntimeCache.ClearCacheByKeyExpression(regexString); } [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetCacheItemsByKeySearch(string keyStartsWith) { - if (_enableCache == false) - { - return _nullHttpCache.GetCacheItemsByKeySearch(keyStartsWith); - } - else - { - return _httpCache.GetCacheItemsByKeySearch(keyStartsWith); - } + return RuntimeCache.GetCacheItemsByKeySearch(keyStartsWith); } /// @@ -251,16 +194,10 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public TT GetCacheItem(string cacheKey) { - if (_enableCache == false) - { - return _nullHttpCache.GetCacheItem(cacheKey); - } - else - { - return _httpCache.GetCacheItem(cacheKey); - } + return RuntimeCache.GetCacheItem(cacheKey); } /// @@ -271,16 +208,11 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public TT GetCacheItem(string cacheKey, Func getCacheItem) { - if (_enableCache == false) - { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem); - } - else - { - return _httpCache.GetCacheItem(cacheKey, getCacheItem); - } + return RuntimeCache.GetCacheItem(cacheKey, getCacheItem); + } /// @@ -292,17 +224,12 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public TT GetCacheItem(string cacheKey, TimeSpan timeout, Func getCacheItem) { - if (_enableCache == false) - { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem, timeout); - } - else - { - return _httpCache.GetCacheItem(cacheKey, getCacheItem, timeout); - } + return RuntimeCache.GetCacheItem(cacheKey, getCacheItem, timeout); + } /// @@ -315,18 +242,13 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public TT GetCacheItem(string cacheKey, CacheItemRemovedCallback refreshAction, TimeSpan timeout, Func getCacheItem) { - if (!_enableCache) - { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem, timeout, removedCallback: refreshAction); - } - else - { - return _httpCache.GetCacheItem(cacheKey, getCacheItem, timeout, removedCallback: refreshAction); - } + return RuntimeCache.GetCacheItem(cacheKey, getCacheItem, timeout, removedCallback: refreshAction); + } /// @@ -340,18 +262,13 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public TT GetCacheItem(string cacheKey, CacheItemPriority priority, CacheItemRemovedCallback refreshAction, TimeSpan timeout, Func getCacheItem) { - if (_enableCache == false) - { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem, timeout, false, priority, refreshAction); - } - else - { - return _httpCache.GetCacheItem(cacheKey, getCacheItem, timeout, false, priority, refreshAction); - } + return RuntimeCache.GetCacheItem(cacheKey, getCacheItem, timeout, false, priority, refreshAction); + } /// @@ -373,20 +290,13 @@ namespace Umbraco.Core TimeSpan timeout, Func getCacheItem) { - if (_enableCache == false) + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); + if (cache != null) { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem, timeout, false, priority, refreshAction, null); - } - else - { - var cache = _httpCache as HttpRuntimeCacheProvider; - if (cache != null) - { - var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); - return result == null ? default(TT) : result.TryConvertTo().Result; - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); + var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); + return result == null ? default(TT) : result.TryConvertTo().Result; } + throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); } /// @@ -404,20 +314,13 @@ namespace Umbraco.Core CacheDependency cacheDependency, Func getCacheItem) { - if (!_enableCache) + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); + if (cache != null) { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem, null, false, priority, null, null); - } - else - { - var cache = _httpCache as HttpRuntimeCacheProvider; - if (cache != null) - { - var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), null, false, priority, null, cacheDependency); - return result == null ? default(TT) : result.TryConvertTo().Result; - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); + var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), null, false, priority, null, cacheDependency); + return result == null ? default(TT) : result.TryConvertTo().Result; } + throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); } /// @@ -427,18 +330,14 @@ namespace Umbraco.Core /// /// /// + [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void InsertCacheItem(string cacheKey, CacheItemPriority priority, Func getCacheItem) { - if (_enableCache == false) - { - _nullHttpCache.InsertCacheItem(cacheKey, getCacheItem, priority: priority); - } - else - { - _httpCache.InsertCacheItem(cacheKey, getCacheItem, priority: priority); - } + RuntimeCache.InsertCacheItem(cacheKey, getCacheItem, priority: priority); + } /// @@ -449,19 +348,14 @@ namespace Umbraco.Core /// /// This will set an absolute expiration from now until the timeout /// + [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void InsertCacheItem(string cacheKey, CacheItemPriority priority, TimeSpan timeout, Func getCacheItem) { - if (_enableCache == false) - { - _nullHttpCache.InsertCacheItem(cacheKey, getCacheItem, timeout, priority: priority); - } - else - { - _httpCache.InsertCacheItem(cacheKey, getCacheItem, timeout, priority: priority); - } + RuntimeCache.InsertCacheItem(cacheKey, getCacheItem, timeout, priority: priority); } /// @@ -480,19 +374,12 @@ namespace Umbraco.Core TimeSpan timeout, Func getCacheItem) { - if (_enableCache == false) + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); + if (cache != null) { - _nullHttpCache.InsertCacheItem(cacheKey, getCacheItem, timeout, priority: priority, dependentFiles:null); - } - else - { - var cache = _httpCache as HttpRuntimeCacheProvider; - if (cache != null) - { - cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, null, cacheDependency); - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); + cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, null, cacheDependency); } + throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); } /// @@ -513,22 +400,29 @@ namespace Umbraco.Core TimeSpan? timeout, Func getCacheItem) { - if (_enableCache == false) + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); + if (cache != null) { - _nullHttpCache.InsertCacheItem(cacheKey, getCacheItem, timeout, false, priority, refreshAction, null); - } - else - { - var cache = _httpCache as HttpRuntimeCacheProvider; - if (cache != null) - { - cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); + cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); } + throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); } #endregion - } + private HttpRuntimeCacheProvider GetHttpRuntimeCacheProvider(IRuntimeCacheProvider runtimeCache) + { + HttpRuntimeCacheProvider cache; + var wrapper = RuntimeCache as IRuntimeCacheProviderWrapper; + if (wrapper != null) + { + cache = wrapper.InnerProvider as HttpRuntimeCacheProvider; + } + else + { + cache = RuntimeCache as HttpRuntimeCacheProvider; + } + return cache; + } + } } diff --git a/src/Umbraco.Core/Collections/DeepCloneableList.cs b/src/Umbraco.Core/Collections/DeepCloneableList.cs new file mode 100644 index 0000000000..5067562aa7 --- /dev/null +++ b/src/Umbraco.Core/Collections/DeepCloneableList.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Collections +{ + /// + /// A List that can be deep cloned with deep cloned elements and can reset the collection's items dirty flags + /// + /// + internal class DeepCloneableList : List, IDeepCloneable, IRememberBeingDirty + { + private readonly ListCloneBehavior _listCloneBehavior; + + public DeepCloneableList(ListCloneBehavior listCloneBehavior) + { + _listCloneBehavior = listCloneBehavior; + } + + public DeepCloneableList(IEnumerable collection, ListCloneBehavior listCloneBehavior) : base(collection) + { + _listCloneBehavior = listCloneBehavior; + } + + /// + /// Default behavior is CloneOnce + /// + /// + public DeepCloneableList(IEnumerable collection) + : this(collection, ListCloneBehavior.CloneOnce) + { + } + + /// + /// Creates a new list and adds each element as a deep cloned element if it is of type IDeepCloneable + /// + /// + public object DeepClone() + { + switch (_listCloneBehavior) + { + case ListCloneBehavior.CloneOnce: + //we are cloning once, so create a new list in none mode + // and deep clone all items into it + var newList = new DeepCloneableList(ListCloneBehavior.None); + foreach (var item in this) + { + var dc = item as IDeepCloneable; + if (dc != null) + { + newList.Add((T)dc.DeepClone()); + } + else + { + newList.Add(item); + } + } + return newList; + case ListCloneBehavior.None: + //we are in none mode, so just return a new list with the same items + return new DeepCloneableList(this, ListCloneBehavior.None); + case ListCloneBehavior.Always: + //always clone to new list + var newList2 = new DeepCloneableList(ListCloneBehavior.Always); + foreach (var item in this) + { + var dc = item as IDeepCloneable; + if (dc != null) + { + newList2.Add((T)dc.DeepClone()); + } + else + { + newList2.Add(item); + } + } + return newList2; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public bool IsDirty() + { + return this.OfType().Any(x => x.IsDirty()); + } + + public bool WasDirty() + { + return this.OfType().Any(x => x.WasDirty()); + } + + /// + /// Always returns false, the list has no properties we need to report + /// + /// + /// + public bool IsPropertyDirty(string propName) + { + return false; + } + + /// + /// Always returns false, the list has no properties we need to report + /// + /// + /// + public bool WasPropertyDirty(string propertyName) + { + return false; + } + + public void ResetDirtyProperties() + { + foreach (var dc in this.OfType()) + { + dc.ResetDirtyProperties(); + } + } + + public void ForgetPreviouslyDirtyProperties() + { + foreach (var dc in this.OfType()) + { + dc.ForgetPreviouslyDirtyProperties(); + } + } + + public void ResetDirtyProperties(bool rememberPreviouslyChangedProperties) + { + foreach (var dc in this.OfType()) + { + dc.ResetDirtyProperties(rememberPreviouslyChangedProperties); + } + } + } +} diff --git a/src/Umbraco.Core/Collections/ListCloneBehavior.cs b/src/Umbraco.Core/Collections/ListCloneBehavior.cs new file mode 100644 index 0000000000..4fe935f7ff --- /dev/null +++ b/src/Umbraco.Core/Collections/ListCloneBehavior.cs @@ -0,0 +1,20 @@ +namespace Umbraco.Core.Collections +{ + internal enum ListCloneBehavior + { + /// + /// When set, DeepClone will clone the items one time and the result list behavior will be None + /// + CloneOnce, + + /// + /// When set, DeepClone will not clone any items + /// + None, + + /// + /// When set, DeepClone will always clone all items + /// + Always + } +} \ 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 083bda6982..5d4acdc0f3 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -245,6 +245,18 @@ namespace Umbraco.Core.Configuration.UmbracoSettings } } + [ConfigurationProperty("EnableInheritedDocumentTypes")] + internal InnerTextConfigurationElement EnableInheritedDocumentTypes + { + get + { + return new OptionalInnerTextConfigurationElement( + (InnerTextConfigurationElement) this["EnableInheritedDocumentTypes"], + //set the default + true); + } + } + string IContentSection.NotificationEmailAddress { get { return Notifications.NotificationEmailAddress; } @@ -364,5 +376,10 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { return DefaultDocumentTypeProperty; } } + + bool IContentSection.EnableInheritedDocumentTypes + { + get { return EnableInheritedDocumentTypes; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index d28dede2e2..c7e3d6b836 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -53,5 +53,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings bool GlobalPreviewStorageEnabled { get; } string DefaultDocumentTypeProperty { get; } + + bool EnableInheritedDocumentTypes { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index ef33e7a966..4f6833e000 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Configuration /// Gets the version comment (like beta or RC). /// /// The version comment. - public static string CurrentComment { get { return "beta"; } } + public static string CurrentComment { get { return ""; } } // Get the version of the umbraco.dll by looking at a class in that dll // Had to do it like this due to medium trust issues, see: http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 60fba0ae40..f596820506 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -10,6 +10,12 @@ namespace Umbraco.Core /// public static class Web { + public const string UmbracoContextDataToken = "umbraco-context"; + public const string UmbracoDataToken = "umbraco"; + public const string PublishedDocumentRequestDataToken = "umbraco-doc-request"; + public const string CustomRouteDataToken = "umbraco-custom-route"; + public const string UmbracoRouteDefinitionDataToken = "umbraco-route-def"; + /// /// The preview cookie name /// diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 50a9fbf847..b4395e53eb 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -140,8 +140,11 @@ namespace Umbraco.Core { try { + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationInitialized", x.GetType()))) + { x.OnApplicationInitialized(UmbracoApplication, ApplicationContext); } + } catch (Exception ex) { ProfilingLogger.Logger.Error("An error occurred running OnApplicationInitialized for handler " + x.GetType(), ex); @@ -202,10 +205,16 @@ namespace Umbraco.Core protected virtual CacheHelper CreateApplicationCache() { var cacheHelper = new CacheHelper( - new ObjectCacheRuntimeCacheProvider(), + //we need to have the dep clone runtime cache provider to ensure + //all entities are cached properly (cloned in and cloned out) + new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()), new StaticCacheProvider(), //we have no request based cache when not running in web-based context - new NullCacheProvider()); + new NullCacheProvider(), + new IsolatedRuntimeCache(type => + //we need to have the dep clone runtime cache provider to ensure + //all entities are cached properly (cloned in and cloned out) + new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); return cacheHelper; } @@ -264,8 +273,11 @@ namespace Umbraco.Core { try { + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationStarting", x.GetType()))) + { x.OnApplicationStarting(UmbracoApplication, ApplicationContext); } + } catch (Exception ex) { ProfilingLogger.Logger.Error("An error occurred running OnApplicationStarting for handler " + x.GetType(), ex); @@ -310,7 +322,10 @@ namespace Umbraco.Core { try { + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationStarted", x.GetType()))) + { x.OnApplicationStarted(UmbracoApplication, ApplicationContext); + } } catch (Exception ex) { diff --git a/src/Umbraco.Core/DisposableTimer.cs b/src/Umbraco.Core/DisposableTimer.cs index 816256360a..c7e8874449 100644 --- a/src/Umbraco.Core/DisposableTimer.cs +++ b/src/Umbraco.Core/DisposableTimer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Web; @@ -12,6 +13,12 @@ namespace Umbraco.Core /// public class DisposableTimer : DisposableObject { + private readonly ILogger _logger; + private readonly LogType? _logType; + private readonly IProfiler _profiler; + private readonly Type _loggerType; + private readonly string _endMessage; + private readonly IDisposable _profilerStep; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private readonly Action _callback; @@ -25,25 +32,12 @@ namespace Umbraco.Core if (logger == null) throw new ArgumentNullException("logger"); if (loggerType == null) throw new ArgumentNullException("loggerType"); - _callback = x => - { - if (profiler != null) - { - profiler.DisposeIfDisposable(); - } - switch (logType) - { - case LogType.Debug: - logger.Debug(loggerType, () => endMessage + " (took " + x + "ms)"); - break; - case LogType.Info: - logger.Info(loggerType, () => endMessage + " (took " + x + "ms)"); - break; - default: - throw new ArgumentOutOfRangeException("logType"); - } - - }; + _logger = logger; + _logType = logType; + _profiler = profiler; + _loggerType = loggerType; + _endMessage = endMessage; + switch (logType) { case LogType.Debug: @@ -58,7 +52,7 @@ namespace Umbraco.Core if (profiler != null) { - profiler.Step(loggerType, startMessage); + _profilerStep = profiler.Step(loggerType, startMessage); } } @@ -223,7 +217,36 @@ namespace Umbraco.Core /// protected override void DisposeResources() { - _callback.Invoke(Stopwatch.ElapsedMilliseconds); + if (_profiler != null) + { + _profiler.DisposeIfDisposable(); + } + + if (_profilerStep != null) + { + _profilerStep.Dispose(); + } + + if (_logType.HasValue && _endMessage.IsNullOrWhiteSpace() == false && _loggerType != null && _logger != null) + { + switch (_logType) + { + case LogType.Debug: + _logger.Debug(_loggerType, () => _endMessage + " (took " + Stopwatch.ElapsedMilliseconds + "ms)"); + break; + case LogType.Info: + _logger.Info(_loggerType, () => _endMessage + " (took " + Stopwatch.ElapsedMilliseconds + "ms)"); + break; + default: + throw new ArgumentOutOfRangeException("logType"); + } + } + + if (_callback != null) + { + _callback.Invoke(Stopwatch.ElapsedMilliseconds); + } + } } diff --git a/src/Umbraco.Core/Dynamics/CaseInsensitiveDynamicObject.cs b/src/Umbraco.Core/Dynamics/CaseInsensitiveDynamicObject.cs new file mode 100644 index 0000000000..43d7adc647 --- /dev/null +++ b/src/Umbraco.Core/Dynamics/CaseInsensitiveDynamicObject.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Reflection; + +namespace Umbraco.Core.Dynamics +{ + /// + /// This will check enable dynamic access to properties and methods in a case insensitive manner + /// + /// + /// + /// This works by using reflection on the type - the reflection lookup is lazy so it will not execute unless a dynamic method needs to be accessed + /// + public abstract class CaseInsensitiveDynamicObject : DynamicObject + where T: class + { + /// + /// Used for dynamic access for case insensitive property access + /// ` + private static readonly Lazy>> CaseInsensitivePropertyAccess = new Lazy>>(() => + { + var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public) + .DistinctBy(x => x.Name); + return props.Select(propInfo => + { + var name = propInfo.Name.ToLowerInvariant(); + Func getVal = propInfo.GetValue; + return new KeyValuePair>(name, getVal); + + }).ToDictionary(x => x.Key, x => x.Value); + }); + + /// + /// Used for dynamic access for case insensitive property access + /// + private static readonly Lazy>>> CaseInsensitiveMethodAccess + = new Lazy>>>(() => + { + var props = typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Where(x => x.IsSpecialName == false && x.IsVirtual == false) + .DistinctBy(x => x.Name); + return props.Select(methodInfo => + { + var name = methodInfo.Name.ToLowerInvariant(); + Func getVal = methodInfo.Invoke; + var val = new Tuple>(methodInfo.GetParameters(), getVal); + return new KeyValuePair>>(name, val); + + }).ToDictionary(x => x.Key, x => x.Value); + }); + + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + var name = binder.Name.ToLowerInvariant(); + if (CaseInsensitiveMethodAccess.Value.ContainsKey(name) == false) + return base.TryInvokeMember(binder, args, out result); + + var val = CaseInsensitiveMethodAccess.Value[name]; + var parameters = val.Item1; + var callback = val.Item2; + var fullArgs = new List(args); + if (args.Length <= parameters.Length) + { + //need to fill them up if they're optional + for (var i = args.Length; i < parameters.Length; i++) + { + if (parameters[i].IsOptional) + { + fullArgs.Add(parameters[i].DefaultValue); + } + } + if (fullArgs.Count == parameters.Length) + { + result = callback((T)(object)this, fullArgs.ToArray()); + return true; + } + } + return base.TryInvokeMember(binder, args, out result); + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + var name = binder.Name.ToLowerInvariant(); + if (CaseInsensitivePropertyAccess.Value.ContainsKey(name) == false) + return base.TryGetMember(binder, out result); + + result = CaseInsensitivePropertyAccess.Value[name]((T)(object)this); + return true; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Dynamics/DynamicXmlConverter.cs b/src/Umbraco.Core/Dynamics/DynamicXmlConverter.cs index f4ace38465..6af13d6887 100644 --- a/src/Umbraco.Core/Dynamics/DynamicXmlConverter.cs +++ b/src/Umbraco.Core/Dynamics/DynamicXmlConverter.cs @@ -64,7 +64,7 @@ namespace Umbraco.Core.Dynamics /// public class DynamicXmlConverter : TypeConverter { - public override bool CanConvertTo(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { var convertableTypes = new[] { @@ -78,8 +78,8 @@ namespace Umbraco.Core.Dynamics typeof(RawXmlDocument) }; - return convertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, sourceType)) - || base.CanConvertFrom(context, sourceType); + return convertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType)) + || base.CanConvertFrom(context, destinationType); } public override object ConvertTo( diff --git a/src/Umbraco.Core/Events/CancellableEventArgs.cs b/src/Umbraco.Core/Events/CancellableEventArgs.cs index 506ba1c22e..72cef19f7a 100644 --- a/src/Umbraco.Core/Events/CancellableEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableEventArgs.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Security.Permissions; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.Events { @@ -11,11 +14,19 @@ namespace Umbraco.Core.Events { private bool _cancel; + public CancellableEventArgs(bool canCancel, EventMessages messages, IDictionary additionalData) + { + CanCancel = canCancel; + Messages = messages; + AdditionalData = new ReadOnlyDictionary(additionalData); + } + public CancellableEventArgs(bool canCancel, EventMessages eventMessages) { if (eventMessages == null) throw new ArgumentNullException("eventMessages"); CanCancel = canCancel; Messages = eventMessages; + AdditionalData = new ReadOnlyDictionary(new Dictionary()); } public CancellableEventArgs(bool canCancel) @@ -23,6 +34,7 @@ namespace Umbraco.Core.Events CanCancel = canCancel; //create a standalone messages Messages = new EventMessages(); + AdditionalData = new ReadOnlyDictionary(new Dictionary()); } public CancellableEventArgs(EventMessages eventMessages) @@ -78,5 +90,14 @@ namespace Umbraco.Core.Events /// Returns the EventMessages object which is used to add messages to the message collection for this event /// public EventMessages Messages { get; private set; } + + /// + /// In some cases raised evens might need to contain additional arbitrary readonly data which can be read by event subscribers + /// + /// + /// This allows for a bit of flexibility in our event raising - it's not pretty but we need to maintain backwards compatibility + /// so we cannot change the strongly typed nature for some events. + /// + public ReadOnlyDictionary AdditionalData { get; private set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs index 726d32d7b0..a05f09ece5 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Security.Permissions; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -11,8 +12,13 @@ namespace Umbraco.Core.Events [HostProtection(SecurityAction.LinkDemand, SharedState = true)] public class CancellableObjectEventArgs : CancellableEventArgs { + public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(canCancel, messages, additionalData) + { + EventObject = eventObject; + } - public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages eventMessages) + public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages eventMessages) : base(canCancel, eventMessages) { EventObject = eventObject; diff --git a/src/Umbraco.Core/Events/EventExtensions.cs b/src/Umbraco.Core/Events/EventExtensions.cs index 8c645aead6..700a02457a 100644 --- a/src/Umbraco.Core/Events/EventExtensions.cs +++ b/src/Umbraco.Core/Events/EventExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace Umbraco.Core.Events diff --git a/src/Umbraco.Core/Events/SaveEventArgs.cs b/src/Umbraco.Core/Events/SaveEventArgs.cs index b84e28285e..dafd326e1c 100644 --- a/src/Umbraco.Core/Events/SaveEventArgs.cs +++ b/src/Umbraco.Core/Events/SaveEventArgs.cs @@ -4,6 +4,18 @@ namespace Umbraco.Core.Events { public class SaveEventArgs : CancellableObjectEventArgs> { + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) + { + } + /// /// Constructor accepting multiple entities that are used in the saving operation /// @@ -25,12 +37,24 @@ namespace Umbraco.Core.Events { } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public SaveEventArgs(TEntity eventObject, EventMessages eventMessages) + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + /// + public SaveEventArgs(TEntity eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(new List { eventObject }, canCancel, messages, additionalData) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public SaveEventArgs(TEntity eventObject, EventMessages eventMessages) : base(new List { eventObject }, eventMessages) { } diff --git a/src/Umbraco.Core/IO/ViewHelper.cs b/src/Umbraco.Core/IO/ViewHelper.cs index 28dad49dc0..933a9d2956 100644 --- a/src/Umbraco.Core/IO/ViewHelper.cs +++ b/src/Umbraco.Core/IO/ViewHelper.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.IO { - internal class ViewHelper + public class ViewHelper { private readonly IFileSystem _viewFileSystem; @@ -66,17 +66,58 @@ namespace Umbraco.Core.IO return viewContent; } - internal static string GetDefaultFileContent(string layoutPageAlias = null) + public static string GetDefaultFileContent(string layoutPageAlias = null, string modelClassName = null, string modelNamespace = null, string modelNamespaceAlias = null) { - var design = @"@inherits Umbraco.Web.Mvc.UmbracoTemplatePage -@{ - Layout = null; -}"; + var content = new StringBuilder(); - if (layoutPageAlias.IsNullOrWhiteSpace() == false) - design = design.Replace("null", string.Format("\"{0}.cshtml\"", layoutPageAlias)); + if (string.IsNullOrWhiteSpace(modelNamespaceAlias)) + modelNamespaceAlias = "ContentModels"; - return design; + // either + // @inherits Umbraco.Web.Mvc.UmbracoTemplatePage + // @inherits Umbraco.Web.Mvc.UmbracoTemplatePage + // @inherits Umbraco.Web.Mvc.UmbracoTemplatePage + content.Append("@inherits Umbraco.Web.Mvc.UmbracoTemplatePage"); + if (modelClassName.IsNullOrWhiteSpace() == false) + { + content.Append("<"); + if (modelNamespace.IsNullOrWhiteSpace() == false) + { + content.Append(modelNamespaceAlias); + content.Append("."); + } + content.Append(modelClassName); + content.Append(">"); + } + content.Append("\r\n"); + + // if required, add + // @using ContentModels = ModelNamespace; + if (modelClassName.IsNullOrWhiteSpace() == false && modelNamespace.IsNullOrWhiteSpace() == false) + { + content.Append("@using "); + content.Append(modelNamespaceAlias); + content.Append(" = "); + content.Append(modelNamespace); + content.Append(";\r\n"); + } + + // either + // Layout = null; + // Layout = "layoutPage.cshtml"; + content.Append("@{\r\n\tLayout = "); + if (layoutPageAlias.IsNullOrWhiteSpace()) + { + content.Append("null"); + } + else + { + content.Append("\""); + content.Append(layoutPageAlias); + content.Append(".cshtml\""); + } + content.Append(";\r\n}"); + return content.ToString(); } private string SaveTemplateToFile(ITemplate template) diff --git a/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs b/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs index 452c05adb8..1be504ea45 100644 --- a/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs +++ b/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs @@ -75,7 +75,7 @@ namespace Umbraco.Core.Media.Exif { if (items.ContainsKey (key)) items.Remove (key); - if (key == ExifTag.WindowsTitle || key == ExifTag.WindowsTitle || key == ExifTag.WindowsComment || key == ExifTag.WindowsAuthor || key == ExifTag.WindowsKeywords || key == ExifTag.WindowsSubject) { + if (key == ExifTag.WindowsTitle || key == ExifTag.WindowsComment || key == ExifTag.WindowsAuthor || key == ExifTag.WindowsKeywords || key == ExifTag.WindowsSubject) { items.Add (key, new WindowsByteString (key, value)); } else { items.Add (key, new ExifAscii (key, value, parent.Encoding)); diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 2238260611..b8e96c2793 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -340,18 +340,6 @@ namespace Umbraco.Core.Models } } - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if(Key == Guid.Empty) - Key = Guid.NewGuid(); - } - /// /// Method to call when Entity is being updated /// diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 6beead06e6..b3d0f693d9 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -367,8 +367,27 @@ namespace Umbraco.Core.Models /// Value to set for the Property public virtual void SetPropertyValue(string propertyTypeAlias, long value) { - string val = value.ToString(); - SetValueOnProperty(propertyTypeAlias, val); + SetValueOnProperty(propertyTypeAlias, value); + } + + /// + /// Sets the value of a Property + /// + /// Alias of the PropertyType + /// Value to set for the Property + public virtual void SetPropertyValue(string propertyTypeAlias, decimal value) + { + SetValueOnProperty(propertyTypeAlias, value); + } + + /// + /// Sets the value of a Property + /// + /// Alias of the PropertyType + /// Value to set for the Property + public virtual void SetPropertyValue(string propertyTypeAlias, double value) + { + SetValueOnProperty(propertyTypeAlias, value); } /// diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 355d724fbe..0926e48e31 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -53,6 +53,9 @@ namespace Umbraco.Core.Models /// /// Gets or sets the alias of the default Template. + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// we should not store direct entity /// [IgnoreDataMember] public ITemplate DefaultTemplate @@ -79,6 +82,9 @@ namespace Umbraco.Core.Models /// /// Gets or Sets a list of Templates which are allowed for the ContentType + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// we should not store direct entity /// [DataMember] public IEnumerable AllowedTemplates @@ -137,19 +143,6 @@ namespace Umbraco.Core.Models return result; } - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } - - /// /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset /// diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs new file mode 100644 index 0000000000..b1d2b45dc5 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Core.Models +{ + /// + /// Used when determining available compositions for a given content type + /// + internal class ContentTypeAvailableCompositionsResult + { + public ContentTypeAvailableCompositionsResult(IContentTypeComposition composition, bool allowed) + { + Composition = composition; + Allowed = allowed; + } + + public IContentTypeComposition Composition { get; private set; } + public bool Allowed { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs new file mode 100644 index 0000000000..653d7a10a9 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models +{ + /// + /// Used when determining available compositions for a given content type + /// + internal class ContentTypeAvailableCompositionsResults + { + public ContentTypeAvailableCompositionsResults() + { + Ancestors = Enumerable.Empty(); + Results = Enumerable.Empty(); + } + + public ContentTypeAvailableCompositionsResults(IEnumerable ancestors, IEnumerable results) + { + Ancestors = ancestors; + Results = results; + } + + public IEnumerable Ancestors { get; private set; } + public IEnumerable Results { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index a83defb7b5..7218a2421d 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -452,11 +452,6 @@ namespace Umbraco.Core.Models /// Returns True if PropertyType was added, otherwise False public bool AddPropertyType(PropertyType propertyType) { - if (propertyType.HasIdentity == false) - { - propertyType.Key = Guid.NewGuid(); - } - if (PropertyTypeExists(propertyType.Alias) == false) { _propertyTypes.Add(propertyType); diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index 5ac21885d7..4cf4a08bf1 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -23,8 +23,8 @@ namespace Umbraco.Core.Models protected ContentTypeCompositionBase(IContentTypeComposition parent) : this(parent, null) - { - } + { + } protected ContentTypeCompositionBase(IContentTypeComposition parent, string alias) : base(parent, alias) @@ -122,10 +122,10 @@ namespace Umbraco.Core.Models return false; RemovedContentTypeKeyTracker.Add(contentTypeComposition.Id); - + //If the ContentType we are removing has Compositions of its own these needs to be removed as well var compositionIdsToRemove = contentTypeComposition.CompositionIds().ToList(); - if(compositionIdsToRemove.Any()) + if (compositionIdsToRemove.Any()) RemovedContentTypeKeyTracker.AddRange(compositionIdsToRemove); OnPropertyChanged(ContentTypeCompositionSelector); @@ -210,16 +210,13 @@ namespace Umbraco.Core.Models /// Returns True if PropertyType was added, otherwise False public override bool AddPropertyType(PropertyType propertyType, string propertyGroupName) { - if (propertyType.HasIdentity == false) - propertyType.Key = Guid.NewGuid(); - // ensure no duplicate alias - over all composition properties if (PropertyTypeExists(propertyType.Alias)) return false; // get and ensure a group local to this content type - var group = PropertyGroups.Contains(propertyGroupName) - ? PropertyGroups[propertyGroupName] + var group = PropertyGroups.Contains(propertyGroupName) + ? PropertyGroups[propertyGroupName] : AddAndReturnPropertyGroup(propertyGroupName); if (group == null) return false; diff --git a/src/Umbraco.Core/Models/DataTypeDefinition.cs b/src/Umbraco.Core/Models/DataTypeDefinition.cs index 5cf3976613..a52d305278 100644 --- a/src/Umbraco.Core/Models/DataTypeDefinition.cs +++ b/src/Umbraco.Core/Models/DataTypeDefinition.cs @@ -228,13 +228,5 @@ namespace Umbraco.Core.Models { get { return _additionalData; } } - - internal override void AddingEntity() - { - base.AddingEntity(); - - if(Key == default(Guid)) - Key = Guid.NewGuid(); - } } } diff --git a/src/Umbraco.Core/Models/DeepCloneHelper.cs b/src/Umbraco.Core/Models/DeepCloneHelper.cs index 7523555c24..c1b45f63ce 100644 --- a/src/Umbraco.Core/Models/DeepCloneHelper.cs +++ b/src/Umbraco.Core/Models/DeepCloneHelper.cs @@ -9,10 +9,30 @@ namespace Umbraco.Core.Models { public static class DeepCloneHelper { + /// + /// Stores the metadata for the properties for a given type so we know how to create them + /// + private struct ClonePropertyInfo + { + public ClonePropertyInfo(PropertyInfo propertyInfo) : this() + { + if (propertyInfo == null) throw new ArgumentNullException("propertyInfo"); + PropertyInfo = propertyInfo; + } + + public PropertyInfo PropertyInfo { get; private set; } + public bool IsDeepCloneable { get; set; } + public Type GenericListType { get; set; } + public bool IsList + { + get { return GenericListType != null; } + } + } + /// /// Used to avoid constant reflection (perf) /// - private static readonly ConcurrentDictionary PropCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary PropCache = new ConcurrentDictionary(); /// /// Used to deep clone any reference properties on the object (should be done after a MemberwiseClone for which the outcome is 'output') @@ -30,81 +50,99 @@ namespace Umbraco.Core.Models throw new InvalidOperationException("Both the input and output types must be the same"); } + //get the property metadata from cache so we only have to figure this out once per type var refProperties = PropCache.GetOrAdd(inputType, type => inputType.GetProperties() - .Where(x => - //is not attributed with the ignore clone attribute - x.GetCustomAttribute() == null + .Select(propertyInfo => + { + if ( + //is not attributed with the ignore clone attribute + propertyInfo.GetCustomAttribute() != null //reference type but not string - && x.PropertyType.IsValueType == false && x.PropertyType != typeof (string) + || propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof (string) //settable - && x.CanWrite + || propertyInfo.CanWrite == false //non-indexed - && x.GetIndexParameters().Any() == false) + || propertyInfo.GetIndexParameters().Any()) + { + return null; + } + + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + { + return new ClonePropertyInfo(propertyInfo) { IsDeepCloneable = true }; + } + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) + && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) + { + if (propertyInfo.PropertyType.IsGenericType + && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))) + { + //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> + var genericType = typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); + return new ClonePropertyInfo(propertyInfo) { GenericListType = genericType }; + } + if (propertyInfo.PropertyType.IsArray + || (propertyInfo.PropertyType.IsInterface && propertyInfo.PropertyType.IsGenericType == false)) + { + //if its an array, we'll create a list to work with first and then convert to array later + //otherwise if its just a regular derivitave of IEnumerable, we can use a list too + return new ClonePropertyInfo(propertyInfo) { GenericListType = typeof(List) }; + } + //skip instead of trying to create instance of abstract or interface + if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) + { + return null; + } + + //its a custom IEnumerable, we'll try to create it + try + { + var custom = Activator.CreateInstance(propertyInfo.PropertyType); + //if it's an IList we can work with it, otherwise we cannot + var newList = custom as IList; + if (newList == null) + { + return null; + } + return new ClonePropertyInfo(propertyInfo) {GenericListType = propertyInfo.PropertyType}; + } + catch (Exception) + { + //could not create this type so we'll skip it + return null; + } + } + return new ClonePropertyInfo(propertyInfo); + }) + .Where(x => x.HasValue) + .Select(x => x.Value) .ToArray()); - foreach (var propertyInfo in refProperties) + foreach (var clonePropertyInfo in refProperties) { - if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + if (clonePropertyInfo.IsDeepCloneable) { //this ref property is also deep cloneable so clone it - var result = (IDeepCloneable)propertyInfo.GetValue(input, null); + var result = (IDeepCloneable)clonePropertyInfo.PropertyInfo.GetValue(input, null); if (result != null) { //set the cloned value to the property - propertyInfo.SetValue(output, result.DeepClone(), null); + clonePropertyInfo.PropertyInfo.SetValue(output, result.DeepClone(), null); } } - else if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) - && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) + else if (clonePropertyInfo.IsList) { - IList newList; - if (propertyInfo.PropertyType.IsGenericType - && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))) - { - //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> - var genericType = typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); - newList = (IList)Activator.CreateInstance(genericType); - } - else if (propertyInfo.PropertyType.IsArray - || (propertyInfo.PropertyType.IsInterface && propertyInfo.PropertyType.IsGenericType == false)) - { - //if its an array, we'll create a list to work with first and then convert to array later - //otherwise if its just a regular derivitave of IEnumerable, we can use a list too - newList = new List(); - } - else - { - //skip instead of trying to create instance of abstract or interface - if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) - { - continue; - } - - //its a custom IEnumerable, we'll try to create it - try - { - var custom = Activator.CreateInstance(propertyInfo.PropertyType); - //if it's an IList we can work with it, otherwise we cannot - newList = custom as IList; - if (newList == null) - { - continue; - } - } - catch (Exception) - { - //could not create this type so we'll skip it - continue; - } - } - - var enumerable = (IEnumerable)propertyInfo.GetValue(input, null); + var enumerable = (IEnumerable)clonePropertyInfo.PropertyInfo.GetValue(input, null); if (enumerable == null) continue; + var newList = (IList)Activator.CreateInstance(clonePropertyInfo.GenericListType); + var isUsableType = true; //now clone each item @@ -136,21 +174,21 @@ namespace Umbraco.Core.Models continue; } - if (propertyInfo.PropertyType.IsArray) + if (clonePropertyInfo.PropertyInfo.PropertyType.IsArray) { //need to convert to array - var arr = (object[])Activator.CreateInstance(propertyInfo.PropertyType, newList.Count); + var arr = (object[])Activator.CreateInstance(clonePropertyInfo.PropertyInfo.PropertyType, newList.Count); for (int i = 0; i < newList.Count; i++) { arr[i] = newList[i]; } //set the cloned collection - propertyInfo.SetValue(output, arr, null); + clonePropertyInfo.PropertyInfo.SetValue(output, arr, null); } else { //set the cloned collection - propertyInfo.SetValue(output, newList, null); + clonePropertyInfo.PropertyInfo.SetValue(output, newList, null); } } diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs index 7e59726c80..749c629d19 100644 --- a/src/Umbraco.Core/Models/DictionaryItem.cs +++ b/src/Umbraco.Core/Models/DictionaryItem.cs @@ -14,6 +14,7 @@ namespace Umbraco.Core.Models [DataContract(IsReference = true)] public class DictionaryItem : Entity, IDictionaryItem { + public Func GetLanguage { get; set; } private Guid? _parentId; private string _itemKey; private IEnumerable _translations; @@ -78,7 +79,17 @@ namespace Umbraco.Core.Models { SetPropertyValueAndDetectChanges(o => { - _translations = value; + var asArray = value.ToArray(); + //ensure the language callback is set on each translation + if (GetLanguage != null) + { + foreach (var translation in asArray.OfType()) + { + translation.GetLanguage = GetLanguage; + } + } + + _translations = asArray; return _translations; }, _translations, TranslationsSelector, //Custom comparer for enumerable @@ -87,16 +98,5 @@ namespace Umbraco.Core.Models enumerable => enumerable.GetHashCode())); } } - - /// - /// Method to call before inserting a new entity in the db - /// - internal override void AddingEntity() - { - base.AddingEntity(); - - Key = Guid.NewGuid(); - } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/DictionaryTranslation.cs b/src/Umbraco.Core/Models/DictionaryTranslation.cs index 782ff14413..59f96dbe85 100644 --- a/src/Umbraco.Core/Models/DictionaryTranslation.cs +++ b/src/Umbraco.Core/Models/DictionaryTranslation.cs @@ -13,13 +13,18 @@ namespace Umbraco.Core.Models [DataContract(IsReference = true)] public class DictionaryTranslation : Entity, IDictionaryTranslation { + internal Func GetLanguage { get; set; } + private ILanguage _language; private string _value; + //note: this will be memberwise cloned + private int _languageId; public DictionaryTranslation(ILanguage language, string value) { if (language == null) throw new ArgumentNullException("language"); _language = language; + _languageId = _language.Id; _value = value; } @@ -27,6 +32,20 @@ namespace Umbraco.Core.Models { if (language == null) throw new ArgumentNullException("language"); _language = language; + _languageId = _language.Id; + _value = value; + Key = uniqueId; + } + + internal DictionaryTranslation(int languageId, string value) + { + _languageId = languageId; + _value = value; + } + + internal DictionaryTranslation(int languageId, string value, Guid uniqueId) + { + _languageId = languageId; _value = value; Key = uniqueId; } @@ -37,20 +56,43 @@ namespace Umbraco.Core.Models /// /// Gets or sets the for the translation /// + /// + /// Marked as DoNotClone - TODO: this member shouldn't really exist here in the first place, the DictionaryItem + /// class will have a deep hierarchy of objects which all get deep cloned which we don't want. This should have simply + /// just referenced a language ID not the actual language object. In v8 we need to fix this. + /// We're going to have to do the same hacky stuff we had to do with the Template/File contents so that this is returned + /// on a callback. + /// [DataMember] + [DoNotClone] public ILanguage Language { - get { return _language; } + get + { + if (_language != null) + return _language; + + // else, must lazy-load + if (GetLanguage != null && _languageId > 0) + _language = GetLanguage(_languageId); + return _language; + } set { SetPropertyValueAndDetectChanges(o => { _language = value; + _languageId = _language == null ? -1 : _language.Id; return _language; }, _language, LanguageSelector); } } + public int LanguageId + { + get { return _languageId; } + } + /// /// Gets or sets the translated text /// @@ -68,5 +110,23 @@ namespace Umbraco.Core.Models } } + public override object DeepClone() + { + var clone = (DictionaryTranslation)base.DeepClone(); + + // clear fields that were memberwise-cloned and that we don't want to clone + clone._language = null; + + // turn off change tracking + clone.DisableChangeTracking(); + + // this shouldn't really be needed since we're not tracking + clone.ResetDirtyProperties(false); + + // re-enable tracking + clone.EnableChangeTracking(); + + return clone; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/EntityBase/Entity.cs b/src/Umbraco.Core/Models/EntityBase/Entity.cs index eeacb771a9..c4838dfd0a 100644 --- a/src/Umbraco.Core/Models/EntityBase/Entity.cs +++ b/src/Umbraco.Core/Models/EntityBase/Entity.cs @@ -62,9 +62,9 @@ namespace Umbraco.Core.Models.EntityBase { get { + // if an entity does NOT have a UniqueId yet, assign one now if (_key == Guid.Empty) - return _id.ToGuid(); - + _key = Guid.NewGuid(); return _key; } set @@ -136,6 +136,7 @@ namespace Umbraco.Core.Models.EntityBase { _hasIdentity = false; _id = default(int); + _key = Guid.Empty; } /// @@ -242,6 +243,7 @@ namespace Umbraco.Core.Models.EntityBase { //Memberwise clone on Entity will work since it doesn't have any deep elements // for any sub class this will work for standard properties as well that aren't complex object's themselves. + var ignored = this.Key; // ensure that 'this' has a key, before cloning var clone = (Entity)MemberwiseClone(); //ensure the clone has it's own dictionaries clone.ResetChangeTrackingCollections(); diff --git a/src/Umbraco.Core/Models/IDictionaryTranslation.cs b/src/Umbraco.Core/Models/IDictionaryTranslation.cs index cf813bf72f..25aa1e4395 100644 --- a/src/Umbraco.Core/Models/IDictionaryTranslation.cs +++ b/src/Umbraco.Core/Models/IDictionaryTranslation.cs @@ -12,6 +12,8 @@ namespace Umbraco.Core.Models [DataMember] ILanguage Language { get; set; } + int LanguageId { get; } + /// /// Gets or sets the translated text /// diff --git a/src/Umbraco.Core/Models/Media.cs b/src/Umbraco.Core/Models/Media.cs index 4f922d28cf..a7e794a400 100644 --- a/src/Umbraco.Core/Models/Media.cs +++ b/src/Umbraco.Core/Models/Media.cs @@ -118,17 +118,5 @@ namespace Umbraco.Core.Models //The Media Recycle Bin Id is -21 so we correct that here ParentId = parentId == -20 ? -21 : parentId; } - - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/MediaExtensions.cs b/src/Umbraco.Core/Models/MediaExtensions.cs new file mode 100644 index 0000000000..1f2e1b62b2 --- /dev/null +++ b/src/Umbraco.Core/Models/MediaExtensions.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Core.Models +{ + internal static class MediaExtensions + { + /// + /// Hack: we need to put this in a real place, this is currently just used to render the urls for a media item in the back office + /// + /// + public static string GetUrl(this IMedia media, string propertyAlias, ILogger logger) + { + var propertyType = media.PropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyAlias)); + if (propertyType != null) + { + var val = media.Properties[propertyType]; + if (val != null) + { + var jsonString = val.Value as string; + if (jsonString != null) + { + if (propertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias) + { + if (jsonString.DetectIsJson()) + { + try + { + var json = JsonConvert.DeserializeObject(jsonString); + if (json["src"] != null) + { + return json["src"].Value(); + } + } + catch (Exception ex) + { + logger.Error("Could not parse the string " + jsonString + " to a json object", ex); + return string.Empty; + } + } + else + { + return jsonString; + } + } + else if (propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias) + { + return jsonString; + } + //hrm, without knowing what it is, just adding a string here might not be very nice + } + } + } + return string.Empty; + } + + /// + /// Hack: we need to put this in a real place, this is currently just used to render the urls for a media item in the back office + /// + /// + public static string[] GetUrls(this IMedia media, IContentSection contentSection, ILogger logger) + { + var links = new List(); + var autoFillProperties = contentSection.ImageAutoFillProperties.ToArray(); + if (autoFillProperties.Any()) + { + links.AddRange( + autoFillProperties + .Select(field => media.GetUrl(field.Alias, logger)) + .Where(link => link.IsNullOrWhiteSpace() == false)); + } + return links.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/MediaType.cs b/src/Umbraco.Core/Models/MediaType.cs index 052e231136..c8e2915afd 100644 --- a/src/Umbraco.Core/Models/MediaType.cs +++ b/src/Umbraco.Core/Models/MediaType.cs @@ -38,26 +38,5 @@ namespace Umbraco.Core.Models : base(parent, alias) { } - - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } - - /// - /// Method to call when Entity is being updated - /// - /// Modified Date is set and a new Version guid is set - internal override void UpdatingEntity() - { - base.UpdatingEntity(); - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 7788fadf75..70c21e4307 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -509,11 +509,8 @@ namespace Umbraco.Core.Models { base.AddingEntity(); - if (Key == Guid.Empty) - { - Key = Guid.NewGuid(); + if (ProviderUserKey == null) ProviderUserKey = Key; - } } /// diff --git a/src/Umbraco.Core/Models/MemberGroup.cs b/src/Umbraco.Core/Models/MemberGroup.cs index e52448a11d..ff7e05be9e 100644 --- a/src/Umbraco.Core/Models/MemberGroup.cs +++ b/src/Umbraco.Core/Models/MemberGroup.cs @@ -60,17 +60,5 @@ namespace Umbraco.Core.Models } public IDictionary AdditionalData { get; private set; } - - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/MemberType.cs b/src/Umbraco.Core/Models/MemberType.cs index 74a879be81..9000a33b54 100644 --- a/src/Umbraco.Core/Models/MemberType.cs +++ b/src/Umbraco.Core/Models/MemberType.cs @@ -131,26 +131,5 @@ namespace Umbraco.Core.Models MemberTypePropertyTypes.Add(propertyTypeAlias, tuple); } } - - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } - - /// - /// Method to call when Entity is being updated - /// - /// Modified Date is set and a new Version guid is set - internal override void UpdatingEntity() - { - base.UpdatingEntity(); - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index 27d1cd2121..9e12d6ab57 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -108,18 +108,6 @@ namespace Umbraco.Core.Models } } - /// - /// Method to call on entity saved when first added - /// - internal override void AddingEntity() - { - if (Key == default(Guid)) - { - Key = Guid.NewGuid(); - } - base.AddingEntity(); - } - [DataMember] public int LoginNodeId { diff --git a/src/Umbraco.Core/Models/PublicAccessRule.cs b/src/Umbraco.Core/Models/PublicAccessRule.cs index 484652e8bd..c785d028d0 100644 --- a/src/Umbraco.Core/Models/PublicAccessRule.cs +++ b/src/Umbraco.Core/Models/PublicAccessRule.cs @@ -28,18 +28,6 @@ namespace Umbraco.Core.Models public Guid AccessEntryId { get; internal set; } - /// - /// Method to call on entity saved when first added - /// - internal override void AddingEntity() - { - if (Key == default(Guid)) - { - Key = Guid.NewGuid(); - } - base.AddingEntity(); - } - public string RuleValue { get { return _ruleValue; } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index 769c876804..1459264972 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -164,6 +164,9 @@ namespace Umbraco.Core.Models.PublishedContent private void InitializeConverters() { + //TODO: Look at optimizing this method, it gets run for every property type for the document being rendered at startup, + // every precious second counts! + var converters = PropertyValueConvertersResolver.Current.Converters.ToArray(); var defaultConvertersWithAttributes = PropertyValueConvertersResolver.Current.DefaultConverters; @@ -229,13 +232,13 @@ namespace Umbraco.Core.Models.PublishedContent { _sourceCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.Source); _objectCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.Object); - _objectCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.XPath); + _xpathCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.XPath); } else { _sourceCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.Source); _objectCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.Object); - _objectCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.XPath); + _xpathCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.XPath); } if (_objectCacheLevel < _sourceCacheLevel) _objectCacheLevel = _sourceCacheLevel; if (_xpathCacheLevel < _sourceCacheLevel) _xpathCacheLevel = _sourceCacheLevel; diff --git a/src/Umbraco.Core/Models/Rdbms/NodeDto.cs b/src/Umbraco.Core/Models/Rdbms/NodeDto.cs index 7003c58e77..c5fac092df 100644 --- a/src/Umbraco.Core/Models/Rdbms/NodeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/NodeDto.cs @@ -10,12 +10,6 @@ namespace Umbraco.Core.Models.Rdbms [ExplicitColumns] internal class NodeDto { - public NodeDto() - { - //By default, always generate a new guid - UniqueId = Guid.NewGuid(); - } - public const int NodeIdSeed = 1050; [Column("id")] diff --git a/src/Umbraco.Core/Models/Rdbms/PropertyTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/PropertyTypeDto.cs index 74a6d34289..2be4b24157 100644 --- a/src/Umbraco.Core/Models/Rdbms/PropertyTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/PropertyTypeDto.cs @@ -10,12 +10,6 @@ namespace Umbraco.Core.Models.Rdbms [ExplicitColumns] internal class PropertyTypeDto { - public PropertyTypeDto() - { - //by default always create a new guid - UniqueId = Guid.NewGuid(); - } - [Column("id")] [PrimaryKeyColumn(IdentitySeed = 50)] public int Id { get; set; } diff --git a/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupDto.cs index 42abd9ed49..68de420a97 100644 --- a/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupDto.cs @@ -11,12 +11,6 @@ namespace Umbraco.Core.Models.Rdbms [ExplicitColumns] internal class PropertyTypeGroupDto { - public PropertyTypeGroupDto() - { - //by default always create a new guid - UniqueId = Guid.NewGuid(); - } - [Column("id")] [PrimaryKeyColumn(IdentitySeed = 12)] public int Id { get; set; } diff --git a/src/Umbraco.Core/Models/Template.cs b/src/Umbraco.Core/Models/Template.cs index b5730ae11a..587e0587cd 100644 --- a/src/Umbraco.Core/Models/Template.cs +++ b/src/Umbraco.Core/Models/Template.cs @@ -112,18 +112,6 @@ namespace Umbraco.Core.Models return ApplicationContext.Current.Services.FileService.DetermineTemplateRenderingEngine(this); } - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } - public void SetMasterTemplate(ITemplate masterTemplate) { if (masterTemplate == null) diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index b75f983cb3..907a91fe10 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -1,15 +1,10 @@ using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; -using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; -using System.Security; using System.Xml; namespace Umbraco.Core @@ -51,7 +46,7 @@ namespace Umbraco.Core public static Attempt TryConvertTo(this object input) { var result = TryConvertTo(input, typeof(T)); - if (!result.Success) + if (result.Success == false) { //just try a straight up conversion try @@ -64,7 +59,7 @@ namespace Umbraco.Core return Attempt.Fail(e); } } - return !result.Success ? Attempt.Fail() : Attempt.Succeed((T)result.Result); + return result.Success == false ? Attempt.Fail() : Attempt.Succeed((T)result.Result); } /// @@ -117,7 +112,7 @@ namespace Umbraco.Core } // we've already dealed with nullables, so any other generic types need to fall through - if (!destinationType.IsGenericType) + if (destinationType.IsGenericType == false) { if (input is string) { @@ -335,11 +330,11 @@ namespace Umbraco.Core return null; // we can't decide... } - private readonly static char[] NumberDecimalSeparatorsToNormalize = new[] {'.', ','}; + private static readonly char[] NumberDecimalSeparatorsToNormalize = new[] {'.', ','}; private static string NormalizeNumberDecimalSeparator(string s) { - var normalized = System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator[0]; + var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); } @@ -442,7 +437,7 @@ namespace Umbraco.Core { var props = TypeDescriptor.GetProperties(o); var d = new Dictionary(); - foreach (var prop in props.Cast().Where(x => !ignoreProperties.Contains(x.Name))) + foreach (var prop in props.Cast().Where(x => ignoreProperties.Contains(x.Name) == false)) { var val = prop.GetValue(o); if (val != null) @@ -484,7 +479,7 @@ namespace Umbraco.Core } var props = obj.GetType().GetProperties(); - if ((props.Count() == 2) && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) + if ((props.Length == 2) && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) { try { @@ -500,10 +495,10 @@ namespace Umbraco.Core if (levels > -1) { var items = - from propertyInfo in props + (from propertyInfo in props let value = GetPropertyDebugString(propertyInfo, obj, levels) where value != null - select "{0}={1}".InvariantFormat(propertyInfo.Name, value); + select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray(); return items.Any() ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, String.Join(", ", items)) diff --git a/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs b/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs index 14dca6b366..1b9d73bdd4 100644 --- a/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs @@ -42,7 +42,7 @@ namespace Umbraco.Core.Persistence.Factories { var text = new LanguageTextDto { - LanguageId = translation.Language.Id, + LanguageId = translation.LanguageId, UniqueId = translation.Key, Value = translation.Value }; diff --git a/src/Umbraco.Core/Persistence/Factories/DictionaryTranslationFactory.cs b/src/Umbraco.Core/Persistence/Factories/DictionaryTranslationFactory.cs index 8dca2494d0..65297b9529 100644 --- a/src/Umbraco.Core/Persistence/Factories/DictionaryTranslationFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DictionaryTranslationFactory.cs @@ -7,20 +7,19 @@ namespace Umbraco.Core.Persistence.Factories internal class DictionaryTranslationFactory { private readonly Guid _uniqueId; - private ILanguage _language; - public DictionaryTranslationFactory(Guid uniqueId, ILanguage language) + public DictionaryTranslationFactory(Guid uniqueId) { _uniqueId = uniqueId; - _language = language; } #region Implementation of IEntityFactory public IDictionaryTranslation BuildEntity(LanguageTextDto dto) { - var item = new DictionaryTranslation(_language, dto.Value, _uniqueId) + var item = new DictionaryTranslation(dto.LanguageId, dto.Value, _uniqueId) {Id = dto.PrimaryKey}; + //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 item.ResetDirtyProperties(false); @@ -31,7 +30,7 @@ namespace Umbraco.Core.Persistence.Factories { var text = new LanguageTextDto { - LanguageId = entity.Language.Id, + LanguageId = entity.LanguageId, UniqueId = _uniqueId, Value = entity.Value }; diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs index 5b2cad3415..2dfb996bb3 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs @@ -99,9 +99,7 @@ namespace Umbraco.Core.Persistence.Factories ContentTypeNodeId = _contentTypeId, SortOrder = propertyGroup.SortOrder, Text = propertyGroup.Name, - UniqueId = propertyGroup.Key == Guid.Empty - ? Guid.NewGuid() - : propertyGroup.Key + UniqueId = propertyGroup.Key }; if (propertyGroup.HasIdentity) @@ -124,9 +122,7 @@ namespace Umbraco.Core.Persistence.Factories Name = propertyType.Name, SortOrder = propertyType.SortOrder, ValidationRegExp = propertyType.ValidationRegExp, - UniqueId = propertyType.Key == Guid.Empty - ? Guid.NewGuid() - : propertyType.Key + UniqueId = propertyType.Key }; if (tabId != default(int)) diff --git a/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs b/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs index 9be10ce905..55a7194511 100644 --- a/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs +++ b/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs @@ -269,7 +269,7 @@ namespace Umbraco.Core.Persistence.Migrations //NOTE: We CANNOT do this as part of the transaction!!! This is because when upgrading to 7.3, we cannot // create the migrations table and then add data to it in the same transaction without issuing things like GO // commands and since we need to support all Dbs, we need to just do this after the fact. - var exists = _migrationEntryService.FindEntry(GlobalSettings.UmbracoMigrationName, _targetVersion); + var exists = _migrationEntryService.FindEntry(_productName, _targetVersion); if (exists == null) { _migrationEntryService.CreateEntry(_productName, _targetVersion); @@ -288,4 +288,4 @@ namespace Umbraco.Core.Persistence.Migrations /// public static event TypedEventHandler Migrated; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs index 5ed60a8de0..d65fc90bcc 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs @@ -39,9 +39,9 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven if (!dataTypeIds.Any()) return string.Empty; - var propertyData = - database.Fetch( - "WHERE propertyTypeId in (SELECT id from cmsPropertyType where dataTypeID IN (@dataTypeIds))", new { dataTypeIds = dataTypeIds }); + // need to use dynamic, as PropertyDataDto has new properties + var propertyData = database.Fetch("SELECT * FROM cmsPropertyData" + + " WHERE propertyTypeId in (SELECT id from cmsPropertyType where dataTypeID IN (@dataTypeIds))", new { dataTypeIds = dataTypeIds }); if (!propertyData.Any()) return string.Empty; var nodesIdsWithProperty = propertyData.Select(x => x.NodeId).Distinct().ToArray(); @@ -75,11 +75,14 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven } catch (Exception ex) { - Logger.Error("The data stored for property id " + data.Id + " on document " + data.NodeId + - " is not valid XML, the data will be removed because it cannot be converted to the new format. The value was: " + data.Text, ex); + int dataId = data.id; + int dataNodeId = data.nodeId; + string dataText = data.dataNText; + Logger.Error("The data stored for property id " + dataId + " on document " + dataNodeId + + " is not valid XML, the data will be removed because it cannot be converted to the new format. The value was: " + dataText, ex); - data.Text = ""; - database.Update(data); + data.dataNText = ""; + database.Update("cmsPropertyData", "id", data, new[] { "dataNText" }); UpdateXmlTable(propertyTypes, data, cmsContentXmlEntries, database); @@ -93,11 +96,11 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { var title = node.Attributes["title"].Value; var type = node.Attributes["type"].Value; - var newwindow = node.Attributes["newwindow"].Value.Equals("1") ? true : false; + var newwindow = node.Attributes["newwindow"].Value.Equals("1"); var lnk = node.Attributes["link"].Value; //create the links in the format the new prop editor expects it to be - var link = new ExpandoObject() as IDictionary; + var link = new ExpandoObject() as IDictionary; link.Add("title", title); link.Add("caption", title); link.Add("link", lnk); @@ -112,9 +115,9 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven } //store the serialized data - data.Text = JsonConvert.SerializeObject(links); + data.dataNText = JsonConvert.SerializeObject(links); - database.Update(data); + database.Update("cmsPropertyData", "id", data, new[] { "dataNText" }); UpdateXmlTable(propertyTypes, data, cmsContentXmlEntries, database); diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs index 2a164b6e0d..fe600f6b69 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; @@ -33,45 +35,57 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZer // fill in the data in a way that is consistent over all environments // (ie cannot use random guids, http://issues.umbraco.org/issue/U4-6942) + Execute.Code(UpdateGuids); + } + } - foreach (var data in Context.Database.Query(@" + private static string UpdateGuids(Database database) + { + var updates = new List>(); + + foreach (var data in database.Query(@" SELECT cmsPropertyTypeGroup.id grId, cmsPropertyTypeGroup.text grName, cmsContentType.alias ctAlias, umbracoNode.nodeObjectType nObjType FROM cmsPropertyTypeGroup INNER JOIN cmsContentType ON cmsPropertyTypeGroup.contentTypeNodeId = cmsContentType.nodeId INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id")) + { + Guid guid; + // see BaseDataCreation... built-in groups have their own guids + if (data.grId == 3) { - Guid guid; - // see BaseDataCreation... built-in groups have their own guids - if (data.grId == 3) - { - guid = new Guid(Constants.PropertyTypeGroups.Image); - } - else if (data.grId == 4) - { - guid = new Guid(Constants.PropertyTypeGroups.File); - } - else if (data.grId == 5) - { - guid = new Guid(Constants.PropertyTypeGroups.Contents); - } - else if (data.grId == 11) - { - guid = new Guid(Constants.PropertyTypeGroups.Membership); - } - else - { - // create a consistent guid from - // group name + content type alias + object type - string guidSource = data.grName + data.ctAlias + data.nObjType; - guid = guidSource.ToGuid(); - } - - // set the Unique Id to the one we've generated - Update.Table("cmsPropertyTypeGroup").Set(new { uniqueID = guid }).Where(new { id = data.grId }); + guid = new Guid(Constants.PropertyTypeGroups.Image); } + else if (data.grId == 4) + { + guid = new Guid(Constants.PropertyTypeGroups.File); + } + else if (data.grId == 5) + { + guid = new Guid(Constants.PropertyTypeGroups.Contents); + } + else if (data.grId == 11) + { + guid = new Guid(Constants.PropertyTypeGroups.Membership); + } + else + { + // create a consistent guid from + // group name + content type alias + object type + string guidSource = data.grName + data.ctAlias + data.nObjType; + guid = guidSource.ToGuid(); + } + + // set the Unique Id to the one we've generated + // but not within the foreach loop (as we already have a data reader open) + updates.Add(Tuple.Create(guid, data.grId)); } + + foreach (var update in updates) + database.Execute("UPDATE cmsPropertyTypeGroup SET uniqueID=@uid WHERE id=@id", new { uid = update.Item1, id = update.Item2 }); + + return string.Empty; } public override void Down() diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs index 5d5900308a..3d285c2715 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs @@ -20,7 +20,17 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZer if (columns.Any(x => x.TableName.InvariantEquals("cmsPropertyTypeGroup") && x.ColumnName.InvariantEquals("parentGroupId")) == false) return; - Delete.ForeignKey("FK_cmsPropertyTypeGroup_cmsPropertyTypeGroup_id").OnTable("cmsPropertyTypeGroup"); + //This constraing can be based on old aliases, before removing them, check they exist + var constraints = SqlSyntax.GetConstraintsPerColumn(Context.Database).Distinct().ToArray(); + if (constraints.Any(x => x.Item1.InvariantEquals("cmsPropertyTypeGroup") && x.Item3.InvariantEquals("FK_cmsPropertyTypeGroup_cmsPropertyTypeGroup_id"))) + { + Delete.ForeignKey("FK_cmsPropertyTypeGroup_cmsPropertyTypeGroup_id").OnTable("cmsPropertyTypeGroup"); + } + if (constraints.Any(x => x.Item1.InvariantEquals("cmsPropertyTypeGroup") && x.Item3.InvariantEquals("FK_cmsPropertyTypeGroup_cmsPropertyTypeGroup"))) + { + Delete.ForeignKey("FK_cmsPropertyTypeGroup_cmsPropertyTypeGroup").OnTable("cmsPropertyTypeGroup"); + } + Delete.Column("parentGroupId").FromTable("cmsPropertyTypeGroup"); } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs index 28ede71ad1..9a2b47d79d 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs @@ -34,36 +34,40 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixZeroOne // won't exist yet var propertyTypes = database.Fetch("SELECT * FROM cmsPropertyType WHERE propertyTypeGroupId > 0"); - var propertyGroups = database.Fetch("WHERE id > 0"); + // need to use dynamic, as PropertyTypeGroupDto has new properties + var propertyGroups = database.Fetch("SELECT * FROM cmsPropertyTypeGroup WHERE id > 0"); foreach (var propertyType in propertyTypes) { // get the PropertyTypeGroup of the current PropertyType, skip if not found - var propertyTypeGroup = propertyGroups.FirstOrDefault(x => x.Id == propertyType.propertyTypeGroupId); + var propertyTypeGroup = propertyGroups.FirstOrDefault(x => x.id == propertyType.propertyTypeGroupId); if (propertyTypeGroup == null) continue; // if the PropretyTypeGroup belongs to the same content type as the PropertyType, then fine - if (propertyTypeGroup.ContentTypeNodeId == propertyType.contentTypeId) continue; + if (propertyTypeGroup.contenttypeNodeId == propertyType.contentTypeId) continue; // else we want to assign the PropertyType to a proper PropertyTypeGroup // ie one that does belong to the same content - look for it var okPropertyTypeGroup = propertyGroups.FirstOrDefault(x => - x.Text == propertyTypeGroup.Text && // same name - x.ContentTypeNodeId == propertyType.contentTypeId); // but for proper content type + x.text == propertyTypeGroup.text && // same name + x.contenttypeNodeId == propertyType.contentTypeId); // but for proper content type if (okPropertyTypeGroup == null) { - // does not exist, create a new PropertyTypeGroup, - var propertyGroup = new PropertyTypeGroupDto + // does not exist, create a new PropertyTypeGroup + // cannot use a PropertyTypeGroupDto because of the new (not-yet-existing) uniqueID property + // cannot use a dynamic because database.Insert fails to set the value of property + var propertyGroup = new PropertyTypeGroupDtoTemp { - ContentTypeNodeId = propertyType.contentTypeId, - Text = propertyTypeGroup.Text, - SortOrder = propertyTypeGroup.SortOrder + id = 0, + contenttypeNodeId = propertyType.contentTypeId, + text = propertyTypeGroup.text, + sortorder = propertyTypeGroup.sortorder }; // save + add to list of groups - int id = Convert.ToInt16(database.Insert(propertyGroup)); - propertyGroup.Id = id; + int id = Convert.ToInt16(database.Insert("cmsPropertyTypeGroup", "id", propertyGroup)); + propertyGroup.id = id; propertyGroups.Add(propertyGroup); // update the PropertyType to use the new PropertyTypeGroup @@ -72,7 +76,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixZeroOne else { // exists, update PropertyType to use the PropertyTypeGroup - propertyType.propertyTypeGroupId = okPropertyTypeGroup.Id; + propertyType.propertyTypeGroupId = okPropertyTypeGroup.id; } database.Update("cmsPropertyType", "id", propertyType); } @@ -80,5 +84,13 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixZeroOne return string.Empty; } + + private class PropertyTypeGroupDtoTemp + { + public int id { get; set; } + public int contenttypeNodeId { get; set; } + public string text { get; set; } + public int sortorder { get; set; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/PetaPoco.cs b/src/Umbraco.Core/Persistence/PetaPoco.cs index 3d4160e4ef..a174255746 100644 --- a/src/Umbraco.Core/Persistence/PetaPoco.cs +++ b/src/Umbraco.Core/Persistence/PetaPoco.cs @@ -834,7 +834,7 @@ namespace Umbraco.Core.Persistence var pd = PocoData.ForType(typeof(T)); try { - r = cmd.ExecuteReader(); + r = cmd.ExecuteReaderWithRetry(); OnExecutedCommand(cmd); } catch (Exception x) diff --git a/src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs b/src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs index 859fa7cf5a..923348e729 100644 --- a/src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs +++ b/src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs @@ -18,8 +18,11 @@ namespace Umbraco.Core.Persistence.Relators // Is this the same DictionaryItem as the current one we're processing if (Current != null && Current.Id == a.Id) { - // Yes, just add this User2AppDto to the current item's collection - Current.User2AppDtos.Add(p); + if (p.AppAlias.IsNullOrWhiteSpace() == false) + { + // Yes, just add this User2AppDto to the current item's collection + Current.User2AppDtos.Add(p); + } // Return null to indicate we're not done with this User yet return null; @@ -35,7 +38,7 @@ namespace Umbraco.Core.Persistence.Relators Current = a; Current.User2AppDtos = new List(); //this can be null since we are doing a left join - if (p.AppAlias != null) + if (p.AppAlias.IsNullOrWhiteSpace() == false) { Current.User2AppDtos.Add(p); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 07d80bdcb0..b00b79813f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -156,8 +156,8 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsContentVersion WHERE ContentId = @Id", "DELETE FROM cmsContentXml WHERE nodeId = @Id", "DELETE FROM cmsContent WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id", - "DELETE FROM umbracoAccess WHERE nodeId = @Id" + "DELETE FROM umbracoAccess WHERE nodeId = @Id", + "DELETE FROM umbracoNode WHERE id = @Id" }; return list; } @@ -235,8 +235,15 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); - + //NOTE: This is an important call, we cannot simply make a call to: + // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + // because that method is used to query 'latest' content items where in this case we don't necessarily + // want latest content items because a pulished content item might not actually be the latest. + // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, + new Tuple("cmsDocument", "nodeId"), + ProcessQuery, "Path", Direction.Ascending); + var xmlItems = (from descendant in descendants let xml = serializer(descendant) select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); @@ -683,8 +690,7 @@ namespace Umbraco.Core.Persistence.Repositories public int CountPublished() { var sql = GetBaseQuery(true).Where(SqlSyntax, x => x.Trashed == false) - .Where(SqlSyntax, x => x.Published == true) - .Where(SqlSyntax, x => x.Newest == true); + .Where(x => x.Published == true); return Database.ExecuteScalar(sql); } @@ -729,7 +735,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// public void AddOrUpdateContentXml(IContent content, Func xml) - { + { _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); } @@ -827,11 +833,11 @@ namespace Umbraco.Core.Persistence.Repositories var contentTypes = _contentTypeRepository.GetAll(dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray()) .ToArray(); - + var ids = dtos .Where(dto => dto.TemplateId.HasValue && dto.TemplateId.Value > 0) .Select(x => x.TemplateId.Value).ToArray(); - + //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types var templates = ids.Length == 0 ? Enumerable.Empty() : _templateRepository.GetAll(ids).ToArray(); @@ -973,4 +979,4 @@ namespace Umbraco.Core.Persistence.Repositories _contentXmlRepository.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index a3698b6ec8..c715a260ef 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -34,14 +34,11 @@ namespace Umbraco.Core.Persistence.Repositories protected ContentTypeBaseRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMappingResolver mappingResolver) : base(work, cache, logger, sqlSyntax, mappingResolver) { - _guidRepo = new GuidReadOnlyContentTypeBaseRepository(this, work, cache, logger, sqlSyntax, mappingResolver); } - private readonly GuidReadOnlyContentTypeBaseRepository _guidRepo; - public IEnumerable> Move(TEntity toMove, EntityContainer container) { - var parentId = -1; + var parentId = Constants.System.Root; if (container != null) { // Check on paths @@ -58,27 +55,36 @@ namespace Umbraco.Core.Persistence.Repositories new MoveEventInfo(toMove, toMove.Path, parentId) }; - //do the move to a new parent + + // get the level delta (old pos to new pos) + var levelDelta = container == null + ? 1 - toMove.Level + : container.Level + 1 - toMove.Level; + + // move to parent (or -1), update path, save toMove.ParentId = parentId; - //schedule it for updating in the transaction + var toMovePath = toMove.Path + ","; // save before changing + toMove.Path = (container == null ? Constants.System.Root.ToString() : container.Path) + "," + toMove.Id; + toMove.Level = container == null ? 1 : container.Level + 1; AddOrUpdate(toMove); - //update all descendants - var descendants = this.GetByQuery( - Query.Where(type => type.Path.StartsWith(toMove.Path + ","))); - foreach (var descendant in descendants) + //update all descendants, update in order of level + var descendants = GetByQuery(new Query().Where(type => type.Path.StartsWith(toMovePath))); + var paths = new Dictionary(); + paths[toMove.Id] = toMove.Path; + + foreach (var descendant in descendants.OrderBy(x => x.Level)) { moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - //all we're doing here is setting the parent Id to be dirty so that it resets the path/level/etc... - descendant.ParentId = descendant.ParentId; - //schedule it for updating in the transaction + descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; + descendant.Level += levelDelta; + AddOrUpdate(descendant); } return moveInfo; } - /// /// Returns the content type ids that match the query /// @@ -280,7 +286,7 @@ AND umbracoNode.id <> @id", { //Find PropertyTypes for the removed ContentType var propertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { Id = key }); - //Loop through the Content that is based on the current ContentType in order to remove the Properties that are + //Loop through the Content that is based on the current ContentType in order to remove the Properties that are //based on the PropertyTypes that belong to the removed ContentType. foreach (var contentDto in contentDtos) { @@ -404,7 +410,7 @@ AND umbracoNode.id <> @id", AssignDataTypeFromPropertyEditor(propertyType); } - //validate the alias! + //validate the alias! ValidateAlias(propertyType); var propertyTypeDto = propertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType); @@ -467,6 +473,7 @@ AND umbracoNode.id <> @id", propType.DataTypeDefinitionId = dto.DataTypeId; propType.Description = dto.Description; propType.Id = dto.Id; + propType.Key = dto.UniqueId; propType.Name = dto.Name; propType.Mandatory = dto.Mandatory; propType.SortOrder = dto.SortOrder; @@ -577,14 +584,13 @@ AND umbracoNode.id <> @id", } } - public static IEnumerable GetMediaTypes( - TId[] mediaTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, + public static IEnumerable GetMediaTypes( + Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository) - where TRepo : IReadRepository - where TId: struct + where TRepo : IReadRepository { - IDictionary> allParentMediaTypeIds; - var mediaTypes = MapMediaTypes(mediaTypeIds, db, sqlSyntax, out allParentMediaTypeIds) + IDictionary> allParentMediaTypeIds; + var mediaTypes = MapMediaTypes(db, sqlSyntax, out allParentMediaTypeIds) .ToArray(); MapContentTypeChildren(mediaTypes, db, sqlSyntax, contentTypeRepository, allParentMediaTypeIds); @@ -592,16 +598,15 @@ AND umbracoNode.id <> @id", return mediaTypes; } - public static IEnumerable GetContentTypes( - TId[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, + public static IEnumerable GetContentTypes( + Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository, ITemplateRepository templateRepository) - where TRepo : IReadRepository - where TId : struct + where TRepo : IReadRepository { - IDictionary> allAssociatedTemplates; - IDictionary> allParentContentTypeIds; - var contentTypes = MapContentTypes(contentTypeIds, db, sqlSyntax, out allAssociatedTemplates, out allParentContentTypeIds) + IDictionary> allAssociatedTemplates; + IDictionary> allParentContentTypeIds; + var contentTypes = MapContentTypes(db, sqlSyntax, out allAssociatedTemplates, out allParentContentTypeIds) .ToArray(); if (contentTypes.Any()) @@ -616,12 +621,11 @@ AND umbracoNode.id <> @id", return contentTypes; } - internal static void MapContentTypeChildren(IContentTypeComposition[] contentTypes, + internal static void MapContentTypeChildren(IContentTypeComposition[] contentTypes, Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository, - IDictionary> allParentContentTypeIds) - where TRepo : IReadRepository - where TId : struct + IDictionary> allParentContentTypeIds) + where TRepo : IReadRepository { //NOTE: SQL call #2 @@ -643,25 +647,22 @@ AND umbracoNode.id <> @id", var allParentIdsAsArray = allParentContentTypeIds.SelectMany(x => x.Value).Distinct().ToArray(); if (allParentIdsAsArray.Any()) { - var allParentContentTypes = contentTypeRepository.GetAll(allParentIdsAsArray).ToArray(); + var allParentContentTypes = contentTypes.Where(x => allParentIdsAsArray.Contains(x.Id)).ToArray(); + foreach (var contentType in contentTypes) - { - //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids - // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be - var entityId = typeof(TId) == typeof(int) ? contentType.Id : (object)contentType.Key; + { + var entityId = contentType.Id; var parentContentTypes = allParentContentTypes.Where(x => { - //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids - // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be - var parentEntityId = typeof(TId) == typeof(int) ? x.Id : (object)x.Key; + var parentEntityId = x.Id; - return allParentContentTypeIds[(TId)entityId].Contains((TId)parentEntityId); + return allParentContentTypeIds[entityId].Contains(parentEntityId); }); foreach (var parentContentType in parentContentTypes) { var result = contentType.AddContentType(parentContentType); - //Do something if adding fails? (Should hopefully not be possible unless someone created a circular reference) + //Do something if adding fails? (Should hopefully not be possible unless someone created a circular reference) } //on initial construction we don't want to have dirty properties tracked @@ -674,13 +675,12 @@ AND umbracoNode.id <> @id", } - internal static void MapContentTypeTemplates(IContentType[] contentTypes, + internal static void MapContentTypeTemplates(IContentType[] contentTypes, Database db, TRepo contentTypeRepository, ITemplateRepository templateRepository, - IDictionary> associatedTemplates) - where TRepo : IReadRepository - where TId: struct + IDictionary> associatedTemplates) + where TRepo : IReadRepository { if (associatedTemplates == null || associatedTemplates.Any() == false) return; @@ -697,11 +697,9 @@ AND umbracoNode.id <> @id", foreach (var contentType in contentTypes) { - //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids - // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be - var entityId = typeof(TId) == typeof(int) ? contentType.Id : (object)contentType.Key; - - var associatedTemplateIds = associatedTemplates[(TId)entityId].Select(x => x.TemplateId) + var entityId = contentType.Id; + + var associatedTemplateIds = associatedTemplates[entityId].Select(x => x.TemplateId) .Distinct() .ToArray(); @@ -713,19 +711,14 @@ AND umbracoNode.id <> @id", } - internal static IEnumerable MapMediaTypes(TId[] mediaTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, - out IDictionary> parentMediaTypeIds) - where TId : struct + internal static IEnumerable MapMediaTypes(Database db, ISqlSyntaxProvider sqlSyntax, + out IDictionary> parentMediaTypeIds) { - Mandate.That(mediaTypeIds.Any(), () => new InvalidOperationException("must be at least one content type id specified")); Mandate.ParameterNotNull(db, "db"); - - //ensure they are unique - mediaTypeIds = mediaTypeIds.Distinct().ToArray(); - + var sql = @"SELECT cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, - AllowedTypes.allowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, + 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, umbracoNode.parentID as nParentId, umbracoNode." + sqlSyntax.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + sqlSyntax.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, @@ -734,40 +727,23 @@ AND umbracoNode.id <> @id", INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id LEFT JOIN ( - SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder - FROM cmsContentTypeAllowedContentType + SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder + FROM cmsContentTypeAllowedContentType INNER JOIN cmsContentType ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId ) AllowedTypes ON AllowedTypes.Id = cmsContentType.nodeId LEFT JOIN ( SELECT cmsContentType2ContentType.parentContentTypeId, umbracoNode.uniqueID AS parentContentTypeKey, cmsContentType2ContentType.childContentTypeId - FROM cmsContentType2ContentType + FROM cmsContentType2ContentType INNER JOIN umbracoNode ON cmsContentType2ContentType.parentContentTypeId = umbracoNode." + sqlSyntax.GetQuotedColumnName("id") + @" - ) ParentTypes - ON ParentTypes.childContentTypeId = cmsContentType.nodeId - WHERE (umbracoNode.nodeObjectType = @nodeObjectType)"; - - if (mediaTypeIds.Any()) - { - //TODO: This is all sorts of hacky but i don't have time to refactor a lot to get both ints and guids working nicely... this will - // work for the time being. - if (typeof(TId) == typeof(int)) - { - sql = sql + " AND (umbracoNode.id IN (@contentTypeIds))"; - } - else if (typeof(TId) == typeof(Guid)) - { - sql = sql + " AND (umbracoNode.uniqueID IN (@contentTypeIds))"; - } - } - - //NOTE: we are going to assume there's not going to be more than 2100 content type ids since that is the max SQL param count! - if ((mediaTypeIds.Length - 1) > 2000) - throw new InvalidOperationException("Cannot perform this lookup, too many sql parameters"); - - var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.MediaType), contentTypeIds = mediaTypeIds }); + ) ParentTypes + ON ParentTypes.childContentTypeId = cmsContentType.nodeId + WHERE (umbracoNode.nodeObjectType = @nodeObjectType) + ORDER BY ctId"; + + var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.MediaType) }); if (result.Any() == false) { @@ -775,95 +751,118 @@ AND umbracoNode.id <> @id", return Enumerable.Empty(); } - parentMediaTypeIds = new Dictionary>(); + parentMediaTypeIds = new Dictionary>(); var mappedMediaTypes = new List(); - foreach (var contentTypeId in mediaTypeIds) + //loop through each result and fill in our required values, each row will contain different requried data than the rest. + // it is much quicker to iterate each result and populate instead of looking up the values over and over in the result like + // we used to do. + var queue = new Queue(result); + var currAllowedContentTypes = new List(); + + while (queue.Count > 0) { - //the current content type id that we're working with + var ct = queue.Dequeue(); - var currentCtId = contentTypeId; - - //first we want to get the main content type data this is 1 : 1 with umbraco node data - - var ct = result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof (TId) == typeof (int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - .Select(x => new { x.ctPk, x.ctId, x.ctAlias, x.ctAllowAtRoot, x.ctDesc, x.ctIcon, x.ctIsContainer, x.ctThumb, x.nName, x.nCreateDate, x.nLevel, x.nObjectType, x.nUser, x.nParentId, x.nPath, x.nSortOrder, x.nTrashed, x.nUniqueId }) - .DistinctBy(x => (int)x.ctId) - .FirstOrDefault(); - - if (ct == null) + //check for allowed content types + int? allowedCtId = ct.ctaAllowedId; + int? allowedCtSort = ct.ctaSortOrder; + string allowedCtAlias = ct.ctaAlias; + if (allowedCtId.HasValue && allowedCtSort.HasValue && allowedCtAlias != null) { - continue; + var ctSort = new ContentTypeSort(new Lazy(() => allowedCtId.Value), allowedCtSort.Value, allowedCtAlias); + if (currAllowedContentTypes.Contains(ctSort) == false) + { + currAllowedContentTypes.Add(ctSort); + } } - var contentTypeDto = new ContentTypeDto + //always ensure there's a list for this content type + if (parentMediaTypeIds.ContainsKey(ct.ctId) == false) + parentMediaTypeIds[ct.ctId] = new List(); + + //check for parent ids and assign to the outgoing collection + int? parentId = ct.chtParentId; + if (parentId.HasValue) { - Alias = ct.ctAlias, - AllowAtRoot = ct.ctAllowAtRoot, - Description = ct.ctDesc, - Icon = ct.ctIcon, - IsContainer = ct.ctIsContainer, - NodeId = ct.ctId, - PrimaryKey = ct.ctPk, - Thumbnail = ct.ctThumb, - //map the underlying node dto - NodeDto = new NodeDto - { - CreateDate = ct.nCreateDate, - Level = (short)ct.nLevel, - NodeId = ct.ctId, - NodeObjectType = ct.nObjectType, - ParentId = ct.nParentId, - Path = ct.nPath, - SortOrder = ct.nSortOrder, - Text = ct.nName, - Trashed = ct.nTrashed, - UniqueId = ct.nUniqueId, - UserId = ct.nUser - } - }; + var associatedParentIds = parentMediaTypeIds[ct.ctId]; + if (associatedParentIds.Contains(parentId.Value) == false) + associatedParentIds.Add(parentId.Value); + } - //now create the media type object + if (queue.Count == 0 || queue.Peek().ctId != ct.ctId) + { + //it's the last in the queue or the content type is changing (moving to the next one) + var mediaType = CreateForMapping(ct, currAllowedContentTypes); + mappedMediaTypes.Add(mediaType); - var factory = new ContentTypeFactory(); - var mediaType = factory.BuildMediaTypeEntity(contentTypeDto); - - //map the allowed content types - //map the child content type ids - MapCommonContentTypeObjects(mediaType, currentCtId, result, parentMediaTypeIds); - - mappedMediaTypes.Add(mediaType); + //Here we need to reset the current variables, we're now collecting data for a different content type + currAllowedContentTypes = new List(); + } } return mappedMediaTypes; } - internal static IEnumerable MapContentTypes(TId[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, - out IDictionary> associatedTemplates, - out IDictionary> parentContentTypeIds) - where TId : struct + private static IMediaType CreateForMapping(dynamic currCt, List currAllowedContentTypes) + { + // * create the DTO object + // * create the content type object + // * map the allowed content types + // * add to the outgoing list + + var contentTypeDto = new ContentTypeDto + { + Alias = currCt.ctAlias, + AllowAtRoot = currCt.ctAllowAtRoot, + Description = currCt.ctDesc, + Icon = currCt.ctIcon, + IsContainer = currCt.ctIsContainer, + NodeId = currCt.ctId, + PrimaryKey = currCt.ctPk, + Thumbnail = currCt.ctThumb, + //map the underlying node dto + NodeDto = new NodeDto + { + CreateDate = currCt.nCreateDate, + Level = (short)currCt.nLevel, + NodeId = currCt.ctId, + NodeObjectType = currCt.nObjectType, + ParentId = currCt.nParentId, + Path = currCt.nPath, + SortOrder = currCt.nSortOrder, + Text = currCt.nName, + Trashed = currCt.nTrashed, + UniqueId = currCt.nUniqueId, + UserId = currCt.nUser + } + }; + + //now create the content type object + + var factory = new ContentTypeFactory(); + var mediaType = factory.BuildMediaTypeEntity(contentTypeDto); + + //map the allowed content types + mediaType.AllowedContentTypes = currAllowedContentTypes; + + return mediaType; + } + + internal static IEnumerable MapContentTypes(Database db, ISqlSyntaxProvider sqlSyntax, + out IDictionary> associatedTemplates, + out IDictionary> parentContentTypeIds) { Mandate.ParameterNotNull(db, "db"); - - //ensure they are unique - contentTypeIds = contentTypeIds.Distinct().ToArray(); - + 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.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, - AllowedTypes.allowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, + 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, umbracoNode.parentID as nParentId, umbracoNode." + sqlSyntax.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + sqlSyntax.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, - umbracoNode.uniqueID as nUniqueId, + umbracoNode.uniqueID as nUniqueId, Template.alias as tAlias, Template.nodeId as tId,Template.text as tText FROM cmsContentType INNER JOIN umbracoNode @@ -871,8 +870,8 @@ AND umbracoNode.id <> @id", LEFT JOIN cmsDocumentType ON cmsDocumentType.contentTypeNodeId = cmsContentType.nodeId LEFT JOIN ( - SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder - FROM cmsContentTypeAllowedContentType + SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder + FROM cmsContentTypeAllowedContentType INNER JOIN cmsContentType ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId ) AllowedTypes @@ -885,33 +884,15 @@ AND umbracoNode.id <> @id", ON Template.nodeId = cmsDocumentType.templateNodeId LEFT JOIN ( SELECT cmsContentType2ContentType.parentContentTypeId, umbracoNode.uniqueID AS parentContentTypeKey, cmsContentType2ContentType.childContentTypeId - FROM cmsContentType2ContentType + FROM cmsContentType2ContentType INNER JOIN umbracoNode ON cmsContentType2ContentType.parentContentTypeId = umbracoNode." + sqlSyntax.GetQuotedColumnName("id") + @" - ) ParentTypes - ON ParentTypes.childContentTypeId = cmsContentType.nodeId - WHERE (umbracoNode.nodeObjectType = @nodeObjectType)"; - - if (contentTypeIds.Any()) - { - //TODO: This is all sorts of hacky but i don't have time to refactor a lot to get both ints and guids working nicely... this will - // work for the time being. - if (typeof(TId) == typeof(int)) - { - sql = sql + " AND (umbracoNode.id IN (@contentTypeIds))"; - } - else if (typeof(TId) == typeof(Guid)) - { - sql = sql + " AND (umbracoNode.uniqueID IN (@contentTypeIds))"; - } - } - - - //NOTE: we are going to assume there's not going to be more than 2100 content type ids since that is the max SQL param count! - if ((contentTypeIds.Length - 1) > 2000) - throw new InvalidOperationException("Cannot perform this lookup, too many sql parameters"); - - var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.DocumentType), contentTypeIds = contentTypeIds }); + ) ParentTypes + ON ParentTypes.childContentTypeId = cmsContentType.nodeId + WHERE (umbracoNode.nodeObjectType = @nodeObjectType) + ORDER BY ctId"; + + var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.DocumentType)}); if (result.Any() == false) { @@ -920,170 +901,139 @@ AND umbracoNode.id <> @id", return Enumerable.Empty(); } - parentContentTypeIds = new Dictionary>(); - associatedTemplates = new Dictionary>(); + parentContentTypeIds = new Dictionary>(); + associatedTemplates = new Dictionary>(); var mappedContentTypes = new List(); - foreach (var contentTypeId in contentTypeIds) + var queue = new Queue(result); + var currDefaultTemplate = -1; + var currAllowedContentTypes = new List(); + while (queue.Count > 0) { - //the current content type id that we're working with + var ct = queue.Dequeue(); - var currentCtId = contentTypeId; - - //first we want to get the main content type data this is 1 : 1 with umbraco node data - - var ct = result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - .Select(x => new { x.ctPk, x.ctId, x.ctAlias, x.ctAllowAtRoot, x.ctDesc, x.ctIcon, x.ctIsContainer, x.ctThumb, x.nName, x.nCreateDate, x.nLevel, x.nObjectType, x.nUser, x.nParentId, x.nPath, x.nSortOrder, x.nTrashed, x.nUniqueId }) - .DistinctBy(x => (int)x.ctId) - .FirstOrDefault(); - - if (ct == null) + //check for default templates + bool? isDefaultTemplate = Convert.ToBoolean(ct.dtIsDefault); + int? templateId = ct.dtTemplateId; + if (currDefaultTemplate == -1 && isDefaultTemplate.HasValue && templateId.HasValue) { - continue; + currDefaultTemplate = templateId.Value; } - //get the unique list of associated templates - var defaultTemplates = result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - //use a tuple so that distinct checks both values (in some rare cases the dtIsDefault will not compute as bool?, so we force it with Convert.ToBoolean) - .Select(x => new Tuple(Convert.ToBoolean(x.dtIsDefault), x.dtTemplateId)) - .Where(x => x.Item1.HasValue && x.Item2.HasValue) - .Distinct() - .OrderByDescending(x => x.Item1.Value) - .ToArray(); - //if there isn't one set to default explicitly, we'll pick the first one - var defaultTemplate = defaultTemplates.FirstOrDefault(x => x.Item1.Value) - ?? defaultTemplates.FirstOrDefault(); + //always ensure there's a list for this content type + if (associatedTemplates.ContainsKey(ct.ctId) == false) + associatedTemplates[ct.ctId] = new List(); - var dtDto = new ContentTypeTemplateDto + //check for associated templates and assign to the outgoing collection + if (ct.tId != null) { - //create the content type dto - ContentTypeDto = new ContentTypeDto + var associatedTemplate = new AssociatedTemplate(ct.tId, ct.tAlias, ct.tText); + var associatedList = associatedTemplates[ct.ctId]; + + if (associatedList.Contains(associatedTemplate) == false) + associatedList.Add(associatedTemplate); + } + + //check for allowed content types + int? allowedCtId = ct.ctaAllowedId; + int? allowedCtSort = ct.ctaSortOrder; + string allowedCtAlias = ct.ctaAlias; + if (allowedCtId.HasValue && allowedCtSort.HasValue && allowedCtAlias != null) + { + var ctSort = new ContentTypeSort(new Lazy(() => allowedCtId.Value), allowedCtSort.Value, allowedCtAlias); + if (currAllowedContentTypes.Contains(ctSort) == false) { - Alias = ct.ctAlias, - AllowAtRoot = ct.ctAllowAtRoot, - Description = ct.ctDesc, - Icon = ct.ctIcon, - IsContainer = ct.ctIsContainer, - NodeId = ct.ctId, - PrimaryKey = ct.ctPk, - Thumbnail = ct.ctThumb, - //map the underlying node dto - NodeDto = new NodeDto - { - CreateDate = ct.nCreateDate, - Level = (short)ct.nLevel, - NodeId = ct.ctId, - NodeObjectType = ct.nObjectType, - ParentId = ct.nParentId, - Path = ct.nPath, - SortOrder = ct.nSortOrder, - Text = ct.nName, - Trashed = ct.nTrashed, - UniqueId = ct.nUniqueId, - UserId = ct.nUser - } - }, - ContentTypeNodeId = ct.ctId, - IsDefault = defaultTemplate != null, - TemplateNodeId = defaultTemplate != null ? defaultTemplate.Item2.Value : 0, - }; + currAllowedContentTypes.Add(ctSort); + } + } - // We will map a subset of the associated template - alias, id, name + //always ensure there's a list for this content type + if (parentContentTypeIds.ContainsKey(ct.ctId) == false) + parentContentTypeIds[ct.ctId] = new List(); - associatedTemplates.Add(currentCtId, result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - .Where(x => x.tId != null) - .Select(x => new AssociatedTemplate(x.tId, x.tAlias, x.tText)) - .Distinct() - .ToArray()); + //check for parent ids and assign to the outgoing collection + int? parentId = ct.chtParentId; + if (parentId.HasValue) + { + var associatedParentIds = parentContentTypeIds[ct.ctId]; - //now create the content type object + if (associatedParentIds.Contains(parentId.Value) == false) + associatedParentIds.Add(parentId.Value); + } - var factory = new ContentTypeFactory(); - var contentType = factory.BuildContentTypeEntity(dtDto.ContentTypeDto); - - // NOTE - // that was done by the factory but makes little sense, moved here, so - // now we have to reset dirty props again (as the factory does it) and yet, - // we are not managing allowed templates... the whole thing is weird. - ((ContentType) contentType).DefaultTemplateId = dtDto.TemplateNodeId; - contentType.ResetDirtyProperties(false); + if (queue.Count == 0 || queue.Peek().ctId != ct.ctId) + { + //it's the last in the queue or the content type is changing (moving to the next one) + var contentType = CreateForMapping(ct, currAllowedContentTypes, currDefaultTemplate); + mappedContentTypes.Add(contentType); - //map the allowed content types - //map the child content type ids - MapCommonContentTypeObjects(contentType, currentCtId, result, parentContentTypeIds); - - mappedContentTypes.Add(contentType); + //Here we need to reset the current variables, we're now collecting data for a different content type + currDefaultTemplate = -1; + currAllowedContentTypes = new List(); + } } return mappedContentTypes; } - private static void MapCommonContentTypeObjects(T contentType, TId currentCtId, List result, IDictionary> parentContentTypeIds) - where T : IContentTypeBase - where TId : struct + private static IContentType CreateForMapping(dynamic currCt, List currAllowedContentTypes, int currDefaultTemplate) { - //map the allowed content types - contentType.AllowedContentTypes = result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - //use tuple so we can use distinct on all vals - .Select(x => new Tuple(x.ctaAllowedId, x.ctaSortOrder, x.ctaAlias)) - .Where(x => x.Item1.HasValue && x.Item2.HasValue && x.Item3 != null) - .Distinct() - .Select(x => new ContentTypeSort(new Lazy(() => x.Item1.Value), x.Item2.Value, x.Item3)) - .ToList(); + // * set the default template to the first one if a default isn't found + // * create the DTO object + // * create the content type object + // * map the allowed content types + // * add to the outgoing list - //map the child content type ids - parentContentTypeIds.Add(currentCtId, result - .Where(x => + var dtDto = new ContentTypeTemplateDto + { + //create the content type dto + ContentTypeDto = new ContentTypeDto { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - .Select(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? (TId?)x.chtParentId - : (TId?)x.chtParentKey; - }) - .Where(x => x.HasValue) - .Distinct() - .Select(x => x.Value).ToList()); + Alias = currCt.ctAlias, + AllowAtRoot = currCt.ctAllowAtRoot, + Description = currCt.ctDesc, + Icon = currCt.ctIcon, + IsContainer = currCt.ctIsContainer, + NodeId = currCt.ctId, + PrimaryKey = currCt.ctPk, + Thumbnail = currCt.ctThumb, + //map the underlying node dto + NodeDto = new NodeDto + { + CreateDate = currCt.nCreateDate, + Level = (short)currCt.nLevel, + NodeId = currCt.ctId, + NodeObjectType = currCt.nObjectType, + ParentId = currCt.nParentId, + Path = currCt.nPath, + SortOrder = currCt.nSortOrder, + Text = currCt.nName, + Trashed = currCt.nTrashed, + UniqueId = currCt.nUniqueId, + UserId = currCt.nUser + } + }, + ContentTypeNodeId = currCt.ctId, + IsDefault = currDefaultTemplate != -1, + TemplateNodeId = currDefaultTemplate != -1 ? currDefaultTemplate : 0, + }; + + //now create the content type object + + var factory = new ContentTypeFactory(); + var contentType = factory.BuildContentTypeEntity(dtDto.ContentTypeDto); + + // NOTE + // that was done by the factory but makes little sense, moved here, so + // now we have to reset dirty props again (as the factory does it) and yet, + // we are not managing allowed templates... the whole thing is weird. + ((ContentType)contentType).DefaultTemplateId = dtDto.TemplateNodeId; + contentType.ResetDirtyProperties(false); + + //map the allowed content types + contentType.AllowedContentTypes = currAllowedContentTypes; + + return contentType; } internal static void MapGroupsAndProperties(int[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, @@ -1101,14 +1051,14 @@ AND umbracoNode.id <> @id", // therefore the union of the two contains all of the property type and property group information we need // NOTE: MySQL requires a SELECT * FROM the inner union in order to be able to sort . lame. - var sqlBuilder = new StringBuilder(@"SELECT PG.contenttypeNodeId as contentTypeId, - PT.ptUniqueId as ptUniqueID, PT.ptId, PT.ptAlias, PT.ptDesc,PT.ptMandatory,PT.ptName,PT.ptSortOrder,PT.ptRegExp, + var sqlBuilder = new StringBuilder(@"SELECT PG.contenttypeNodeId as contentTypeId, + PT.ptUniqueId as ptUniqueID, PT.ptId, PT.ptAlias, PT.ptDesc,PT.ptMandatory,PT.ptName,PT.ptSortOrder,PT.ptRegExp, PT.dtId,PT.dtDbType,PT.dtPropEdAlias, PG.id as pgId, PG.uniqueID as pgKey, PG.sortorder as pgSortOrder, PG." + sqlSyntax.GetQuotedColumnName("text") + @" as pgText FROM cmsPropertyTypeGroup as PG LEFT JOIN ( - SELECT PT.uniqueID as ptUniqueId, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, + SELECT PT.uniqueID as ptUniqueId, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, PT.mandatory as ptMandatory, PT.Name as ptName, PT.sortOrder as ptSortOrder, PT.validationRegExp as ptRegExp, PT.propertyTypeGroupId as ptGroupId, DT.dbType as dtDbType, DT.nodeId as dtId, DT.propertyEditorAlias as dtPropEdAlias @@ -1118,11 +1068,11 @@ AND umbracoNode.id <> @id", ) as PT ON PT.ptGroupId = PG.id WHERE (PG.contenttypeNodeId in (@contentTypeIds)) - + UNION SELECT PT.contentTypeId as contentTypeId, - PT.uniqueID as ptUniqueID, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, + PT.uniqueID as ptUniqueID, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, PT.mandatory as ptMandatory, PT.Name as ptName, PT.sortOrder as ptSortOrder, PT.validationRegExp as ptRegExp, DT.nodeId as dtId, DT.dbType as dtDbType, DT.propertyEditorAlias as dtPropEdAlias, PG.id as pgId, PG.uniqueID as pgKey, PG.sortorder as pgSortOrder, PG." + sqlSyntax.GetQuotedColumnName("text") + @" as pgText @@ -1216,71 +1166,20 @@ AND umbracoNode.id <> @id", } - /// - /// Inner repository to support the GUID lookups and keep the caching consistent - /// - internal class GuidReadOnlyContentTypeBaseRepository : PetaPocoRepositoryBase - { - private readonly ContentTypeBaseRepository _parentRepo; - - public GuidReadOnlyContentTypeBaseRepository( - ContentTypeBaseRepository parentRepo, - IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMappingResolver mappingResolver) - : base(work, cache, logger, sqlSyntax, mappingResolver) - { - _parentRepo = parentRepo; - } - - protected override TEntity PerformGet(Guid id) - { - return _parentRepo.PerformGet(id); - } - - protected override IEnumerable PerformGetAll(params Guid[] ids) - { - return _parentRepo.PerformGetAll(ids); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return _parentRepo.GetBaseQuery(isCount); - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.uniqueID = @Id"; - } - - #region No implementation required - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new NotImplementedException(); - } - - protected override IEnumerable GetDeleteClauses() - { - throw new NotImplementedException(); - } - - protected override Guid NodeObjectTypeId - { - get { throw new NotImplementedException(); } - } - - protected override void PersistNewItem(TEntity entity) - { - throw new NotImplementedException(); - } - - protected override void PersistUpdatedItem(TEntity entity) - { - throw new NotImplementedException(); - } - #endregion - } - protected abstract TEntity PerformGet(Guid id); + protected abstract TEntity PerformGet(string alias); protected abstract IEnumerable PerformGetAll(params Guid[] ids); + protected abstract bool PerformExists(Guid id); + + /// + /// Gets an Entity by alias + /// + /// + /// + public TEntity Get(string alias) + { + return PerformGet(alias); + } /// /// Gets an Entity by Id @@ -1289,7 +1188,7 @@ AND umbracoNode.id <> @id", /// public TEntity Get(Guid id) { - return _guidRepo.Get(id); + return PerformGet(id); } /// @@ -1297,9 +1196,12 @@ AND umbracoNode.id <> @id", /// /// /// - public IEnumerable GetAll(params Guid[] ids) + /// + /// Ensure explicit implementation, we don't want to have any accidental calls to this since it is essentially the same signature as the main GetAll when there are no parameters + /// + IEnumerable IReadRepository.GetAll(params Guid[] ids) { - return _guidRepo.GetAll(ids); + return PerformGetAll(ids); } /// @@ -1309,7 +1211,7 @@ AND umbracoNode.id <> @id", /// public bool Exists(Guid id) { - return _guidRepo.Exists(id); + return PerformExists(id); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index a1d2e92564..39c6184dc2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Cache; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; @@ -29,42 +30,54 @@ namespace Umbraco.Core.Persistence.Repositories : base(work, cache, logger, sqlSyntax, mappingResolver) { _templateRepository = templateRepository; - } + } + + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), + //allow this cache to expire + expires:true)); + } + } protected override IContentType PerformGet(int id) { - var contentTypes = ContentTypeQueryMapper.GetContentTypes( - new[] {id}, Database, SqlSyntax, this, _templateRepository); - - var contentType = contentTypes.SingleOrDefault(); - return contentType; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Id == id); } protected override IEnumerable PerformGetAll(params int[] ids) { if (ids.Any()) { - return ContentTypeQueryMapper.GetContentTypes(ids, Database, SqlSyntax, this, _templateRepository); - } - else - { - var sql = new Sql().Select("id").From(SqlSyntax).Where(SqlSyntax, dto => dto.NodeObjectType == NodeObjectTypeId); - var allIds = Database.Fetch(sql).ToArray(); - return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); + //NOTE: This logic should never be executed according to our cache policy + return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, this, _templateRepository) + .Where(x => ids.Contains(x.Id)); } + + return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, this, _templateRepository); } protected override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(SqlSyntax, x => x.Text); + var sql = translator.Translate(); var dtos = Database.Fetch(sql); - return dtos.Any() - ? GetAll(dtos.DistinctBy(x => x.ContentTypeDto.NodeId).Select(x => x.ContentTypeDto.NodeId).ToArray()) - : Enumerable.Empty(); + + return + //This returns a lookup from the GetAll cached looup + (dtos.Any() + ? GetAll(dtos.DistinctBy(x => x.ContentTypeDto.NodeId).Select(x => x.ContentTypeDto.NodeId).ToArray()) + : Enumerable.Empty()) + //order the result by name + .OrderBy(x => x.Name); } /// @@ -88,7 +101,30 @@ namespace Umbraco.Core.Persistence.Repositories { return Database.Fetch("SELECT DISTINCT Alias FROM cmsPropertyType ORDER BY Alias"); } - + + /// + /// Gets all content type aliases + /// + /// + /// If this list is empty, it will return all content type aliases for media, members and content, otherwise + /// it will only return content type aliases for the object types specified + /// + /// + public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) + { + var sql = new Sql().Select("cmsContentType.alias") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, dto => dto.NodeId, dto => dto.NodeId); + + if (objectTypes.Any()) + { + sql = sql.Where("umbracoNode.nodeObjectType IN (@objectTypes)", objectTypes); + } + + return Database.Fetch(sql); + } + protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); @@ -152,6 +188,22 @@ namespace Umbraco.Core.Persistence.Repositories PersistDeletedItem((IEntity)child); } + //Before we call the base class methods to run all delete clauses, we need to first + // delete all of the property data associated with this document type. Normally this will + // be done in the ContentTypeService by deleting all associated content first, but in some cases + // like when we switch a document type, there is property data left over that is linked + // to the previous document type. So we need to ensure it's removed. + var sql = new Sql().Select("DISTINCT cmsPropertyData.propertytypeid") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, dto => dto.PropertyTypeId, dto => dto.Id) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, dto => dto.NodeId, dto => dto.ContentTypeId) + .Where(dto => dto.NodeId == entity.Id); + + //Delete all cmsPropertyData where propertytypeid EXISTS in the subquery above + Database.Execute(SqlSyntax.GetDeleteSubquery("cmsPropertyData", "propertytypeid", sql)); + base.PersistDeletedItem(entity); } @@ -233,25 +285,33 @@ namespace Umbraco.Core.Persistence.Repositories protected override IContentType PerformGet(Guid id) { - var contentTypes = ContentTypeQueryMapper.GetContentTypes( - new[] { id }, Database, SqlSyntax, this, _templateRepository); + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Key == id); + } - var contentType = contentTypes.SingleOrDefault(); - return contentType; + protected override IContentType PerformGet(string alias) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); } protected override IEnumerable PerformGetAll(params Guid[] ids) { + //use the underlying GetAll which will force cache all content types + if (ids.Any()) { - return ContentTypeQueryMapper.GetContentTypes(ids, Database, SqlSyntax, this, _templateRepository); + return GetAll().Where(x => ids.Contains(x.Key)); } else { - var sql = new Sql().Select("id").From(SqlSyntax).Where(SqlSyntax, dto => dto.NodeObjectType == NodeObjectTypeId); - var allIds = Database.Fetch(sql).ToArray(); - return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); + return GetAll(); } } + + protected override bool PerformExists(Guid id) + { + return GetAll().FirstOrDefault(x => x.Key == id) != null; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index d50d0ac29a..1a9fd21c6b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -27,7 +27,6 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class DataTypeDefinitionRepository : PetaPocoRepositoryBase, IDataTypeDefinitionRepository { - private readonly CacheHelper _cacheHelper; private readonly IContentTypeRepository _contentTypeRepository; private readonly DataTypePreValueRepository _preValRepository; @@ -35,7 +34,6 @@ namespace Umbraco.Core.Persistence.Repositories IContentTypeRepository contentTypeRepository, IMappingResolver mappingResolver) : base(work, cache, logger, sqlSyntax, mappingResolver) { - _cacheHelper = cache; _contentTypeRepository = contentTypeRepository; _preValRepository = new DataTypePreValueRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax, mappingResolver); } @@ -240,7 +238,7 @@ AND umbracoNode.id <> @id", //NOTE: This is a special case, we need to clear the custom cache for pre-values here so they are not stale if devs // are querying for them in the Saved event (before the distributed call cache is clearing it) - _cacheHelper.RuntimeCache.ClearCacheItem(GetPrefixedCacheKey(entity.Id)); + RuntimeCache.ClearCacheItem(GetPrefixedCacheKey(entity.Id)); entity.ResetDirtyProperties(); } @@ -281,7 +279,7 @@ AND umbracoNode.id <> @id", public PreValueCollection GetPreValuesCollectionByDataTypeId(int dataTypeId) { - var cached = _cacheHelper.RuntimeCache.GetCacheItemsByKeySearch(GetPrefixedCacheKey(dataTypeId)); + var cached = RuntimeCache.GetCacheItemsByKeySearch(GetPrefixedCacheKey(dataTypeId)); if (cached != null && cached.Any()) { //return from the cache, ensure it's a cloned result @@ -300,7 +298,7 @@ AND umbracoNode.id <> @id", { //We need to see if we can find the cached PreValueCollection based on the cache key above - var cached = _cacheHelper.RuntimeCache.GetCacheItemsByKeyExpression(GetCacheKeyRegex(preValueId)); + var cached = RuntimeCache.GetCacheItemsByKeyExpression(GetCacheKeyRegex(preValueId)); if (cached != null && cached.Any()) { //return from the cache @@ -353,20 +351,29 @@ AND umbracoNode.id <> @id", new MoveEventInfo(toMove, toMove.Path, parentId) }; + var origPath = toMove.Path; + //do the move to a new parent toMove.ParentId = parentId; + + //set the updated path + toMove.Path = string.Concat(container == null ? parentId.ToInvariantString() : container.Path, ",", toMove.Id); + //schedule it for updating in the transaction AddOrUpdate(toMove); - //update all descendants - var descendants = this.GetByQuery(Query.Where(type => type.Path.StartsWith(toMove.Path + ","))); - foreach (var descendant in descendants) + //update all descendants from the original path, update in order of level + var descendants = this.GetByQuery( + new Query().Where(type => type.Path.StartsWith(origPath + ","))); + + var lastParent = toMove; + foreach (var descendant in descendants.OrderBy(x => x.Level)) { moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - //all we're doing here is setting the parent Id to be dirty so that it resets the path/level/etc... - descendant.ParentId = descendant.ParentId + 1; - descendant.ParentId = descendant.ParentId - 1; + descendant.ParentId = lastParent.Id; + descendant.Path = string.Concat(lastParent.Path, ",", descendant.Id); + //schedule it for updating in the transaction AddOrUpdate(descendant); } @@ -459,7 +466,7 @@ AND umbracoNode.id <> @id", + string.Join(",", collection.FormatAsDictionary().Select(x => x.Value.Id).ToArray()); //store into cache - _cacheHelper.RuntimeCache.InsertCacheItem(key, () => collection, + RuntimeCache.InsertCacheItem(key, () => collection, //30 mins new TimeSpan(0, 0, 30), //sliding is true diff --git a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs index 8c8dd2e3fa..02c2ea72b1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -20,14 +21,25 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class DictionaryRepository : PetaPocoRepositoryBase, IDictionaryRepository { - private readonly ILanguageRepository _languageRepository; - private readonly IMappingResolver _mappingResolver; - - public DictionaryRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider syntax, ILanguageRepository languageRepository, IMappingResolver mappingResolver) - : base(work, cache, logger, syntax, mappingResolver) + public DictionaryRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider syntax) + : base(work, cache, logger, syntax) { - _languageRepository = languageRepository; - _mappingResolver = mappingResolver; + } + + private IRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //custom cache policy which will not cache any results for GetAll + return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( + RuntimeCache, + new RepositoryCachePolicyOptions + { + //allow zero to be cached + GetAllCacheAllowZeroCount = true + })); + } } #region Overrides of RepositoryBase @@ -35,17 +47,14 @@ namespace Umbraco.Core.Persistence.Repositories protected override IDictionaryItem PerformGet(int id) { var sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new {Id = id}) + .Where(GetBaseWhereClause(), new { Id = id }) .OrderBy(SqlSyntax, x => x.UniqueId); var dto = Database.Fetch(new DictionaryLanguageTextRelator().Map, sql).FirstOrDefault(); if (dto == null) return null; - - //This will be cached - var allLanguages = _languageRepository.GetAll().ToArray(); - - var entity = ConvertFromDto(dto, allLanguages); + + var entity = ConvertFromDto(dto); //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 @@ -59,14 +68,11 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false).Where("cmsDictionary.pk > 0"); if (ids.Any()) { - sql.Where("cmsDictionary.pk in (@ids)", new { ids = ids }); + sql.Where("cmsDictionary.pk in (@ids)", new { ids = ids }); } - //This will be cached - var allLanguages = _languageRepository.GetAll().ToArray(); - return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql) - .Select(dto => ConvertFromDto(dto, allLanguages)); + .Select(dto => ConvertFromDto(dto)); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -75,12 +81,9 @@ namespace Umbraco.Core.Persistence.Repositories var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); sql.OrderBy(SqlSyntax, x => x.UniqueId); - - //This will be cached - var allLanguages = _languageRepository.GetAll().ToArray(); - + return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql) - .Select(x => ConvertFromDto(x, allLanguages)); + .Select(x => ConvertFromDto(x)); } #endregion @@ -90,7 +93,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); - if(isCount) + if (isCount) { sql.Select("COUNT(*)") .From(SqlSyntax); @@ -126,26 +129,28 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistNewItem(IDictionaryItem entity) { - ((DictionaryItem)entity).AddingEntity(); + var dictionaryItem = ((DictionaryItem) entity); - foreach (var translation in entity.Translations) + dictionaryItem.AddingEntity(); + + foreach (var translation in dictionaryItem.Translations) translation.Value = translation.Value.ToValidXmlString(); var factory = new DictionaryItemFactory(); - var dto = factory.BuildDto(entity); + var dto = factory.BuildDto(dictionaryItem); var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; + dictionaryItem.Id = id; - var translationFactory = new DictionaryTranslationFactory(entity.Key, null); - foreach (var translation in entity.Translations) + var translationFactory = new DictionaryTranslationFactory(dictionaryItem.Key); + foreach (var translation in dictionaryItem.Translations) { var textDto = translationFactory.BuildDto(translation); translation.Id = Convert.ToInt32(Database.Insert(textDto)); - translation.Key = entity.Key; + translation.Key = dictionaryItem.Key; } - entity.ResetDirtyProperties(); + dictionaryItem.ResetDirtyProperties(); } protected override void PersistUpdatedItem(IDictionaryItem entity) @@ -160,11 +165,11 @@ namespace Umbraco.Core.Persistence.Repositories Database.Update(dto); - var translationFactory = new DictionaryTranslationFactory(entity.Key, null); + var translationFactory = new DictionaryTranslationFactory(entity.Key); foreach (var translation in entity.Translations) { var textDto = translationFactory.BuildDto(translation); - if(translation.HasIdentity) + if (translation.HasIdentity) { Database.Update(textDto); } @@ -186,7 +191,7 @@ namespace Umbraco.Core.Persistence.Repositories { RecursiveDelete(entity.Key); - Database.Delete("WHERE UniqueId = @Id", new { Id = entity.Key}); + Database.Delete("WHERE UniqueId = @Id", new { Id = entity.Key }); Database.Delete("WHERE id = @Id", new { Id = entity.Key }); //Clear the cache entries that exist by uniqueid/item key @@ -196,7 +201,7 @@ namespace Umbraco.Core.Persistence.Repositories private void RecursiveDelete(Guid parentId) { - var list = Database.Fetch("WHERE parent = @ParentId", new {ParentId = parentId}); + var list = Database.Fetch("WHERE parent = @ParentId", new { ParentId = parentId }); foreach (var dto in list) { RecursiveDelete(dto.UniqueId); @@ -212,20 +217,18 @@ namespace Umbraco.Core.Persistence.Repositories #endregion - protected IDictionaryItem ConvertFromDto(DictionaryDto dto, ILanguage[] allLanguages) + protected IDictionaryItem ConvertFromDto(DictionaryDto dto) { var factory = new DictionaryItemFactory(); var entity = factory.BuildEntity(dto); var list = new List(); foreach (var textDto in dto.LanguageTextDtos) - { - //Assuming this is cached! - var language = allLanguages.FirstOrDefault(x => x.Id == textDto.LanguageId); - if (language == null) + { + if (textDto.LanguageId <= 0) continue; - var translationFactory = new DictionaryTranslationFactory(dto.UniqueId, language); + var translationFactory = new DictionaryTranslationFactory(dto.UniqueId); list.Add(translationFactory.BuildEntity(textDto)); } entity.Translations = list; @@ -237,7 +240,7 @@ namespace Umbraco.Core.Persistence.Repositories { using (var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, UnitOfWork, RepositoryCache, Logger, SqlSyntax, _mappingResolver)) { - return uniqueIdRepo.Get(uniqueId); + return uniqueIdRepo.Get(uniqueId); } } @@ -245,10 +248,10 @@ namespace Umbraco.Core.Persistence.Repositories { using (var keyRepo = new DictionaryByKeyRepository(this, UnitOfWork, RepositoryCache, Logger, SqlSyntax, _mappingResolver)) { - return keyRepo.Get(key); + return keyRepo.Get(key); } } - + private IEnumerable GetRootDictionaryItems() { var query = Query.Where(x => x.ParentId == null); @@ -257,9 +260,6 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetDictionaryItemDescendants(Guid? parentId) { - //This will be cached - var allLanguages = _languageRepository.GetAll().ToArray(); - //This methods will look up children at each level, since we do not store a path for dictionary (ATM), we need to do a recursive // lookup to get descendants. Currently this is the most efficient way to do it @@ -278,7 +278,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.OrderBy(SqlSyntax, x => x.UniqueId); return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql) - .Select(x => ConvertFromDto(x, allLanguages)); + .Select(x => ConvertFromDto(x)); }); }; @@ -287,7 +287,7 @@ namespace Umbraco.Core.Persistence.Repositories : getItemsFromParents(new[] { parentId.Value }); return childItems.SelectRecursive(items => getItemsFromParents(items.Select(x => x.Key).ToArray())).SelectMany(items => items); - + } private class DictionaryByUniqueIdRepository : SimpleGetRepository @@ -317,20 +317,34 @@ namespace Umbraco.Core.Persistence.Repositories protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) { - //This will be cached - var allLanguages = _dictionaryRepository._languageRepository.GetAll().ToArray(); - return _dictionaryRepository.ConvertFromDto(dto, allLanguages); + return _dictionaryRepository.ConvertFromDto(dto); } protected override object GetBaseWhereClauseArguments(Guid id) { - return new {Id = id}; + return new { Id = id }; } protected override string GetWhereInClauseForGetAll() { return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; } + + private IRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //custom cache policy which will not cache any results for GetAll + return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( + RuntimeCache, + new RepositoryCachePolicyOptions + { + //allow zero to be cached + GetAllCacheAllowZeroCount = true + })); + } + } } private class DictionaryByKeyRepository : SimpleGetRepository @@ -360,9 +374,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) { - //This will be cached - var allLanguages = _dictionaryRepository._languageRepository.GetAll().ToArray(); - return _dictionaryRepository.ConvertFromDto(dto, allLanguages); + return _dictionaryRepository.ConvertFromDto(dto); } protected override object GetBaseWhereClauseArguments(string id) @@ -374,17 +386,24 @@ namespace Umbraco.Core.Persistence.Repositories { return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; } + + private IRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //custom cache policy which will not cache any results for GetAll + return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( + RuntimeCache, + new RepositoryCachePolicyOptions + { + //allow zero to be cached + GetAllCacheAllowZeroCount = true + })); + } + } } - /// - /// Dispose disposable properties - /// - /// - /// Ensure the unit of work is disposed - /// - protected override void DisposeResources() - { - _languageRepository.Dispose(); - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs index 9f166a7e4c..c24c0cd3fa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs @@ -19,25 +19,20 @@ namespace Umbraco.Core.Persistence.Repositories internal class DomainRepository : PetaPocoRepositoryBase, IDomainRepository { - private readonly RepositoryCacheOptions _cacheOptions; - public DomainRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMappingResolver mappingResolver) : base(work, cache, logger, sqlSyntax, mappingResolver) - { - //Custom cache options for better performance - _cacheOptions = new RepositoryCacheOptions - { - GetAllCacheAllowZeroCount = true, - GetAllCacheValidateCount = false - }; + { } - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { - get { return _cacheOptions; } + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + } } protected override IDomain PerformGet(int id) diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs index f8e48e78d0..07ff2a99f2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs @@ -19,9 +19,16 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class EntityContainerRepository : PetaPocoRepositoryBase { - public EntityContainerRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMappingResolver mappingResolver) - : base(work, cache, logger, sqlSyntax, mappingResolver) - { } + private readonly Guid _containerObjectType; + + public EntityContainerRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, Guid containerObjectType) + : base(work, cache, logger, sqlSyntax) + { + var allowedContainers = new[] {Constants.ObjectTypes.DocumentTypeContainerGuid, Constants.ObjectTypes.MediaTypeContainerGuid, Constants.ObjectTypes.DataTypeContainerGuid}; + _containerObjectType = containerObjectType; + if (allowedContainers.Contains(_containerObjectType) == false) + throw new InvalidOperationException("No container type exists with ID: " + _containerObjectType); + } /// /// Do not cache anything @@ -33,7 +40,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override EntityContainer PerformGet(int id) { - var sql = GetBaseQuery(false).Where(GetBaseWhereClause(), new { id }); + var sql = GetBaseQuery(false).Where(GetBaseWhereClause(), new { id = id, NodeObjectType = NodeObjectTypeId }); var nodeDto = Database.Fetch(sql).FirstOrDefault(); return nodeDto == null ? null : CreateEntity(nodeDto); @@ -48,9 +55,25 @@ namespace Umbraco.Core.Persistence.Repositories return nodeDto == null ? null : CreateEntity(nodeDto); } + public IEnumerable Get(string name, int level) + { + var sql = GetBaseQuery(false).Where("text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", new { name, level, umbracoObjectTypeId = NodeObjectTypeId }); + return Database.Fetch(sql).Select(CreateEntity); + } + protected override IEnumerable PerformGetAll(params int[] ids) { - throw new NotImplementedException(); + //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit + return ids.InGroupsOf(2000).SelectMany(@group => + { + var sql = GetBaseQuery(false) + .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) + .Where(string.Format("{0} IN (@ids)", SqlSyntax.GetQuotedColumnName("id")), new { ids = @group }); + + sql.OrderBy(x => x.Level, SqlSyntax); + + return Database.Fetch(sql).Select(CreateEntity); + }); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -94,7 +117,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override string GetBaseWhereClause() { - return "umbracoNode.id = @id"; //" and nodeObjectType = @NodeObjectType"; + return "umbracoNode.id = @id and nodeObjectType = @NodeObjectType"; } protected override IEnumerable GetDeleteClauses() @@ -104,11 +127,13 @@ namespace Umbraco.Core.Persistence.Repositories protected override Guid NodeObjectTypeId { - get { throw new NotImplementedException(); } + get { return _containerObjectType; } } protected override void PersistDeletedItem(EntityContainer entity) { + EnsureContainerType(entity); + var nodeDto = Database.FirstOrDefault(new Sql().Select("*") .From(SqlSyntax) .Where(SqlSyntax, dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); @@ -138,6 +163,8 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistNewItem(EntityContainer entity) { + EnsureContainerType(entity); + entity.Name = entity.Name.Trim(); Mandate.ParameterNotNullOrEmpty(entity.Name, "entity.Name"); @@ -175,12 +202,10 @@ namespace Umbraco.Core.Persistence.Repositories Path = path, SortOrder = 0, Text = entity.Name, - UserId = entity.CreatorId + UserId = entity.CreatorId, + UniqueId = entity.Key }; - if (entity.Key != default(Guid)) - nodeDto.UniqueId = entity.Key; - // insert, get the id, update the path with the id var id = Convert.ToInt32(Database.Insert(nodeDto)); nodeDto.Path = nodeDto.Path + "," + nodeDto.NodeId; @@ -188,7 +213,6 @@ namespace Umbraco.Core.Persistence.Repositories // refresh the entity entity.Id = id; - entity.Key = nodeDto.UniqueId; entity.Path = nodeDto.Path; entity.Level = nodeDto.Level; entity.SortOrder = 0; @@ -200,6 +224,8 @@ namespace Umbraco.Core.Persistence.Repositories // protected override void PersistUpdatedItem(EntityContainer entity) { + EnsureContainerType(entity); + entity.Name = entity.Name.Trim(); Mandate.ParameterNotNullOrEmpty(entity.Name, "entity.Name"); @@ -249,5 +275,13 @@ namespace Umbraco.Core.Persistence.Repositories entity.SortOrder = 0; entity.ResetDirtyProperties(); } + + private void EnsureContainerType(EntityContainer entity) + { + if (entity.ContainerObjectType != NodeObjectTypeId) + { + throw new InvalidOperationException("The container type does not match the repository object type"); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeCompositionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeCompositionRepository.cs new file mode 100644 index 0000000000..649726b100 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeCompositionRepository.cs @@ -0,0 +1,11 @@ +using System; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IContentTypeCompositionRepository : IRepositoryQueryable, IReadRepository + where TEntity : IContentTypeComposition + { + TEntity Get(string alias); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs index 625023fd9e..49520387cd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IContentTypeRepository : IRepositoryQueryable, IReadRepository + public interface IContentTypeRepository : IContentTypeCompositionRepository { /// /// Gets all entities of the specified query @@ -22,5 +22,15 @@ namespace Umbraco.Core.Persistence.Repositories IEnumerable GetAllPropertyTypeAliases(); IEnumerable> Move(IContentType toMove, EntityContainer container); + + /// + /// Gets all content type aliases + /// + /// + /// If this list is empty, it will return all content type aliases for media, members and content, otherwise + /// it will only return content type aliases for the object types specified + /// + /// + IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs index 6d9331a22a..1cec8005c9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs @@ -1,12 +1,11 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IMediaTypeRepository : IRepositoryQueryable, IReadRepository + public interface IMediaTypeRepository : IContentTypeCompositionRepository { /// /// Gets all entities of the specified query diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs index ae7739b28b..fc877d5227 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { - public interface IMemberTypeRepository : IRepositoryQueryable, IReadRepository + public interface IMemberTypeRepository : IContentTypeCompositionRepository { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs index 14957ac49f..3faba9e282 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories @@ -22,6 +23,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// [Obsolete("Use GetDescendants instead")] + [EditorBrowsable(EditorBrowsableState.Never)] TemplateNode GetTemplateNode(string alias); /// @@ -31,6 +33,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// [Obsolete("Use GetDescendants instead")] + [EditorBrowsable(EditorBrowsableState.Never)] TemplateNode FindTemplateInTree(TemplateNode anyNode, string alias); /// diff --git a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs index ed37ec8885..8fbc122821 100644 --- a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -21,23 +22,18 @@ namespace Umbraco.Core.Persistence.Repositories { public LanguageRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMappingResolver mappingResolver) : base(work, cache, logger, sqlSyntax, mappingResolver) - { - //Custom cache options for better performance - _cacheOptions = new RepositoryCacheOptions - { - GetAllCacheAllowZeroCount = true, - GetAllCacheValidateCount = false - }; + { } - private readonly RepositoryCacheOptions _cacheOptions; - - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { - get { return _cacheOptions; } + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + } } #region Overrides of RepositoryBase @@ -160,13 +156,13 @@ namespace Umbraco.Core.Persistence.Repositories public ILanguage GetByCultureName(string cultureName) { - //use the underlying GetAll which will force cache all domains + //use the underlying GetAll which will force cache all languages return GetAll().FirstOrDefault(x => x.CultureName.InvariantEquals(cultureName)); } public ILanguage GetByIsoCode(string isoCode) { - //use the underlying GetAll which will force cache all domains + //use the underlying GetAll which will force cache all languages return GetAll().FirstOrDefault(x => x.IsoCode.InvariantEquals(isoCode)); } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index 1bf84c55d4..4e1654c2ad 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Cache; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; @@ -27,41 +28,53 @@ namespace Umbraco.Core.Persistence.Repositories : base(work, cache, logger, sqlSyntax, mappingResolver) { } - + + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), + //allow this cache to expire + expires: true)); + } + } + protected override IMediaType PerformGet(int id) { - var contentTypes = ContentTypeQueryMapper.GetMediaTypes( - new[] { id }, Database, SqlSyntax, this); - - var contentType = contentTypes.SingleOrDefault(); - return contentType; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Id == id); } protected override IEnumerable PerformGetAll(params int[] ids) { if (ids.Any()) { - return ContentTypeQueryMapper.GetMediaTypes(ids, Database, SqlSyntax, this); - } - else - { - var sql = new Sql().Select("id").From(SqlSyntax).Where(SqlSyntax, dto => dto.NodeObjectType == NodeObjectTypeId); - var allIds = Database.Fetch(sql).ToArray(); - return ContentTypeQueryMapper.GetMediaTypes(allIds, Database, SqlSyntax, this); + //NOTE: This logic should never be executed according to our cache policy + return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, this) + .Where(x => ids.Contains(x.Id)); } + + return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, this); } protected override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(SqlSyntax, x => x.Text); + var sql = translator.Translate(); var dtos = Database.Fetch(sql); - return dtos.Any() - ? GetAll(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray()) - : Enumerable.Empty(); + + return + //This returns a lookup from the GetAll cached looup + (dtos.Any() + ? GetAll(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray()) + : Enumerable.Empty()) + //order the result by name + .OrderBy(x => x.Name); } /// @@ -150,28 +163,36 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); } - + protected override IMediaType PerformGet(Guid id) { - var contentTypes = ContentTypeQueryMapper.GetMediaTypes( - new[] { id }, Database, SqlSyntax, this); - - var contentType = contentTypes.SingleOrDefault(); - return contentType; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Key == id); } protected override IEnumerable PerformGetAll(params Guid[] ids) { + //use the underlying GetAll which will force cache all content types + if (ids.Any()) { - return ContentTypeQueryMapper.GetMediaTypes(ids, Database, SqlSyntax, this); + return GetAll().Where(x => ids.Contains(x.Key)); } else { - var sql = new Sql().Select("id").From(SqlSyntax).Where(SqlSyntax, dto => dto.NodeObjectType == NodeObjectTypeId); - var allIds = Database.Fetch(sql).ToArray(); - return ContentTypeQueryMapper.GetMediaTypes(allIds, Database, SqlSyntax, this); + return GetAll(); } } + + protected override bool PerformExists(Guid id) + { + return GetAll().FirstOrDefault(x => x.Key == id) != null; + } + + protected override IMediaType PerformGet(string alias) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs index 6f41d91659..087e1d6be4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs @@ -21,12 +21,9 @@ namespace Umbraco.Core.Persistence.Repositories internal class MemberGroupRepository : PetaPocoRepositoryBase, IMemberGroupRepository { - private readonly CacheHelper _cacheHelper; - - public MemberGroupRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMappingResolver mappingResolver) - : base(work, cache, logger, sqlSyntax, mappingResolver) + public MemberGroupRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) { - _cacheHelper = cache; } private readonly MemberGroupFactory _modelFactory = new MemberGroupFactory(); @@ -135,7 +132,7 @@ namespace Umbraco.Core.Persistence.Repositories public IMemberGroup GetByName(string name) { - return _cacheHelper.RuntimeCache.GetCacheItem( + return RuntimeCache.GetCacheItem( string.Format("{0}.{1}", typeof (IMemberGroup).FullName, name), () => { diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index a1f8ae4061..33f318b493 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using log4net; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models; @@ -27,25 +28,23 @@ namespace Umbraco.Core.Persistence.Repositories { } - #region Overrides of RepositoryBase - + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), + //allow this cache to expire + expires: true)); + } + } + protected override IMemberType PerformGet(int id) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - sql.OrderByDescending(SqlSyntax, x => x.NodeId); - - var dtos = - Database.Fetch( - new PropertyTypePropertyGroupRelator().Map, sql); - - if (dtos == null || dtos.Any() == false) - return null; - - var factory = new MemberTypeReadOnlyFactory(); - var member = factory.BuildEntity(dtos.First()); - - return member; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Id == id); } protected override IEnumerable PerformGetAll(params int[] ids) @@ -53,10 +52,11 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); if (ids.Any()) { + //NOTE: This logic should never be executed according to our cache policy var statement = string.Join(" OR ", ids.Select(x => string.Format("umbracoNode.id='{0}'", x))); sql.Where(statement); } - sql.OrderByDescending(SqlSyntax, x => x.NodeId); + sql.OrderByDescending(x => x.NodeId, SqlSyntax); var dtos = Database.Fetch( @@ -72,7 +72,7 @@ namespace Umbraco.Core.Persistence.Repositories var subquery = translator.Translate(); var sql = GetBaseQuery(false) .Append(new Sql("WHERE umbracoNode.id IN (" + subquery.SQL + ")", subquery.Arguments)) - .OrderBy(SqlSyntax, x => x.SortOrder); + .OrderBy(x => x.SortOrder, SqlSyntax); var dtos = Database.Fetch( @@ -80,11 +80,7 @@ namespace Umbraco.Core.Persistence.Repositories return BuildFromDtos(dtos); } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - + protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); @@ -158,11 +154,7 @@ namespace Umbraco.Core.Persistence.Repositories { get { return new Guid(Constants.ObjectTypes.MemberType); } } - - #endregion - - #region Unit of Work Implementation - + protected override void PersistNewItem(IMemberType entity) { ValidateAlias(entity); @@ -233,8 +225,6 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); } - - #endregion /// /// Override so we can specify explicit db type's on any property types that are built-in. @@ -257,38 +247,33 @@ namespace Umbraco.Core.Persistence.Repositories protected override IMemberType PerformGet(Guid id) { - var sql = GetBaseQuery(false); - sql.Where("umbracoNode.uniqueID = @Id", new { Id = id }); - sql.OrderByDescending(SqlSyntax, x => x.NodeId); - - var dtos = - Database.Fetch( - new PropertyTypePropertyGroupRelator().Map, sql); - - if (dtos == null || dtos.Any() == false) - return null; - - var factory = new MemberTypeReadOnlyFactory(); - var member = factory.BuildEntity(dtos.First()); - - return member; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Key == id); } protected override IEnumerable PerformGetAll(params Guid[] ids) { - var sql = GetBaseQuery(false); + //use the underlying GetAll which will force cache all content types + if (ids.Any()) { - var statement = string.Join(" OR ", ids.Select(x => string.Format("umbracoNode.uniqueID='{0}'", x))); - sql.Where(statement); + return GetAll().Where(x => ids.Contains(x.Key)); } - sql.OrderByDescending(SqlSyntax, x => x.NodeId); + else + { + return GetAll(); + } + } - var dtos = - Database.Fetch( - new PropertyTypePropertyGroupRelator().Map, sql); + protected override bool PerformExists(Guid id) + { + return GetAll().FirstOrDefault(x => x.Key == id) != null; + } - return BuildFromDtos(dtos); + protected override IMemberType PerformGet(string alias) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index 4a6a7e7a30..8be1dc1b6d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -26,13 +26,14 @@ namespace Umbraco.Core.Persistence.Repositories where TEntity : class, IAggregateRoot { private readonly IDatabaseUnitOfWork _unitOfWork; - private readonly CacheHelper _cache; + private readonly IRuntimeCacheProvider _runtimeCache; private readonly ISqlSyntaxProvider _sqlSyntax; internal PermissionRepository(IDatabaseUnitOfWork unitOfWork, CacheHelper cache, ISqlSyntaxProvider sqlSyntax) { _unitOfWork = unitOfWork; - _cache = cache; + //Make this repository use an isolated cache + _runtimeCache = cache.IsolatedRuntimeCache.GetOrCreateCache(); _sqlSyntax = sqlSyntax; } @@ -45,7 +46,7 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetUserPermissionsForEntities(int userId, params int[] entityIds) { var entityIdKey = string.Join(",", entityIds.Select(x => x.ToString(CultureInfo.InvariantCulture))); - return _cache.RuntimeCache.GetCacheItem>( + return _runtimeCache.GetCacheItem>( string.Format("{0}{1}{2}", CacheKeys.UserPermissionsCacheKey, userId, entityIdKey), () => { diff --git a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs index 28c80ab165..3b3538b220 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs @@ -18,33 +18,24 @@ namespace Umbraco.Core.Persistence.Repositories { public PublicAccessRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMappingResolver mappingResolver) : base(work, cache, logger, sqlSyntax, mappingResolver) - { - _options = new RepositoryCacheOptions - { - //We want to ensure that a zero count gets cached, even if there is nothing in the db we don't want it to lookup nothing each time - GetAllCacheAllowZeroCount = true, - //Set to 1000 just to ensure that all of them are cached, The GetAll on this repository gets called *A lot*, we want max performance - GetAllCacheThresholdLimit = 1000, - //Override to false so that a Count check against the db is NOT performed when doing a GetAll without params, we just want to - // return the raw cache without validation. The GetAll on this repository gets called *A lot*, we want max performance - GetAllCacheValidateCount = false - }; + { } - private readonly RepositoryCacheOptions _options; + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + } + } protected override PublicAccessEntry PerformGet(Guid id) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - - var taskDto = Database.Fetch(new AccessRulesRelator().Map, sql).FirstOrDefault(); - if (taskDto == null) - return null; - - var factory = new PublicAccessEntryFactory(); - var entity = factory.BuildEntity(taskDto); - return entity; + //return from GetAll - this will be cached as a collection + return GetAll().FirstOrDefault(x => x.Key == id); } protected override IEnumerable PerformGetAll(params Guid[] ids) @@ -103,15 +94,6 @@ namespace Umbraco.Core.Persistence.Repositories get { throw new NotImplementedException(); } } - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions - { - get { return _options; } - } - - protected override void PersistNewItem(PublicAccessEntry entity) { entity.AddingEntity(); diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index 172619e2f7..5534a9ea40 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -1,7 +1,9 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Cache; +using Umbraco.Core.Collections; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; @@ -22,11 +24,7 @@ namespace Umbraco.Core.Persistence.Repositories if (logger == null) throw new ArgumentNullException("logger"); Logger = logger; _work = work; - - //IMPORTANT: We will force the DeepCloneRuntimeCacheProvider to be used here which is a wrapper for the underlying - // runtime cache to ensure that anything that can be deep cloned in/out is done so, this also ensures that our tracks - // changes entities are reset. - _cache = new CacheHelper(new DeepCloneRuntimeCacheProvider(cache.RuntimeCache), cache.StaticCache, cache.RequestCache); + _cache = cache; } /// @@ -84,20 +82,41 @@ namespace Umbraco.Core.Persistence.Repositories { } - private readonly RepositoryCacheOptions _cacheOptions = new RepositoryCacheOptions(); + + protected virtual TId GetEntityId(TEntity entity) + { + return (TId)(object)entity.Id; + } /// - /// Used to create a new query instance + /// The runtime cache used for this repo by default is the isolated cache for this type /// - /// - public abstract Query Query { get; } + protected override IRuntimeCacheProvider RuntimeCache + { + get { return RepositoryCache.IsolatedRuntimeCache.GetOrCreateCache(); } + } + private IRepositoryCachePolicyFactory _cachePolicyFactory; /// - /// Returns a query factory instance + /// Returns the Cache Policy for the repository /// - public abstract QueryFactory QueryFactory { get; } - - #region IRepository Members + /// + /// The Cache Policy determines how each entity or entity collection is cached + /// + protected virtual IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + return _cachePolicyFactory ?? (_cachePolicyFactory = new DefaultRepositoryCachePolicyFactory( + RuntimeCache, + new RepositoryCachePolicyOptions(() => + { + //Get count of all entities of current type (TEntity) to ensure cached result is correct + var query = Query.Builder.Where(x => x.Id != 0); + return PerformCount(query); + }))); + } + } /// /// Adds or Updates an entity of type TEntity @@ -130,23 +149,16 @@ namespace Umbraco.Core.Persistence.Repositories protected abstract TEntity PerformGet(TId id); /// - /// Gets an entity by the passed in Id utilizing the repository's runtime cache + /// Gets an entity by the passed in Id utilizing the repository's cache policy /// /// /// public TEntity Get(TId id) { - var cacheKey = GetCacheIdKey(id); - var fromCache = RuntimeCache.GetCacheItem(cacheKey); - - if (fromCache != null) return fromCache; - - var entity = PerformGet(id); - if (entity == null) return null; - - RuntimeCache.InsertCacheItem(cacheKey, () => entity); - - return entity; + using (var p = CachePolicyFactory.CreatePolicy()) + { + return p.Get(id, PerformGet); + } } protected abstract IEnumerable PerformGetAll(params TId[] ids); @@ -169,88 +181,13 @@ namespace Umbraco.Core.Persistence.Repositories throw new InvalidOperationException("Cannot perform a query with more than 2000 parameters"); } - if (ids.Any()) + using (var p = CachePolicyFactory.CreatePolicy()) { - var entities = ids.Select(x => RuntimeCache.GetCacheItem(GetCacheIdKey(x))).ToArray(); - - if (ids.Count().Equals(entities.Count()) && entities.Any(x => x == null) == false) - return entities; - } - else - { - var allEntities = RuntimeCache.GetCacheItemsByKeySearch(GetCacheTypeKey()) - .WhereNotNull() - .ToArray(); - - if (allEntities.Any()) - { - - if (RepositoryCacheOptions.GetAllCacheValidateCount) - { - //Get count of all entities of current type (TEntity) to ensure cached result is correct - var query = Query.Where(x => x.Id != 0); - int totalCount = PerformCount(query); - - if (allEntities.Count() == totalCount) - return allEntities; - } - else - { - return allEntities; - } - } - else if (RepositoryCacheOptions.GetAllCacheAllowZeroCount) - { - //if the repository allows caching a zero count, then check the zero count cache - var zeroCount = RuntimeCache.GetCacheItem(GetCacheTypeKey()); - if (zeroCount != null && zeroCount.Any() == false) - { - //there is a zero count cache so return an empty list - return Enumerable.Empty(); - } - } - - } - - var entityCollection = PerformGetAll(ids) - //ensure we don't include any null refs in the returned collection! - .WhereNotNull() - .ToArray(); - - //We need to put a threshold here! IF there's an insane amount of items - // coming back here we don't want to chuck it all into memory, this added cache here - // is more for convenience when paging stuff temporarily - - if (entityCollection.Length > RepositoryCacheOptions.GetAllCacheThresholdLimit) - return entityCollection; - - if (entityCollection.Length == 0 && RepositoryCacheOptions.GetAllCacheAllowZeroCount) - { - //there was nothing returned but we want to cache a zero count result so add an TEntity[] to the cache - // to signify that there is a zero count cache - RuntimeCache.InsertCacheItem(GetCacheTypeKey(), () => new TEntity[] {}); - } - - foreach (var entity in entityCollection) - { - if (entity != null) - { - var localCopy = entity; - RuntimeCache.InsertCacheItem(GetCacheIdKey(entity.Id), () => localCopy); - } - } - - return entityCollection; + var result = p.GetAll(ids, PerformGetAll); + return result; + } } - - /// - /// Returns the repository cache options - /// - protected virtual RepositoryCacheOptions RepositoryCacheOptions - { - get { return _cacheOptions; } - } - + protected abstract IEnumerable PerformGetByQuery(IQuery query); /// /// Gets a list of entities by the passed in query @@ -272,12 +209,10 @@ namespace Umbraco.Core.Persistence.Repositories /// public bool Exists(TId id) { - var fromCache = RuntimeCache.GetCacheItem(GetCacheIdKey(id)); - if (fromCache != null) + using (var p = CachePolicyFactory.CreatePolicy()) { - return true; + return p.Exists(id, PerformExists); } - return PerformExists(id); } protected abstract int PerformCount(IQuery query); @@ -290,34 +225,19 @@ namespace Umbraco.Core.Persistence.Repositories { return PerformCount(query); } - - #endregion - - #region IUnitOfWorkRepository Members - + /// /// Unit of work method that tells the repository to persist the new entity /// /// public virtual void PersistNewItem(IEntity entity) { - try - { - PersistNewItem((TEntity)entity); - RuntimeCache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - } - catch (Exception ex) - { - //if an exception is thrown we need to remove the entry from cache, this is ONLY a work around because of the way - // that we cache entities: http://issues.umbraco.org/issue/U4-4259 - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Id)); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - throw; - } + var casted = (TEntity)entity; + using (var p = CachePolicyFactory.CreatePolicy()) + { + p.CreateOrUpdate(casted, PersistNewItem); + } } /// @@ -326,23 +246,12 @@ namespace Umbraco.Core.Persistence.Repositories /// public virtual void PersistUpdatedItem(IEntity entity) { - try - { - PersistUpdatedItem((TEntity)entity); - RuntimeCache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - } - catch (Exception) - { - //if an exception is thrown we need to remove the entry from cache, this is ONLY a work around because of the way - // that we cache entities: http://issues.umbraco.org/issue/U4-4259 - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Id)); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - throw; - } + var casted = (TEntity)entity; + using (var p = CachePolicyFactory.CreatePolicy()) + { + p.CreateOrUpdate(casted, PersistUpdatedItem); + } } /// @@ -351,21 +260,19 @@ namespace Umbraco.Core.Persistence.Repositories /// public virtual void PersistDeletedItem(IEntity entity) { - PersistDeletedItem((TEntity)entity); - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Id)); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); + var casted = (TEntity)entity; + + using (var p = CachePolicyFactory.CreatePolicy()) + { + p.Remove(casted, PersistDeletedItem); + } } - - #endregion - - #region Abstract IUnitOfWorkRepository Methods + protected abstract void PersistNewItem(TEntity item); protected abstract void PersistUpdatedItem(TEntity item); protected abstract void PersistDeletedItem(TEntity item); - #endregion /// /// Dispose disposable properties diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index c1ec5aab19..b162af53aa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; @@ -33,7 +34,6 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ITemplatesSection _templateConfig; private readonly ViewHelper _viewHelper; private readonly MasterPageHelper _masterPageHelper; - private readonly RepositoryCacheOptions _cacheOptions; public TemplateRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IFileSystem masterpageFileSystem, IFileSystem viewFileSystem, ITemplatesSection templateConfig, IMappingResolver mappingResolver) : base(work, cache, logger, sqlSyntax, mappingResolver) @@ -42,41 +42,27 @@ namespace Umbraco.Core.Persistence.Repositories _viewsFileSystem = viewFileSystem; _templateConfig = templateConfig; _viewHelper = new ViewHelper(_viewsFileSystem); - _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); - - _cacheOptions = new RepositoryCacheOptions - { - //Allow a zero count cache entry because GetAll() gets used quite a lot and we want to ensure - // if there are no templates, that it doesn't keep going to the db. - GetAllCacheAllowZeroCount = true, - //GetAll gets called a lot, we want to ensure that all templates are in the cache, default is 100 which - // would normally be fine but we'll increase it in case people have a ton of templates. - GetAllCacheThresholdLimit = 500 - }; + _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); } - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { - get { return _cacheOptions; } + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + } } #region Overrides of RepositoryBase protected override ITemplate PerformGet(int id) { - var sql = GetBaseQuery(false).Where(SqlSyntax, x => x.NodeId == id); - var result = Database.Fetch(sql).FirstOrDefault(); - if (result == null) return null; - - //look up the simple template definitions that have a master template assigned, this is used - // later to populate the template item's properties - var childIds = GetAxisDefinitions(result).ToArray(); - - return MapFromDto(result, childIds); + //use the underlying GetAll which will force cache all templates + return base.GetAll().FirstOrDefault(x => x.Id == id); } protected override IEnumerable PerformGetAll(params int[] ids) @@ -480,126 +466,102 @@ namespace Umbraco.Core.Persistence.Repositories public ITemplate Get(string alias) { - var sql = GetBaseQuery(false).Where(SqlSyntax, x => x.Alias == alias); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - return MapFromDto(dto, GetAxisDefinitions(dto).ToArray()); + return GetAll(alias).FirstOrDefault(); } public IEnumerable GetAll(params string[] aliases) { - var sql = GetBaseQuery(false); + //We must call the base (normal) GetAll method + // which is cached. This is a specialized method and unfortunatley with the params[] it + // overlaps with the normal GetAll method. + if (aliases.Any() == false) return base.GetAll(); - if (aliases.Any()) - { - sql.Where("cmsTemplate.alias IN (@aliases)", new {aliases = aliases}); - } - - var dtos = Database.Fetch(sql).ToArray(); - if (dtos.Length == 0) return Enumerable.Empty(); - - var axisDefos = GetAxisDefinitions(dtos).ToArray(); - return dtos.Select(x => MapFromDto(x, axisDefos)); + //return from base.GetAll, this is all cached + return base.GetAll().Where(x => aliases.InvariantContains(x.Alias)); } public IEnumerable GetChildren(int masterTemplateId) { - var sql = GetBaseQuery(false); - if (masterTemplateId <= 0) - { - sql.Where(SqlSyntax, x => x.ParentId <= 0); - } - else - { - sql.Where(SqlSyntax, x => x.ParentId == masterTemplateId); - } + //return from base.GetAll, this is all cached + var all = base.GetAll().ToArray(); - var dtos = Database.Fetch(sql).ToArray(); - if (dtos.Length == 0) return Enumerable.Empty(); + if (masterTemplateId <= 0) return all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace()); - var axisDefos = GetAxisDefinitions(dtos).ToArray(); - return dtos.Select(x => MapFromDto(x, axisDefos)); + var parent = all.FirstOrDefault(x => x.Id == masterTemplateId); + if (parent == null) return Enumerable.Empty(); + + var children = all.Where(x => x.MasterTemplateAlias.InvariantEquals(parent.Alias)); + return children; } public IEnumerable GetChildren(string alias) { - var sql = GetBaseQuery(false); - if (alias.IsNullOrWhiteSpace()) - { - sql.Where(SqlSyntax, x => x.ParentId <= 0); - } - else - { - //unfortunately SQLCE doesn't support scalar subqueries in the where clause, otherwise we could have done this - // in a single query, now we have to lookup the path to acheive the same thing - var parent = Database.ExecuteScalar(new Sql().Select("nodeId").From(SqlSyntax).Where(SqlSyntax, dto => dto.Alias == alias)); - if (parent.HasValue == false) return Enumerable.Empty(); - - sql.Where(SqlSyntax, x => x.ParentId == parent.Value); - } - - var dtos = Database.Fetch(sql).ToArray(); - if (dtos.Length == 0) return Enumerable.Empty(); - - var axisDefos = GetAxisDefinitions(dtos).ToArray(); - return dtos.Select(x => MapFromDto(x, axisDefos)); + //return from base.GetAll, this is all cached + return base.GetAll().Where(x => alias.IsNullOrWhiteSpace() + ? x.MasterTemplateAlias.IsNullOrWhiteSpace() + : x.MasterTemplateAlias.InvariantEquals(alias)); } public IEnumerable GetDescendants(int masterTemplateId) { - var sql = GetBaseQuery(false); + //return from base.GetAll, this is all cached + var all = base.GetAll().ToArray(); + var descendants = new List(); if (masterTemplateId > 0) { - //unfortunately SQLCE doesn't support scalar subqueries in the where clause, otherwise we could have done this - // in a single query, now we have to lookup the path to acheive the same thing - var path = Database.ExecuteScalar( - new Sql().Select(SqlSyntax.GetQuotedColumnName("path")) - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, dto => dto.NodeId, dto => dto.NodeId) - .Where(SqlSyntax, dto => dto.NodeId == masterTemplateId)); - - if (path.IsNullOrWhiteSpace()) return Enumerable.Empty(); - - sql.Where(@"(umbracoNode." + SqlSyntax.GetQuotedColumnName("path") + @" LIKE @query)", new { query = path + ",%" }); + var parent = all.FirstOrDefault(x => x.Id == masterTemplateId); + if (parent == null) return Enumerable.Empty(); + //recursively add all children with a level + AddChildren(all, descendants, parent.Alias); + } + else + { + descendants.AddRange(all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace())); + foreach (var parent in descendants) + { + //recursively add all children with a level + AddChildren(all, descendants, parent.Alias); + } } - sql.OrderBy("umbracoNode." + SqlSyntax.GetQuotedColumnName("level")); - - var dtos = Database.Fetch(sql).ToArray(); - if (dtos.Length == 0) return Enumerable.Empty(); - - var axisDefos = GetAxisDefinitions(dtos).ToArray(); - return dtos.Select(x => MapFromDto(x, axisDefos)); - + //return the list - it will be naturally ordered by level + return descendants; } - + public IEnumerable GetDescendants(string alias) { - var sql = GetBaseQuery(false); + var all = base.GetAll().ToArray(); + var descendants = new List(); if (alias.IsNullOrWhiteSpace() == false) { - //unfortunately SQLCE doesn't support scalar subqueries in the where clause, otherwise we could have done this - // in a single query, now we have to lookup the path to acheive the same thing - var path = Database.ExecuteScalar( - "SELECT umbracoNode.path FROM cmsTemplate INNER JOIN umbracoNode ON cmsTemplate.nodeId = umbracoNode.id WHERE cmsTemplate.alias = @alias", new { alias = alias }); - - if (path.IsNullOrWhiteSpace()) return Enumerable.Empty(); - - sql.Where(@"(umbracoNode." + SqlSyntax.GetQuotedColumnName("path") + @" LIKE @query)", new {query = path + ",%" }); + var parent = all.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + if (parent == null) return Enumerable.Empty(); + //recursively add all children + AddChildren(all, descendants, parent.Alias); } + else + { + descendants.AddRange(all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace())); + foreach (var parent in descendants) + { + //recursively add all children with a level + AddChildren(all, descendants, parent.Alias); + } + } + //return the list - it will be naturally ordered by level + return descendants; + } - sql.OrderBy("umbracoNode." + SqlSyntax.GetQuotedColumnName("level")); - - var dtos = Database.Fetch(sql).ToArray(); - if (dtos.Length == 0) return Enumerable.Empty(); - - var axisDefos = GetAxisDefinitions(dtos).ToArray(); - return dtos.Select(x => MapFromDto(x, axisDefos)); + private void AddChildren(ITemplate[] all, List descendants, string masterAlias) + { + var c = all.Where(x => x.MasterTemplateAlias.InvariantEquals(masterAlias)).ToArray(); + descendants.AddRange(c); + if (c.Any() == false) return; + //recurse through all children + foreach (var child in c) + { + AddChildren(all, descendants, child.Alias); + } } /// @@ -611,9 +573,9 @@ namespace Umbraco.Core.Persistence.Repositories public TemplateNode GetTemplateNode(string alias) { //first get all template objects - var allTemplates = GetAll().ToArray(); + var allTemplates = base.GetAll().ToArray(); - var selfTemplate = allTemplates.SingleOrDefault(x => x.Alias == alias); + var selfTemplate = allTemplates.SingleOrDefault(x => x.Alias.InvariantEquals(alias)); if (selfTemplate == null) { return null; @@ -622,11 +584,11 @@ namespace Umbraco.Core.Persistence.Repositories var top = selfTemplate; while (top.MasterTemplateAlias.IsNullOrWhiteSpace() == false) { - top = allTemplates.Single(x => x.Alias == top.MasterTemplateAlias); + top = allTemplates.Single(x => x.Alias.InvariantEquals(top.MasterTemplateAlias)); } var topNode = new TemplateNode(allTemplates.Single(x => x.Id == top.Id)); - var childTemplates = allTemplates.Where(x => x.MasterTemplateAlias == top.Alias); + var childTemplates = allTemplates.Where(x => x.MasterTemplateAlias.InvariantEquals(top.Alias)); //This now creates the hierarchy recursively topNode.Children = CreateChildren(topNode, childTemplates, allTemplates); @@ -634,10 +596,11 @@ namespace Umbraco.Core.Persistence.Repositories return FindTemplateInTree(topNode, alias); } + [Obsolete("Only used by obsolete code")] private static TemplateNode WalkTree(TemplateNode current, string alias) { //now walk the tree to find the node - if (current.Template.Alias == alias) + if (current.Template.Alias.InvariantEquals(alias)) { return current; } @@ -684,7 +647,7 @@ namespace Umbraco.Core.Persistence.Repositories { var engine = _templateConfig.DefaultRenderingEngine; var viewHelper = new ViewHelper(_viewsFileSystem); - if (!viewHelper.ViewExists(template)) + if (viewHelper.ViewExists(template) == false) { if (template.Content.IsNullOrWhiteSpace() == false && MasterPageHelper.IsMasterPageSyntax(template.Content)) { @@ -769,7 +732,7 @@ namespace Umbraco.Core.Persistence.Repositories //get this node's children var local = childTemplate; - var kids = allTemplates.Where(x => x.MasterTemplateAlias == local.Alias); + var kids = allTemplates.Where(x => x.MasterTemplateAlias.InvariantEquals(local.Alias)); //recurse child.Children = CreateChildren(child, kids, allTemplates); @@ -799,7 +762,7 @@ namespace Umbraco.Core.Persistence.Repositories private bool AliasAlreadExists(ITemplate template) { - var sql = GetBaseQuery(true).Where(SqlSyntax, x => x.Alias == template.Alias && x.NodeId != template.Id); + var sql = GetBaseQuery(true).Where(x => x.Alias.InvariantEquals(template.Alias) && x.NodeId != template.Id); var count = Database.ExecuteScalar(sql); return count > 0; } diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index 6735e3e778..3d68174865 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -1,5 +1,6 @@ using Umbraco.Core.Configuration; using System; +using Umbraco.Core.Cache; using LightInject; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; @@ -181,9 +182,10 @@ namespace Umbraco.Core.Persistence return _container.GetInstance(uow); } - internal virtual EntityContainerRepository CreateEntityContainerRepository(IDatabaseUnitOfWork uow) + internal virtual EntityContainerRepository CreateEntityContainerRepository(IDatabaseUnitOfWork uow, Guid containerObjectType) { return _container.GetInstance(uow); + containerObjectType); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 17770fd23a..8125c183ff 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -12,9 +12,9 @@ namespace Umbraco.Core.Persistence /// Represents the Umbraco implementation of the PetaPoco Database object /// /// - /// Currently this object exists for 'future proofing' our implementation. By having our own inheritied implementation we + /// Currently this object exists for 'future proofing' our implementation. By having our own inheritied implementation we /// can then override any additional execution (such as additional loggging, functionality, etc...) that we need to without breaking compatibility since we'll always be exposing - /// this object instead of the base PetaPoco database object. + /// this object instead of the base PetaPoco database object. /// public class UmbracoDatabase : Database, IDisposeOnRequestEnd { @@ -87,7 +87,9 @@ namespace Umbraco.Core.Persistence public override IDbConnection OnConnectionOpened(IDbConnection connection) { - // wrap the connection with a profiling connection that tracks timings + // propagate timeout if none yet + + // wrap the connection with a profiling connection that tracks timings return new StackExchange.Profiling.Data.ProfiledDbConnection(connection as DbConnection, MiniProfiler.Current); } @@ -97,6 +99,14 @@ namespace Umbraco.Core.Persistence base.OnException(x); } + public override void OnExecutingCommand(IDbCommand cmd) + { + // if no timeout is specified, and the connection has a longer timeout, use it + if (OneTimeCommandTimeout == 0 && CommandTimeout == 0 && cmd.Connection.ConnectionTimeout > 30) + cmd.CommandTimeout = cmd.Connection.ConnectionTimeout; + base.OnExecutingCommand(cmd); + } + public override void OnExecutedCommand(IDbCommand cmd) { if (EnableSqlTrace) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index 11f915fabd..b3db026c89 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -12,6 +12,9 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + /// + /// This ensures that the grid config is merged in with the front-end value + /// [DefaultPropertyValueConverter(typeof(JsonValueConverter))] //this shadows the JsonValueConverter [PropertyValueType(typeof(JToken))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] @@ -90,7 +93,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters } catch (Exception ex) { - LogHelper.Error("Could not parse the string " + sourceString + " to a json object", ex); + LogHelper.Error("Could not parse the string " + sourceString + " to a json object", ex); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs new file mode 100644 index 0000000000..3b018d8c10 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -0,0 +1,125 @@ +using System; +using System.Globalization; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + /// + /// This ensures that the cropper config (pre-values/crops) are merged in with the front-end value. + /// + [DefaultPropertyValueConverter(typeof (JsonValueConverter))] //this shadows the JsonValueConverter + [PropertyValueType(typeof (JToken))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class ImageCropperValueConverter : JsonValueConverter + { + private readonly IDataTypeService _dataTypeService; + + public ImageCropperValueConverter() + { + _dataTypeService = ApplicationContext.Current.Services.DataTypeService; + } + + public ImageCropperValueConverter(IDataTypeService dataTypeService) + { + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + _dataTypeService = dataTypeService; + } + + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.ImageCropperAlias); + } + + internal static void MergePreValues(JObject currentValue, IDataTypeService dataTypeService, int dataTypeId) + { + //need to lookup the pre-values for this data type + //TODO: Change all singleton access to use ctor injection in v8!!! + var dt = dataTypeService.GetPreValuesCollectionByDataTypeId(dataTypeId); + + if (dt != null && dt.IsDictionaryBased && dt.PreValuesAsDictionary.ContainsKey("crops")) + { + var cropsString = dt.PreValuesAsDictionary["crops"].Value; + JArray preValueCrops; + try + { + preValueCrops = JsonConvert.DeserializeObject(cropsString); + } + catch (Exception ex) + { + LogHelper.Error("Could not parse the string " + cropsString + " to a json object", ex); + return; + } + + //now we need to merge the crop values - the alias + width + height comes from pre-configured pre-values, + // however, each crop can store it's own coordinates + + JArray existingCropsArray; + if (currentValue["crops"] != null) + { + existingCropsArray = (JArray)currentValue["crops"]; + } + else + { + currentValue["crops"] = existingCropsArray = new JArray(); + } + + foreach (var preValueCrop in preValueCrops.Where(x => x.HasValues)) + { + var found = existingCropsArray.FirstOrDefault(x => + { + if (x.HasValues && x["alias"] != null) + { + return x["alias"].Value() == preValueCrop["alias"].Value(); + } + return false; + }); + if (found != null) + { + found["width"] = preValueCrop["width"]; + found["height"] = preValueCrop["height"]; + } + else + { + existingCropsArray.Add(preValueCrop); + } + } + } + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) return null; + var sourceString = source.ToString(); + + if (sourceString.DetectIsJson()) + { + JObject obj; + try + { + obj = JsonConvert.DeserializeObject(sourceString, new JsonSerializerSettings + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }); + } + catch (Exception ex) + { + LogHelper.Error("Could not parse the string " + sourceString + " to a json object", ex); + return sourceString; + } + + MergePreValues(obj, _dataTypeService, propertyType.DataTypeId); + + return obj; + } + + //it's not json, just return the string + return sourceString; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs new file mode 100644 index 0000000000..cf034c8c29 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs @@ -0,0 +1,28 @@ +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + /// + /// We need this property converter so that we always force the value of a label to be a string + /// + /// + /// Without a property converter defined for the label type, the value will be converted with + /// the `ConvertUsingDarkMagic` method which will try to parse the value into it's correct type, but this + /// can cause issues if the string is detected as a number and then strips leading zeros. + /// Example: http://issues.umbraco.org/issue/U4-7929 + /// + [DefaultPropertyValueConverter] + [PropertyValueType(typeof (string))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class LabelValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return Constants.PropertyEditors.NoEditAlias.Equals(propertyType.PropertyEditorAlias); + } + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + return source == null ? string.Empty : source.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 79fdc1bed1..b8064474bb 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -167,74 +167,18 @@ namespace Umbraco.Core.Security /// This clears the forms authentication cookie for webapi since cookies are handled differently /// /// - [Obsolete("Use OWIN IAuthenticationManager.SignOut instead")] + [Obsolete("Use OWIN IAuthenticationManager.SignOut instead", true)] [EditorBrowsable(EditorBrowsableState.Never)] public static void UmbracoLogoutWebApi(this HttpResponseMessage response) { - if (response == null) throw new ArgumentNullException("response"); - //remove the cookie - var authCookie = new CookieHeaderValue(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, "") - { - Expires = DateTime.Now.AddYears(-1), - Path = "/" - }; - //remove the preview cookie too - var prevCookie = new CookieHeaderValue(Constants.Web.PreviewCookieName, "") - { - Expires = DateTime.Now.AddYears(-1), - Path = "/" - }; - //remove the external login cookie too - var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalCookieName, "") - { - Expires = DateTime.Now.AddYears(-1), - Path = "/" - }; - - response.Headers.AddCookies(new[] { authCookie, prevCookie, extLoginCookie }); + throw new NotSupportedException("This method is not supported and should not be used, it has been removed in Umbraco 7.4"); } - [Obsolete("Use WebSecurity.SetPrincipalForRequest")] + [Obsolete("Use WebSecurity.SetPrincipalForRequest", true)] [EditorBrowsable(EditorBrowsableState.Never)] public static FormsAuthenticationTicket UmbracoLoginWebApi(this HttpResponseMessage response, IUser user) { - if (response == null) throw new ArgumentNullException("response"); - - //remove the external login cookie - var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalCookieName, "") - { - Expires = DateTime.Now.AddYears(-1), - Path = "/" - }; - - var userDataString = JsonConvert.SerializeObject(Mapper.Map(user)); - - var ticket = new FormsAuthenticationTicket( - 4, - user.Username, - DateTime.Now, - DateTime.Now.AddMinutes(GlobalSettings.TimeOutInMinutes), - true, - userDataString, - "/" - ); - - // Encrypt the cookie using the machine key for secure transport - var encrypted = FormsAuthentication.Encrypt(ticket); - - //add the cookie - var authCookie = new CookieHeaderValue(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, encrypted) - { - //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way - Expires = DateTime.Now.AddMinutes(1440), - Path = "/", - Secure = GlobalSettings.UseSSL, - HttpOnly = true - }; - - response.Headers.AddCookies(new[] { authCookie, extLoginCookie }); - - return ticket; + throw new NotSupportedException("This method is not supported and should not be used, it has been removed in Umbraco 7.4"); } /// diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index 8529132bb5..9c46ae69f4 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -9,6 +9,12 @@ namespace Umbraco.Core.Security { public class BackOfficeClaimsIdentityFactory : ClaimsIdentityFactory { + public BackOfficeClaimsIdentityFactory() + { + SecurityStampClaimType = Constants.Security.SessionIdClaimType; + UserNameClaimType = ClaimTypes.Name; + } + /// /// Create a ClaimsIdentity from a user /// @@ -20,7 +26,7 @@ namespace Umbraco.Core.Security var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, //set a new session id - new UserData(Guid.NewGuid().ToString("N")) + new UserData { Id = user.Id, Username = user.UserName, @@ -29,7 +35,8 @@ namespace Umbraco.Core.Security Culture = user.Culture, Roles = user.Roles.Select(x => x.RoleId).ToArray(), StartContentNode = user.StartContentId, - StartMediaNode = user.StartMediaId + StartMediaNode = user.StartMediaId, + SessionId = user.SecurityStamp }); return umbracoIdentity; diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index e37f9d1a54..c6714e256a 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Data; +using System.Data.Common; using System.Linq; using System.Threading.Tasks; using System.Web.Security; @@ -97,6 +99,8 @@ namespace Umbraco.Core.Security } _userService.Save(member); + if (member.Id == 0) throw new DataException("Could not create the user, check logs for details"); + //re-assign id user.Id = member.Id; diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index 392c1545d1..3e02d6aec2 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -759,12 +759,13 @@ namespace Umbraco.Core.Security //This is the correct way to implement this (as per the sql membership provider) - switch ((int)PasswordFormat) + switch (PasswordFormat) { - case 0: + case MembershipPasswordFormat.Clear: return pass; - case 1: + case MembershipPasswordFormat.Hashed: throw new ProviderException("Provider can not decrypt hashed password"); + case MembershipPasswordFormat.Encrypted: default: var bytes = DecryptPassword(Convert.FromBase64String(pass)); return bytes == null ? null : Encoding.Unicode.GetString(bytes, 16, bytes.Length - 16); diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index 6ce81f3e9f..1bc9902da5 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -209,17 +209,19 @@ namespace Umbraco.Core.Security if (HasClaim(x => x.Type == ClaimTypes.Locality) == false) AddClaim(new Claim(ClaimTypes.Locality, Culture, ClaimValueTypes.String, Issuer, Issuer, this)); - - ////TODO: Not sure why this is null sometimes, it shouldn't be. Somewhere it's not being set - /// I think it's due to some bug I had in chrome, we'll see - //if (UserData.SessionId.IsNullOrWhiteSpace()) - //{ - // UserData.SessionId = Guid.NewGuid().ToString(); - //} - if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false) + if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false && SessionId.IsNullOrWhiteSpace() == false) + { AddClaim(new Claim(Constants.Security.SessionIdClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this)); + //The security stamp claim is also required... this is because this claim type is hard coded + // by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444 + if (HasClaim(x => x.Type == Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType) == false) + { + AddClaim(new Claim(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this)); + } + } + //Add each app as a separate claim if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false) { diff --git a/src/Umbraco.Core/Security/UserData.cs b/src/Umbraco.Core/Security/UserData.cs index ff49636217..407d2782dd 100644 --- a/src/Umbraco.Core/Security/UserData.cs +++ b/src/Umbraco.Core/Security/UserData.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Security /// Use this constructor to create/assign new UserData to the ticket /// /// - /// A unique id that is assigned to this ticket + /// The security stamp for the user /// public UserData(string sessionId) { @@ -30,8 +30,7 @@ namespace Umbraco.Core.Security } /// - /// This is used to Id the current ticket which we can then use to mitigate csrf attacks - /// and other things that require request validation. + /// This is the 'security stamp' for validation /// [DataMember(Name = "sessionId")] public string SessionId { get; set; } @@ -42,8 +41,6 @@ namespace Umbraco.Core.Security [DataMember(Name = "roles")] public string[] Roles { get; set; } - //public int SessionTimeout { get; set; } - [DataMember(Name = "username")] public string Username { get; set; } diff --git a/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs b/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs new file mode 100644 index 0000000000..78dda6c4c6 --- /dev/null +++ b/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs @@ -0,0 +1,52 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Umbraco.Core.Serialization +{ + /// + /// This is required if we want to force JSON.Net to not use .Net TypeConverters during serialization/deserialization + /// + /// + /// + /// In some cases thsi is required if your model has an explicit type converter, see: http://stackoverflow.com/a/31328131/694494 + /// + /// NOTE: I was going to use this for the ImageCropDataSetConverter to convert to String, which would have worked by putting this attribute: + /// [JsonConverter(typeof(NoTypeConverterJsonConverter{ImageCropDataSet}))] on top of the ImageCropDataSet class, however it turns out we + /// don't require this because to convert to string, we just override ToString(). + /// I'll leave this class here for the future though. + /// + internal class NoTypeConverterJsonConverter : JsonConverter + { + static readonly IContractResolver resolver = new NoTypeConverterContractResolver(); + + private class NoTypeConverterContractResolver : DefaultContractResolver + { + protected override JsonContract CreateContract(Type objectType) + { + if (typeof(T).IsAssignableFrom(objectType)) + { + var contract = this.CreateObjectContract(objectType); + contract.Converter = null; // Also null out the converter to prevent infinite recursion. + return contract; + } + return base.CreateContract(objectType); + } + } + + public override bool CanConvert(Type objectType) + { + return typeof(T).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = resolver }).Deserialize(reader, objectType); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = resolver }).Serialize(writer, value); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 1605b86b4c..8195fb9432 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -911,7 +911,7 @@ namespace Umbraco.Core.Services new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } var moveInfo = new List> @@ -958,7 +958,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } } @@ -1081,7 +1081,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(asArray, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } } using (new WriteLock(Locker)) @@ -1125,7 +1125,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } } @@ -1148,7 +1148,7 @@ namespace Umbraco.Core.Services new DeleteEventArgs(content, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } //Make sure that published content is unpublished before being deleted @@ -1179,7 +1179,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } } @@ -1228,6 +1228,13 @@ namespace Umbraco.Core.Services /// Optional Id of the user issueing the delete operation public void DeleteContentOfType(int contentTypeId, int userId = 0) { + //TODO: This currently this is called from the ContentTypeService but that needs to change, + // if we are deleting a content type, we should just delete the data and do this operation slightly differently. + // This method will recursively go lookup every content item, check if any of it's descendants are + // of a different type, move them to the recycle bin, then permanently delete the content items. + // The main problem with this is that for every content item being deleted, events are raised... + // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. + using (new WriteLock(Locker)) { using (var uow = UowProvider.GetUnitOfWork()) @@ -1710,6 +1717,7 @@ namespace Umbraco.Core.Services } } + //TODO: All of this needs to be moved to the repository private void PerformMove(IContent content, int parentId, int userId, ICollection> moveInfo) { //add a tracking item to use in the Moved event @@ -1978,6 +1986,10 @@ namespace Umbraco.Core.Services var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { + if (published == false) + { + content.ChangePublishedState(PublishedState.Saved); + } //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed if (content.HasIdentity == false) { @@ -2039,7 +2051,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(content, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } } @@ -2071,7 +2083,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } } @@ -2120,6 +2132,22 @@ namespace Umbraco.Core.Services content.Name, content.Id)); return PublishStatusType.FailedPathNotPublished; } + else if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' has expired and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedHasExpired; + } + else if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' is awaiting release and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedAwaitingRelease; + } return PublishStatusType.Success; } diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 59a13b6aea..d69ca85085 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Xml.Linq; using System.Threading; +using AutoMapper; using Umbraco.Core.Auditing; using Umbraco.Core.Configuration; using Umbraco.Core.Events; @@ -28,7 +29,7 @@ namespace Umbraco.Core.Services private readonly IContentService _contentService; private readonly IMediaService _mediaService; - //Support recursive locks because some of the methods that require locking call other methods that require locking. + //Support recursive locks because some of the methods that require locking call other methods that require locking. //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); @@ -43,10 +44,11 @@ namespace Umbraco.Core.Services #region Containers - public Attempt CreateContentTypeContainer(int parentId, string name, int userId = 0) + public Attempt> CreateContentTypeContainer(int parentId, string name, int userId = 0) { + var evtMsgs = EventMessagesFactory.Get(); var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) { try { @@ -56,22 +58,34 @@ namespace Umbraco.Core.Services ParentId = parentId, CreatorId = userId }; + + if (SavingContentTypeContainer.IsRaisedEventCancelled( + new SaveEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + repo.AddOrUpdate(container); uow.Commit(); - return Attempt.Succeed(container.Id); + + SavedContentTypeContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + //TODO: Audit trail ? + + return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); } catch (Exception ex) { - return Attempt.Fail(ex); + return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex); } - //TODO: Audit trail ? } } - public Attempt CreateMediaTypeContainer(int parentId, string name, int userId = 0) + public Attempt> CreateMediaTypeContainer(int parentId, string name, int userId = 0) { + var evtMsgs = EventMessagesFactory.Get(); var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) { try { @@ -81,112 +95,239 @@ namespace Umbraco.Core.Services ParentId = parentId, CreatorId = userId }; + + if (SavingMediaTypeContainer.IsRaisedEventCancelled( + new SaveEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + repo.AddOrUpdate(container); uow.Commit(); - return Attempt.Succeed(container.Id); + + SavedMediaTypeContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + //TODO: Audit trail ? + + return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); } catch (Exception ex) { - return Attempt.Fail(ex); + return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex); } - //TODO: Audit trail ? } } - public void SaveContentTypeContainer(EntityContainer container, int userId = 0) + public Attempt SaveContentTypeContainer(EntityContainer container, int userId = 0) { - SaveContainer(container, Constants.ObjectTypes.DocumentTypeGuid, "document type", userId); + return SaveContainer( + SavingContentTypeContainer, SavedContentTypeContainer, + container, Constants.ObjectTypes.DocumentTypeContainerGuid, "document type", userId); } - public void SaveMediaTypeContainer(EntityContainer container, int userId = 0) + public Attempt SaveMediaTypeContainer(EntityContainer container, int userId = 0) { - SaveContainer(container, Constants.ObjectTypes.MediaTypeGuid, "media type", userId); + return SaveContainer( + SavingMediaTypeContainer, SavedMediaTypeContainer, + container, Constants.ObjectTypes.MediaTypeContainerGuid, "media type", userId); } - private void SaveContainer(EntityContainer container, Guid containedObjectType, string objectTypeName, int userId) + private Attempt SaveContainer( + TypedEventHandler> savingEvent, + TypedEventHandler> savedEvent, + EntityContainer container, + Guid containerObjectType, + string objectTypeName, int userId) { - if (container.ContainedObjectType != containedObjectType) - throw new InvalidOperationException("Not a " + objectTypeName + " container."); + var evtMsgs = EventMessagesFactory.Get(); + + if (container.ContainedObjectType != containerObjectType) + { + var ex = new InvalidOperationException("Not a " + objectTypeName + " container."); + return OperationStatus.Exception(evtMsgs, ex); + } + if (container.HasIdentity && container.IsPropertyDirty("ParentId")) - throw new InvalidOperationException("Cannot save a container with a modified parent, move the container instead."); + { + var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead."); + return OperationStatus.Exception(evtMsgs, ex); + } + + if (savingEvent.IsRaisedEventCancelled( + new SaveEventArgs(container, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType)) { repo.AddOrUpdate(container); uow.Commit(); - //TODO: Audit trail ? } + + savedEvent.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + + //TODO: Audit trail ? + + return OperationStatus.Success(evtMsgs); } public EntityContainer GetContentTypeContainer(int containerId) { - return GetContainer(containerId, Constants.ObjectTypes.DocumentTypeGuid); + return GetContainer(containerId, Constants.ObjectTypes.DocumentTypeContainerGuid); } public EntityContainer GetMediaTypeContainer(int containerId) { - return GetContainer(containerId, Constants.ObjectTypes.MediaTypeGuid); + return GetContainer(containerId, Constants.ObjectTypes.MediaTypeContainerGuid); } - private EntityContainer GetContainer(int containerId, Guid containedObjectType) + private EntityContainer GetContainer(int containerId, Guid containerObjectType) { var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType)) { var container = repo.Get(containerId); - return container != null && container.ContainedObjectType == containedObjectType - ? container - : null; + return container; } } + public IEnumerable GetMediaTypeContainers(int[] containerIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) + { + return repo.GetAll(containerIds); + } + } + + public IEnumerable GetMediaTypeContainers(string name, int level) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) + { + return repo.Get(name, level); + } + } + + public IEnumerable GetMediaTypeContainers(IMediaType mediaType) + { + var ancestorIds = mediaType.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var asInt = x.TryConvertTo(); + if (asInt) return asInt.Result; + return int.MinValue; + }) + .Where(x => x != int.MinValue && x != mediaType.Id) + .ToArray(); + + return GetMediaTypeContainers(ancestorIds); + } + public EntityContainer GetContentTypeContainer(Guid containerId) { - return GetContainer(containerId, Constants.ObjectTypes.DocumentTypeGuid); + return GetContainer(containerId, Constants.ObjectTypes.DocumentTypeContainerGuid); + } + + public IEnumerable GetContentTypeContainers(int[] containerIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + { + return repo.GetAll(containerIds); + } + } + + public IEnumerable GetContentTypeContainers(IContentType contentType) + { + var ancestorIds = contentType.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var asInt = x.TryConvertTo(); + if (asInt) return asInt.Result; + return int.MinValue; + }) + .Where(x => x != int.MinValue && x != contentType.Id) + .ToArray(); + + return GetContentTypeContainers(ancestorIds); } public EntityContainer GetMediaTypeContainer(Guid containerId) { - return GetContainer(containerId, Constants.ObjectTypes.MediaTypeGuid); + return GetContainer(containerId, Constants.ObjectTypes.MediaTypeContainerGuid); } - private EntityContainer GetContainer(Guid containerId, Guid containedObjectType) + private EntityContainer GetContainer(Guid containerId, Guid containerObjectType) { var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType)) { var container = repo.Get(containerId); - return container != null && container.ContainedObjectType == containedObjectType - ? container - : null; + return container; } } - public void DeleteContentTypeContainer(int containerId, int userId = 0) + public IEnumerable GetContentTypeContainers(string name, int level) { var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + { + return repo.Get(name, level); + } + } + + public Attempt DeleteContentTypeContainer(int containerId, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) { var container = repo.Get(containerId); - if (container == null) return; - if (container.ContainedObjectType != Constants.ObjectTypes.DocumentTypeGuid) return; + if (container == null) return OperationStatus.NoOperation(evtMsgs); + + if (DeletingContentTypeContainer.IsRaisedEventCancelled( + new DeleteEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + repo.Delete(container); uow.Commit(); + + DeletedContentTypeContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); + + return OperationStatus.Success(evtMsgs); //TODO: Audit trail ? } } - public void DeleteMediaTypeContainer(int containerId, int userId = 0) + public Attempt DeleteMediaTypeContainer(int containerId, int userId = 0) { + var evtMsgs = EventMessagesFactory.Get(); var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) { var container = repo.Get(containerId); - if (container == null) return; - if (container.ContainedObjectType != Constants.ObjectTypes.MediaTypeGuid) return; + if (container == null) return OperationStatus.NoOperation(evtMsgs); + + if (DeletingMediaTypeContainer.IsRaisedEventCancelled( + new DeleteEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + repo.Delete(container); uow.Commit(); + + DeletedMediaTypeContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); + + return OperationStatus.Success(evtMsgs); //TODO: Audit trail ? } } @@ -205,6 +346,22 @@ namespace Umbraco.Core.Services } } + /// + /// Gets all content type aliases + /// + /// + /// If this list is empty, it will return all content type aliases for media, members and content, otherwise + /// it will only return content type aliases for the object types specified + /// + /// + public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) + { + using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAllContentTypeAliases(objectTypes); + } + } + /// /// Copies a content type as a child under the specified parent if specified (otherwise to the root) /// @@ -223,7 +380,7 @@ namespace Umbraco.Core.Services /// public IContentType Copy(IContentType original, string alias, string name, int parentId = -1) { - IContentType parent = null; + IContentType parent = null; if (parentId > 0) { parent = GetContentType(parentId); @@ -257,7 +414,7 @@ namespace Umbraco.Core.Services Mandate.ParameterNotNullOrEmpty(alias, "alias"); if (parent != null) { - Mandate.That(parent.HasIdentity, () => new InvalidOperationException("The parent content type must have an identity")); + Mandate.That(parent.HasIdentity, () => new InvalidOperationException("The parent content type must have an identity")); } var clone = original.DeepCloneWithResetIdentities(alias); @@ -283,7 +440,7 @@ namespace Umbraco.Core.Services //set to root clone.ParentId = -1; } - + Save(clone); return clone; } @@ -310,10 +467,7 @@ namespace Umbraco.Core.Services { using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) { - var query = repository.Query.Where(x => x.Alias == alias); - var contentTypes = repository.GetByQuery(query); - - return contentTypes.FirstOrDefault(); + return repository.Get(alias); } } @@ -351,7 +505,7 @@ namespace Umbraco.Core.Services public IEnumerable GetAllContentTypes(IEnumerable ids) { using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) - { + { return repository.GetAll(ids.ToArray()); } } @@ -445,7 +599,7 @@ namespace Umbraco.Core.Services { //this should never occur, the content service should always be typed but we'll check anyways. _contentService.RePublishAll(); - } + } } else if (firstType is IMediaType) { @@ -454,10 +608,26 @@ namespace Umbraco.Core.Services if (typedContentService != null) { typedContentService.RebuildXmlStructures(toUpdate.Select(x => x.Id).ToArray()); - } + } } } - + + } + + public int CountContentTypes() + { + using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + { + return repository.Count(Query.Builder); + } + } + + public int CountMediaTypes() + { + using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + { + return repository.Count(Query.Builder); + } } /// @@ -491,14 +661,17 @@ namespace Umbraco.Core.Services var contentType = compositionContentType as IContentType; var mediaType = compositionContentType as IMediaType; + var memberType = compositionContentType as IMemberType; // should NOT do it here but... v8! IContentTypeComposition[] allContentTypes; if (contentType != null) allContentTypes = GetAllContentTypes().Cast().ToArray(); else if (mediaType != null) allContentTypes = GetAllMediaTypes().Cast().ToArray(); + else if (memberType != null) + return; // no compositions on members, always validate else - throw new Exception("Composition is neither IContentType nor IMediaType?"); + throw new Exception("Composition is neither IContentType nor IMediaType nor IMemberType?"); var compositionAliases = compositionContentType.CompositionAliases(); var compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y))); @@ -514,7 +687,7 @@ namespace Umbraco.Core.Services dependencies.Add(indirectReference); //Get all compositions for the current indirect reference var directReferences = indirectReference.ContentTypeComposition; - + foreach (var directReference in directReferences) { if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias)) continue; @@ -534,7 +707,7 @@ namespace Umbraco.Core.Services if (contentTypeDependency == null) continue; var intersect = contentTypeDependency.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).Intersect(propertyTypeAliases).ToArray(); if (intersect.Length == 0) continue; - + throw new InvalidCompositionException(compositionContentType.Alias, intersect.ToArray()); } } @@ -546,7 +719,7 @@ namespace Umbraco.Core.Services /// Optional id of the user saving the ContentType public void Save(IContentType contentType, int userId = 0) { - if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(contentType), this)) + if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(contentType), this)) return; using (new WriteLock(Locker)) @@ -576,7 +749,7 @@ namespace Umbraco.Core.Services { var asArray = contentTypes.ToArray(); - if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) + if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) return; using (new WriteLock(Locker)) @@ -612,12 +785,19 @@ namespace Umbraco.Core.Services /// Optional id of the user issueing the delete /// Deleting a will delete all the objects based on this public void Delete(IContentType contentType, int userId = 0) - { - if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(contentType), this)) + { + if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(contentType), this)) return; using (new WriteLock(Locker)) { + + //TODO: This needs to change, if we are deleting a content type, we should just delete the data, + // this method will recursively go lookup every content item, check if any of it's descendants are + // of a different type, move them to the recycle bin, then permanently delete the content items. + // The main problem with this is that for every content item being deleted, events are raised... + // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. + _contentService.DeleteContentOfType(contentType.Id); var uow = UowProvider.GetUnitOfWork(); @@ -645,7 +825,7 @@ namespace Umbraco.Core.Services { var asArray = contentTypes.ToArray(); - if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) + if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) return; using (new WriteLock(Locker)) @@ -671,7 +851,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1); } } - + /// /// Gets an object by its Id /// @@ -694,10 +874,7 @@ namespace Umbraco.Core.Services { using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) { - var query = repository.Query.Where(x => x.Alias == alias); - var contentTypes = repository.GetByQuery(query); - - return contentTypes.FirstOrDefault(); + return repository.Get(alias); } } @@ -807,7 +984,7 @@ namespace Umbraco.Core.Services public Attempt> MoveMediaType(IMediaType toMove, int containerId) { var evtMsgs = EventMessagesFactory.Get(); - + if (MovingMediaType.IsRaisedEventCancelled( new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId)), this)) @@ -819,7 +996,7 @@ namespace Umbraco.Core.Services var moveInfo = new List>(); var uow = UowProvider.GetUnitOfWork(); - using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) { try @@ -828,7 +1005,7 @@ namespace Umbraco.Core.Services if (containerId > 0) { container = containerRepository.Get(containerId); - if (container == null || container.ContainedObjectType != Constants.ObjectTypes.MediaTypeGuid) + if (container == null) throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); } moveInfo.AddRange(repository.Move(toMove, container)); @@ -862,7 +1039,7 @@ namespace Umbraco.Core.Services var moveInfo = new List>(); var uow = UowProvider.GetUnitOfWork(); - using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) { try @@ -871,7 +1048,7 @@ namespace Umbraco.Core.Services if (containerId > 0) { container = containerRepository.Get(containerId); - if (container == null || container.ContainedObjectType != Constants.ObjectTypes.DocumentTypeGuid) + if (container == null) throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); } moveInfo.AddRange(repository.Move(toMove, container)); @@ -897,7 +1074,7 @@ namespace Umbraco.Core.Services /// Optional Id of the user saving the MediaType public void Save(IMediaType mediaType, int userId = 0) { - if (SavingMediaType.IsRaisedEventCancelled(new SaveEventArgs(mediaType), this)) + if (SavingMediaType.IsRaisedEventCancelled(new SaveEventArgs(mediaType), this)) return; using (new WriteLock(Locker)) @@ -909,7 +1086,7 @@ namespace Umbraco.Core.Services mediaType.CreatorId = userId; repository.AddOrUpdate(mediaType); uow.Commit(); - + } UpdateContentXmlStructure(mediaType); @@ -948,7 +1125,7 @@ namespace Umbraco.Core.Services } //save it all in one go - uow.Commit(); + uow.Commit(); } UpdateContentXmlStructure(asArray.Cast().ToArray()); @@ -966,7 +1143,7 @@ namespace Umbraco.Core.Services /// Deleting a will delete all the objects based on this public void Delete(IMediaType mediaType, int userId = 0) { - if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(mediaType), this)) + if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(mediaType), this)) return; using (new WriteLock(Locker)) { @@ -996,7 +1173,7 @@ namespace Umbraco.Core.Services { var asArray = mediaTypes.ToArray(); - if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) + if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) return; using (new WriteLock(Locker)) { @@ -1018,7 +1195,7 @@ namespace Umbraco.Core.Services } Audit(AuditType.Delete, string.Format("Delete MediaTypes performed by user"), userId, -1); - } + } } /// @@ -1077,19 +1254,29 @@ namespace Umbraco.Core.Services uow.Commit(); } } - + #region Event Handlers - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> DeletingContentType; + public static event TypedEventHandler> SavingContentTypeContainer; + public static event TypedEventHandler> SavedContentTypeContainer; + public static event TypedEventHandler> DeletingContentTypeContainer; + public static event TypedEventHandler> DeletedContentTypeContainer; + public static event TypedEventHandler> SavingMediaTypeContainer; + public static event TypedEventHandler> SavedMediaTypeContainer; + public static event TypedEventHandler> DeletingMediaTypeContainer; + public static event TypedEventHandler> DeletedMediaTypeContainer; + + + /// + /// Occurs before Delete + /// + public static event TypedEventHandler> DeletingContentType; /// /// Occurs after Delete /// public static event TypedEventHandler> DeletedContentType; - + /// /// Occurs before Delete /// @@ -1099,7 +1286,7 @@ namespace Umbraco.Core.Services /// Occurs after Delete /// public static event TypedEventHandler> DeletedMediaType; - + /// /// Occurs before Save /// diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs new file mode 100644 index 0000000000..ed04edc6bf --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + public static class ContentTypeServiceExtensions + { + /// + /// Returns the available composite content types for a given content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + internal static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(this IContentTypeService ctService, + IContentTypeComposition source, + IContentTypeComposition[] allContentTypes, + string[] filterContentTypes = null, + string[] filterPropertyTypes = null) + { + filterContentTypes = filterContentTypes == null + ? new string[] { } + : filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); + + filterPropertyTypes = filterPropertyTypes == null + ? new string[] {} + : filterPropertyTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); + + //create the full list of property types to use as the filter + //this is the combination of all property type aliases found in the content types passed in for the filter + //as well as the specific property types passed in for the filter + filterPropertyTypes = allContentTypes + .Where(c => filterContentTypes.InvariantContains(c.Alias)) + .SelectMany(c => c.PropertyTypes) + .Select(c => c.Alias) + .Union(filterPropertyTypes) + .ToArray(); + + var sourceId = source != null ? source.Id : 0; + + // find out if any content type uses this content type + var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); + if (isUsing.Length > 0) + { + //if already in use a composition, do not allow any composited types + return new ContentTypeAvailableCompositionsResults(); + } + + // if it is not used then composition is possible + // hashset guarantees unicity on Id + var list = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id)); + + // usable types are those that are top-level + var usableContentTypes = allContentTypes + .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); + foreach (var x in usableContentTypes) + list.Add(x); + + // indirect types are those that we use, directly or indirectly + var indirectContentTypes = GetDirectOrIndirect(source).ToArray(); + foreach (var x in indirectContentTypes) + list.Add(x); + + //At this point we have a list of content types that 'could' be compositions + + //now we'll filter this list based on the filters requested + var filtered = list + .Where(x => + { + //need to filter any content types that are included in this list + return filterContentTypes.Any(c => c.InvariantEquals(x.Alias)) == false; + }) + .Where(x => + { + //need to filter any content types that have matching property aliases that are included in this list + //ensure that we don't return if there's any overlapping property aliases from the filtered ones specified + return filterPropertyTypes.Intersect( + x.PropertyTypes.Select(p => p.Alias), + StringComparer.InvariantCultureIgnoreCase).Any() == false; + }) + .OrderBy(x => x.Name) + .ToList(); + + //get ancestor ids - we will filter all ancestors + var ancestors = GetAncestors(source, allContentTypes); + var ancestorIds = ancestors.Select(x => x.Id).ToArray(); + + //now we can create our result based on what is still available and the ancestors + var result = list + //not itself + .Where(x => x.Id != sourceId) + .OrderBy(x => x.Name) + .Select(composition => filtered.Contains(composition) + ? new ContentTypeAvailableCompositionsResult(composition, ancestorIds.Contains(composition.Id) == false) + : new ContentTypeAvailableCompositionsResult(composition, false)).ToList(); + + return new ContentTypeAvailableCompositionsResults(ancestors, result); + } + + private static IContentTypeComposition[] GetAncestors(IContentTypeComposition ctype, IContentTypeComposition[] allContentTypes) + { + if (ctype == null) return new IContentTypeComposition[] {}; + var ancestors = new List(); + var parentId = ctype.ParentId; + while (parentId > 0) + { + var parent = allContentTypes.FirstOrDefault(x => x.Id == parentId); + if (parent != null) + { + ancestors.Add(parent); + parentId = parent.ParentId; + } + else + { + parentId = -1; + } + } + return ancestors.ToArray(); + } + + /// + /// Get those that we use directly + /// + /// + /// + private static IEnumerable GetDirectOrIndirect(IContentTypeComposition ctype) + { + if (ctype == null) return Enumerable.Empty(); + + // hashset guarantees unicity on Id + var all = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id)); + + var stack = new Stack(); + + foreach (var x in ctype.ContentTypeComposition) + stack.Push(x); + + while (stack.Count > 0) + { + var x = stack.Pop(); + all.Add(x); + foreach (var y in x.ContentTypeComposition) + stack.Push(y); + } + + return all; + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 22fd686310..c558621b95 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -29,10 +29,11 @@ namespace Umbraco.Core.Services #region Containers - public Attempt CreateContainer(int parentId, string name, int userId = 0) + public Attempt> CreateContainer(int parentId, string name, int userId = 0) { + var evtMsgs = EventMessagesFactory.Get(); var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) { try { @@ -42,68 +43,141 @@ namespace Umbraco.Core.Services ParentId = parentId, CreatorId = userId }; + + if (SavingContainer.IsRaisedEventCancelled( + new SaveEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + repo.AddOrUpdate(container); uow.Commit(); - return Attempt.Succeed(container.Id); + + SavedContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + //TODO: Audit trail ? + + return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); } catch (Exception ex) { - return Attempt.Fail(ex); + return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex); } - //TODO: Audit trail ? } } public EntityContainer GetContainer(int containerId) { var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) { var container = repo.Get(containerId); - return container != null && container.ContainedObjectType == Constants.ObjectTypes.DataTypeGuid - ? container - : null; + return container; } } public EntityContainer GetContainer(Guid containerId) { var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) { var container = repo.Get(containerId); - return container != null && container.ContainedObjectType == Constants.ObjectTypes.DataTypeGuid - ? container - : null; + return container; } } - public void SaveContainer(EntityContainer container, int userId = 0) + public IEnumerable GetContainers(string name, int level) { - if (container.ContainedObjectType != Constants.ObjectTypes.DataTypeGuid) - throw new InvalidOperationException("Not a data type container."); + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + { + return repo.Get(name, level); + } + } + + public IEnumerable GetContainers(IDataTypeDefinition dataTypeDefinition) + { + var ancestorIds = dataTypeDefinition.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var asInt = x.TryConvertTo(); + if (asInt) return asInt.Result; + return int.MinValue; + }) + .Where(x => x != int.MinValue && x != dataTypeDefinition.Id) + .ToArray(); + + return GetContainers(ancestorIds); + } + + public IEnumerable GetContainers(int[] containerIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + { + return repo.GetAll(containerIds); + } + } + + public Attempt SaveContainer(EntityContainer container, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (container.ContainedObjectType != Constants.ObjectTypes.DataTypeGuid) + { + var ex = new InvalidOperationException("Not a " + Constants.ObjectTypes.DataTypeGuid + " container."); + return OperationStatus.Exception(evtMsgs, ex); + } + if (container.HasIdentity && container.IsPropertyDirty("ParentId")) - throw new InvalidOperationException("Cannot save a container with a modified parent, move the container instead."); + { + var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead."); + return OperationStatus.Exception(evtMsgs, ex); + } + + if (SavingContainer.IsRaisedEventCancelled( + new SaveEventArgs(container, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) { repo.AddOrUpdate(container); uow.Commit(); - //TODO: Audit trail ? } + + SavedContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + + //TODO: Audit trail ? + + return OperationStatus.Success(evtMsgs); } - public void DeleteContainer(int containerId, int userId = 0) + public Attempt DeleteContainer(int containerId, int userId = 0) { + var evtMsgs = EventMessagesFactory.Get(); var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) { var container = repo.Get(containerId); - if (container == null) return; - if (container.ContainedObjectType != Constants.ObjectTypes.DataTypeGuid) return; + if (container == null) return OperationStatus.NoOperation(evtMsgs); + + if (DeletingContainer.IsRaisedEventCancelled( + new DeleteEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + repo.Delete(container); uow.Commit(); + + DeletedContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); + + return OperationStatus.Success(evtMsgs); //TODO: Audit trail ? } } @@ -238,7 +312,7 @@ namespace Umbraco.Core.Services var moveInfo = new List>(); var uow = UowProvider.GetUnitOfWork(); - using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow)) + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow)) { try @@ -247,7 +321,7 @@ namespace Umbraco.Core.Services if (parentId > 0) { container = containerRepository.Get(parentId); - if (container == null || container.ContainedObjectType != Constants.ObjectTypes.DataTypeGuid) + if (container == null) throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); } moveInfo.AddRange(repository.Move(toMove, container)); @@ -474,6 +548,12 @@ namespace Umbraco.Core.Services } #region Event Handlers + + public static event TypedEventHandler> SavingContainer; + public static event TypedEventHandler> SavedContainer; + public static event TypedEventHandler> DeletingContainer; + public static event TypedEventHandler> DeletedContainer; + /// /// Occurs before Delete /// diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs index ca9fe03dcb..3ffcb92778 100644 --- a/src/Umbraco.Core/Services/DomainService.cs +++ b/src/Umbraco.Core/Services/DomainService.cs @@ -33,7 +33,7 @@ namespace Umbraco.Core.Services new DeleteEventArgs(domain, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } var uow = UowProvider.GetUnitOfWork(); @@ -45,7 +45,7 @@ namespace Umbraco.Core.Services var args = new DeleteEventArgs(domain, false, evtMsgs); Deleted.RaiseEvent(args, this); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } public IDomain GetByName(string name) @@ -91,7 +91,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(domainEntity, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } var uow = UowProvider.GetUnitOfWork(); @@ -102,7 +102,7 @@ namespace Umbraco.Core.Services } Saved.RaiseEvent(new SaveEventArgs(domainEntity, false, evtMsgs), this); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } #region Event Handlers diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs index 5bbd4eeaa2..f450bf52e0 100644 --- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Web; using System.Xml; using System.Xml.Linq; using Umbraco.Core.Configuration; @@ -181,6 +182,20 @@ namespace Umbraco.Core.Services xml.Add(new XAttribute("Definition", dataTypeDefinition.Key)); xml.Add(new XAttribute("DatabaseType", dataTypeDefinition.DatabaseType.ToString())); + var folderNames = string.Empty; + if (dataTypeDefinition.Level != 1) + { + //get url encoded folder names + var folders = dataTypeService.GetContainers(dataTypeDefinition) + .OrderBy(x => x.Level) + .Select(x => HttpUtility.UrlEncode(x.Name)); + + folderNames = string.Join("/", folders.ToArray()); + } + + if (string.IsNullOrWhiteSpace(folderNames) == false) + xml.Add(new XAttribute("Folders", folderNames)); + return xml; } @@ -340,9 +355,10 @@ namespace Umbraco.Core.Services /// Exports an item to xml as an /// /// + /// /// Content type to export /// containing the xml representation of the IContentType object - public XElement Serialize(IDataTypeService dataTypeService, IContentType contentType) + public XElement Serialize(IDataTypeService dataTypeService, IContentTypeService contentTypeService, IContentType contentType) { var info = new XElement("Info", new XElement("Name", contentType.Name), @@ -398,9 +414,11 @@ namespace Umbraco.Core.Services new XElement("Type", propertyType.PropertyEditorAlias), new XElement("Definition", definition.Key), new XElement("Tab", propertyGroup == null ? "" : propertyGroup.Name), + new XElement("SortOrder", propertyType.SortOrder), new XElement("Mandatory", propertyType.Mandatory.ToString()), - new XElement("Validation", propertyType.ValidationRegExp), - new XElement("Description", new XCData(propertyType.Description))); + propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null, + propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null); + genericProperties.Add(genericProperty); } @@ -414,11 +432,28 @@ namespace Umbraco.Core.Services tabs.Add(tab); } - return new XElement("DocumentType", - info, - structure, - genericProperties, - tabs); + var xml = new XElement("DocumentType", + info, + structure, + genericProperties, + tabs); + + var folderNames = string.Empty; + //don't add folders if this is a child doc type + if (contentType.Level != 1 && masterContentType == null) + { + //get url encoded folder names + var folders = contentTypeService.GetContentTypeContainers(contentType) + .OrderBy(x => x.Level) + .Select(x => HttpUtility.UrlEncode(x.Name)); + + folderNames = string.Join("/", folders.ToArray()); + } + + if (string.IsNullOrWhiteSpace(folderNames) == false) + xml.Add(new XAttribute("Folders", folderNames)); + + return xml; } /// diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index c93df1acfa..c91ecb44a9 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Runtime.Remoting.Messaging; @@ -22,23 +23,22 @@ namespace Umbraco.Core.Services /// /// Represents the File Service, which is an easy access to operations involving objects like Scripts, Stylesheets and Templates /// - public class FileService : IFileService + public class FileService : RepositoryService, IFileService { - private readonly RepositoryFactory _repositoryFactory; private readonly IUnitOfWorkProvider _fileUowProvider; - private readonly IDatabaseUnitOfWorkProvider _dataUowProvider; private const string PartialViewHeader = "@inherits Umbraco.Web.Mvc.UmbracoTemplatePage"; private const string PartialViewMacroHeader = "@inherits Umbraco.Web.Macros.PartialViewMacroPage"; - public FileService(IUnitOfWorkProvider fileProvider, IDatabaseUnitOfWorkProvider dataProvider, RepositoryFactory repositoryFactory) + public FileService( + IUnitOfWorkProvider fileProvider, + IDatabaseUnitOfWorkProvider dataProvider, + RepositoryFactory repositoryFactory, + ILogger logger, + IEventMessagesFactory eventMessagesFactory) + : base(dataProvider, repositoryFactory, logger, eventMessagesFactory) { - if (fileProvider == null) throw new ArgumentNullException("fileProvider"); - if (dataProvider == null) throw new ArgumentNullException("dataProvider"); - if (repositoryFactory == null) throw new ArgumentNullException("repositoryFactory"); - _repositoryFactory = repositoryFactory; - _fileUowProvider = fileProvider; - _dataUowProvider = dataProvider; + _fileUowProvider = fileProvider; } @@ -50,7 +50,7 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public IEnumerable GetStylesheets(params string[] names) { - using (var repository = _repositoryFactory.CreateStylesheetRepository(_fileUowProvider.GetUnitOfWork(), _dataUowProvider.GetUnitOfWork())) + using (var repository = RepositoryFactory.CreateStylesheetRepository(_fileUowProvider.GetUnitOfWork(), UowProvider.GetUnitOfWork())) { return repository.GetAll(names); } @@ -63,7 +63,7 @@ namespace Umbraco.Core.Services /// A object public Stylesheet GetStylesheetByName(string name) { - using (var repository = _repositoryFactory.CreateStylesheetRepository(_fileUowProvider.GetUnitOfWork(), _dataUowProvider.GetUnitOfWork())) + using (var repository = RepositoryFactory.CreateStylesheetRepository(_fileUowProvider.GetUnitOfWork(), UowProvider.GetUnitOfWork())) { return repository.Get(name); } @@ -80,7 +80,7 @@ namespace Umbraco.Core.Services return; var uow = _fileUowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateStylesheetRepository(uow, _dataUowProvider.GetUnitOfWork())) + using (var repository = RepositoryFactory.CreateStylesheetRepository(uow, UowProvider.GetUnitOfWork())) { repository.AddOrUpdate(stylesheet); uow.Commit(); @@ -99,7 +99,7 @@ namespace Umbraco.Core.Services public void DeleteStylesheet(string path, int userId = 0) { var uow = _fileUowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateStylesheetRepository(uow, _dataUowProvider.GetUnitOfWork())) + using (var repository = RepositoryFactory.CreateStylesheetRepository(uow, UowProvider.GetUnitOfWork())) { var stylesheet = repository.Get(path); if (stylesheet == null) return; @@ -125,7 +125,7 @@ namespace Umbraco.Core.Services { var uow = _fileUowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateStylesheetRepository(uow, _dataUowProvider.GetUnitOfWork())) + using (var repository = RepositoryFactory.CreateStylesheetRepository(uow, UowProvider.GetUnitOfWork())) { return repository.ValidateStylesheet(stylesheet); } @@ -140,7 +140,7 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public IEnumerable - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoClientManager.js b/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoClientManager.js index a127232962..aef27c6128 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoClientManager.js +++ b/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoClientManager.js @@ -149,7 +149,6 @@ Umbraco.Sys.registerNamespace("Umbraco.Application"); /** This is used to launch an angular based modal window instead of the legacy window */ openAngularModalWindow: function (options) { - if (!this.mainWindow().UmbClientMgr) { throw "An angular modal window can only be launched when the modal is running within the main Umbraco application"; } @@ -171,6 +170,12 @@ Umbraco.Sys.registerNamespace("Umbraco.Application"); }, + reloadLocation: function () { + if (this.mainWindow().UmbClientMgr) { + this.mainWindow().UmbClientMgr.reloadLocation(); + } + }, + openModalWindow: function(url, name, showHeader, width, height, top, leftOffset, closeTriggers, onCloseCallback) { //need to create the modal on the top window if the top window has a client manager, if not, create it on the current window diff --git a/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js b/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js index d9b1a28505..b8c1a203d6 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js +++ b/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js @@ -9,12 +9,12 @@ _openMacroModal: function(alias) { var self = this; - + UmbClientMgr.openAngularModalWindow({ template: "views/common/dialogs/insertmacro.html", dialogData: { renderingEngine: "WebForms", - selectedAlias: alias + macroData: { macroAlias: alias } }, callback: function(data) { UmbEditor.Insert(data.syntax, '', self._opts.editorClientId); diff --git a/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js b/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js index 509de8ed28..96ea4d14a6 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js +++ b/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js @@ -45,12 +45,12 @@ /// callback used to display the modal dialog to insert a macro with parameters var self = this; - + UmbClientMgr.openAngularModalWindow({ template: "views/common/dialogs/insertmacro.html", dialogData: { renderingEngine: "Mvc", - selectedAlias: alias + macroData: {macroAlias: alias} }, callback: function (data) { UmbEditor.Insert(data.syntax, '', self._opts.codeEditorElementId); @@ -77,9 +77,9 @@ if (type === 'rendersection') { if (data.required) { - code = "\n@RenderSection(\"" + data.name + "\", true);\n"; + code = "\n@RenderSection(\"" + data.name + "\", true)\n"; } else { - code = "\n@RenderSection(\"" + data.name + "\" false);\n"; + code = "\n@RenderSection(\"" + data.name + "\", false)\n"; } } diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index c18f1e1a8e..7816568561 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -4,15 +4,15 @@ @@ -58,8 +58,6 @@ - - @@ -87,7 +85,7 @@ - + @@ -98,14 +96,211 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -115,6 +310,14 @@ + + + + + + + + diff --git a/src/Umbraco.Web.UI/web.Template.Release.config b/src/Umbraco.Web.UI/web.Template.Release.config index 3ddea3b6a6..2b1f0d12d9 100644 --- a/src/Umbraco.Web.UI/web.Template.Release.config +++ b/src/Umbraco.Web.UI/web.Template.Release.config @@ -10,8 +10,6 @@ --> - - diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 1904c47d7e..4171020d7b 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -1,315 +1,423 @@ - -
-
-
-
-
-
- - -
-
-
- - + +
+
+
+
+
+
- - - - - + +
+
+
+ + - - - - - - + + + + + - - - - - - - - - - - + + + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - - - + + - - - - - - - + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - + + - - + + - - + + - - + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - + + + + + + - - - - - - - - - + - - - - - - - - - - - - - - - - - - - + + - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - diff --git a/src/Umbraco.Web/Cache/ApplicationCacheRefresher.cs b/src/Umbraco.Web/Cache/ApplicationCacheRefresher.cs index 8b742b1b2c..5c1ec9e1d4 100644 --- a/src/Umbraco.Web/Cache/ApplicationCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ApplicationCacheRefresher.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(CacheKeys.ApplicationsCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationsCacheKey); base.RefreshAll(); } @@ -38,7 +38,7 @@ namespace Umbraco.Web.Cache public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(CacheKeys.ApplicationsCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationsCacheKey); base.Remove(id); } diff --git a/src/Umbraco.Web/Cache/ApplicationTreeCacheRefresher.cs b/src/Umbraco.Web/Cache/ApplicationTreeCacheRefresher.cs index e267b441a2..2f1ab4b891 100644 --- a/src/Umbraco.Web/Cache/ApplicationTreeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ApplicationTreeCacheRefresher.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); base.RefreshAll(); } @@ -38,7 +38,7 @@ namespace Umbraco.Web.Cache public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); base.Remove(id); } diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index f5465dcca6..e350b15824 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -125,8 +125,9 @@ namespace Umbraco.Web.Cache //public access events PublicAccessService.Saved += PublicAccessService_Saved; + PublicAccessService.Deleted += PublicAccessService_Deleted; ; } - + #region Publishing void PublishingStrategy_UnPublished(IPublishingStrategy sender, PublishEventArgs e) @@ -211,6 +212,11 @@ namespace Umbraco.Web.Cache DistributedCache.Instance.RefreshPublicAccess(); } + private void PublicAccessService_Deleted(IPublicAccessService sender, DeleteEventArgs e) + { + DistributedCache.Instance.RefreshPublicAccess(); + } + #endregion #region Content service event handlers diff --git a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index 502e215c90..44a6efe9ff 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -126,8 +126,12 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); //all property type cache ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.PropertyTypeCacheKey); @@ -179,7 +183,7 @@ namespace Umbraco.Web.Cache /// - InMemoryCacheProvider.Current.Clear(); /// - RoutesCache.Clear(); /// - private static void ClearContentTypeCache(JsonPayload[] payloads) + private void ClearContentTypeCache(JsonPayload[] payloads) { var needsContentRefresh = false; @@ -214,18 +218,18 @@ namespace Umbraco.Web.Cache { if (payloads.Any(x => x.Type == typeof (IContentType).Name)) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); } if (payloads.Any(x => x.Type == typeof(IMediaType).Name)) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); } if (payloads.Any(x => x.Type == typeof(IMemberType).Name)) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); } @@ -291,7 +295,7 @@ namespace Umbraco.Web.Cache ///
/// true if the entity was deleted, false if it is just an update /// - private static void ClearContentTypeCache(bool isDeleted, params int[] ids) + private void ClearContentTypeCache(bool isDeleted, params int[] ids) { ClearContentTypeCache( ids.Select( diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index 86114d5f77..11b3ab6294 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -92,27 +92,22 @@ namespace Umbraco.Web.Cache // db data type to store the value against and anytime a datatype changes, this also might change // we basically need to clear all sorts of runtime caches here because so many things depend upon a data type - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); payloads.ForEach(payload => { - //clear both the Id and Unique Id cache since we cache both in the legacy classes :( - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}{1}", CacheKeys.DataTypeCacheKey, payload.Id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}{1}", CacheKeys.DataTypeCacheKey, payload.UniqueId)); - //clears the prevalue cache - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}{1}", CacheKeys.DataTypePreValuesCacheKey, payload.Id)); - + var dataTypeCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (dataTypeCache) + dataTypeCache.Result.ClearCacheByKeySearch(string.Format("{0}{1}", CacheKeys.DataTypePreValuesCacheKey, payload.Id)); + PublishedContentType.ClearDataType(payload.Id); }); diff --git a/src/Umbraco.Web/Cache/DictionaryCacheRefresher.cs b/src/Umbraco.Web/Cache/DictionaryCacheRefresher.cs index 8209d247a2..ae36be33df 100644 --- a/src/Umbraco.Web/Cache/DictionaryCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DictionaryCacheRefresher.cs @@ -28,13 +28,13 @@ namespace Umbraco.Web.Cache public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Refresh(id); } public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Remove(id); } } diff --git a/src/Umbraco.Web/Cache/DistributedCache.cs b/src/Umbraco.Web/Cache/DistributedCache.cs index 27c30d89e8..6848ce2496 100644 --- a/src/Umbraco.Web/Cache/DistributedCache.cs +++ b/src/Umbraco.Web/Cache/DistributedCache.cs @@ -38,6 +38,9 @@ namespace Umbraco.Web.Cache public const string ContentTypeCacheRefresherId = "6902E22C-9C10-483C-91F3-66B7CAE9E2F5"; public const string LanguageCacheRefresherId = "3E0F95D8-0BE5-44B8-8394-2B8750B62654"; public const string DomainCacheRefresherId = "11290A79-4B57-4C99-AD72-7748A3CF38AF"; + + [Obsolete("This is no longer used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string StylesheetCacheRefresherId = "E0633648-0DEB-44AE-9A48-75C3A55CB670"; public const string StylesheetPropertyCacheRefresherId = "2BC7A3A4-6EB1-4FBC-BAA3-C9E7B6D36D38"; public const string DataTypeCacheRefresherId = "35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2"; diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 66cb82ff7c..750872d8af 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -442,7 +442,7 @@ namespace Umbraco.Web.Cache public static void ClearXsltCacheOnCurrentServer(this DistributedCache dc) { if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration <= 0) return; - ApplicationContext.Current.ApplicationCache.ClearCacheObjectTypes("MS.Internal.Xml.XPath.XPathSelectionIterator"); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes("MS.Internal.Xml.XPath.XPathSelectionIterator"); } #endregion diff --git a/src/Umbraco.Web/Cache/DomainCacheRefresher.cs b/src/Umbraco.Web/Cache/DomainCacheRefresher.cs index 51a2c79b2d..955d87ff59 100644 --- a/src/Umbraco.Web/Cache/DomainCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DomainCacheRefresher.cs @@ -41,8 +41,8 @@ namespace Umbraco.Web.Cache } private void ClearCache() - { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + { + ClearAllIsolatedCacheByEntityType(); // SD: we need to clear the routes cache here! // diff --git a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs index 019be66b15..087aa36764 100644 --- a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs @@ -28,13 +28,15 @@ namespace Umbraco.Web.Cache public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Refresh(id); } public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + //if a language is removed, then all dictionary cache needs to be removed + ClearAllIsolatedCacheByEntityType(); base.Remove(id); } } diff --git a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs index a06501032f..cb62dbeb39 100644 --- a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs @@ -176,7 +176,7 @@ namespace Umbraco.Web.Cache prefix => ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(prefix)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.RefreshAll(); } @@ -191,7 +191,11 @@ namespace Umbraco.Web.Cache alias => ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(alias)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(payload.Id)); + var macroRepoCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (macroRepoCache) + { + macroRepoCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(payload.Id)); + } }); base.Refresh(jsonPayload); diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index dce512b1dc..a0e037e110 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.Cache public class MediaCacheRefresher : JsonCacheRefresherBase { #region Static helpers - + /// /// Converts the json to a JsonPayload object /// @@ -143,13 +143,13 @@ namespace Umbraco.Web.Cache } public override void Remove(int id) - { + { ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id), //NOTE: we'll just default to trashed for this one. OperationType.Trashed)); base.Remove(id); } - + private static void ClearCache(params JsonPayload[] payloads) { if (payloads == null) return; @@ -159,40 +159,40 @@ namespace Umbraco.Web.Cache ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); payloads.ForEach(payload => + { + var mediaCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + + //if there's no path, then just use id (this will occur on permanent deletion like emptying recycle bin) + if (payload.Path.IsNullOrWhiteSpace()) { - - //if there's no path, then just use id (this will occur on permanent deletion like emptying recycle bin) - if (payload.Path.IsNullOrWhiteSpace()) + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( + string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); + } + else + { + foreach (var idPart in payload.Path.Split(',')) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); - } - else - { - foreach (var idPart in payload.Path.Split(',')) + int idPartAsInt; + if (int.TryParse(idPart, out idPartAsInt) && mediaCache) { - int idPartAsInt; - if (int.TryParse(idPart, out idPartAsInt)) - { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem( - RepositoryBase.GetCacheIdKey(idPartAsInt)); - } + mediaCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(idPartAsInt)); + } + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( + string.Format("{0}_{1}_True", CacheKeys.MediaCacheKey, idPart)); + + // Also clear calls that only query this specific item! + if (idPart == payload.Id.ToString(CultureInfo.InvariantCulture)) ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}_{1}_True", CacheKeys.MediaCacheKey, idPart)); - - // Also clear calls that only query this specific item! - if (idPart == payload.Id.ToString(CultureInfo.InvariantCulture)) - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); - } + string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); } + } + + // published cache... + PublishedMediaCache.ClearCache(payload.Id); + }); - // published cache... - PublishedMediaCache.ClearCache(payload.Id); - }); - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs index a0167454a5..32e6a69717 100644 --- a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs @@ -69,7 +69,9 @@ namespace Umbraco.Web.Cache ApplicationContext.Current.ApplicationCache.RuntimeCache. ClearCacheByKeySearch(string.Format("{0}{1}", CacheKeys.MemberBusinessLogicCacheKey, id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + var memberCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (memberCache) + memberCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs index e410ab560c..dc2ba39b9d 100644 --- a/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs @@ -104,13 +104,13 @@ namespace Umbraco.Web.Cache { if (payloads == null) return; + var memberGroupCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); payloads.ForEach(payload => { - if (payload != null) + if (payload != null && memberGroupCache) { - ApplicationContext.Current.ApplicationCache.RuntimeCache - .ClearCacheByKeySearch(string.Format("{0}.{1}", typeof(IMemberGroup).FullName, payload.Name)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(payload.Id)); + memberGroupCache.Result.ClearCacheByKeySearch(string.Format("{0}.{1}", typeof(IMemberGroup).FullName, payload.Name)); + memberGroupCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(payload.Id)); } }); diff --git a/src/Umbraco.Web/Cache/PageCacheRefresher.cs b/src/Umbraco.Web/Cache/PageCacheRefresher.cs index 3922b6dd3c..472824eef6 100644 --- a/src/Umbraco.Web/Cache/PageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/PageCacheRefresher.cs @@ -76,7 +76,7 @@ namespace Umbraco.Web.Cache content.Instance.ClearDocumentCache(id); DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Remove(id); } @@ -86,7 +86,7 @@ namespace Umbraco.Web.Cache content.Instance.UpdateDocumentCache(new Document(instance)); DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Refresh(instance); } @@ -96,7 +96,7 @@ namespace Umbraco.Web.Cache content.Instance.ClearDocumentCache(new Document(instance)); DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Remove(instance); } } diff --git a/src/Umbraco.Web/Cache/PublicAccessCacheRefresher.cs b/src/Umbraco.Web/Cache/PublicAccessCacheRefresher.cs index 09b38f1ac3..922e01e2a2 100644 --- a/src/Umbraco.Web/Cache/PublicAccessCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/PublicAccessCacheRefresher.cs @@ -27,25 +27,25 @@ namespace Umbraco.Web.Cache public override void Refresh(Guid id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Refresh(id); } public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Refresh(id); } public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.RefreshAll(); } public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Remove(id); } } diff --git a/src/Umbraco.Web/Cache/StylesheetCacheRefresher.cs b/src/Umbraco.Web/Cache/StylesheetCacheRefresher.cs index 5d98671a76..959a937e7b 100644 --- a/src/Umbraco.Web/Cache/StylesheetCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/StylesheetCacheRefresher.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Umbraco.Core; using Umbraco.Core.Cache; @@ -7,6 +8,8 @@ namespace Umbraco.Web.Cache /// /// A cache refresher to ensure stylesheet cache is refreshed when stylesheets change /// + [Obsolete("This is no longer used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] public sealed class StylesheetCacheRefresher : CacheRefresherBase { protected override StylesheetCacheRefresher Instance @@ -24,27 +27,5 @@ namespace Umbraco.Web.Cache get { return "Stylesheet cache refresher"; } } - public override void RefreshAll() - { - ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch(CacheKeys.StylesheetCacheKey); - base.RefreshAll(); - } - - public override void Refresh(int id) - { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(GetStylesheetCacheKey(id)); - base.Refresh(id); - } - - public override void Remove(int id) - { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(GetStylesheetCacheKey(id)); - base.Remove(id); - } - - private static string GetStylesheetCacheKey(int id) - { - return CacheKeys.StylesheetCacheKey + id; - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/StylesheetPropertyCacheRefresher.cs b/src/Umbraco.Web/Cache/StylesheetPropertyCacheRefresher.cs index d7006c8531..5b72e0384b 100644 --- a/src/Umbraco.Web/Cache/StylesheetPropertyCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/StylesheetPropertyCacheRefresher.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Umbraco.Core; using Umbraco.Core.Cache; @@ -7,6 +8,8 @@ namespace Umbraco.Web.Cache /// /// A cache refresher to ensure stylesheet property cache is refreshed when stylesheet properties change /// + [Obsolete("This is no longer used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] public sealed class StylesheetPropertyCacheRefresher : CacheRefresherBase { protected override StylesheetPropertyCacheRefresher Instance @@ -23,28 +26,6 @@ namespace Umbraco.Web.Cache { get { return "Stylesheet property cache refresher"; } } - - public override void RefreshAll() - { - ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch(CacheKeys.StylesheetPropertyCacheKey); - base.RefreshAll(); - } - - public override void Refresh(int id) - { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(GetStylesheetPropertyCacheKey(id)); - base.Refresh(id); - } - - public override void Remove(int id) - { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(GetStylesheetPropertyCacheKey(id)); - base.Remove(id); - } - - private static string GetStylesheetPropertyCacheKey(int id) - { - return CacheKeys.StylesheetPropertyCacheKey + id; - } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs b/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs index 22bce3d5bf..4d6c0c612b 100644 --- a/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs @@ -52,8 +52,8 @@ namespace Umbraco.Web.Cache // all three of these types are referenced by templates, and the cache needs to be cleared on every server, // otherwise things like looking up content type's after a template is removed is still going to show that // it has an associated template. - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); base.Remove(id); } @@ -66,7 +66,7 @@ namespace Umbraco.Web.Cache string.Format("{0}{1}", CacheKeys.TemplateFrontEndCacheKey, id)); //need to clear the runtime cache for templates - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); } } diff --git a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs index e3601188f4..beb5b8db7d 100644 --- a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs @@ -77,16 +77,16 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.RefreshAll(); } public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearRepositoryCacheItemById(id); + ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(id); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Refresh(id); @@ -94,8 +94,8 @@ namespace Umbraco.Web.Cache public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearRepositoryCacheItemById(id); + ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Remove(id); } @@ -103,8 +103,8 @@ namespace Umbraco.Web.Cache public override void Refresh(IContent instance) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(instance.Id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearRepositoryCacheItemById(instance.Id); + ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(instance); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Refresh(instance); @@ -112,8 +112,8 @@ namespace Umbraco.Web.Cache public override void Remove(IContent instance) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(instance.Id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearRepositoryCacheItemById(instance.Id); + ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Remove(instance); } @@ -124,11 +124,11 @@ namespace Umbraco.Web.Cache /// public void Refresh(string jsonPayload) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); foreach (var payload in DeserializeFromJsonPayload(jsonPayload)) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(payload.Id)); + ClearRepositoryCacheItemById(payload.Id); content.Instance.UpdateSortOrder(payload.Id); } @@ -136,6 +136,15 @@ namespace Umbraco.Web.Cache OnCacheUpdated(Instance, new CacheRefresherEventArgs(jsonPayload, MessageType.RefreshByJson)); } + + private void ClearRepositoryCacheItemById(int id) + { + var contentCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (contentCache) + { + contentCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/UserCacheRefresher.cs b/src/Umbraco.Web/Cache/UserCacheRefresher.cs index 95a7fdac49..2f8a370cd4 100644 --- a/src/Umbraco.Web/Cache/UserCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserCacheRefresher.cs @@ -30,9 +30,9 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.UserPermissionsCacheKey); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.UserContextCacheKey); + ClearAllIsolatedCacheByEntityType(); + if (UserPermissionsCache) + UserPermissionsCache.Result.ClearCacheByKeySearch(CacheKeys.UserPermissionsCacheKey); base.RefreshAll(); } @@ -44,16 +44,20 @@ namespace Umbraco.Web.Cache public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); - - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(string.Format("{0}{1}", CacheKeys.UserPermissionsCacheKey, id)); - - //we need to clear all UserContextCacheKey since we cannot invalidate based on ID since the cache is done so based - //on the current contextId stored in the database - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.UserContextCacheKey); + var userCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (userCache) + userCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + if (UserPermissionsCache) + UserPermissionsCache.Result.ClearCacheItem(string.Format("{0}{1}", CacheKeys.UserPermissionsCacheKey, id)); + base.Remove(id); } + private Attempt UserPermissionsCache + { + get { return ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); } + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/UserPermissionsCacheRefresher.cs b/src/Umbraco.Web/Cache/UserPermissionsCacheRefresher.cs index db1baf1bcd..64744024ed 100644 --- a/src/Umbraco.Web/Cache/UserPermissionsCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserPermissionsCacheRefresher.cs @@ -1,6 +1,7 @@ using System; using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Models.Membership; namespace Umbraco.Web.Cache { @@ -31,7 +32,8 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch(CacheKeys.UserPermissionsCacheKey); + if (UserPermissionsCache) + UserPermissionsCache.Result.ClearCacheByKeySearch(CacheKeys.UserPermissionsCacheKey); base.RefreshAll(); } @@ -43,8 +45,14 @@ namespace Umbraco.Web.Cache public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(string.Format("{0}{1}", CacheKeys.UserPermissionsCacheKey, id)); + if (UserPermissionsCache) + UserPermissionsCache.Result.ClearCacheItem(string.Format("{0}{1}", CacheKeys.UserPermissionsCacheKey, id)); base.Remove(id); } + + private Attempt UserPermissionsCache + { + get { return ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/UserTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/UserTypeCacheRefresher.cs index 2beaa4347d..ca3f93c068 100644 --- a/src/Umbraco.Web/Cache/UserTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserTypeCacheRefresher.cs @@ -29,19 +29,23 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.RefreshAll(); } public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + var userTypeCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (userTypeCache) + userTypeCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); base.Refresh(id); } public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + var userTypeCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (userTypeCache) + userTypeCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); base.Remove(id); } diff --git a/src/Umbraco.Web/CacheHelperExtensions.cs b/src/Umbraco.Web/CacheHelperExtensions.cs index f23bb6d070..1b0451a999 100644 --- a/src/Umbraco.Web/CacheHelperExtensions.cs +++ b/src/Umbraco.Web/CacheHelperExtensions.cs @@ -58,7 +58,7 @@ namespace Umbraco.Web /// public static void ClearPartialViewCache(this CacheHelper cacheHelper) { - cacheHelper.ClearCacheByKeySearch(PartialViewCacheKey); + cacheHelper.RuntimeCache.ClearCacheByKeySearch(PartialViewCacheKey); } } } diff --git a/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs b/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs index 29954701ea..fdb4145160 100644 --- a/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs +++ b/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs @@ -18,6 +18,11 @@ namespace Umbraco.Web.Dictionary /// /// A culture dictionary that uses the Umbraco ILocalizationService /// + /// + /// TODO: The ICultureDictionary needs to represent the 'fast' way to do dictionary item retrieval - for front-end and back office. + /// The ILocalizationService is the service used for interacting with this data from the database which isn't all that fast + /// (even though there is caching involved, if there's lots of dictionary items the caching is not great) + /// public class DefaultCultureDictionary : Umbraco.Core.Dictionary.ICultureDictionary { private readonly ILocalizationService _localizationService; @@ -93,6 +98,11 @@ namespace Umbraco.Web.Dictionary ///
/// /// + /// + /// NOTE: The result of this is not cached anywhere - the underlying repository does not cache + /// the child lookups because that is done by a query lookup. This method isn't used in our codebase + /// so I don't think this is a performance issue but if devs are using this it could be optimized here. + /// public IDictionary GetChildren(string key) { var result = new Dictionary(); @@ -126,6 +136,7 @@ namespace Umbraco.Web.Dictionary get { //ensure it's stored/retrieved from request cache + //NOTE: This is no longer necessary since these are cached at the runtime level, but we can leave it here for now. return _requestCacheProvider.GetCacheItem(typeof (DefaultCultureDictionary).Name + "Culture", () => _localizationService.GetLanguageByIsoCode(Culture.Name)); } diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 1a571a7bcb..0f770b89f9 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Configuration; @@ -105,7 +106,8 @@ namespace Umbraco.Web.Editors var cultureInfo = string.IsNullOrWhiteSpace(culture) //if the user is logged in, get their culture, otherwise default to 'en' ? Security.IsAuthenticated() - ? Security.CurrentUser.GetUserCulture(Services.TextService) + //current culture is set at the very beginning of each request + ? Thread.CurrentThread.CurrentCulture : CultureInfo.GetCultureInfo("en") : CultureInfo.GetCultureInfo(culture); @@ -352,6 +354,10 @@ namespace Umbraco.Web.Editors "imageFileTypes", string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes) }, + { + "disallowedUploadFiles", + string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles) + }, { "maxFileSize", GetMaxRequestLength() @@ -404,7 +410,7 @@ namespace Umbraco.Web.Editors return JavaScript(result); } - + [HttpPost] public ActionResult ExternalLogin(string provider, string redirectUrl = null) { @@ -572,6 +578,9 @@ namespace Umbraco.Web.Editors else { + if (loginInfo.Email.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Email value cannot be null"); + if (loginInfo.ExternalIdentity.Name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); + var autoLinkUser = new BackOfficeIdentityUser() { Email = loginInfo.Email, @@ -648,6 +657,7 @@ namespace Umbraco.Web.Editors app.Add("applicationPath", HttpContext.Request.ApplicationPath.EnsureEndsWith('/')); return app; } + private IEnumerable> GetTreePluginsMetaData() { diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 6b4ea0416a..eee51d4ab5 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -29,6 +29,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Dynamics; using umbraco.cms.businesslogic.web; using umbraco.presentation.preview; +using Umbraco.Core.PropertyEditors; using Umbraco.Web.UI; using Constants = Umbraco.Core.Constants; using Notification = Umbraco.Web.Models.ContentEditing.Notification; @@ -75,11 +76,35 @@ namespace Umbraco.Web.Editors return foundContent.Select(Mapper.Map); } + /// + /// Returns an item to be used to display the recycle bin for content + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinContent, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinContent + }; + + TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); + + return display; + } + /// /// Gets the content json for the content id /// /// /// + [OutgoingEditorModelEvent] [EnsureUserPermissionForContent("id")] public ContentItemDisplay GetById(int id) { @@ -115,6 +140,7 @@ namespace Umbraco.Web.Editors /// If this is a container type, we'll remove the umbContainerView tab for a new item since /// it cannot actually list children if it doesn't exist yet. /// + [OutgoingEditorModelEvent] public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) { var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); @@ -661,9 +687,17 @@ namespace Umbraco.Web.Editors new[] {string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id)}).Trim()); break; case PublishStatusType.FailedHasExpired: - //TODO: We should add proper error messaging for this! + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + }).Trim()); + break; case PublishStatusType.FailedIsTrashed: //TODO: We should add proper error messaging for this! + break; case PublishStatusType.FailedContentInvalid: display.AddWarningNotification( Services.TextService.Localize("publish"), diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index aea6690bf2..eaa587fc6c 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -1,22 +1,24 @@ using System.Collections.Generic; +using System.Configuration; +using System.Linq; using System.Net; using System.Web.Http; using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Dictionary; using Umbraco.Core.Models; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using System.Linq; -using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; -using Newtonsoft.Json; -using Umbraco.Core.PropertyEditors; -using System; -using System.Net.Http; using Umbraco.Core.Services; +using Umbraco.Core.PropertyEditors; +using System.Net.Http; +using umbraco; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Strings; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Logging; +using Umbraco.Web.Models; namespace Umbraco.Web.Editors { @@ -50,8 +52,12 @@ namespace Umbraco.Web.Editors { } + public int GetCount() + { + return Services.ContentTypeService.CountContentTypes(); + } - public ContentTypeDisplay GetById(int id) + public DocumentTypeDisplay GetById(int id) { var ct = Services.ContentTypeService.GetContentType(id); if (ct == null) @@ -59,7 +65,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var dto = Mapper.Map(ct); + var dto = Mapper.Map(ct); return dto; } @@ -95,9 +101,31 @@ namespace Umbraco.Web.Editors return ApplicationContext.Services.ContentTypeService.GetAllPropertyTypeAliases(); } - public IEnumerable GetAvailableCompositeContentTypes(int contentTypeId) + /// + /// Returns the avilable compositions for this content type + /// This has been wrapped in a dto instead of simple parameters to support having multiple parameters in post request body + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + [HttpPost] + public HttpResponseMessage GetAvailableCompositeContentTypes(GetAvailableCompositionsFilter filter) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.DocumentType); + var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.DocumentType, filter.FilterContentTypes, filter.FilterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); } [UmbracoTreeAuthorize( @@ -148,31 +176,38 @@ namespace Umbraco.Web.Editors : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); } - public ContentTypeDisplay PostSave(ContentTypeSave contentTypeSave) + public DocumentTypeDisplay PostSave(DocumentTypeSave contentTypeSave) { - var savedCt = PerformPostSave( + var savedCt = PerformPostSave( contentTypeSave: contentTypeSave, getContentType: i => Services.ContentTypeService.GetContentType(i), saveContentType: type => Services.ContentTypeService.Save(type), beforeCreateNew: ctSave => { //create a default template if it doesnt exist -but only if default template is == to the content type - //TODO: Is this really what we want? What if we don't want any template assigned at all ? if (ctSave.DefaultTemplate.IsNullOrWhiteSpace() == false && ctSave.DefaultTemplate == ctSave.Alias) { var template = Services.FileService.GetTemplate(ctSave.Alias); if (template == null) { - template = new Template(ctSave.Name, ctSave.Alias); - Services.FileService.SaveTemplate(template); + var tryCreateTemplate = Services.FileService.CreateTemplateForContentType(ctSave.Alias, ctSave.Name); + if (tryCreateTemplate == false) + { + Logger.Warn( + "Could not create a template for the Content Type: {0}, status: {1}", + () => ctSave.Alias, + () => tryCreateTemplate.Result.StatusType); + } + template = tryCreateTemplate.Result.Entity; } //make sure the template alias is set on the default and allowed template so we can map it back ctSave.DefaultTemplate = template.Alias; + } }); - var display = Mapper.Map(savedCt); + var display = Mapper.Map(savedCt); display.AddSuccessNotification( Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"), @@ -186,12 +221,20 @@ namespace Umbraco.Web.Editors ///
/// /// - public ContentTypeDisplay GetEmpty(int parentId) + public DocumentTypeDisplay GetEmpty(int parentId) { - var ct = new ContentType(parentId); + IContentType ct; + if (parentId != Constants.System.Root) + { + var parent = Services.ContentTypeService.GetContentType(parentId); + ct = parent != null ? new ContentType(parent, string.Empty) : new ContentType(parentId); + } + else + ct = new ContentType(parentId); + ct.Icon = "icon-document"; - var dto = Mapper.Map(ct); + var dto = Mapper.Map(ct); return dto; } @@ -248,10 +291,11 @@ namespace Umbraco.Web.Editors var basics = types.Select(Mapper.Map).ToList(); + var localizedTextService = Services.TextService; foreach (var basic in basics) { - basic.Name = TranslateItem(basic.Name); - basic.Description = TranslateItem(basic.Description); + basic.Name = localizedTextService.UmbracoDictionaryTranslate(basic.Name); + basic.Description = localizedTextService.UmbracoDictionaryTranslate(basic.Description); } return basics; diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 443b1dcbff..bc49e10f02 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -1,67 +1,83 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Text.RegularExpressions; -using System.Web.Http; -using AutoMapper; -using Newtonsoft.Json; -using Umbraco.Core; -using Umbraco.Core.Dictionary; -using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Constants = Umbraco.Core.Constants; - -namespace Umbraco.Web.Editors -{ - /// - /// Am abstract API controller providing functionality used for dealing with content and media types - /// - [PluginController("UmbracoApi")] - [PrefixlessBodyModelValidator] - public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; +using System.Web.Http; +using AutoMapper; +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Dictionary; +using Umbraco.Core.Exceptions; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Editors +{ + /// + /// Am abstract API controller providing functionality used for dealing with content and media types + /// + [PluginController("UmbracoApi")] + [PrefixlessBodyModelValidator] + public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController { - private ICultureDictionary _cultureDictionary; - - /// - /// Constructor - /// - protected ContentTypeControllerBase() - : this(UmbracoContext.Current) - { - } - - /// - /// Constructor - /// - /// - protected ContentTypeControllerBase(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - + private ICultureDictionary _cultureDictionary; + + /// + /// Constructor + /// + protected ContentTypeControllerBase() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + protected ContentTypeControllerBase(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + /// /// Returns the available composite content types for a given content type /// - /// - protected IEnumerable PerformGetAvailableCompositeContentTypes(int contentTypeId, UmbracoObjectTypes type) - { - IContentTypeComposition source = null; - + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + /// + protected IEnumerable> PerformGetAvailableCompositeContentTypes(int contentTypeId, + UmbracoObjectTypes type, + string[] filterContentTypes, + string[] filterPropertyTypes) + { + IContentTypeComposition source = null; + //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic IContentTypeComposition[] allContentTypes; switch (type) { - case UmbracoObjectTypes.DocumentType: - if (contentTypeId > 0) + case UmbracoObjectTypes.DocumentType: + if (contentTypeId > 0) { source = Services.ContentTypeService.GetContentType(contentTypeId); if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); @@ -69,21 +85,21 @@ namespace Umbraco.Web.Editors allContentTypes = Services.ContentTypeService.GetAllContentTypes().Cast().ToArray(); break; - case UmbracoObjectTypes.MediaType: - if (contentTypeId > 0) - { - source = Services.ContentTypeService.GetMediaType(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } + case UmbracoObjectTypes.MediaType: + if (contentTypeId > 0) + { + source = Services.ContentTypeService.GetMediaType(contentTypeId); + if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } allContentTypes = Services.ContentTypeService.GetAllMediaTypes().Cast().ToArray(); break; - case UmbracoObjectTypes.MemberType: - if (contentTypeId > 0) - { - source = Services.MemberTypeService.Get(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } + case UmbracoObjectTypes.MemberType: + if (contentTypeId > 0) + { + source = Services.MemberTypeService.Get(contentTypeId); + if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } allContentTypes = Services.MemberTypeService.GetAll().Cast().ToArray(); break; @@ -91,119 +107,32 @@ namespace Umbraco.Web.Editors throw new ArgumentOutOfRangeException("The entity type was not a content type"); } - // note: there are many sanity checks missing here and there ;-(( - // make sure once and for all - //if (allContentTypes.Any(x => x.ParentId > 0 && x.ContentTypeComposition.Any(y => y.Id == x.ParentId) == false)) - // throw new Exception("A parent does not belong to a composition."); + var availableCompositions = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, filterContentTypes, filterPropertyTypes); - // find out if any content type uses this content type - var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == contentTypeId)).ToArray(); - if (isUsing.Length > 0) - { - //if already in use a composition, do not allow any composited types - return new List(); - } + var currCompositions = source == null ? new IContentTypeComposition[] { } : source.ContentTypeComposition.ToArray(); + var compAliases = currCompositions.Select(x => x.Alias).ToArray(); + var ancestors = availableCompositions.Ancestors.Select(x => x.Alias); - // if it is not used then composition is possible - // hashset guarantees unicity on Id - var list = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id)); - - // usable types are those that are top-level - var usableContentTypes = allContentTypes - .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); - foreach (var x in usableContentTypes) - list.Add(x); - - // indirect types are those that we use, directly or indirectly - var indirectContentTypes = GetIndirect(source).ToArray(); - foreach (var x in indirectContentTypes) - list.Add(x); - - //// directContentTypes are those we use directly - //// they are already in indirectContentTypes, no need to add to the list - //var directContentTypes = source.ContentTypeComposition.ToArray(); - - //var enabled = usableContentTypes.Select(x => x.Id) // those we can use - // .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used - // .Union(directContentTypes.Select(x => x.Id)) // but those that are directly used - // .Where(x => x != source.ParentId) // but not the parent - // .Distinct() - // .ToArray(); - - return list - .Where(x => x.Id != contentTypeId) - .OrderBy(x => x.Name) - .Select(Mapper.Map) - .Select(x => - { - x.Name = TranslateItem(x.Name); - return x; - }) - .ToList(); - } - - private static IEnumerable GetIndirect(IContentTypeComposition ctype) - { - // hashset guarantees unicity on Id - var all = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id)); - - var stack = new Stack(); - - if (ctype != null) - { - foreach (var x in ctype.ContentTypeComposition) - stack.Push(x); - } - - while (stack.Count > 0) - { - var x = stack.Pop(); - all.Add(x); - foreach (var y in x.ContentTypeComposition) - stack.Push(y); - } - - return all; - } - - /// - /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors - /// - /// - /// - /// - protected void ValidateComposition(ContentTypeSave contentTypeSave, IContentTypeComposition composition) - { - var validateAttempt = Services.ContentTypeService.ValidateComposition(composition); - if (validateAttempt == false) - { - //if it's not successful then we need to return some model state for the property aliases that - // are duplicated - var propertyAliases = validateAttempt.Result.Distinct(); - foreach (var propertyAlias in propertyAliases) + return availableCompositions.Results + .Select(x => new Tuple(Mapper.Map(x.Composition), x.Allowed)) + .Select(x => { - //find the property relating to these - var prop = contentTypeSave.Groups.SelectMany(x => x.Properties).Single(x => x.Alias == propertyAlias); - var group = contentTypeSave.Groups.Single(x => x.Properties.Contains(prop)); - var propIndex = group.Properties.IndexOf(prop); - var groupIndex = contentTypeSave.Groups.IndexOf(group); + //translate the name + x.Item1.Name = TranslateItem(x.Item1.Name); - var key = string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propIndex); - ModelState.AddModelError(key, "Duplicate property aliases not allowed between compositions"); - } - - var display = Mapper.Map(composition); - //map the 'save' data on top - display = Mapper.Map(contentTypeSave, display); - display.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } + //we need to ensure that the item is enabled if it is already selected + // but do not allow it if it is any of the ancestors + if (compAliases.Contains(x.Item1.Alias) && ancestors.Contains(x.Item1.Alias) == false) + { + //re-set x to be allowed (NOTE: I didn't know you could set an enumerable item in a lambda!) + x = new Tuple(x.Item1, true); + } + return x; + }) + .ToList(); } + protected string TranslateItem(string text) { @@ -219,26 +148,37 @@ namespace Umbraco.Web.Editors return CultureDictionary[text].IfNullOrWhiteSpace(text); } - protected TContentType PerformPostSave( - ContentTypeSave contentTypeSave, + protected TContentType PerformPostSave( + TContentTypeSave contentTypeSave, Func getContentType, Action saveContentType, - bool validateComposition = true, - Action beforeCreateNew = null) - where TContentType : IContentTypeComposition + Action beforeCreateNew = null) + where TContentType : class, IContentTypeComposition where TContentTypeDisplay : ContentTypeCompositionDisplay + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic { var ctId = Convert.ToInt32(contentTypeSave.Id); - + var ct = ctId > 0 ? getContentType(ctId) : null; + if (ctId > 0 && ct == null) throw new HttpResponseException(HttpStatusCode.NotFound); + + //Validate that there's no other ct with the same alias + // it in fact cannot be the same as any content type alias (member, content or media) because + // this would interfere with how ModelsBuilder works and also how many of the published caches + // works since that is based on aliases. + var allAliases = Services.ContentTypeService.GetAllContentTypeAliases(); + var exists = allAliases.InvariantContains(contentTypeSave.Alias); + if ((exists) && (ctId == 0 || ct.Alias != contentTypeSave.Alias)) + { + ModelState.AddModelError("Alias", "A content type, media type or member type with this alias already exists"); + } + + //now let the external validators execute + ValidationHelper.ValidateEditorModelWithResolver(ModelState, contentTypeSave); + if (ModelState.IsValid == false) { - var ct = getContentType(ctId); - //Required data is invalid so we cannot continue - var forDisplay = Mapper.Map(ct); - //map the 'save' data on top - forDisplay = Mapper.Map(contentTypeSave, forDisplay); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + throw CreateModelStateValidationException(ctId, contentTypeSave, ct); } //filter out empty properties @@ -250,35 +190,33 @@ namespace Umbraco.Web.Editors if (ctId > 0) { - //its an update to an existing - var found = getContentType(ctId); - if (found == null) - throw new HttpResponseException(HttpStatusCode.NotFound); + //its an update to an existing content type - Mapper.Map(contentTypeSave, found); - - if (validateComposition) + //This mapping will cause a lot of content type validation to occur which we need to deal with + try { - //NOTE: this throws an error response if it is not valid - ValidateComposition(contentTypeSave, found); + Mapper.Map(contentTypeSave, ct); + } + catch (Exception ex) + { + var responseEx = CreateInvalidCompositionResponseException(ex, contentTypeSave, ct, ctId); + if (responseEx != null) throw responseEx; } - saveContentType(found); - - return found; + var exResult = CreateCompositionValidationExceptionIfInvalid(contentTypeSave, ct); + if (exResult != null) throw exResult; + + saveContentType(ct); + + return ct; } else - { - if (beforeCreateNew != null) - { - beforeCreateNew(contentTypeSave); + { + if (beforeCreateNew != null) + { + beforeCreateNew(contentTypeSave); } - - //set id to null to ensure its handled as a new type - contentTypeSave.Id = null; - contentTypeSave.CreateDate = DateTime.Now; - contentTypeSave.UpdateDate = DateTime.Now; - + //check if the type is trying to allow type 0 below itself - id zero refers to the currently unsaved type //always filter these 0 types out var allowItselfAsChild = false; @@ -289,13 +227,26 @@ namespace Umbraco.Web.Editors } //save as new - var newCt = Mapper.Map(contentTypeSave); - - if (validateComposition) + + TContentType newCt = null; + try { - //NOTE: this throws an error response if it is not valid - ValidateComposition(contentTypeSave, newCt); + //This mapping will cause a lot of content type validation to occur which we need to deal with + newCt = Mapper.Map(contentTypeSave); } + catch (Exception ex) + { + var responseEx = CreateInvalidCompositionResponseException(ex, contentTypeSave, ct, ctId); + if (responseEx != null) throw responseEx; + } + + var exResult = CreateCompositionValidationExceptionIfInvalid(contentTypeSave, newCt); + if (exResult != null) throw exResult; + + //set id to null to ensure its handled as a new type + contentTypeSave.Id = null; + contentTypeSave.CreateDate = DateTime.Now; + contentTypeSave.UpdateDate = DateTime.Now; saveContentType(newCt); @@ -305,18 +256,18 @@ namespace Umbraco.Web.Editors //NOTE: This will throw if the composition isn't right... but it shouldn't be at this stage newCt.AddContentType(newCt); saveContentType(newCt); - } - return newCt; + } + return newCt; } - } - - /// - /// Change the sort order for media - /// - /// - /// - /// - /// + } + + /// + /// Change the sort order for media + /// + /// + /// + /// + /// protected HttpResponseMessage PerformMove( MoveOrCopy move, Func getContentType, @@ -329,29 +280,146 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.NotFound); } - var result = doMove(toMove, move.ParentId); - if (result.Success) - { - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; - } - - switch (result.Result.StatusType) - { - case MoveOperationStatusType.FailedParentNotFound: - return Request.CreateResponse(HttpStatusCode.NotFound); + var result = doMove(toMove, move.ParentId); + if (result.Success) + { + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + switch (result.Result.StatusType) + { + case MoveOperationStatusType.FailedParentNotFound: + return Request.CreateResponse(HttpStatusCode.NotFound); case MoveOperationStatusType.FailedCancelledByEvent: //returning an object of INotificationModel will ensure that any pending // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - case MoveOperationStatusType.FailedNotAllowedByPath: + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + case MoveOperationStatusType.FailedNotAllowedByPath: var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); return Request.CreateValidationErrorResponse(notificationModel); - default: - throw new ArgumentOutOfRangeException(); - } + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors + /// + /// + /// + /// + private HttpResponseException CreateCompositionValidationExceptionIfInvalid(TContentTypeSave contentTypeSave, IContentTypeComposition composition) + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + where TContentTypeDisplay : ContentTypeCompositionDisplay + { + var validateAttempt = Services.ContentTypeService.ValidateComposition(composition); + if (validateAttempt == false) + { + //if it's not successful then we need to return some model state for the property aliases that + // are duplicated + var invalidPropertyAliases = validateAttempt.Result.Distinct(); + AddCompositionValidationErrors(contentTypeSave, invalidPropertyAliases); + + var display = Mapper.Map(composition); + //map the 'save' data on top + display = Mapper.Map(contentTypeSave, display); + display.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } + return null; + } + + /// + /// Adds errors to the model state if any invalid aliases are found then throws an error response if there are errors + /// + /// + /// + /// + private void AddCompositionValidationErrors(TContentTypeSave contentTypeSave, IEnumerable invalidPropertyAliases) + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + { + foreach (var propertyAlias in invalidPropertyAliases) + { + //find the property relating to these + var prop = contentTypeSave.Groups.SelectMany(x => x.Properties).Single(x => x.Alias == propertyAlias); + var group = contentTypeSave.Groups.Single(x => x.Properties.Contains(prop)); + + var key = string.Format("Groups[{0}].Properties[{1}].Alias", group.SortOrder, prop.SortOrder); + ModelState.AddModelError(key, "Duplicate property aliases not allowed between compositions"); + } + } + + /// + /// If the exception is an InvalidCompositionException create a response exception to be thrown for validation errors + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + private HttpResponseException CreateInvalidCompositionResponseException( + Exception ex, TContentTypeSave contentTypeSave, TContentType ct, int ctId) + where TContentType : class, IContentTypeComposition + where TContentTypeDisplay : ContentTypeCompositionDisplay + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + { + InvalidCompositionException invalidCompositionException = null; + if (ex is AutoMapperMappingException && ex.InnerException is InvalidCompositionException) + { + invalidCompositionException = (InvalidCompositionException)ex.InnerException; + } + else if (ex.InnerException is InvalidCompositionException) + { + invalidCompositionException = (InvalidCompositionException)ex; + } + if (invalidCompositionException != null) + { + AddCompositionValidationErrors(contentTypeSave, invalidCompositionException.PropertyTypeAliases); + return CreateModelStateValidationException(ctId, contentTypeSave, ct); + } + return null; + } + + /// + /// Used to throw the ModelState validation results when the ModelState is invalid + /// + /// + /// + /// + /// + /// + /// + private HttpResponseException CreateModelStateValidationException(int ctId, TContentTypeSave contentTypeSave, TContentType ct) + where TContentType : class, IContentTypeComposition + where TContentTypeDisplay : ContentTypeCompositionDisplay + where TContentTypeSave : ContentTypeSave + { + TContentTypeDisplay forDisplay; + if (ctId > 0) + { + //Required data is invalid so we cannot continue + forDisplay = Mapper.Map(ct); + //map the 'save' data on top + forDisplay = Mapper.Map(contentTypeSave, forDisplay); + } + else + { + //map the 'save' data to display + forDisplay = Mapper.Map(contentTypeSave); + } + + forDisplay.Errors = ModelState.ToErrorDictionary(); + return new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } private ICultureDictionary CultureDictionary @@ -362,7 +430,8 @@ namespace Umbraco.Web.Editors _cultureDictionary ?? (_cultureDictionary = CultureDictionaryFactoryResolver.Current.Factory.CreateDictionary()); } - } - - } + } + + + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index c2d7ec37d2..931d00968b 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -299,14 +299,16 @@ namespace Umbraco.Web.Editors { var dataTypes = Services.DataTypeService .GetAllDataTypeDefinitions() - .Select(Mapper.Map); + .Select(Mapper.Map) + .ToArray(); var propertyEditors = PropertyEditorResolver.Current.PropertyEditors.ToArray(); foreach (var dataType in dataTypes) { - var propertyEditor = propertyEditors.Single(x => x.Alias == dataType.Alias); - dataType.HasPrevalues = propertyEditor.PreValueEditor.Fields.Any(); ; + var propertyEditor = propertyEditors.SingleOrDefault(x => x.Alias == dataType.Alias); + if(propertyEditor != null) + dataType.HasPrevalues = propertyEditor.PreValueEditor.Fields.Any(); ; } var grouped = dataTypes diff --git a/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs b/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs index ddf694f630..7f513b35c7 100644 --- a/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs +++ b/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs @@ -42,8 +42,8 @@ namespace Umbraco.Web.Editors { var dataType = (DataTypeSave)actionContext.ActionArguments["dataType"]; - dataType.Name = dataType.Name.CleanForXss('[', ']', '(', ')'); - dataType.Alias = dataType.Name.CleanForXss('[', ']', '(', ')'); + dataType.Name = dataType.Name.CleanForXss('[', ']', '(', ')', ':'); + dataType.Alias = dataType.Name.CleanForXss('[', ']', '(', ')', ':'); //Validate that the property editor exists var propertyEditor = PropertyEditorResolver.Current.GetByAlias(dataType.SelectedEditor); diff --git a/src/Umbraco.Web/Editors/EditorModelEventManager.cs b/src/Umbraco.Web/Editors/EditorModelEventManager.cs new file mode 100644 index 0000000000..44454ca6c3 --- /dev/null +++ b/src/Umbraco.Web/Editors/EditorModelEventManager.cs @@ -0,0 +1,91 @@ +using System; +using System.Web.Http.Filters; +using Umbraco.Core.Events; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors +{ + public class EditorModelEventArgs : EventArgs + { + public EditorModelEventArgs(object model, UmbracoContext umbracoContext) + { + Model = model; + UmbracoContext = umbracoContext; + } + + public object Model { get; private set; } + public UmbracoContext UmbracoContext { get; private set; } + } + + public sealed class EditorModelEventArgs : EditorModelEventArgs + { + public EditorModelEventArgs(EditorModelEventArgs baseArgs) + : base(baseArgs.Model, baseArgs.UmbracoContext) + { + Model = (T)baseArgs.Model; + } + + public EditorModelEventArgs(T model, UmbracoContext umbracoContext) + : base(model, umbracoContext) + { + Model = model; + } + + public new T Model { get; private set; } + } + + /// + /// Used to emit events for editor models in the back office + /// + public sealed class EditorModelEventManager + { + public static event TypedEventHandler> SendingContentModel; + public static event TypedEventHandler> SendingMediaModel; + public static event TypedEventHandler> SendingMemberModel; + + private static void OnSendingContentModel(HttpActionExecutedContext sender, EditorModelEventArgs e) + { + var handler = SendingContentModel; + if (handler != null) handler(sender, e); + } + + private static void OnSendingMediaModel(HttpActionExecutedContext sender, EditorModelEventArgs e) + { + var handler = SendingMediaModel; + if (handler != null) handler(sender, e); + } + + private static void OnSendingMemberModel(HttpActionExecutedContext sender, EditorModelEventArgs e) + { + var handler = SendingMemberModel; + if (handler != null) handler(sender, e); + } + + /// + /// Based on the type, emit's a specific event + /// + /// + /// + internal static void EmitEvent(HttpActionExecutedContext sender, EditorModelEventArgs e) + { + var contentItemDisplay = e.Model as ContentItemDisplay; + if (contentItemDisplay != null) + { + OnSendingContentModel(sender, new EditorModelEventArgs(e)); + } + + var mediaItemDisplay = e.Model as MediaItemDisplay; + if (mediaItemDisplay != null) + { + OnSendingMediaModel(sender, new EditorModelEventArgs(e)); + } + + var memberItemDisplay = e.Model as MemberDisplay; + if (memberItemDisplay != null) + { + OnSendingMemberModel(sender, new EditorModelEventArgs(e)); + } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/EditorValidationResolver.cs b/src/Umbraco.Web/Editors/EditorValidationResolver.cs new file mode 100644 index 0000000000..d0c5edf5af --- /dev/null +++ b/src/Umbraco.Web/Editors/EditorValidationResolver.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Web.Editors +{ + internal class EditorValidationResolver : LazyManyObjectsResolverBase + { + public EditorValidationResolver(IServiceProvider serviceProvider, ILogger logger, Func> migrations) + : base(serviceProvider, logger, migrations, ObjectLifetimeScope.Application) + { + } + + public virtual IEnumerable EditorValidators + { + get { return Values; } + } + + public IEnumerable Validate(object model) + { + return EditorValidators + .Where(x => model.GetType() == x.ModelType) + .WhereNotNull() + .SelectMany(x => x.Validate(model)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/EditorValidator.cs b/src/Umbraco.Web/Editors/EditorValidator.cs new file mode 100644 index 0000000000..d123838f52 --- /dev/null +++ b/src/Umbraco.Web/Editors/EditorValidator.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Web.Editors +{ + internal abstract class EditorValidator : IEditorValidator + { + public Type ModelType + { + get { return typeof (T); } + } + + protected abstract IEnumerable PerformValidate(T model); + + public IEnumerable Validate(object model) + { + return PerformValidate((T) model); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/IEditorValidator.cs b/src/Umbraco.Web/Editors/IEditorValidator.cs new file mode 100644 index 0000000000..e1d4e68ed2 --- /dev/null +++ b/src/Umbraco.Web/Editors/IEditorValidator.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Web.Editors +{ + internal interface IEditorValidator + { + Type ModelType { get; } + IEnumerable Validate(object model); + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index f69f84ca86..416ffc2553 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -104,8 +104,6 @@ namespace Umbraco.Web.Editors return GetResized(imagePath, width, Convert.ToString(width)); } - //TODO: We should delegate this to ImageProcessing - /// /// Gets a resized image - if the requested max width is greater than the original image, only the original image will be returned. /// @@ -124,53 +122,11 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.NotFound); } - var thumbFilePath = imagePath.TrimEnd(ext) + "_" + suffix + ext; - var fullOrgPath = mediaFileSystem.GetFullPath(mediaFileSystem.GetRelativePath(imagePath)); - var fullNewPath = mediaFileSystem.GetFullPath(mediaFileSystem.GetRelativePath(thumbFilePath)); - var thumbIsNew = mediaFileSystem.FileExists(fullNewPath) == false; - if (thumbIsNew) - { - //we need to generate it - if (mediaFileSystem.FileExists(fullOrgPath) == false) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - using (var fileStream = mediaFileSystem.OpenFile(fullOrgPath)) - { - if (fileStream.CanSeek) fileStream.Seek(0, 0); - using (var originalImage = Image.FromStream(fileStream)) - { - //If it is bigger, then do the resize - if (originalImage.Width >= width && originalImage.Height >= width) - { - ImageHelper.GenerateThumbnail( - originalImage, - width, - fullNewPath, - ext.Replace(".", ""), - mediaFileSystem); - } - else - { - //just return the original image - fullNewPath = fullOrgPath; - } - - } - } - } - - var result = Request.CreateResponse(HttpStatusCode.OK); - //NOTE: That we are not closing this stream as the framework will do that for us, if we try it will - // fail. See http://stackoverflow.com/questions/9541351/returning-binary-file-from-controller-in-asp-net-web-api - var stream = mediaFileSystem.OpenFile(fullNewPath); - if (stream.CanSeek) stream.Seek(0, 0); - result.Content = new StreamContent(stream); - result.Headers.Date = mediaFileSystem.GetLastModified(imagePath); - result.Content.Headers.ContentType = new MediaTypeHeaderValue(System.Web.MimeMapping.GetMimeMapping(imagePath)); - - return result; + //redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file + var response = Request.CreateResponse( HttpStatusCode.Found ); + var imageLastModified = mediaFileSystem.GetLastModified( imagePath ); + response.Headers.Location = new Uri( string.Format( "{0}?rnd={1}&width={2}", imagePath, string.Format( "{0:yyyyMMddHHmmss}", imageLastModified ), width ), UriKind.Relative ); + return response; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 07389a246f..de6e508eef 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -62,13 +62,14 @@ namespace Umbraco.Web.Editors : base(umbracoContext) { } - + /// /// Gets an empty content item for the /// /// /// /// + [OutgoingEditorModelEvent] public MediaItemDisplay GetEmpty(string contentTypeAlias, int parentId) { var contentType = Services.ContentTypeService.GetMediaType(contentTypeAlias); @@ -86,11 +87,35 @@ namespace Umbraco.Web.Editors return mapped; } + /// + /// Returns an item to be used to display the recycle bin for media + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinMedia, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinMedia + }; + + TabsAndPropertiesResolver.AddListView(display, "media", Services.DataTypeService, Services.TextService); + + return display; + } + /// /// Gets the content json for the content id /// /// /// + [OutgoingEditorModelEvent] [EnsureUserPermissionForMedia("id")] public MediaItemDisplay GetById(int id) { @@ -120,7 +145,7 @@ namespace Umbraco.Web.Editors /// /// Returns media items known to be a container of other media items /// - /// + /// /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] public IEnumerable> GetChildFolders(int id = -1) @@ -504,7 +529,17 @@ namespace Umbraco.Web.Editors if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.Contains(ext)) mediaType = Constants.Conventions.MediaTypes.Image; - var f = mediaService.CreateMedia(fileName, parentId, mediaType, Security.CurrentUser.Id); + //TODO: make the media item name "nice" since file names could be pretty ugly, we have + // string extensions to do much of this but we'll need: + // * Pascalcase the name (use string extensions) + // * strip the file extension + // * underscores to spaces + // * probably remove 'ugly' characters - let's discuss + // All of this logic should exist in a string extensions method and be unit tested + // http://issues.umbraco.org/issue/U4-5572 + var mediaItemName = fileName; + + var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id); var fileInfo = new FileInfo(file.LocalFileName); var fs = fileInfo.OpenReadWithRetry(); @@ -518,7 +553,7 @@ namespace Umbraco.Web.Editors if (saveResult == false) { AddCancelMessage(tempFiles, - message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + fileName, + message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName, localizeMessage: false); } else diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index d54981ff45..43071125de 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -18,6 +18,7 @@ using System.Text; using Umbraco.Web.WebApi; using ContentType = System.Net.Mime.ContentType; using Umbraco.Core.Services; +using Umbraco.Web.Models; namespace Umbraco.Web.Editors { @@ -51,8 +52,12 @@ namespace Umbraco.Web.Editors } + public int GetCount() + { + return Services.ContentTypeService.CountContentTypes(); + } - public ContentTypeCompositionDisplay GetById(int id) + public MediaTypeDisplay GetById(int id) { var ct = Services.ContentTypeService.GetMediaType(id); if (ct == null) @@ -60,7 +65,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var dto = Mapper.Map(ct); + var dto = Mapper.Map(ct); return dto; } @@ -83,17 +88,39 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - public IEnumerable GetAvailableCompositeMediaTypes(int contentTypeId) + /// + /// Returns the avilable compositions for this content type + /// This has been wrapped in a dto instead of simple parameters to support having multiple parameters in post request body + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + [HttpPost] + public HttpResponseMessage GetAvailableCompositeMediaTypes(GetAvailableCompositionsFilter filter) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MediaType); + var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.MediaType, filter.FilterContentTypes, filter.FilterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); } - public ContentTypeCompositionDisplay GetEmpty(int parentId) + public MediaTypeDisplay GetEmpty(int parentId) { var ct = new MediaType(parentId); ct.Icon = "icon-picture"; - var dto = Mapper.Map(ct); + var dto = Mapper.Map(ct); return dto; } @@ -131,17 +158,17 @@ namespace Umbraco.Web.Editors : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); } - public ContentTypeCompositionDisplay PostSave(ContentTypeSave contentTypeSave) + public MediaTypeDisplay PostSave(MediaTypeSave contentTypeSave) { - var savedCt = PerformPostSave( - contentTypeSave: contentTypeSave, - getContentType: i => Services.ContentTypeService.GetMediaType(i), - saveContentType: type => Services.ContentTypeService.Save(type)); + var savedCt = PerformPostSave( + contentTypeSave: contentTypeSave, + getContentType: i => Services.ContentTypeService.GetMediaType(i), + saveContentType: type => Services.ContentTypeService.Save(type)); - var display = Mapper.Map(savedCt); + var display = Mapper.Map(savedCt); display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"), + Services.TextService.Localize("speechBubbles/mediaTypeSavedHeader"), string.Empty); return display; diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 4405f257c4..f03cda1bba 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -135,7 +135,7 @@ namespace Umbraco.Web.Editors ParentId = -1 }; - TabsAndPropertiesResolver.AddListView(display, "member", Services.DataTypeService); + TabsAndPropertiesResolver.AddListView(display, "member", Services.DataTypeService, Services.TextService); return display; } @@ -145,6 +145,7 @@ namespace Umbraco.Web.Editors ///
/// /// + [OutgoingEditorModelEvent] public MemberDisplay GetByKey(Guid key) { MembershipUser foundMembershipMember; @@ -196,6 +197,7 @@ namespace Umbraco.Web.Editors ///
/// /// + [OutgoingEditorModelEvent] public MemberDisplay GetEmpty(string contentTypeAlias = null) { IMember emptyContent; @@ -343,12 +345,13 @@ namespace Umbraco.Web.Editors //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); + var localizedTextService = Services.TextService; //put the correct msgs in switch (contentItem.Action) { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: - display.AddSuccessNotification(ui.Text("speechBubbles", "editMemberSaved"), ui.Text("speechBubbles", "editMemberSaved")); + display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); break; } diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 7716f6ea00..1d8766f253 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -48,7 +48,7 @@ namespace Umbraco.Web.Editors private readonly MembershipProvider _provider; - public ContentTypeCompositionDisplay GetById(int id) + public MemberTypeDisplay GetById(int id) { var ct = Services.MemberTypeService.Get(id); if (ct == null) @@ -56,7 +56,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var dto = Mapper.Map(ct); + var dto = Mapper.Map(ct); return dto; } @@ -79,17 +79,39 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - public IEnumerable GetAvailableCompositeMemberTypes(int contentTypeId) + /// + /// Returns the avilable compositions for this content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + public HttpResponseMessage GetAvailableCompositeMemberTypes(int contentTypeId, + [FromUri]string[] filterContentTypes, + [FromUri]string[] filterPropertyTypes) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType); + var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType, filterContentTypes, filterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); } - public ContentTypeCompositionDisplay GetEmpty() + public MemberTypeDisplay GetEmpty() { var ct = new MemberType(-1); ct.Icon = "icon-user"; - var dto = Mapper.Map(ct); + var dto = Mapper.Map(ct); return dto; } @@ -107,18 +129,17 @@ namespace Umbraco.Web.Editors return Enumerable.Empty(); } - public ContentTypeCompositionDisplay PostSave(ContentTypeSave contentTypeSave) + public MemberTypeDisplay PostSave(MemberTypeSave contentTypeSave) { - var savedCt = PerformPostSave( - contentTypeSave: contentTypeSave, - getContentType: i => Services.MemberTypeService.Get(i), - saveContentType: type => Services.MemberTypeService.Save(type), - validateComposition: false); + var savedCt = PerformPostSave( + contentTypeSave: contentTypeSave, + getContentType: i => Services.MemberTypeService.Get(i), + saveContentType: type => Services.MemberTypeService.Save(type)); - var display = Mapper.Map(savedCt); + var display = Mapper.Map(savedCt); display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"), + Services.TextService.Localize("speechBubbles/memberTypeSavedHeader"), string.Empty); return display; diff --git a/src/Umbraco.Web/Editors/ValidationHelper.cs b/src/Umbraco.Web/Editors/ValidationHelper.cs index 7bad8ceb8c..e1633fa6bf 100644 --- a/src/Umbraco.Web/Editors/ValidationHelper.cs +++ b/src/Umbraco.Web/Editors/ValidationHelper.cs @@ -1,12 +1,31 @@ using System; using System.ComponentModel; using System.Linq; +using System.Web; +using System.Web.Http.ModelBinding; +using Umbraco.Core; using Umbraco.Core.Models.Validation; +using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Editors { internal class ValidationHelper { + internal static void ValidateEditorModelWithResolver(ModelStateDictionary modelState, object model) + { + var validationResult = EditorValidationResolver.Current.Validate(model); + foreach (var vr in validationResult + .WhereNotNull() + .Where(x => x.ErrorMessage.IsNullOrWhiteSpace() == false) + .Where(x => x.MemberNames.Any())) + { + foreach (var memberName in vr.MemberNames) + { + modelState.AddModelError(memberName, vr.ErrorMessage); + } + } + } + /// /// This will check if any properties of the model are attributed with the RequiredForPersistenceAttribute attribute and if they are it will /// check if that property validates, if it doesn't it means that the current model cannot be persisted because it doesn't have the necessary information diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index df304a0b79..c161e32ec6 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using System.Web; @@ -197,50 +198,22 @@ namespace Umbraco.Web #region GetCropUrl - /// - /// Gets the ImageProcessor Url of a media item by the crop alias (using default media item property alias of "umbracoFile") - /// - /// - /// - /// - /// + [Obsolete("Use the UrlHelper.GetCropUrl extension instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public static IHtmlString GetCropUrl(this HtmlHelper htmlHelper, IPublishedContent mediaItem, string cropAlias) { return new HtmlString(mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true)); } - /// - /// Gets the ImageProcessor Url of a media item by the property alias and crop alias. - /// - /// - /// - /// - /// - /// + [Obsolete("Use the UrlHelper.GetCropUrl extension instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public static IHtmlString GetCropUrl(this HtmlHelper htmlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias) { return new HtmlString(mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true)); } - /// - /// Gets the ImageProcessor Url of a media item - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + [Obsolete("Use the UrlHelper.GetCropUrl extension instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public static IHtmlString GetCropUrl(this HtmlHelper htmlHelper, IPublishedContent mediaItem, int? width = null, @@ -263,25 +236,8 @@ namespace Umbraco.Web upScale)); } - /// - /// Gets the ImageProcessor Url from the media path - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + [Obsolete("Use the UrlHelper.GetCropUrl extension instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public static IHtmlString GetCropUrl(this HtmlHelper htmlHelper, string imageUrl, int? width = null, diff --git a/src/Umbraco.Web/IHttpContextAccessor.cs b/src/Umbraco.Web/IHttpContextAccessor.cs new file mode 100644 index 0000000000..068783725a --- /dev/null +++ b/src/Umbraco.Web/IHttpContextAccessor.cs @@ -0,0 +1,15 @@ +using System.Web; + +namespace Umbraco.Web +{ + /// + /// Used to retrieve the HttpContext + /// + /// + /// NOTE: This has a singleton lifespan + /// + public interface IHttpContextAccessor + { + HttpContextBase Value { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/IUmbracoContextAccessor.cs b/src/Umbraco.Web/IUmbracoContextAccessor.cs index 997b25145b..8111606602 100644 --- a/src/Umbraco.Web/IUmbracoContextAccessor.cs +++ b/src/Umbraco.Web/IUmbracoContextAccessor.cs @@ -2,7 +2,10 @@ namespace Umbraco.Web { /// /// Used to retrieve the Umbraco context - /// + /// + /// + /// NOTE: This has a singleton lifespan + /// public interface IUmbracoContextAccessor { UmbracoContext Value { get; } diff --git a/src/Umbraco.Web/ImageCropperBaseExtensions.cs b/src/Umbraco.Web/ImageCropperBaseExtensions.cs index b870335c91..e993559f1e 100644 --- a/src/Umbraco.Web/ImageCropperBaseExtensions.cs +++ b/src/Umbraco.Web/ImageCropperBaseExtensions.cs @@ -1,38 +1,18 @@ -namespace Umbraco.Web +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Web.Models; + +namespace Umbraco.Web { - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Text; - - using Newtonsoft.Json; - - using Umbraco.Core; - using Umbraco.Core.Logging; - using Umbraco.Web.Models; - internal static class ImageCropperBaseExtensions { - - internal static ImageCropData GetImageCrop(this string json, string id) - { - var ic = new ImageCropData(); - if (json.DetectIsJson()) - { - try - { - var imageCropperSettings = JsonConvert.DeserializeObject>(json); - ic = imageCropperSettings.GetCrop(id); - } - catch (Exception ex) - { - LogHelper.Error(typeof(ImageCropperBaseExtensions), "Could not parse the json string: " + json, ex); - } - } - return ic; - } - internal static ImageCropDataSet SerializeToCropDataSet(this string json) { var imageCrops = new ImageCropDataSet(); @@ -40,7 +20,11 @@ { try { - imageCrops = JsonConvert.DeserializeObject(json); + imageCrops = JsonConvert.DeserializeObject(json, new JsonSerializerSettings + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }); } catch (Exception ex) { @@ -61,13 +45,14 @@ internal static ImageCropData GetCrop(this IEnumerable dataset, string cropAlias) { - if (dataset == null || !dataset.Any()) + var imageCropDatas = dataset.ToArray(); + if (dataset == null || imageCropDatas.Any() == false) return null; if (string.IsNullOrEmpty(cropAlias)) - return dataset.FirstOrDefault(); + return imageCropDatas.FirstOrDefault(); - return dataset.FirstOrDefault(x => x.Alias.ToLowerInvariant() == cropAlias.ToLowerInvariant()); + return imageCropDatas.FirstOrDefault(x => x.Alias.ToLowerInvariant() == cropAlias.ToLowerInvariant()); } internal static string GetCropBaseUrl(this ImageCropDataSet cropDataSet, string cropAlias, bool preferFocalPoint) diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index a76b39f187..bcdea6eb03 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -1,12 +1,13 @@ -namespace Umbraco.Web +using System; +using Newtonsoft.Json.Linq; +using System.Globalization; +using System.Text; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models; + +namespace Umbraco.Web { - using System.Globalization; - using System.Text; - - using Umbraco.Core; - using Umbraco.Core.Models; - using Umbraco.Web.Models; - /// /// Provides extension methods for getting ImageProcessor Url from the core Image Cropper property editor /// @@ -113,31 +114,43 @@ ImageCropRatioMode? ratioMode = null, bool upScale = true) { - string imageCropperValue = null; - - string mediaItemUrl; - - if (mediaItem.HasProperty(propertyAlias) && mediaItem.HasValue(propertyAlias)) - { - imageCropperValue = mediaItem.GetPropertyValue(propertyAlias); - - // get the raw value (this will be json) - var urlValue = mediaItem.GetPropertyValue(propertyAlias); - - mediaItemUrl = urlValue.DetectIsJson() - ? urlValue.SerializeToCropDataSet().Src - : urlValue; - } - else - { - mediaItemUrl = mediaItem.Url; - } + if (mediaItem == null) throw new ArgumentNullException("mediaItem"); var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; - return mediaItemUrl != null - ? GetCropUrl(mediaItemUrl, width, height, imageCropperValue, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale) - : string.Empty; + if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) + return string.Empty; + + //get the default obj from the value converter + var cropperValue = mediaItem.GetPropertyValue(propertyAlias); + + //is it strongly typed? + var stronglyTyped = cropperValue as ImageCropDataSet; + string mediaItemUrl; + if (stronglyTyped != null) + { + mediaItemUrl = stronglyTyped.Src; + return GetCropUrl( + mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + //this shouldn't be the case but we'll check + var jobj = cropperValue as JObject; + if (jobj != null) + { + stronglyTyped = jobj.ToObject(); + mediaItemUrl = stronglyTyped.Src; + return GetCropUrl( + mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + //it's a single string + mediaItemUrl = cropperValue.ToString(); + return GetCropUrl( + mediaItemUrl, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); } /// @@ -203,49 +216,73 @@ string furtherOptions = null, ImageCropRatioMode? ratioMode = null, bool upScale = true) + { + if (string.IsNullOrEmpty(imageUrl)) return string.Empty; + + ImageCropDataSet cropDataSet = null; + if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + { + cropDataSet = imageCropperValue.SerializeToCropDataSet(); + } + return GetCropUrl( + imageUrl, cropDataSet, width, height, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + public static string GetCropUrl( + this string imageUrl, + ImageCropDataSet cropDataSet, + int? width = null, + int? height = null, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) { if (string.IsNullOrEmpty(imageUrl) == false) { var imageProcessorUrl = new StringBuilder(); - if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) { - var cropDataSet = imageCropperValue.SerializeToCropDataSet(); - if (cropDataSet != null) + var crop = cropDataSet.GetCrop(cropAlias); + + imageProcessorUrl.Append(cropDataSet.Src); + + var cropBaseUrl = cropDataSet.GetCropBaseUrl(cropAlias, preferFocalPoint); + if (cropBaseUrl != null) { - var crop = cropDataSet.GetCrop(cropAlias); + imageProcessorUrl.Append(cropBaseUrl); + } + else + { + return null; + } - imageProcessorUrl.Append(cropDataSet.Src); + if (crop != null & useCropDimensions) + { + width = crop.Width; + height = crop.Height; + } - var cropBaseUrl = cropDataSet.GetCropBaseUrl(cropAlias, preferFocalPoint); - if (cropBaseUrl != null) - { - imageProcessorUrl.Append(cropBaseUrl); - } - else - { - return null; - } + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) + { + var heightRatio = (decimal)crop.Height / (decimal)crop.Width; + imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); + } - if (crop != null & useCropDimensions) - { - width = crop.Width; - height = crop.Height; - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) - { - var heightRatio = (decimal)crop.Height / (decimal)crop.Width; - imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) - { - var widthRatio = (decimal)crop.Width / (decimal)crop.Height; - imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); - } + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) + { + var widthRatio = (decimal)crop.Width / (decimal)crop.Height; + imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); } } else @@ -306,7 +343,7 @@ if (upScale == false) { - imageProcessorUrl.Append("&upscale=false"); + imageProcessorUrl.Append("&upscale=false"); } if (furtherOptions != null) diff --git a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs index 3e8c4abbaa..4657b67ad4 100644 --- a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs @@ -27,6 +27,20 @@ namespace Umbraco.Web.Install.InstallSteps public override InstallSetupResult Execute(object model) { + var ih = new InstallHelper(UmbracoContext.Current); + + //During a new install we'll log the default user in (which is id = 0). + // During an upgrade, the user will already need to be logged in in order to run the installer. + + var security = new WebSecurity(_httpContext, _applicationContext); + //we do this check here because for upgrades the user will already be logged in, for brand new installs, + // they will not be logged in, however we cannot check the current installation status because it will tell + // us that it is in 'upgrade' because we already have a database conn configured and a database. + if (security.IsAuthenticated() == false && GlobalSettings.ConfigurationStatus.IsNullOrWhiteSpace()) + { + security.PerformLogin(0); + } + //This is synonymous with library.RefreshContent() - but we don't want to use library // for anything anymore so welll use the method that it is wrapping. This will just make sure // the correct xml structure exists in the xml cache file. This is required by some upgrade scripts @@ -39,12 +53,8 @@ namespace Umbraco.Web.Install.InstallSteps // Update ClientDependency version var clientDependencyConfig = new ClientDependencyConfiguration(_applicationContext.ProfilingLogger.Logger); var clientDependencyUpdated = clientDependencyConfig.IncreaseVersionNumber(); - - var security = new WebSecurity(_httpContext, _applicationContext); - security.PerformLogin(0); - - //reports the ended install - var ih = new InstallHelper(UmbracoContext.Current); + + //reports the ended install ih.InstallStatus(true, ""); return null; diff --git a/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs b/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs index b4171add35..10d105883f 100644 --- a/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs +++ b/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs @@ -124,7 +124,7 @@ namespace Umbraco.Web.Macros var routeVals = new RouteData(); routeVals.Values.Add("controller", "PartialViewMacro"); routeVals.Values.Add("action", "Index"); - routeVals.DataTokens.Add("umbraco-context", umbCtx); //required for UmbracoViewPage + routeVals.DataTokens.Add(Umbraco.Core.Constants.Web.UmbracoContextDataToken, umbCtx); //required for UmbracoViewPage //lets render this controller as a child action var viewContext = new ViewContext {ViewData = new ViewDataDictionary()};; diff --git a/src/Umbraco.Web/Media/ImageUrl.cs b/src/Umbraco.Web/Media/ImageUrl.cs index 8f56d8bfa4..dff9358a38 100644 --- a/src/Umbraco.Web/Media/ImageUrl.cs +++ b/src/Umbraco.Web/Media/ImageUrl.cs @@ -87,7 +87,7 @@ namespace Umbraco.Web.Media private static object GetContentFromCache(int nodeIdInt, string field) { - var content = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var content = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( string.Format("{0}{1}_{2}", CacheKeys.ContentItemCacheKey, nodeIdInt.ToString(CultureInfo.InvariantCulture), field)); return content; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs index 9a138dd828..fb50fdec5c 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs @@ -10,32 +10,26 @@ using Umbraco.Core.Models.Validation; namespace Umbraco.Web.Models.ContentEditing { - [DataContract(Name = "contentType", Namespace = "")] - public class ContentTypeCompositionDisplay : ContentTypeBasic, INotificationModel - { - public ContentTypeCompositionDisplay() + public abstract class ContentTypeCompositionDisplay : ContentTypeBasic, INotificationModel + { + protected ContentTypeCompositionDisplay() { //initialize collections so at least their never null - Groups = new List(); AllowedContentTypes = new List(); CompositeContentTypes = new List(); Notifications = new List(); - } - - //name, alias, icon, thumb, desc, inherited from basic - + } + + //name, alias, icon, thumb, desc, inherited from basic + //List view [DataMember(Name = "isContainer")] public bool IsContainer { get; set; } [DataMember(Name = "listViewEditorName")] [ReadOnly(true)] - public string ListViewEditorName { get; set; } + public string ListViewEditorName { get; set; } - //Tabs - [DataMember(Name = "groups")] - public IEnumerable Groups { get; set; } - //Allowed child types [DataMember(Name = "allowedContentTypes")] public IEnumerable AllowedContentTypes { get; set; } @@ -43,7 +37,11 @@ namespace Umbraco.Web.Models.ContentEditing //Compositions [DataMember(Name = "compositeContentTypes")] public IEnumerable CompositeContentTypes { get; set; } - + + //Locked compositions + [DataMember(Name = "lockedCompositeContentTypes")] + public IEnumerable LockedCompositeContentTypes { get; set; } + [DataMember(Name = "allowAsRoot")] public bool AllowAsRoot { get; set; } @@ -68,4 +66,20 @@ namespace Umbraco.Web.Models.ContentEditing [ReadOnly(true)] public IDictionary Errors { get; set; } } + + [DataContract(Name = "contentType", Namespace = "")] + public abstract class ContentTypeCompositionDisplay : ContentTypeCompositionDisplay + where TPropertyTypeDisplay : PropertyTypeDisplay + { + protected ContentTypeCompositionDisplay() + { + //initialize collections so at least their never null + Groups = new List>(); + } + + //Tabs + [DataMember(Name = "groups")] + public IEnumerable> Groups { get; set; } + + } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs index 17eb6bb35e..62e1979076 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs @@ -6,13 +6,14 @@ using Umbraco.Core; namespace Umbraco.Web.Models.ContentEditing { + /// + /// Abstract model used to save content types + /// [DataContract(Name = "contentType", Namespace = "")] - public class ContentTypeSave : ContentTypeBasic, IValidatableObject + public abstract class ContentTypeSave : ContentTypeBasic, IValidatableObject { - public ContentTypeSave() + protected ContentTypeSave() { - //initialize collections so at least their never null - Groups = new List>(); AllowedContentTypes = new List(); CompositeContentTypes = new List(); } @@ -27,65 +28,75 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "allowAsRoot")] public bool AllowAsRoot { get; set; } - /// - /// The list of allowed templates to assign (template alias) - /// - [DataMember(Name = "allowedTemplates")] - public IEnumerable AllowedTemplates { get; set; } - //Allowed child types [DataMember(Name = "allowedContentTypes")] public IEnumerable AllowedContentTypes { get; set; } - /// - /// The default template to assign (template alias) - /// - [DataMember(Name = "defaultTemplate")] - public string DefaultTemplate { get; set; } - - //Tabs - [DataMember(Name = "groups")] - public IEnumerable> Groups { get; set; } - /// /// Custom validation /// /// /// - public IEnumerable Validate(ValidationContext validationContext) + public virtual IEnumerable Validate(ValidationContext validationContext) { - if (AllowedTemplates.Any(x => x.IsNullOrWhiteSpace())) - yield return new ValidationResult("Template value cannot be null", new[] {"AllowedTemplates"}); - if (CompositeContentTypes.Any(x => x.IsNullOrWhiteSpace())) - yield return new ValidationResult("Composite Content Type value cannot be null", new[] { "CompositeContentTypes" }); + yield return new ValidationResult("Composite Content Type value cannot be null", new[] {"CompositeContentTypes"}); + } + } + + /// + /// Abstract model used to save content types + /// + /// + [DataContract(Name = "contentType", Namespace = "")] + public abstract class ContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + { + protected ContentTypeSave() + { + Groups = new List>(); + } + + //Tabs + [DataMember(Name = "groups")] + public IEnumerable> Groups { get; set; } + + /// + /// Custom validation + /// + /// + /// + public override IEnumerable Validate(ValidationContext validationContext) + { + foreach (var validationResult in base.Validate(validationContext)) + { + yield return validationResult; + } var duplicateGroups = Groups.GroupBy(x => x.Name).Where(x => x.Count() > 1).ToArray(); if (duplicateGroups.Any()) { //we need to return the field name with an index so it's wired up correctly - var firstIndex = Groups.IndexOf(duplicateGroups.First().First()); + var lastIndex = Groups.IndexOf(duplicateGroups.Last().Last()); yield return new ValidationResult("Duplicate group names not allowed", new[] { - string.Format("Groups[{0}].Name", firstIndex) + string.Format("Groups[{0}].Name", lastIndex) }); } - + var duplicateProperties = Groups.SelectMany(x => x.Properties).Where(x => x.Inherited == false).GroupBy(x => x.Alias).Where(x => x.Count() > 1).ToArray(); if (duplicateProperties.Any()) { //we need to return the field name with an index so it's wired up correctly - var firstProperty = duplicateProperties.First().First(); - var propertyGroup = Groups.Single(x => x.Properties.Contains(firstProperty)); - var groupIndex = Groups.IndexOf(propertyGroup); - var propertyIndex = propertyGroup.Properties.IndexOf(firstProperty); + var lastProperty = duplicateProperties.Last().Last(); + var propertyGroup = Groups.Single(x => x.Properties.Contains(lastProperty)); - yield return new ValidationResult("Duplicate property aliases not allowed", new[] + yield return new ValidationResult("Duplicate property aliases not allowed: " + lastProperty.Alias, new[] { - string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propertyIndex) + string.Format("Groups[{0}].Properties[{1}].Alias", propertyGroup.SortOrder, lastProperty.SortOrder) }); } - + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs similarity index 70% rename from src/Umbraco.Web/Models/ContentEditing/ContentTypeDisplay.cs rename to src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs index e0165450ed..65641e7f63 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs @@ -1,18 +1,12 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; +using System.Collections.Generic; using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; namespace Umbraco.Web.Models.ContentEditing { - [DataContract(Name = "contentType", Namespace = "")] - public class ContentTypeDisplay : ContentTypeCompositionDisplay - { - public ContentTypeDisplay() + public class DocumentTypeDisplay : ContentTypeCompositionDisplay + { + public DocumentTypeDisplay() { //initialize collections so at least their never null AllowedTemplates = new List(); @@ -26,6 +20,6 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "defaultTemplate")] public EntityBasic DefaultTemplate { get; set; } - + } } diff --git a/src/Umbraco.Web/Models/ContentEditing/DocumentTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeSave.cs new file mode 100644 index 0000000000..cdf20dba8d --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeSave.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using Umbraco.Core; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Model used to save a document type + /// + [DataContract(Name = "contentType", Namespace = "")] + public class DocumentTypeSave : ContentTypeSave + { + /// + /// The list of allowed templates to assign (template alias) + /// + [DataMember(Name = "allowedTemplates")] + public IEnumerable AllowedTemplates { get; set; } + + /// + /// The default template to assign (template alias) + /// + [DataMember(Name = "defaultTemplate")] + public string DefaultTemplate { get; set; } + + /// + /// Custom validation + /// + /// + /// + public override IEnumerable Validate(ValidationContext validationContext) + { + if (AllowedTemplates.Any(x => StringExtensions.IsNullOrWhiteSpace(x))) + yield return new ValidationResult("Template value cannot be null", new[] { "AllowedTemplates" }); + + foreach (var v in base.Validate(validationContext)) + { + yield return v; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs new file mode 100644 index 0000000000..d9bc169c8f --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Web.Models.ContentEditing +{ + public class GetAvailableCompositionsFilter + { + public int ContentTypeId { get; set; } + public string[] FilterPropertyTypes { get; set; } + public string[] FilterContentTypes { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MediaTypeDisplay.cs new file mode 100644 index 0000000000..7063613d66 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MediaTypeDisplay.cs @@ -0,0 +1,10 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "contentType", Namespace = "")] + public class MediaTypeDisplay : ContentTypeCompositionDisplay + { + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/MediaTypeSave.cs new file mode 100644 index 0000000000..e20eb23125 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MediaTypeSave.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Model used to save a media type + /// + [DataContract(Name = "contentType", Namespace = "")] + public class MediaTypeSave : ContentTypeSave + { + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeBasic.cs new file mode 100644 index 0000000000..dfe98461dd --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeBasic.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Basic member property type + /// + [DataContract(Name = "contentType", Namespace = "")] + public class MemberPropertyTypeBasic : PropertyTypeBasic + { + [DataMember(Name = "showOnMemberProfile")] + public bool MemberCanViewProperty { get; set; } + + [DataMember(Name = "memberCanEdit")] + public bool MemberCanEditProperty { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeDisplay.cs new file mode 100644 index 0000000000..8f7d4be310 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeDisplay.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "propertyType")] + public class MemberPropertyTypeDisplay : PropertyTypeDisplay + { + [DataMember(Name = "showOnMemberProfile")] + public bool MemberCanViewProperty { get; set; } + + [DataMember(Name = "memberCanEdit")] + public bool MemberCanEditProperty { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MemberTypeDisplay.cs new file mode 100644 index 0000000000..e6092a10f4 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MemberTypeDisplay.cs @@ -0,0 +1,10 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "contentType", Namespace = "")] + public class MemberTypeDisplay : ContentTypeCompositionDisplay + { + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/MemberTypeSave.cs new file mode 100644 index 0000000000..98bfbf0fce --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MemberTypeSave.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Model used to save a member type + /// + public class MemberTypeSave : ContentTypeSave + { + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyGroupBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupBasic.cs index b8560ba342..6bfe1dc76e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyGroupBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupBasic.cs @@ -5,14 +5,8 @@ using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing { [DataContract(Name = "propertyGroup", Namespace = "")] - public class PropertyGroupBasic - where TPropertyType: PropertyTypeBasic + public abstract class PropertyGroupBasic { - public PropertyGroupBasic() - { - Properties = new List(); - } - /// /// Gets the special generic properties tab identifier. /// @@ -38,9 +32,6 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "id")] public int Id { get; set; } - [DataMember(Name = "properties")] - public IEnumerable Properties { get; set; } - [DataMember(Name = "sortOrder")] public int SortOrder { get; set; } @@ -48,4 +39,21 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "name")] public string Name { get; set; } } + + [DataContract(Name = "propertyGroup", Namespace = "")] + public class PropertyGroupBasic : PropertyGroupBasic + where TPropertyType: PropertyTypeBasic + { + public PropertyGroupBasic() + { + Properties = new List(); + } + + + + [DataMember(Name = "properties")] + public IEnumerable Properties { get; set; } + + + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyGroupDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupDisplay.cs index 1bae9c96d5..c31c93b206 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyGroupDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupDisplay.cs @@ -5,11 +5,12 @@ using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing { [DataContract(Name = "propertyGroup", Namespace = "")] - public class PropertyGroupDisplay : PropertyGroupBasic + public class PropertyGroupDisplay : PropertyGroupBasic + where TPropertyTypeDisplay : PropertyTypeDisplay { public PropertyGroupDisplay() { - Properties = new List(); + Properties = new List(); ParentTabContentTypeNames = new List(); ParentTabContentTypes = new List(); } diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeDisplay.cs index a05924ad5c..c76f4b3a9d 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeDisplay.cs @@ -1,16 +1,12 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; namespace Umbraco.Web.Models.ContentEditing { [DataContract(Name = "propertyType")] public class PropertyTypeDisplay : PropertyTypeBasic - { + { [DataMember(Name = "editor")] [ReadOnly(true)] public string Editor { get; set; } @@ -23,14 +19,22 @@ namespace Umbraco.Web.Models.ContentEditing [ReadOnly(true)] public IDictionary Config { get; set; } - //SD: Seems strange that this is needed + /// + /// Gets a value indicating whether this property should be locked when editing. + /// + /// This is used for built in properties like the default MemberType + /// properties that should not be editable from the backoffice. + [DataMember(Name = "locked")] + [ReadOnly(true)] + public bool Locked { get; set; } + + /// + /// This is required for the UI editor to know if this particular property belongs to + /// an inherited item or the current item. + /// [DataMember(Name = "contentTypeId")] [ReadOnly(true)] public int ContentTypeId { get; set; } - - //SD: Seems strange that this is needed - [DataMember(Name = "contentTypeName")] - [ReadOnly(true)] - public string ContentTypeName { get; set; } + } } diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs index 37fe9151b0..0d280c4c49 100644 --- a/src/Umbraco.Web/Models/ContentExtensions.cs +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -78,16 +78,14 @@ namespace Umbraco.Web.Models domain = pos == 0 ? null : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; - } + } - if (domain == null || domain.LanguageIsoCode.IsNullOrWhiteSpace()) - return GetDefaultCulture(localizationService); + var rootContentId = domain == null ? -1 : domain.RootContentId; + var wcDomain = DomainHelper.FindWildcardDomainInPath(domainService.GetAll(true), contentPath, rootContentId); - var wcDomain = DomainHelper.FindWildcardDomainInPath(domainService.GetAll(true), contentPath, domain.RootContentId); - - return wcDomain == null - ? new CultureInfo(domain.LanguageIsoCode) - : new CultureInfo(wcDomain.LanguageIsoCode); + if (wcDomain != null) return new CultureInfo(wcDomain.LanguageIsoCode); + if (domain != null) return new CultureInfo(domain.LanguageIsoCode); + return GetDefaultCulture(localizationService); } private static CultureInfo GetDefaultCulture(ILocalizationService localizationService) diff --git a/src/Umbraco.Web/Models/ImageCropCoordinates.cs b/src/Umbraco.Web/Models/ImageCropCoordinates.cs index 5152ba5b42..4a480b4beb 100644 --- a/src/Umbraco.Web/Models/ImageCropCoordinates.cs +++ b/src/Umbraco.Web/Models/ImageCropCoordinates.cs @@ -1,9 +1,11 @@ +using System; using System.Runtime.Serialization; +using Umbraco.Core.Dynamics; namespace Umbraco.Web.Models { [DataContract(Name = "imageCropCoordinates")] - public class ImageCropCoordinates + public class ImageCropCoordinates : CaseInsensitiveDynamicObject, IEquatable { [DataMember(Name = "x1")] public decimal X1 { get; set; } @@ -16,5 +18,62 @@ namespace Umbraco.Web.Models [DataMember(Name = "y2")] public decimal Y2 { get; set; } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(ImageCropCoordinates other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return X1 == other.X1 && Y1 == other.Y1 && X2 == other.X2 && Y2 == other.Y2; + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + 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((ImageCropCoordinates) obj); + } + + /// + /// Serves as the default hash function. + /// + /// + /// A hash code for the current object. + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = X1.GetHashCode(); + hashCode = (hashCode*397) ^ Y1.GetHashCode(); + hashCode = (hashCode*397) ^ X2.GetHashCode(); + hashCode = (hashCode*397) ^ Y2.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(ImageCropCoordinates left, ImageCropCoordinates right) + { + return Equals(left, right); + } + + public static bool operator !=(ImageCropCoordinates left, ImageCropCoordinates right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ImageCropData.cs b/src/Umbraco.Web/Models/ImageCropData.cs index f96e209906..fb1b3c7424 100644 --- a/src/Umbraco.Web/Models/ImageCropData.cs +++ b/src/Umbraco.Web/Models/ImageCropData.cs @@ -1,17 +1,16 @@ -using System.Runtime.Serialization; +using System; +using System.Runtime.Serialization; using System.Threading.Tasks; +using Umbraco.Core.Dynamics; namespace Umbraco.Web.Models { [DataContract(Name = "imageCropData")] - public class ImageCropData + public class ImageCropData : CaseInsensitiveDynamicObject, IEquatable { [DataMember(Name = "alias")] public string Alias { get; set; } - - //[DataMember(Name = "name")] - //public string Name { get; set; } - + [DataMember(Name = "width")] public int Width { get; set; } @@ -20,6 +19,63 @@ namespace Umbraco.Web.Models [DataMember(Name = "coordinates")] public ImageCropCoordinates Coordinates { get; set; } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(ImageCropData other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return string.Equals(Alias, other.Alias) && Width == other.Width && Height == other.Height && Equals(Coordinates, other.Coordinates); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + 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((ImageCropData) obj); + } + + /// + /// Serves as the default hash function. + /// + /// + /// A hash code for the current object. + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = (Alias != null ? Alias.GetHashCode() : 0); + hashCode = (hashCode*397) ^ Width; + hashCode = (hashCode*397) ^ Height; + hashCode = (hashCode*397) ^ (Coordinates != null ? Coordinates.GetHashCode() : 0); + return hashCode; + } + } + + public static bool operator ==(ImageCropData left, ImageCropData right) + { + return Equals(left, right); + } + + public static bool operator !=(ImageCropData left, ImageCropData right) + { + return !Equals(left, right); + } } } diff --git a/src/Umbraco.Web/Models/ImageCropDataSet.cs b/src/Umbraco.Web/Models/ImageCropDataSet.cs index b3b584682a..4c5a68a6b9 100644 --- a/src/Umbraco.Web/Models/ImageCropDataSet.cs +++ b/src/Umbraco.Web/Models/ImageCropDataSet.cs @@ -1,15 +1,27 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Dynamic; using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Text; using System.Web; +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.Dynamics; +using Umbraco.Core.Serialization; +using Umbraco.Web.PropertyEditors.ValueConverters; namespace Umbraco.Web.Models { + [JsonConverter(typeof(NoTypeConverterJsonConverter))] + [TypeConverter(typeof(ImageCropDataSetConverter))] [DataContract(Name="imageCropDataSet")] - public class ImageCropDataSet : IHtmlString - { + public class ImageCropDataSet : CaseInsensitiveDynamicObject, IHtmlString, IEquatable + { + [DataMember(Name="src")] public string Src { get; set;} @@ -19,7 +31,6 @@ namespace Umbraco.Web.Models [DataMember(Name = "crops")] public IEnumerable Crops { get; set; } - public string GetCropUrl(string alias, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) { @@ -55,7 +66,7 @@ namespace Umbraco.Web.Models public bool HasFocalPoint() { - return FocalPoint != null && FocalPoint.Top != 0.5m && FocalPoint.Top != 0.5m; + return FocalPoint != null && FocalPoint.Left != 0.5m && FocalPoint.Top != 0.5m; } public bool HasCrop(string alias) @@ -72,5 +83,74 @@ namespace Umbraco.Web.Models { return this.Src; } + + /// + /// Returns a string that represents the current object. + /// + /// + /// If there are crops defined, it will return the JSON value, otherwise it will just return the Src value + /// + public override string ToString() + { + return Crops.Any() ? JsonConvert.SerializeObject(this) : Src; + } + + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(ImageCropDataSet other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return string.Equals(Src, other.Src) && Equals(FocalPoint, other.FocalPoint) + && Crops.SequenceEqual(other.Crops); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + 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((ImageCropDataSet) obj); + } + + /// + /// Serves as the default hash function. + /// + /// + /// A hash code for the current object. + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = (Src != null ? Src.GetHashCode() : 0); + hashCode = (hashCode*397) ^ (FocalPoint != null ? FocalPoint.GetHashCode() : 0); + hashCode = (hashCode*397) ^ (Crops != null ? Crops.GetHashCode() : 0); + return hashCode; + } + } + + public static bool operator ==(ImageCropDataSet left, ImageCropDataSet right) + { + return Equals(left, right); + } + + public static bool operator !=(ImageCropDataSet left, ImageCropDataSet right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ImageCropFocalPoint.cs b/src/Umbraco.Web/Models/ImageCropFocalPoint.cs index e6f12ea81f..e47bab5b7a 100644 --- a/src/Umbraco.Web/Models/ImageCropFocalPoint.cs +++ b/src/Umbraco.Web/Models/ImageCropFocalPoint.cs @@ -1,14 +1,69 @@ +using System; using System.Runtime.Serialization; +using Umbraco.Core.Dynamics; namespace Umbraco.Web.Models { [DataContract(Name = "imageCropFocalPoint")] - public class ImageCropFocalPoint{ - + public class ImageCropFocalPoint : CaseInsensitiveDynamicObject, IEquatable + { [DataMember(Name = "left")] public decimal Left { get; set; } [DataMember(Name = "top")] public decimal Top { get; set; } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(ImageCropFocalPoint other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Left == other.Left && Top == other.Top; + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + 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((ImageCropFocalPoint) obj); + } + + /// + /// Serves as the default hash function. + /// + /// + /// A hash code for the current object. + /// + public override int GetHashCode() + { + unchecked + { + return (Left.GetHashCode()*397) ^ Top.GetHashCode(); + } + } + + public static bool operator ==(ImageCropFocalPoint left, ImageCropFocalPoint right) + { + return Equals(left, right); + } + + public static bool operator !=(ImageCropFocalPoint left, ImageCropFocalPoint right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs b/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs index fd9d1f388f..624c641d3b 100644 --- a/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using AutoMapper; using Umbraco.Core.Models; diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index 4bdafb029f..a480fd24be 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -6,6 +6,7 @@ using System.Web; using System.Web.Mvc; using System.Web.Routing; using AutoMapper; +using umbraco; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; @@ -14,6 +15,7 @@ using Umbraco.Web.LegacyActions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Trees; using Umbraco.Web.Routing; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.Models.Mapping { @@ -65,7 +67,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(display => display.Notifications, expression => expression.Ignore()) .ForMember(display => display.Errors, expression => expression.Ignore()) .ForMember(display => display.Alias, expression => expression.Ignore()) - .ForMember(display => display.Tabs, expression => expression.ResolveUsing()) + .ForMember(display => display.Tabs, expression => expression.ResolveUsing(new TabsAndPropertiesResolver(applicationContext.Services.TextService))) .ForMember(display => display.AllowedActions, expression => expression.ResolveUsing( new ActionButtonsResolver(new Lazy(() => applicationContext.Services.UserService)))) .AfterMap((media, display) => AfterMap(media, display, applicationContext.Services.DataTypeService, applicationContext.Services.TextService, @@ -159,11 +161,18 @@ namespace Umbraco.Web.Models.Mapping if (content.ContentType.IsContainer) { - TabsAndPropertiesResolver.AddListView(display, "content", dataTypeService); + TabsAndPropertiesResolver.AddListView(display, "content", dataTypeService, localizedText); } - + var properties = new List { + new ContentPropertyDisplay + { + Alias = string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("content/documentType"), + Value = localizedText.UmbracoDictionaryTranslate(display.ContentTypeName), + View = PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + }, new ContentPropertyDisplay { Alias = string.Format("{0}releasedate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), @@ -181,7 +190,7 @@ namespace Umbraco.Web.Models.Mapping new ContentPropertyDisplay { Alias = string.Format("{0}template", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = "Template", //TODO: localize this? + Label = localizedText.Localize("template/template"), Value = display.TemplateAlias, View = "dropdown", //TODO: Hard coding until we make a real dropdown property editor to lookup Config = new Dictionary @@ -198,7 +207,7 @@ namespace Umbraco.Web.Models.Mapping } }; - TabsAndPropertiesResolver.MapGenericProperties(content, display, properties.ToArray(), + TabsAndPropertiesResolver.MapGenericProperties(content, display, localizedText, properties.ToArray(), genericProperties => { //TODO: This would be much nicer with the IUmbracoContextAccessor so we don't use singletons diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs index 675ea185ef..06395ced73 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs @@ -9,6 +9,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Web.Models.ContentEditing; using System.Collections.Generic; using AutoMapper.Internal; +using Umbraco.Core.Services; namespace Umbraco.Web.Models.Mapping { @@ -28,7 +29,7 @@ namespace Umbraco.Web.Models.Mapping //ctor can be used for testing public ContentTypeModelMapper(Lazy propertyEditorResolver) { - _propertyEditorResolver = propertyEditorResolver; + _propertyEditorResolver = propertyEditorResolver; } public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) @@ -48,9 +49,9 @@ namespace Umbraco.Web.Models.Mapping .ForMember(type => type.UpdateDate, expression => expression.Ignore()) .ForMember(type => type.HasIdentity, expression => expression.Ignore()); - config.CreateMap() + config.CreateMap() //do the base mapping - .MapBaseContentTypeSaveToEntity(applicationContext) + .MapBaseContentTypeSaveToEntity(applicationContext) .ConstructUsing((source) => new ContentType(source.ParentId)) .ForMember(source => source.AllowedTemplates, expression => expression.Ignore()) .ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore()) @@ -67,33 +68,59 @@ namespace Umbraco.Web.Models.Mapping ContentTypeModelMapperExtensions.AfterMapContentTypeSaveToEntity(source, dest, applicationContext); }); - config.CreateMap() + config.CreateMap() //do the base mapping - .MapBaseContentTypeSaveToEntity(applicationContext) + .MapBaseContentTypeSaveToEntity(applicationContext) .ConstructUsing((source) => new MediaType(source.ParentId)) .AfterMap((source, dest) => { - ContentTypeModelMapperExtensions.AfterMapContentTypeSaveToEntity(source, dest, applicationContext); + ContentTypeModelMapperExtensions.AfterMapMediaTypeSaveToEntity(source, dest, applicationContext); }); - config.CreateMap() + config.CreateMap() //do the base mapping - .MapBaseContentTypeSaveToEntity(applicationContext) + .MapBaseContentTypeSaveToEntity(applicationContext) .ConstructUsing((source) => new MemberType(source.ParentId)) .AfterMap((source, dest) => { ContentTypeModelMapperExtensions.AfterMapContentTypeSaveToEntity(source, dest, applicationContext); + + //map the MemberCanEditProperty,MemberCanViewProperty + foreach (var propertyType in source.Groups.SelectMany(x => x.Properties)) + { + var localCopy = propertyType; + var destProp = dest.PropertyTypes.SingleOrDefault(x => x.Alias.InvariantEquals(localCopy.Alias)); + if (destProp != null) + { + dest.SetMemberCanEditProperty(localCopy.Alias, localCopy.MemberCanEditProperty); + dest.SetMemberCanViewProperty(localCopy.Alias, localCopy.MemberCanViewProperty); + } + } }); config.CreateMap().ConvertUsing(x => x.Alias); - config.CreateMap() + config.CreateMap() //map base logic - .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver); + .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver) + .AfterMap((memberType, display) => + { + //map the MemberCanEditProperty,MemberCanViewProperty + foreach (var propertyType in memberType.PropertyTypes) + { + var localCopy = propertyType; + var displayProp = display.Groups.SelectMany(x => x.Properties).SingleOrDefault(x => x.Alias.InvariantEquals(localCopy.Alias)); + if (displayProp != null) + { + displayProp.MemberCanEditProperty = memberType.MemberCanEditProperty(localCopy.Alias); + displayProp.MemberCanViewProperty = memberType.MemberCanViewProperty(localCopy.Alias); + } + } + }); - config.CreateMap() + config.CreateMap() //map base logic - .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver) + .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver) .AfterMap((source, dest) => { //default listview @@ -107,9 +134,9 @@ namespace Umbraco.Web.Models.Mapping } }); - config.CreateMap() + config.CreateMap() //map base logic - .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver) + .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver) .ForMember(dto => dto.AllowedTemplates, expression => expression.Ignore()) .ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore()) .ForMember(display => display.Notifications, expression => expression.Ignore()) @@ -124,9 +151,9 @@ namespace Umbraco.Web.Models.Mapping //default listview dest.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Content"; - if (string.IsNullOrEmpty(source.Name) == false) + if (string.IsNullOrEmpty(source.Alias) == false) { - var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name; + var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Alias; if (applicationContext.Services.DataTypeService.GetDataTypeDefinitionByName(name) != null) dest.ListViewEditorName = name; } @@ -139,7 +166,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() - .ConstructUsing((PropertyTypeBasic propertyTypeBasic) => + .ConstructUsing(propertyTypeBasic => { var dataType = applicationContext.Services.DataTypeService.GetDataTypeDefinitionById(propertyTypeBasic.DataTypeId); if (dataType == null) throw new NullReferenceException("No data type found with id " + propertyTypeBasic.DataTypeId); @@ -168,11 +195,14 @@ namespace Umbraco.Web.Models.Mapping #region *** Used for mapping on top of an existing display object from a save object *** - config.CreateMap() - .MapBaseContentTypeSaveToDisplay(); + config.CreateMap() + .MapBaseContentTypeSaveToDisplay(); + + config.CreateMap() + .MapBaseContentTypeSaveToDisplay(); - config.CreateMap() - .MapBaseContentTypeSaveToDisplay() + config.CreateMap() + .MapBaseContentTypeSaveToDisplay() .ForMember(dto => dto.AllowedTemplates, expression => expression.Ignore()) .ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore()) .AfterMap((source, dest) => @@ -183,7 +213,10 @@ namespace Umbraco.Web.Models.Mapping if (destAllowedTemplateAliases.SequenceEqual(source.AllowedTemplates) == false) { var templates = applicationContext.Services.FileService.GetTemplates(source.AllowedTemplates.ToArray()); - dest.AllowedTemplates = source.AllowedTemplates.Select(x => Mapper.Map(templates.Single(t => t.Alias == x))).ToArray(); + dest.AllowedTemplates = source.AllowedTemplates + .Select(x => Mapper.Map(templates.SingleOrDefault(t => t.Alias == x))) + .WhereNotNull() + .ToArray(); } if (source.DefaultTemplate.IsNullOrWhiteSpace() == false) @@ -201,40 +234,41 @@ namespace Umbraco.Web.Models.Mapping } }); + //for doc types, media types config.CreateMap, PropertyGroup>() - .ForMember(dest => dest.Id, map => map.Condition(source => source.Id > 0)) - .ForMember(dest => dest.Key, map => map.Ignore()) - .ForMember(dest => dest.HasIdentity, map => map.Ignore()) - .ForMember(dest => dest.CreateDate, map => map.Ignore()) - .ForMember(dest => dest.UpdateDate, map => map.Ignore()) - // fixme - // this is basically *replacing* dest properties by a mapped version of - // *every* source properties (including, I guess, inherited properties?) - // also, ContentTypeModelMapperExtensions will map properties *again* so - // this makes little sense - ignore for now - .ForMember(dest => dest.PropertyTypes, map => map.Ignore()); - //.ForMember(dest => dest.PropertyTypes, map => map.MapFrom(source => - // source.Properties.Select(Mapper.Map))); + .MapPropertyGroupBasicToPropertyGroupPersistence, PropertyTypeBasic>(); - config.CreateMap, PropertyGroupDisplay>() - .ForMember(dest => dest.Id, expression => expression.Condition(source => source.Id > 0)) - .ForMember(g => g.ContentTypeId, expression => expression.Ignore()) - .ForMember(g => g.ParentTabContentTypes, expression => expression.Ignore()) - .ForMember(g => g.ParentTabContentTypeNames, expression => expression.Ignore()) - .ForMember(g => g.Properties, expression => expression.MapFrom(display => display.Properties.Select(Mapper.Map))); + //for members + config.CreateMap, PropertyGroup>() + .MapPropertyGroupBasicToPropertyGroupPersistence, MemberPropertyTypeBasic>(); + + //for doc types, media types + config.CreateMap, PropertyGroupDisplay>() + .MapPropertyGroupBasicToPropertyGroupDisplay, PropertyTypeBasic, PropertyTypeDisplay>(); + + //for members + config.CreateMap, PropertyGroupDisplay>() + .MapPropertyGroupBasicToPropertyGroupDisplay, MemberPropertyTypeBasic, MemberPropertyTypeDisplay>(); config.CreateMap() .ForMember(g => g.Editor, expression => expression.Ignore()) .ForMember(g => g.View, expression => expression.Ignore()) .ForMember(g => g.Config, expression => expression.Ignore()) .ForMember(g => g.ContentTypeId, expression => expression.Ignore()) - .ForMember(g => g.ContentTypeName, expression => expression.Ignore()); + .ForMember(g => g.Locked, exp => exp.Ignore()); + + config.CreateMap() + .ForMember(g => g.Editor, expression => expression.Ignore()) + .ForMember(g => g.View, expression => expression.Ignore()) + .ForMember(g => g.Config, expression => expression.Ignore()) + .ForMember(g => g.ContentTypeId, expression => expression.Ignore()) + .ForMember(g => g.Locked, exp => exp.Ignore()); #endregion - - + + } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs index e59a474afb..52f2dbad4b 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs @@ -20,6 +20,34 @@ namespace Umbraco.Web.Models.Mapping internal static class ContentTypeModelMapperExtensions { + public static IMappingExpression MapPropertyGroupBasicToPropertyGroupPersistence( + this IMappingExpression mapping) + where TSource : PropertyGroupBasic + where TPropertyTypeBasic : PropertyTypeBasic + { + return mapping + .ForMember(dest => dest.Id, map => map.Condition(source => source.Id > 0)) + .ForMember(dest => dest.Key, map => map.Ignore()) + .ForMember(dest => dest.HasIdentity, map => map.Ignore()) + .ForMember(dest => dest.CreateDate, map => map.Ignore()) + .ForMember(dest => dest.UpdateDate, map => map.Ignore()) + .ForMember(dest => dest.PropertyTypes, map => map.Ignore()); + } + + public static IMappingExpression> MapPropertyGroupBasicToPropertyGroupDisplay( + this IMappingExpression> mapping) + where TSource : PropertyGroupBasic + where TPropertyTypeBasic : PropertyTypeBasic + where TPropertyTypeDisplay : PropertyTypeDisplay + { + return mapping + .ForMember(dest => dest.Id, expression => expression.Condition(source => source.Id > 0)) + .ForMember(g => g.ContentTypeId, expression => expression.Ignore()) + .ForMember(g => g.ParentTabContentTypes, expression => expression.Ignore()) + .ForMember(g => g.ParentTabContentTypeNames, expression => expression.Ignore()) + .ForMember(g => g.Properties, expression => expression.MapFrom(display => display.Properties.Select(Mapper.Map))); + } + public static void AfterMapContentTypeSaveToEntity( TSource source, TDestination dest, ApplicationContext applicationContext) @@ -47,23 +75,55 @@ namespace Umbraco.Web.Models.Mapping } } - public static IMappingExpression MapBaseContentTypeSaveToDisplay( + public static void AfterMapMediaTypeSaveToEntity( + TSource source, TDestination dest, + ApplicationContext applicationContext) + where TSource : MediaTypeSave + where TDestination : IContentTypeComposition + { + //sync compositions + var current = dest.CompositionAliases().ToArray(); + var proposed = source.CompositeContentTypes; + + var remove = current.Where(x => proposed.Contains(x) == false); + var add = proposed.Where(x => current.Contains(x) == false); + + foreach (var rem in remove) + { + dest.RemoveContentType(rem); + } + + foreach (var a in add) + { + //TODO: Remove N+1 lookup + var addCt = applicationContext.Services.ContentTypeService.GetMediaType(a); + if (addCt != null) + dest.AddContentType(addCt); + } + } + + public static IMappingExpression MapBaseContentTypeSaveToDisplay( this IMappingExpression mapping) - where TSource : ContentTypeSave - where TDestination : ContentTypeCompositionDisplay + where TSource : ContentTypeSave + where TDestination : ContentTypeCompositionDisplay + where TPropertyTypeDestination : PropertyTypeDisplay + where TPropertyTypeSource : PropertyTypeBasic { return mapping .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) - .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) + .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) .ForMember(dto => dto.ListViewEditorName, expression => expression.Ignore()) .ForMember(dto => dto.Notifications, expression => expression.Ignore()) - .ForMember(dto => dto.Errors, expression => expression.Ignore()); + .ForMember(dto => dto.Errors, expression => expression.Ignore()) + .ForMember(dto => dto.LockedCompositeContentTypes, exp => exp.Ignore()) + .ForMember(dto => dto.Groups, expression => expression.ResolveUsing(new PropertyGroupDisplayResolver())); } - public static IMappingExpression MapBaseContentTypeEntityToDisplay( + public static IMappingExpression MapBaseContentTypeEntityToDisplay( this IMappingExpression mapping, ApplicationContext applicationContext, Lazy propertyEditorResolver) where TSource : IContentTypeComposition - where TDestination : ContentTypeCompositionDisplay + where TDestination : ContentTypeCompositionDisplay + where TPropertyTypeDisplay : PropertyTypeDisplay, new() { return mapping .ForMember(display => display.Notifications, expression => expression.Ignore()) @@ -81,9 +141,13 @@ namespace Umbraco.Web.Models.Mapping dto => dto.CompositeContentTypes, expression => expression.MapFrom(dto => dto.ContentTypeComposition)) + .ForMember( + dto => dto.LockedCompositeContentTypes, + expression => expression.ResolveUsing(new LockedCompositionsResolver(applicationContext))) + .ForMember( dto => dto.Groups, - expression => expression.ResolveUsing(new PropertyTypeGroupResolver(applicationContext, propertyEditorResolver))); + expression => expression.ResolveUsing(new PropertyTypeGroupResolver(applicationContext, propertyEditorResolver))); } /// @@ -91,14 +155,16 @@ namespace Umbraco.Web.Models.Mapping /// /// /// + /// /// /// /// - public static IMappingExpression MapBaseContentTypeSaveToEntity( + public static IMappingExpression MapBaseContentTypeSaveToEntity( this IMappingExpression mapping, ApplicationContext applicationContext) //where TSource : ContentTypeCompositionDisplay - where TSource : ContentTypeSave - where TDestination : IContentTypeComposition + where TSource : ContentTypeSave + where TDestination : IContentTypeComposition + where TSourcePropertyType : PropertyTypeBasic { return mapping //only map id if set to something higher then zero @@ -195,7 +261,8 @@ namespace Umbraco.Web.Models.Mapping }); } - private static PropertyGroup MapSaveGroup(PropertyGroupBasic sourceGroup, IEnumerable destOrigGroups) + private static PropertyGroup MapSaveGroup(PropertyGroupBasic sourceGroup, IEnumerable destOrigGroups) + where TPropertyType: PropertyTypeBasic { PropertyGroup destGroup; if (sourceGroup.Id > 0) diff --git a/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs new file mode 100644 index 0000000000..d3d692d10d --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs @@ -0,0 +1,43 @@ +using AutoMapper; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models.Mapping +{ + internal class LockedCompositionsResolver : ValueResolver> + { + private readonly ApplicationContext _applicationContext; + + public LockedCompositionsResolver(ApplicationContext applicationContext) + { + _applicationContext = applicationContext; + } + + protected override IEnumerable ResolveCore(IContentTypeComposition source) + { + var aliases = new List(); + // get ancestor ids from path of parent if not root + if (source.ParentId != Constants.System.Root) + { + var parent = _applicationContext.Services.ContentTypeService.GetContentType(source.ParentId); + if (parent != null) + { + var ancestorIds = parent.Path.Split(',').Select(int.Parse); + // loop through all content types and return ordered aliases of ancestors + var allContentTypes = _applicationContext.Services.ContentTypeService.GetAllContentTypes().ToArray(); + foreach (var ancestorId in ancestorIds) + { + var ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); + if (ancestor != null) + { + aliases.Add(ancestor.Alias); + } + } + } + } + return aliases.OrderBy(x => x); + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index 3c27d0f183..799a93a220 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -7,7 +7,10 @@ using System.Web; using System.Web.Mvc; using System.Web.Routing; using AutoMapper; +using umbraco; using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; using Umbraco.Core.PropertyEditors; @@ -50,8 +53,8 @@ namespace Umbraco.Web.Models.Mapping .ForMember(display => display.Updater, expression => expression.Ignore()) .ForMember(display => display.Alias, expression => expression.Ignore()) .ForMember(display => display.IsContainer, expression => expression.Ignore()) - .ForMember(display => display.Tabs, expression => expression.ResolveUsing()) - .AfterMap((media, display) => AfterMap(media, display, applicationContext.Services.DataTypeService)); + .ForMember(display => display.Tabs, expression => expression.ResolveUsing(new TabsAndPropertiesResolver(applicationContext.Services.TextService))) + .AfterMap((media, display) => AfterMap(media, display, applicationContext.Services.DataTypeService, applicationContext.Services.TextService, applicationContext.ProfilingLogger.Logger)); //FROM IMedia TO ContentItemBasic config.CreateMap>() @@ -82,7 +85,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(x => x.Alias, expression => expression.Ignore()); } - private static void AfterMap(IMedia media, MediaItemDisplay display, IDataTypeService dataTypeService) + private static void AfterMap(IMedia media, MediaItemDisplay display, IDataTypeService dataTypeService, ILocalizedTextService localizedText, ILogger logger) { // Adapted from ContentModelMapper //map the IsChildOfListView (this is actually if it is a descendant of a list view!) @@ -121,10 +124,35 @@ namespace Umbraco.Web.Models.Mapping if (media.ContentType.IsContainer) { - TabsAndPropertiesResolver.AddListView(display, "media", dataTypeService); + TabsAndPropertiesResolver.AddListView(display, "media", dataTypeService, localizedText); + } + + var genericProperties = new List + { + new ContentPropertyDisplay + { + Alias = string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("content/mediatype"), + Value = localizedText.UmbracoDictionaryTranslate(display.ContentTypeName), + View = PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + } + }; + + var links = media.GetUrls(UmbracoConfig.For.UmbracoSettings().Content, logger); + + if (links.Any()) + { + var link = new ContentPropertyDisplay + { + Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("media/urls"), + Value = string.Join(",", links), + View = "urllist" + }; + genericProperties.Add(link); } - TabsAndPropertiesResolver.MapGenericProperties(media, display); + TabsAndPropertiesResolver.MapGenericProperties(media, display, localizedText, genericProperties); } } diff --git a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs index 44d4fb0f50..06dd39124d 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using umbraco; using System.Linq; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Security; using Umbraco.Web.Trees; @@ -75,7 +76,7 @@ namespace Umbraco.Web.Models.Mapping expression => expression.MapFrom(content => content.ContentType.Name)) .ForMember(display => display.Properties, expression => expression.Ignore()) .ForMember(display => display.Tabs, - expression => expression.ResolveUsing()) + expression => expression.ResolveUsing(new MemberTabsAndPropertiesResolver(applicationContext.Services.TextService))) .ForMember(display => display.MemberProviderFieldMapping, expression => expression.ResolveUsing()) .ForMember(display => display.MembershipScenario, @@ -89,7 +90,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(display => display.Trashed, expression => expression.Ignore()) .ForMember(display => display.IsContainer, expression => expression.Ignore()) .ForMember(display => display.TreeNodeUrl, expression => expression.Ignore()) - .AfterMap((member, display) => MapGenericCustomProperties(applicationContext.Services.MemberService, member, display)); + .AfterMap((member, display) => MapGenericCustomProperties(applicationContext.Services.MemberService, member, display, applicationContext.Services.TextService)); //FROM IMember TO MemberBasic config.CreateMap() @@ -163,10 +164,11 @@ namespace Umbraco.Web.Models.Mapping /// /// /// + /// /// /// If this is a new entity and there is an approved field then we'll set it to true by default. /// - private static void MapGenericCustomProperties(IMemberService memberService, IMember member, MemberDisplay display) + private static void MapGenericCustomProperties(IMemberService memberService, IMember member, MemberDisplay display, ILocalizedTextService localizedText) { var membersProvider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); @@ -177,14 +179,21 @@ namespace Umbraco.Web.Models.Mapping var url = urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(display.Key.ToString("N"), null)); display.TreeNodeUrl = url; } - + var genericProperties = new List { - GetLoginProperty(memberService, member, display), + new ContentPropertyDisplay + { + Alias = string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("content/membertype"), + Value = localizedText.UmbracoDictionaryTranslate(display.ContentTypeName), + View = PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + }, + GetLoginProperty(memberService, member, display, localizedText), new ContentPropertyDisplay { Alias = string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("general", "email"), + Label = localizedText.Localize("general/email"), Value = display.Email, View = "email", Validation = {Mandatory = true} @@ -192,7 +201,7 @@ namespace Umbraco.Web.Models.Mapping new ContentPropertyDisplay { Alias = string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("password"), + Label = localizedText.Localize("password"), //NOTE: The value here is a json value - but the only property we care about is the generatedPassword one if it exists, the newPassword exists // only when creating a new member and we want to have a generated password pre-filled. Value = new Dictionary @@ -212,15 +221,16 @@ namespace Umbraco.Web.Models.Mapping new ContentPropertyDisplay { Alias = string.Format("{0}membergroup", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("content", "membergroup"), + Label = localizedText.Localize("content/membergroup"), Value = GetMemberGroupValue(display.Username), View = "membergroups", Config = new Dictionary {{"IsRequired", true}} } }; - TabsAndPropertiesResolver.MapGenericProperties(member, display, genericProperties); + TabsAndPropertiesResolver.MapGenericProperties(member, display, localizedText, genericProperties); + //check if there's an approval field var provider = membersProvider as IUmbracoMemberTypeMembershipProvider; if (member.HasIdentity == false && provider != null) @@ -247,12 +257,12 @@ namespace Umbraco.Web.Models.Mapping /// the membership provider is a custom one, we cannot allow chaning the username because MembershipProvider's do not actually natively /// allow that. /// - internal static ContentPropertyDisplay GetLoginProperty(IMemberService memberService, IMember member, MemberDisplay display) + internal static ContentPropertyDisplay GetLoginProperty(IMemberService memberService, IMember member, MemberDisplay display, ILocalizedTextService localizedText) { var prop = new ContentPropertyDisplay { Alias = string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("login"), + Label = localizedText.Localize("login"), Value = display.Username }; @@ -321,6 +331,20 @@ namespace Umbraco.Web.Models.Mapping /// internal class MemberTabsAndPropertiesResolver : TabsAndPropertiesResolver { + private readonly ILocalizedTextService _localizedTextService; + + public MemberTabsAndPropertiesResolver(ILocalizedTextService localizedTextService) + : base(localizedTextService) + { + _localizedTextService = localizedTextService; + } + + public MemberTabsAndPropertiesResolver(ILocalizedTextService localizedTextService, + IEnumerable ignoreProperties) : base(localizedTextService, ignoreProperties) + { + _localizedTextService = localizedTextService; + } + protected override IEnumerable> ResolveCore(IContentBase content) { var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); @@ -339,7 +363,7 @@ namespace Umbraco.Web.Models.Mapping if (isLockedOutProperty != null && isLockedOutProperty.Value.ToString() != "1") { isLockedOutProperty.View = "readonlyvalue"; - isLockedOutProperty.Value = ui.Text("general", "no"); + isLockedOutProperty.Value = _localizedTextService.Localize("general/no"); } return result; @@ -355,7 +379,7 @@ namespace Umbraco.Web.Models.Mapping if (isLockedOutProperty != null && isLockedOutProperty.Value.ToString() != "1") { isLockedOutProperty.View = "readonlyvalue"; - isLockedOutProperty.Value = ui.Text("general", "no"); + isLockedOutProperty.Value = _localizedTextService.Localize("general/no"); } return result; diff --git a/src/Umbraco.Web/Models/Mapping/PropertyGroupDisplayResolver.cs b/src/Umbraco.Web/Models/Mapping/PropertyGroupDisplayResolver.cs new file mode 100644 index 0000000000..2f11eb04c4 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/PropertyGroupDisplayResolver.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using AutoMapper; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + internal class PropertyGroupDisplayResolver : ValueResolver>> + where TSource : ContentTypeSave + where TPropertyTypeDestination : PropertyTypeDisplay + where TPropertyTypeSource : PropertyTypeBasic + { + protected override IEnumerable> ResolveCore(TSource source) + { + return source.Groups.Select(Mapper.Map>); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs index d548a9722f..6409fe8009 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs @@ -9,7 +9,8 @@ using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { - internal class PropertyTypeGroupResolver : ValueResolver> + internal class PropertyTypeGroupResolver : ValueResolver>> + where TPropertyType : PropertyTypeDisplay, new() { private readonly ApplicationContext _applicationContext; private readonly Lazy _propertyEditorResolver; @@ -40,15 +41,35 @@ namespace Umbraco.Web.Models.Mapping .FirstOrDefault(x => x != null); } - protected override IEnumerable ResolveCore(IContentTypeComposition source) + /// + /// Gets the content type that defines a property group, within a composition. + /// + /// The composition. + /// The identifier of the property type. + /// The composition content type that defines the specified property group. + private static IContentTypeComposition GetContentTypeForPropertyType(IContentTypeComposition contentType, int propertyTypeId) + { + // test local property types + if (contentType.PropertyTypes.Any(x => x.Id == propertyTypeId)) + return contentType; + + // test composition property types + // .ContentTypeComposition is just the local ones, not recursive, + // so we have to recurse here + return contentType.ContentTypeComposition + .Select(x => GetContentTypeForPropertyType(x, propertyTypeId)) + .FirstOrDefault(x => x != null); + } + + protected override IEnumerable> ResolveCore(IContentTypeComposition source) { // deal with groups - var groups = new List(); + var groups = new List>(); // add groups local to this content type foreach (var tab in source.PropertyGroups) { - var group = new PropertyGroupDisplay + var group = new PropertyGroupDisplay { Id = tab.Id, Inherited = false, @@ -73,7 +94,7 @@ namespace Umbraco.Web.Models.Mapping if (definingContentType == null) throw new Exception("PropertyGroup with id=" + tab.Id + " was not found on any of the content type's compositions."); - var group = new PropertyGroupDisplay + var group = new PropertyGroupDisplay { Id = tab.Id, Inherited = true, @@ -89,25 +110,31 @@ namespace Umbraco.Web.Models.Mapping } // deal with generic properties - var genericProperties = new List(); + var genericProperties = new List(); // add generic properties local to this content type var entityGenericProperties = source.PropertyTypes.Where(x => x.PropertyGroupId == null); - genericProperties.AddRange(MapProperties(entityGenericProperties, source, PropertyGroupDisplay.GenericPropertiesGroupId, false)); + genericProperties.AddRange(MapProperties(entityGenericProperties, source, PropertyGroupBasic.GenericPropertiesGroupId, false)); // add generic properties inherited through compositions var localGenericPropertyIds = genericProperties.Select(x => x.Id).ToArray(); var compositionGenericProperties = source.CompositionPropertyTypes .Where(x => x.PropertyGroupId == null // generic && localGenericPropertyIds.Contains(x.Id) == false); // skip those that are local - genericProperties.AddRange(MapProperties(compositionGenericProperties, source, PropertyGroupDisplay.GenericPropertiesGroupId, true)); + foreach (var compositionGenericProperty in compositionGenericProperties) + { + var definingContentType = GetContentTypeForPropertyType(source, compositionGenericProperty.Id); + if (definingContentType == null) + throw new Exception("PropertyType with id=" + compositionGenericProperty.Id + " was not found on any of the content type's compositions."); + genericProperties.AddRange(MapProperties(new [] { compositionGenericProperty }, definingContentType, PropertyGroupBasic.GenericPropertiesGroupId, true)); + } // if there are any generic properties, add the corresponding tab if (genericProperties.Any()) { - var genericTab = new PropertyGroupDisplay + var genericTab = new PropertyGroupDisplay { - Id = PropertyGroupDisplay.GenericPropertiesGroupId, + Id = PropertyGroupBasic.GenericPropertiesGroupId, Name = "Generic properties", ContentTypeId = source.Id, SortOrder = 999, @@ -117,10 +144,23 @@ namespace Umbraco.Web.Models.Mapping groups.Add(genericTab); } + // handle locked properties + var lockedPropertyAliases = new List(); + // add built-in member property aliases to list of aliases to be locked + foreach (var propertyAlias in Constants.Conventions.Member.GetStandardPropertyTypeStubs().Keys) + { + lockedPropertyAliases.Add(propertyAlias); + } + // lock properties by aliases + foreach (var property in groups.SelectMany(x => x.Properties)) + { + property.Locked = lockedPropertyAliases.Contains(property.Alias); + } + // now merge tabs based on names // as for one name, we might have one local tab, plus some inherited tabs var groupsGroupsByName = groups.GroupBy(x => x.Name).ToArray(); - groups = new List(); // start with a fresh list + groups = new List>(); // start with a fresh list foreach (var groupsByName in groupsGroupsByName) { // single group, just use it @@ -151,33 +191,35 @@ namespace Umbraco.Web.Models.Mapping return groups.OrderBy(x => x.SortOrder); } - private IEnumerable MapProperties(IEnumerable properties, IContentTypeBase contentType, int groupId, bool inherited) + private IEnumerable MapProperties(IEnumerable properties, IContentTypeBase contentType, int groupId, bool inherited) { - var mappedProperties = new List(); + var mappedProperties = new List(); foreach (var p in properties.Where(x => x.DataTypeDefinitionId != 0).OrderBy(x => x.SortOrder)) { var propertyEditor = _propertyEditorResolver.Value.GetByAlias(p.PropertyEditorAlias); var preValues = _applicationContext.Services.DataTypeService.GetPreValuesCollectionByDataTypeId(p.DataTypeDefinitionId); - mappedProperties.Add(new PropertyTypeDisplay - { - Id = p.Id, - Alias = p.Alias, - Description = p.Description, - Editor = p.PropertyEditorAlias, - Validation = new PropertyTypeValidation { Mandatory = p.Mandatory, Pattern = p.ValidationRegExp }, - Label = p.Name, - View = propertyEditor.ValueEditor.View, - Config = propertyEditor.PreValueEditor.ConvertDbToEditor(propertyEditor.DefaultPreValues, preValues) , - //Value = "", - ContentTypeId = contentType.Id, - ContentTypeName = contentType.Name, - GroupId = groupId, - Inherited = inherited, - DataTypeId = p.DataTypeDefinitionId, - SortOrder = p.SortOrder - }); + if (propertyEditor == null) + throw new InvalidOperationException("No property editor could be resolved with the alias: " + p.PropertyEditorAlias + ", ensure all packages are installed correctly."); + + mappedProperties.Add(new TPropertyType + { + Id = p.Id, + Alias = p.Alias, + Description = p.Description, + Editor = p.PropertyEditorAlias, + Validation = new PropertyTypeValidation {Mandatory = p.Mandatory, Pattern = p.ValidationRegExp}, + Label = p.Name, + View = propertyEditor.ValueEditor.View, + Config = propertyEditor.PreValueEditor.ConvertDbToEditor(propertyEditor.DefaultPreValues, preValues), + //Value = "", + GroupId = groupId, + Inherited = inherited, + DataTypeId = p.DataTypeDefinitionId, + SortOrder = p.SortOrder, + ContentTypeId = contentType.Id + }); } return mappedProperties; diff --git a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs index 3a1a756940..76de186579 100644 --- a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Web; using AutoMapper; using Umbraco.Core; -using Umbraco.Core.Dictionary; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; @@ -18,16 +15,19 @@ namespace Umbraco.Web.Models.Mapping /// Creates the tabs collection with properties assigned for display models /// internal class TabsAndPropertiesResolver : ValueResolver>> - { - private ICultureDictionary _cultureDictionary; + { + private readonly ILocalizedTextService _localizedTextService; protected IEnumerable IgnoreProperties { get; set; } - public TabsAndPropertiesResolver() + public TabsAndPropertiesResolver(ILocalizedTextService localizedTextService) { + if (localizedTextService == null) throw new ArgumentNullException("localizedTextService"); + _localizedTextService = localizedTextService; IgnoreProperties = new List(); } - public TabsAndPropertiesResolver(IEnumerable ignoreProperties) + public TabsAndPropertiesResolver(ILocalizedTextService localizedTextService, IEnumerable ignoreProperties) + : this(localizedTextService) { if (ignoreProperties == null) throw new ArgumentNullException("ignoreProperties"); IgnoreProperties = ignoreProperties; @@ -38,6 +38,7 @@ namespace Umbraco.Web.Models.Mapping /// /// /// + /// /// /// Any additional custom properties to assign to the generic properties tab. /// @@ -49,6 +50,7 @@ namespace Umbraco.Web.Models.Mapping public static void MapGenericProperties( TPersisted content, ContentItemDisplayBase display, + ILocalizedTextService localizedTextService, IEnumerable customProperties = null, Action> onGenericPropertiesMapped = null) where TPersisted : IContentBase @@ -72,33 +74,26 @@ namespace Umbraco.Web.Models.Mapping new ContentPropertyDisplay { Alias = string.Format("{0}creator", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("content", "createBy"), - Description = ui.Text("content", "createByDesc"), //TODO: Localize this + Label = localizedTextService.Localize("content/createBy"), + Description = localizedTextService.Localize("content/createByDesc"), Value = display.Owner.Name, View = labelEditor }, new ContentPropertyDisplay { Alias = string.Format("{0}createdate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("content", "createDate"), - Description = ui.Text("content", "createDateDesc"), + Label = localizedTextService.Localize("content/createDate"), + Description = localizedTextService.Localize("content/createDateDesc"), Value = display.CreateDate.ToIsoString(), View = labelEditor }, new ContentPropertyDisplay { Alias = string.Format("{0}updatedate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("content", "updateDate"), - Description = ui.Text("content", "updateDateDesc"), + Label = localizedTextService.Localize("content/updateDate"), + Description = localizedTextService.Localize("content/updateDateDesc"), Value = display.UpdateDate.ToIsoString(), View = labelEditor - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("content", "documentType"), - Value = TranslateItem(display.ContentTypeName, CreateDictionary()), - View = labelEditor } }; @@ -131,7 +126,8 @@ namespace Umbraco.Web.Models.Mapping /// /// This must be either 'content' or 'media' /// - internal static void AddListView(TabbedContentItem display, string entityType, IDataTypeService dataTypeService) + /// + internal static void AddListView(TabbedContentItem display, string entityType, IDataTypeService dataTypeService, ILocalizedTextService localizedTextService) where TPersisted : IContentBase { int dtdId; @@ -171,7 +167,7 @@ namespace Umbraco.Web.Models.Mapping var listViewTab = new Tab(); listViewTab.Alias = Constants.Conventions.PropertyGroups.ListViewGroupName; - listViewTab.Label = ui.Text("content", "childItems"); + listViewTab.Label = localizedTextService.Localize("content/childItems"); listViewTab.Id = 25; listViewTab.IsActive = true; @@ -236,7 +232,7 @@ namespace Umbraco.Web.Models.Mapping { Id = groupId, Alias = groupName, - Label = TranslateItem(groupName), + Label = _localizedTextService.UmbracoDictionaryTranslate(groupName), Properties = properties, IsActive = false }); @@ -252,7 +248,7 @@ namespace Umbraco.Web.Models.Mapping tabs.Add(new Tab { Id = 0, - Label = ui.Text("general", "properties"), + Label = _localizedTextService.Localize("general/properties"), Alias = "Generic properties", Properties = genericproperties }); @@ -268,45 +264,9 @@ namespace Umbraco.Web.Models.Mapping // Not sure whether it's a good idea to add this to the ContentPropertyDisplay mapper foreach (var prop in properties) { - prop.Label = TranslateItem(prop.Label); - prop.Description = TranslateItem(prop.Description); + prop.Label = _localizedTextService.UmbracoDictionaryTranslate(prop.Label); + prop.Description = _localizedTextService.UmbracoDictionaryTranslate(prop.Description); } } - - // TODO: This should really be centralized and used anywhere globalization applies. - internal string TranslateItem(string text) - { - var cultureDictionary = CultureDictionary; - return TranslateItem(text, cultureDictionary); - } - - private static string TranslateItem(string text, ICultureDictionary cultureDictionary) - { - if (text == null) - { - return null; - } - - if (text.StartsWith("#") == false) - return text; - - text = text.Substring(1); - return cultureDictionary[text].IfNullOrWhiteSpace(text); - } - - private ICultureDictionary CultureDictionary - { - get - { - return - _cultureDictionary ?? - (_cultureDictionary = CreateDictionary()); - } - } - - private static ICultureDictionary CreateDictionary() - { - return CultureDictionaryFactoryResolver.Current.Factory.CreateDictionary(); - } } } diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index e1c3cc4f1e..72093b19a8 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -42,7 +42,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id))); config.CreateMap() - .ConstructUsing((IUser user) => new UserData(Guid.NewGuid().ToString("N"))) //this is the 'session id' + .ConstructUsing((IUser user) => new UserData()) .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index aa801fbe2f..98d7085a78 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; @@ -28,56 +31,67 @@ namespace Umbraco.Web.Models /// public virtual string Url { - get - { - // should be thread-safe although it won't prevent url from being resolved more than once - if (_url != null) - return _url; + get + { + // should be thread-safe although it won't prevent url from being resolved more than once + if (_url != null) + return _url; - switch (ItemType) - { - case PublishedItemType.Content: - if (UmbracoContext.Current == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when UmbracoContext.Current is null."); - if (UmbracoContext.Current.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when UmbracoContext.Current.UrlProvider is null."); - _url= UmbracoContext.Current.UrlProvider.GetUrl(Id); - break; - case PublishedItemType.Media: - var prop = GetProperty(Constants.Conventions.Media.File); - if (prop == null) - throw new NotSupportedException("Cannot resolve a Url for a media item when there is no 'umbracoFile' property defined."); + switch (ItemType) + { + case PublishedItemType.Content: + if (UmbracoContext.Current == null) + throw new InvalidOperationException( + "Cannot resolve a Url for a content item when UmbracoContext.Current is null."); + if (UmbracoContext.Current.UrlProvider == null) + throw new InvalidOperationException( + "Cannot resolve a Url for a content item when UmbracoContext.Current.UrlProvider is null."); + _url = UmbracoContext.Current.UrlProvider.GetUrl(Id); + break; + case PublishedItemType.Media: + var prop = GetProperty(Constants.Conventions.Media.File); + if (prop == null || prop.Value == null) + { + _url = string.Empty; + return _url; + } - if (prop.Value == null) - { - _url = string.Empty; - return _url; - } + var propType = ContentType.GetPropertyType(Constants.Conventions.Media.File); - var propType = ContentType.GetPropertyType(Constants.Conventions.Media.File); - - //This is a hack - since we now have 2 properties that support a URL: upload and cropper, we need to detect this since we always - // want to return the normal URL and the cropper stores data as json - switch (propType.PropertyEditorAlias) - { - case Constants.PropertyEditors.UploadFieldAlias: - _url = prop.Value.ToString(); - break; - case Constants.PropertyEditors.ImageCropperAlias: - //get the url from the json format - var val = prop.Value.ToString(); - var crops = val.SerializeToCropDataSet(); - _url = crops != null ? crops.Src : string.Empty; - break; - } - - break; - default: - throw new NotSupportedException(); - } + //This is a hack - since we now have 2 properties that support a URL: upload and cropper, we need to detect this since we always + // want to return the normal URL and the cropper stores data as json + switch (propType.PropertyEditorAlias) + { + case Constants.PropertyEditors.UploadFieldAlias: + _url = prop.Value.ToString(); + break; + case Constants.PropertyEditors.ImageCropperAlias: + //get the url from the json format - return _url; - } + var stronglyTyped = prop.Value as ImageCropDataSet; + if (stronglyTyped != null) + { + _url = stronglyTyped.Src; + break; + } + + var json = prop.Value as JObject; + if (json != null) + { + _url = json.ToObject(new JsonSerializer { Culture = CultureInfo.InvariantCulture, FloatParseHandling = FloatParseHandling.Decimal }).Src; + break; + } + + _url = prop.Value.ToString(); + break; + } + break; + default: + throw new NotSupportedException(); + } + + return _url; + } } public abstract PublishedItemType ItemType { get; } diff --git a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs index 2836c458fb..982286c24b 100644 --- a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs +++ b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs @@ -110,7 +110,7 @@ namespace Umbraco.Web.Mvc //match this area controllerPluginRoute.DataTokens.Add("area", area.AreaName); - controllerPluginRoute.DataTokens.Add("umbraco", umbracoTokenValue); //ensure the umbraco token is set + controllerPluginRoute.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, umbracoTokenValue); //ensure the umbraco token is set return controllerPluginRoute; } diff --git a/src/Umbraco.Web/Mvc/ControllerContextExtensions.cs b/src/Umbraco.Web/Mvc/ControllerContextExtensions.cs new file mode 100644 index 0000000000..c998ed931e --- /dev/null +++ b/src/Umbraco.Web/Mvc/ControllerContextExtensions.cs @@ -0,0 +1,50 @@ +using System.Web.Mvc; + +namespace Umbraco.Web.Mvc +{ + public static class ControllerContextExtensions + { + /// + /// Tries to get the Umbraco context from the whole ControllerContext hierarchy based on data tokens and if that fails + /// it will attempt to fallback to retrieving it from the HttpContext. + /// + /// + /// + public static UmbracoContext GetUmbracoContext(this ControllerContext controllerContext) + { + var umbCtx = controllerContext.RouteData.GetUmbracoContext(); + if (umbCtx != null) return umbCtx; + + if (controllerContext.ParentActionViewContext != null) + { + //recurse + return controllerContext.ParentActionViewContext.GetUmbracoContext(); + } + + //fallback to getting from HttpContext + return controllerContext.HttpContext.GetUmbracoContext(); + } + + /// + /// Find a data token in the whole ControllerContext hierarchy of execution + /// + /// + /// + /// + internal static object GetDataTokenInViewContextHierarchy(this ControllerContext controllerContext, string dataTokenName) + { + if (controllerContext.RouteData.DataTokens.ContainsKey(dataTokenName)) + { + return controllerContext.RouteData.DataTokens[dataTokenName]; + } + + if (controllerContext.ParentActionViewContext != null) + { + //recurse! + return controllerContext.ParentActionViewContext.GetDataTokenInViewContextHierarchy(dataTokenName); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/ControllerExtensions.cs b/src/Umbraco.Web/Mvc/ControllerExtensions.cs index c15520b57c..734e60e8f5 100644 --- a/src/Umbraco.Web/Mvc/ControllerExtensions.cs +++ b/src/Umbraco.Web/Mvc/ControllerExtensions.cs @@ -5,24 +5,8 @@ using System.Web.Mvc; namespace Umbraco.Web.Mvc { - internal static class ControllerExtensions + internal static class ControllerExtensions { - internal static object GetDataTokenInViewContextHierarchy(this ControllerContext controllerContext, string dataTokenName) - { - if (controllerContext.RouteData.DataTokens.ContainsKey(dataTokenName)) - { - return controllerContext.RouteData.DataTokens[dataTokenName]; - } - - if (controllerContext.ParentActionViewContext != null) - { - //recurse! - return controllerContext.ParentActionViewContext.GetDataTokenInViewContextHierarchy(dataTokenName); - } - - return null; - } - /// /// Return the controller name from the controller type /// diff --git a/src/Umbraco.Web/Mvc/ModelBindingException.cs b/src/Umbraco.Web/Mvc/ModelBindingException.cs new file mode 100644 index 0000000000..d675ae4a65 --- /dev/null +++ b/src/Umbraco.Web/Mvc/ModelBindingException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Umbraco.Web.Mvc +{ + public class ModelBindingException : Exception + { + public ModelBindingException() + { } + + public ModelBindingException(string message) + : base(message) + { } + } +} diff --git a/src/Umbraco.Web/Mvc/RenderModelBinder.cs b/src/Umbraco.Web/Mvc/RenderModelBinder.cs index f8b1df7520..f8e7ee8a4e 100644 --- a/src/Umbraco.Web/Mvc/RenderModelBinder.cs +++ b/src/Umbraco.Web/Mvc/RenderModelBinder.cs @@ -1,11 +1,14 @@ +using System; +using System.Globalization; using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Web.Models; namespace Umbraco.Web.Mvc { - public class RenderModelBinder : IModelBinder - { - + public class RenderModelBinder : IModelBinder, IModelBinderProvider + { /// /// Binds the model to a value by using the specified controller context and binding context. /// @@ -15,17 +18,112 @@ namespace Umbraco.Web.Mvc /// The controller context.The binding context. public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { - var requestMatchesType = typeof(RenderModel) == bindingContext.ModelType; + object model; + if (controllerContext.RouteData.DataTokens.TryGetValue(Core.Constants.Web.UmbracoDataToken, out model) == false) + return null; - if (requestMatchesType) - { - //get the model from the route data - if (!controllerContext.RouteData.DataTokens.ContainsKey("umbraco")) - return null; - var model = controllerContext.RouteData.DataTokens["umbraco"] as RenderModel; - return model; - } - return null; - } - } + //default culture + var culture = CultureInfo.CurrentCulture; + + var umbracoContext = controllerContext.GetUmbracoContext() + ?? UmbracoContext.Current; + + if (umbracoContext != null && umbracoContext.PublishedContentRequest != null) + { + culture = umbracoContext.PublishedContentRequest.Culture; + } + + return BindModel(model, bindingContext.ModelType, culture); + } + + // source is the model that we have + // modelType is the type of the model that we need to bind to + // culture is the CultureInfo that we have, used by RenderModel + // + // create a model object of the modelType by mapping: + // { RenderModel, RenderModel, IPublishedContent } + // to + // { RenderModel, RenderModel, IPublishedContent } + // + public static object BindModel(object source, Type modelType, CultureInfo culture) + { + // null model, return + if (source == null) return null; + + // if types already match, return + var sourceType = source.GetType(); + if (sourceType.Inherits(modelType)) // includes == + return source; + + // try to grab the content + var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent + if (sourceContent == null && sourceType.Implements()) + { + // else check if it's an IRenderModel, and get the content + sourceContent = ((IRenderModel)source).Content; + } + if (sourceContent == null) + { + // else check if we can convert it to a content + var attempt1 = source.TryConvertTo(); + if (attempt1.Success) sourceContent = attempt1.Result; + } + + // if we have a content + if (sourceContent != null) + { + // try to grab the culture + // using supplied culture by default + var sourceRenderModel = source as RenderModel; + if (sourceRenderModel != null) + culture = sourceRenderModel.CurrentCulture; + + // if model is IPublishedContent, check content type and return + if (modelType.Implements()) + { + if ((sourceContent.GetType().Inherits(modelType)) == false) + throw new ModelBindingException(string.Format("Cannot bind source content type {0} to model type {1}.", + sourceContent.GetType(), modelType)); + return sourceContent; + } + + // if model is RenderModel, create and return + if (modelType == typeof(RenderModel)) + { + return new RenderModel(sourceContent, culture); + } + + // if model is RenderModel, check content type, then create and return + if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(RenderModel<>)) + { + var targetContentType = modelType.GetGenericArguments()[0]; + if ((sourceContent.GetType().Inherits(targetContentType)) == false) + throw new ModelBindingException(string.Format("Cannot bind source content type {0} to model content type {1}.", + sourceContent.GetType(), targetContentType)); + return Activator.CreateInstance(modelType, sourceContent, culture); + } + } + + // last chance : try to convert + var attempt2 = source.TryConvertTo(modelType); + if (attempt2.Success) return attempt2.Result; + + // fail + throw new ModelBindingException(string.Format("Cannot bind source type {0} to model type {1}.", + sourceType, modelType)); + } + + public IModelBinder GetBinder(Type modelType) + { + // can bind to RenderModel + if (modelType == typeof(RenderModel)) return this; + + // can bind to RenderModel + if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(RenderModel<>)) return this; + + // can bind to TContent where TContent : IPublishedContent + if (typeof(IPublishedContent).IsAssignableFrom(modelType)) return this; + return null; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/RenderMvcController.cs b/src/Umbraco.Web/Mvc/RenderMvcController.cs index 07b9247ade..571b9ba1b8 100644 --- a/src/Umbraco.Web/Mvc/RenderMvcController.cs +++ b/src/Umbraco.Web/Mvc/RenderMvcController.cs @@ -60,11 +60,11 @@ namespace Umbraco.Web.Mvc { if (_publishedContentRequest != null) return _publishedContentRequest; - if (RouteData.DataTokens.ContainsKey("umbraco-doc-request") == false) + if (RouteData.DataTokens.ContainsKey(Core.Constants.Web.PublishedDocumentRequestDataToken) == false) { throw new InvalidOperationException("DataTokens must contain an 'umbraco-doc-request' key with a PublishedContentRequest object"); } - _publishedContentRequest = (PublishedContentRequest)RouteData.DataTokens["umbraco-doc-request"]; + _publishedContentRequest = (PublishedContentRequest)RouteData.DataTokens[Core.Constants.Web.PublishedDocumentRequestDataToken]; return _publishedContentRequest; } } diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index bce3515c1b..d7ce36b4a4 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -97,9 +97,9 @@ namespace Umbraco.Web.Mvc internal void SetupRouteDataForRequest(RenderModel renderModel, RequestContext requestContext, PublishedContentRequest docRequest) { //put essential data into the data tokens, the 'umbraco' key is required to be there for the view engine - requestContext.RouteData.DataTokens.Add("umbraco", renderModel); //required for the RenderModelBinder and view engine - requestContext.RouteData.DataTokens.Add("umbraco-doc-request", docRequest); //required for RenderMvcController - requestContext.RouteData.DataTokens.Add("umbraco-context", UmbracoContext); //required for UmbracoTemplatePage + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, renderModel); //required for the RenderModelBinder and view engine + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.PublishedDocumentRequestDataToken, docRequest); //required for RenderMvcController + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, UmbracoContext); //required for UmbracoTemplatePage } private void UpdateRouteDataForRequest(RenderModel renderModel, RequestContext requestContext) @@ -107,7 +107,7 @@ namespace Umbraco.Web.Mvc if (renderModel == null) throw new ArgumentNullException("renderModel"); if (requestContext == null) throw new ArgumentNullException("requestContext"); - requestContext.RouteData.DataTokens["umbraco"] = renderModel; + requestContext.RouteData.DataTokens[Core.Constants.Web.UmbracoDataToken] = renderModel; // the rest should not change -- it's only the published content that has changed } @@ -337,7 +337,7 @@ namespace Umbraco.Web.Mvc } //store the route definition - requestContext.RouteData.DataTokens["umbraco-route-def"] = def; + requestContext.RouteData.DataTokens[Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken] = def; return def; } diff --git a/src/Umbraco.Web/Mvc/RenderViewEngine.cs b/src/Umbraco.Web/Mvc/RenderViewEngine.cs index ed7b941580..fdf61059e0 100644 --- a/src/Umbraco.Web/Mvc/RenderViewEngine.cs +++ b/src/Umbraco.Web/Mvc/RenderViewEngine.cs @@ -93,7 +93,7 @@ namespace Umbraco.Web.Mvc /// private bool ShouldFindView(ControllerContext controllerContext, bool isPartial) { - var umbracoToken = controllerContext.GetDataTokenInViewContextHierarchy("umbraco"); + var umbracoToken = controllerContext.GetDataTokenInViewContextHierarchy(Core.Constants.Web.UmbracoDataToken); //first check if we're rendering a partial view for the back office, or surface controller, etc... //anything that is not IUmbracoRenderModel as this should only pertain to Umbraco views. diff --git a/src/Umbraco.Web/Mvc/RouteValueDictionaryExtensions.cs b/src/Umbraco.Web/Mvc/RouteValueDictionaryExtensions.cs index 445441aafa..ceb4012a66 100644 --- a/src/Umbraco.Web/Mvc/RouteValueDictionaryExtensions.cs +++ b/src/Umbraco.Web/Mvc/RouteValueDictionaryExtensions.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Mvc public static object GetRequiredObject(this RouteValueDictionary items, string key) { if (key == null) throw new ArgumentNullException("key"); - if (!items.Keys.Contains(key)) + if (items.Keys.Contains(key) == false) throw new ArgumentNullException("The " + key + " parameter was not found but is required"); return items[key]; } diff --git a/src/Umbraco.Web/Mvc/SurfaceController.cs b/src/Umbraco.Web/Mvc/SurfaceController.cs index 817ed97902..420573d745 100644 --- a/src/Umbraco.Web/Mvc/SurfaceController.cs +++ b/src/Umbraco.Web/Mvc/SurfaceController.cs @@ -185,9 +185,9 @@ namespace Umbraco.Web.Mvc while (currentContext != null) { var currentRouteData = currentContext.RouteData; - if (currentRouteData.DataTokens.ContainsKey("umbraco-route-def")) + if (currentRouteData.DataTokens.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken)) { - return Attempt.Succeed((RouteDefinition)currentRouteData.DataTokens["umbraco-route-def"]); + return Attempt.Succeed((RouteDefinition)currentRouteData.DataTokens[Core.Constants.Web.UmbracoRouteDefinitionDataToken]); } if (currentContext.IsChildAction) { diff --git a/src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs b/src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs index c82c59db65..8af018e833 100644 --- a/src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs +++ b/src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.Mvc return false; //ensure there is an umbraco token set - var umbracoToken = request.RouteData.DataTokens["umbraco"]; + var umbracoToken = request.RouteData.DataTokens[Core.Constants.Web.UmbracoDataToken]; if (umbracoToken == null || string.IsNullOrWhiteSpace(umbracoToken.ToString())) return false; diff --git a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs index f23515c1d9..da4cb198ac 100644 --- a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs +++ b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs @@ -34,7 +34,7 @@ namespace Umbraco.Web.Mvc ValidateRouteData(context.RouteData); - var routeDef = (RouteDefinition)context.RouteData.DataTokens["umbraco-route-def"]; + var routeDef = (RouteDefinition)context.RouteData.DataTokens[Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken]; //Special case, if it is webforms but we're posting to an MVC surface controller, then we // need to return the webforms result instead @@ -91,7 +91,7 @@ namespace Umbraco.Web.Mvc /// private static void ValidateRouteData(RouteData routeData) { - if (routeData.DataTokens.ContainsKey("umbraco-route-def") == false) + if (routeData.DataTokens.ContainsKey(Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken) == false) { throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name + " in the context of an Http POST when using a SurfaceController form"); diff --git a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs index 480f182855..861b872275 100644 --- a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs +++ b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Text; using System.Web; using System.Web.Mvc; @@ -26,24 +27,13 @@ namespace Umbraco.Web.Mvc get { //we should always try to return the context from the data tokens just in case its a custom context and not - //using the UmbracoContext.Current. - //we will fallback to the singleton if necessary. - if (ViewContext.RouteData.DataTokens.ContainsKey("umbraco-context")) - { - return (UmbracoContext)ViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-context"); - } - //next check if it is a child action and see if the parent has it set in data tokens - if (ViewContext.IsChildAction) - { - if (ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey("umbraco-context")) - { - return (UmbracoContext)ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-context"); - } - } - - //lastly, we will use the singleton, the only reason this should ever happen is is someone is rendering a page that inherits from this - //class and are rendering it outside of the normal Umbraco routing process. Very unlikely. - return UmbracoContext.Current; + //using the UmbracoContext.Current, we will fallback to the singleton if necessary. + var umbCtx = ViewContext.GetUmbracoContext() + //lastly, we will use the singleton, the only reason this should ever happen is is someone is rendering a page that inherits from this + //class and are rendering it outside of the normal Umbraco routing process. Very unlikely. + ?? UmbracoContext.Current; + + return umbCtx; } } @@ -65,16 +55,16 @@ namespace Umbraco.Web.Mvc //we should always try to return the object from the data tokens just in case its a custom object and not //using the UmbracoContext.Current. //we will fallback to the singleton if necessary. - if (ViewContext.RouteData.DataTokens.ContainsKey("umbraco-doc-request")) + if (ViewContext.RouteData.DataTokens.ContainsKey(Core.Constants.Web.PublishedDocumentRequestDataToken)) { - return (PublishedContentRequest)ViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-doc-request"); + return (PublishedContentRequest)ViewContext.RouteData.DataTokens.GetRequiredObject(Core.Constants.Web.PublishedDocumentRequestDataToken); } //next check if it is a child action and see if the parent has it set in data tokens if (ViewContext.IsChildAction) { - if (ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey("umbraco-doc-request")) + if (ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey(Core.Constants.Web.PublishedDocumentRequestDataToken)) { - return (PublishedContentRequest)ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-doc-request"); + return (PublishedContentRequest)ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject(Core.Constants.Web.PublishedDocumentRequestDataToken); } } @@ -141,92 +131,58 @@ namespace Umbraco.Web.Mvc // maps model protected override void SetViewData(ViewDataDictionary viewData) { - // if view data contains no model, nothing to do - var source = viewData.Model; - if (source == null) - { - base.SetViewData(viewData); - return; - } + // capture the model before we tinker with the viewData + var viewDataModel = viewData.Model; - // get the type of the view data model (what we have) - // get the type of this view model (what we want) - var sourceType = source.GetType(); - var targetType = typeof (TModel); + // map the view data (may change its type, may set model to null) + viewData = MapViewDataDictionary(viewData, typeof (TModel)); - // it types already match, nothing to do - if (sourceType.Inherits()) // includes == - { - base.SetViewData(viewData); - return; - } - - // try to grab the content - // if no content is found, return, nothing we can do - var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent - if (sourceContent == null && sourceType.Implements()) - { - // else check if it's an IRenderModel => get the content - sourceContent = ((IRenderModel)source).Content; - } - if (sourceContent == null) - { - // else check if we can convert it to a content - var attempt = source.TryConvertTo(); - if (attempt.Success) sourceContent = attempt.Result; - } - - var ok = sourceContent != null; - if (sourceContent != null) - { - // try to grab the culture - // using context's culture by default - var culture = UmbracoContext.PublishedContentRequest.Culture; - var sourceRenderModel = source as RenderModel; - if (sourceRenderModel != null) - culture = sourceRenderModel.CurrentCulture; - - // reassign the model depending on its type - if (targetType.Implements()) - { - // it TModel implements IPublishedContent then use the content - // provided that the content is of the proper type - if ((sourceContent is TModel) == false) - throw new InvalidCastException(string.Format("Cannot cast source content type {0} to view model type {1}.", - sourceContent.GetType(), targetType)); - viewData.Model = sourceContent; - } - else if (targetType == typeof(RenderModel)) - { - // if TModel is a basic RenderModel just create it - viewData.Model = new RenderModel(sourceContent, culture); - } - else if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(RenderModel<>)) - { - // if TModel is a strongly-typed RenderModel<> then create it - // provided that the content is of the proper type - var targetContentType = targetType.GetGenericArguments()[0]; - if ((sourceContent.GetType().Inherits(targetContentType)) == false) - throw new InvalidCastException(string.Format("Cannot cast source content type {0} to view model content type {1}.", - sourceContent.GetType(), targetContentType)); - viewData.Model = Activator.CreateInstance(targetType, sourceContent, culture); - } - else - { - ok = false; - } - } - - if (ok == false) - { - // last chance : try to convert - var attempt = source.TryConvertTo(); - if (attempt.Success) viewData.Model = attempt.Result; - } + var culture = CultureInfo.CurrentCulture; + // bind the model (use context culture as default, if available) + if (UmbracoContext.PublishedContentRequest != null && UmbracoContext.PublishedContentRequest.Culture != null) + culture = UmbracoContext.PublishedContentRequest.Culture; + viewData.Model = RenderModelBinder.BindModel(viewDataModel, typeof (TModel), culture); + // set the view data base.SetViewData(viewData); } + // viewData is the ViewDataDictionary (maybe ) that we have + // modelType is the type of the model that we need to bind to + // + // figure out whether viewData can accept modelType else replace it + // + private static ViewDataDictionary MapViewDataDictionary(ViewDataDictionary viewData, Type modelType) + { + var viewDataType = viewData.GetType(); + + // if viewData is not generic then it is a simple ViewDataDictionary instance and its + // Model property is of type 'object' and will accept anything, so it is safe to use + // viewData + if (viewDataType.IsGenericType == false) + return viewData; + + // ensure it is the proper generic type + var def = viewDataType.GetGenericTypeDefinition(); + if (def != typeof(ViewDataDictionary<>)) + throw new Exception("Could not map viewData of type \"" + viewDataType.FullName + "\"."); + + // get the viewData model type and compare with the actual view model type: + // viewData is ViewDataDictionary and we will want to assign an + // object of type modelType to the Model property of type viewDataModelType, we + // need to check whether that is possible + var viewDataModelType = viewDataType.GenericTypeArguments[0]; + + if (viewDataModelType.IsAssignableFrom(modelType)) + return viewData; + + // if not possible then we need to create a new ViewDataDictionary + var nViewDataType = typeof(ViewDataDictionary<>).MakeGenericType(modelType); + var tViewData = new ViewDataDictionary(viewData) { Model = null }; // temp view data to copy values + var nViewData = (ViewDataDictionary)Activator.CreateInstance(nViewDataType, tViewData); + return nViewData; + } + /// /// This will detect the end /body tag and insert the preview badge if in preview mode /// diff --git a/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs index feb5b30ce2..5c948d2e0b 100644 --- a/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; +using System.Web.Security; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Web.Models; using Umbraco.Web.Routing; @@ -19,7 +21,8 @@ namespace Umbraco.Web.Mvc if (found == null) return new NotFoundHandler(); umbracoContext.PublishedContentRequest = new PublishedContentRequest( - umbracoContext.CleanedUmbracoUrl, umbracoContext.RoutingContext) + umbracoContext.CleanedUmbracoUrl, umbracoContext.RoutingContext, + UmbracoConfig.For.UmbracoSettings().WebRouting, s => Roles.Provider.GetRolesForUser(s)) { PublishedContent = found }; @@ -31,11 +34,11 @@ namespace Umbraco.Web.Mvc var renderModel = new RenderModel(umbracoContext.PublishedContentRequest.PublishedContent, umbracoContext.PublishedContentRequest.Culture); //assigns the required tokens to the request - requestContext.RouteData.DataTokens.Add("umbraco", renderModel); - requestContext.RouteData.DataTokens.Add("umbraco-doc-request", umbracoContext.PublishedContentRequest); - requestContext.RouteData.DataTokens.Add("umbraco-context", umbracoContext); + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, renderModel); + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.PublishedDocumentRequestDataToken, umbracoContext.PublishedContentRequest); + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbracoContext); //this is used just for a flag that this is an umbraco custom route - requestContext.RouteData.DataTokens.Add("umbraco-custom-route", true); + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.CustomRouteDataToken, true); //Here we need to detect if a SurfaceController has posted var formInfo = RenderRouteHandler.GetFormInfo(requestContext); @@ -49,7 +52,7 @@ namespace Umbraco.Web.Mvc }; //set the special data token to the current route definition - requestContext.RouteData.DataTokens["umbraco-route-def"] = def; + requestContext.RouteData.DataTokens[Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken] = def; return RenderRouteHandler.HandlePostedValues(requestContext, formInfo); } diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs index c187b754c3..1ae4bc289b 100644 --- a/src/Umbraco.Web/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs @@ -29,5 +29,6 @@ using System.Security; [assembly: InternalsVisibleTo("Umbraco.Courier.Core")] [assembly: InternalsVisibleTo("Umbraco.Courier.Persistence")] [assembly: InternalsVisibleTo("Umbraco.VisualStudio")] - +[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder")] +[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.AspNet")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs index 50cd5a5c2d..f143c5382f 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -134,13 +134,13 @@ namespace Umbraco.Web.PropertyEditors if (umbracoFile.SupportsResizing) { var additionalSizes = new List(); - //get the pre-vals value + //get the pre-vals value var thumbs = editorValue.PreValues.FormatAsDictionary(); if (thumbs.Any()) { var thumbnailSizes = thumbs.First().Value.Value; - // additional thumbnails configured as prevalues on the DataType - foreach (var thumb in thumbnailSizes.Split(new[] { ";", "," }, StringSplitOptions.RemoveEmptyEntries)) + // additional thumbnails configured as prevalues on the DataType + foreach (var thumb in thumbnailSizes.Split(new[] { ";", "," }, StringSplitOptions.RemoveEmptyEntries)) { int thumbSize; if (thumb == "" || int.TryParse(thumb, out thumbSize) == false) continue; @@ -152,8 +152,8 @@ namespace Umbraco.Web.PropertyEditors { ImageHelper.GenerateMediaThumbnails(fs, fileName, umbracoFile.Extension, image, additionalSizes); } - } + newValue.Add(umbracoFile.Url); //add to the saved paths savedFilePaths.Add(umbracoFile.Url); diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index 07482fa1bf..1dcefe6c40 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -16,6 +16,8 @@ using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.PropertyEditors @@ -37,6 +39,23 @@ namespace Umbraco.Web.PropertyEditors _contentConfig = contentConfig; } + /// + /// This is called to merge in the prevalue crops with the value that is saved - similar to the property value converter for the front-end + /// + + public override object ConvertDbToEditor(Property property, PropertyType propertyType, IDataTypeService dataTypeService) + { + var val = base.ConvertDbToEditor(property, propertyType, dataTypeService); + + var json = val as JObject; + if (json != null) + { + ImageCropperValueConverter.MergePreValues(json, dataTypeService, propertyType.DataTypeDefinitionId); + return json; + } + + return val; + } /// /// Overrides the deserialize value so that we can save the file accordingly /// diff --git a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs index 9184892c3d..6cb3a032b6 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Logging; -using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors @@ -48,14 +43,21 @@ namespace Umbraco.Web.PropertyEditors new {name = "List", path = "views/propertyeditors/listview/layouts/list/list.html", icon = "icon-list", isSystem = 1, selected = true}, new {name = "Grid", path = "views/propertyeditors/listview/layouts/grid/grid.html", icon = "icon-thumbnails-small", isSystem = 1, selected = true} } - } + }, + {"bulkActionPermissions", new + { + allowBulkPublish = true, + allowBulkUnpublish = true, + allowBulkCopy = true, + allowBulkMove = true, + allowBulkDelete = true + }} }; } } internal class ListViewPreValueEditor : PreValueEditor { - [PreValueField("pageSize", "Page Size", "number", Description = "Number of items per page")] public int PageSize { get; set; } @@ -72,8 +74,23 @@ namespace Umbraco.Web.PropertyEditors [PreValueField("includeProperties", "Columns Displayed", "views/propertyeditors/listview/includeproperties.prevalues.html", Description = "The properties that will be displayed for each column")] public object IncludeProperties { get; set; } + + [PreValueField("bulkActionPermissions", "Bulk Action Permissions", "views/propertyeditors/listview/bulkactionpermissions.prevalues.html", + Description = "The bulk actions that are allowed from the list view")] + public BulkActionPermissionSettings BulkActionPermissions { get; set; } + + internal class BulkActionPermissionSettings + { + public bool AllowBulkPublish { get; set; } + + public bool AllowBulkUnpublish { get; set; } + + public bool AllowBulkCopy { get; set; } + + public bool AllowBulkMove { get; set; } + + public bool AllowBulkDelete { get; set; } + } } - - } } diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index e90669711c..23b2621ce5 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -17,7 +17,8 @@ namespace Umbraco.Web.PropertyEditors { InternalPreValues = new Dictionary { - {"multiPicker", "0"} + {"multiPicker", "0"}, + {"onlyImages", "0"} }; } diff --git a/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs index 5f3409cfc8..f3185b72a4 100644 --- a/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs @@ -23,6 +23,9 @@ namespace Umbraco.Web.PropertyEditors [PreValueField("multiPicker", "Pick multiple items", "boolean")] public bool MultiPicker { get; set; } + [PreValueField("onlyImages", "Pick only images", "boolean", Description = "Only let the editor choose images from media.")] + public bool OnlyImages { get; set; } + [PreValueField("startNodeId", "Start node", "mediapicker")] public int StartNodeId { get; set; } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropDataSetConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropDataSetConverter.cs new file mode 100644 index 0000000000..82278676cd --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropDataSetConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Web.Models; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + /// + /// Used to do some type conversions from ImageCropDataSet to string and JObject + /// + public class ImageCropDataSetConverter : TypeConverter + { + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + var convertableTypes = new[] + { + typeof(JObject) + }; + + return convertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType)) + || base.CanConvertFrom(context, destinationType); + } + + public override object ConvertTo( + ITypeDescriptorContext context, + CultureInfo culture, + object value, + Type destinationType) + { + var cropDataSet = value as ImageCropDataSet; + if (cropDataSet == null) + return null; + + //JObject + if (TypeHelper.IsTypeAssignableFrom(destinationType)) + { + return JObject.FromObject(cropDataSet); + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs new file mode 100644 index 0000000000..c959d26721 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -0,0 +1,47 @@ +using System.Globalization; +using System.Xml; +using System.Xml.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Dynamics; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Core.Services; +using Umbraco.Web.Models; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + /// + /// Used to strongly type the value for the image cropper + /// + [DefaultPropertyValueConverter(typeof (JsonValueConverter))] //this shadows the JsonValueConverter + [PropertyValueType(typeof (ImageCropDataSet))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class ImageCropperValueConverter : Core.PropertyEditors.ValueConverters.ImageCropperValueConverter + { + public ImageCropperValueConverter() + { + } + + public ImageCropperValueConverter(IDataTypeService dataTypeService) : base(dataTypeService) + { + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + var baseVal = base.ConvertDataToSource(propertyType, source, preview); + var json = baseVal as JObject; + if (json == null) return baseVal; + + var serializer = new JsonSerializer + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }; + + //return the strongly typed model + return json.ToObject(serializer); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index af66278815..681aa2ee72 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -1,4 +1,5 @@ -using System; +using MarkdownSharp; +using System; using System.Web; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; @@ -31,8 +32,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) { + // Convert markup to html for frontend rendering. + Markdown mark = new Markdown(); + // source should come from ConvertSource and be a string (or null) already - return new HtmlString(source == null ? string.Empty : (string)source); + return new HtmlString(source == null ? string.Empty : mark.Transform((string)source)); } public override object ConvertSourceToXPath(PublishedPropertyType propertyType, object source, bool preview) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs index 2278b18b59..8938325c35 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -1,7 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Web.Templates; namespace Umbraco.Web.PropertyEditors.ValueConverters @@ -10,9 +14,15 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Request)] public class TextStringValueConverter : PropertyValueConverterBase { + private readonly static string[] PropertyTypeAliases = + { + Constants.PropertyEditors.TextboxAlias, + Constants.PropertyEditors.TextboxMultipleAlias + }; + public override bool IsConverter(PublishedPropertyType propertyType) { - return Constants.PropertyEditors.TextboxAlias.Equals(propertyType.PropertyEditorAlias); + return PropertyTypeAliases.Contains(propertyType.PropertyEditorAlias); } public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index f7c9a70500..715f00a735 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -94,12 +94,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (content != null && preview == false) { var domainRootNodeId = route.StartsWith("/") ? -1 : int.Parse(route.Substring(0, route.IndexOf('/'))); - var iscanon = - UnitTesting == false + var iscanon = + UnitTesting == false && DomainHelper.ExistsDomainInPath(umbracoContext.Application.Services.DomainService.GetAll(false), content.Path, domainRootNodeId) == false; // and only if this is the canonical url (the one GetUrl would return) if (iscanon) - _routesCache.Store(contentId, route); + _routesCache.Store(content.Id, route); } return content; @@ -196,7 +196,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // we add this check - we look for the document matching "/" and if it's not us, then // we do not hide the top level path // it has to be taken care of in GetByRoute too so if - // "/foo" fails (looking for "/*/foo") we try also "/foo". + // "/foo" fails (looking for "/*/foo") we try also "/foo". // this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but // that's the way it works pre-4.10 and we try to be backward compat for the time being if (node.Parent == null) @@ -281,8 +281,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private static IPublishedContent ConvertToDocument(XmlNode xmlNode, bool isPreviewing) { - return xmlNode == null - ? null + return xmlNode == null + ? null : (new XmlPublishedContent(xmlNode, isPreviewing)).CreateModel(); } @@ -400,7 +400,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (startNodeId > 0) { // if in a domain then use the root node of the domain - xpath = string.Format(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId); + xpath = string.Format(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId); } else { @@ -411,7 +411,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // umbraco does not consistently guarantee that sortOrder starts with 0 // so the one that we want is the one with the smallest sortOrder // read http://stackoverflow.com/questions/1128745/how-can-i-use-xpath-to-find-the-minimum-value-of-an-attribute-in-a-set-of-elemen - + // so that one does not work, because min(@sortOrder) maybe 1 // xpath = "/root/*[@isDoc and @sortOrder='0']"; @@ -455,7 +455,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache else { xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlName, part); - + } } diff --git a/src/Umbraco.Web/RequestLifespanMessagesFactory.cs b/src/Umbraco.Web/RequestLifespanMessagesFactory.cs index ec3a8b2442..26ac3bd5df 100644 --- a/src/Umbraco.Web/RequestLifespanMessagesFactory.cs +++ b/src/Umbraco.Web/RequestLifespanMessagesFactory.cs @@ -8,21 +8,21 @@ namespace Umbraco.Web /// internal class RequestLifespanMessagesFactory : IEventMessagesFactory { - private readonly IUmbracoContextAccessor _ctxAccessor; + private readonly IHttpContextAccessor _httpAccessor; - public RequestLifespanMessagesFactory(IUmbracoContextAccessor ctxAccessor) + public RequestLifespanMessagesFactory(IHttpContextAccessor httpAccessor) { - if (ctxAccessor == null) throw new ArgumentNullException("ctxAccessor"); - _ctxAccessor = ctxAccessor; + if (httpAccessor == null) throw new ArgumentNullException("httpAccessor"); + _httpAccessor = httpAccessor; } public EventMessages Get() { - if (_ctxAccessor.Value.HttpContext.Items[typeof (RequestLifespanMessagesFactory).Name] == null) + if (_httpAccessor.Value.Items[typeof (RequestLifespanMessagesFactory).Name] == null) { - _ctxAccessor.Value.HttpContext.Items[typeof(RequestLifespanMessagesFactory).Name] = new EventMessages(); + _httpAccessor.Value.Items[typeof(RequestLifespanMessagesFactory).Name] = new EventMessages(); } - return (EventMessages)_ctxAccessor.Value.HttpContext.Items[typeof (RequestLifespanMessagesFactory).Name]; + return (EventMessages)_httpAccessor.Value.Items[typeof (RequestLifespanMessagesFactory).Name]; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/RouteDataExtensions.cs b/src/Umbraco.Web/RouteDataExtensions.cs new file mode 100644 index 0000000000..036d30c746 --- /dev/null +++ b/src/Umbraco.Web/RouteDataExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Web.Routing; + +namespace Umbraco.Web +{ + public static class RouteDataExtensions + { + /// + /// Tries to get the Umbraco context from the DataTokens + /// + /// + /// + /// + /// This is useful when working on async threads since the UmbracoContext is not copied over explicitly + /// + public static UmbracoContext GetUmbracoContext(this RouteData routeData) + { + if (routeData == null) throw new ArgumentNullException("routeData"); + + if (routeData.DataTokens.ContainsKey(Core.Constants.Web.UmbracoContextDataToken)) + { + var umbCtx = routeData.DataTokens[Core.Constants.Web.UmbracoContextDataToken] as UmbracoContext; + return umbCtx; + } + return null; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs b/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs index 2bb2903aa0..989cabe388 100644 --- a/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs +++ b/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Routing if (umbracoContext.HttpContext.Request.RequestContext == null) return null; if (umbracoContext.HttpContext.Request.RequestContext.RouteData == null) return null; if (umbracoContext.HttpContext.Request.RequestContext.RouteData.DataTokens == null) return null; - if (umbracoContext.HttpContext.Request.RequestContext.RouteData.DataTokens.ContainsKey("umbraco-custom-route") == false) return null; + if (umbracoContext.HttpContext.Request.RequestContext.RouteData.DataTokens.ContainsKey(Umbraco.Core.Constants.Web.CustomRouteDataToken) == false) return null; //ok so it's a custom route with published content assigned, check if the id being requested for is the same id as the assigned published content return id == umbracoContext.PublishedContentRequest.PublishedContent.Id ? umbracoContext.PublishedContentRequest.PublishedContent.Url diff --git a/src/Umbraco.Web/Routing/PublishedContentRequest.cs b/src/Umbraco.Web/Routing/PublishedContentRequest.cs index 78f4449c88..02ffc475d3 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequest.cs @@ -166,7 +166,8 @@ namespace Umbraco.Web.Routing /// preserve or reset the template, if any. public void SetInternalRedirectPublishedContent(IPublishedContent content) { - EnsureWriteable(); + if (content == null) throw new ArgumentNullException("content"); + EnsureWriteable(); // unless a template has been set already by the finder, // template should be null at that point. diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs index 65371ecba4..2e075d8ed1 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs @@ -515,11 +515,11 @@ namespace Umbraco.Web.Routing { // redirect to another page var node = _routingContext.UmbracoContext.ContentCache.GetById(internalRedirectId); - - _pcr.SetInternalRedirectPublishedContent(node); // don't use .PublishedContent here + if (node != null) { - redirect = true; + _pcr.SetInternalRedirectPublishedContent(node); // don't use .PublishedContent here + redirect = true; ProfilingLogger.Logger.Debug("{0}Redirecting to id={1}", () => tracePrefix, () => internalRedirectId); } else diff --git a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs index b7e71a3f66..3cdcf50074 100644 --- a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs +++ b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs @@ -73,6 +73,16 @@ namespace Umbraco.Web.Security.Identity /// internal bool ShouldAuthenticateRequest(IOwinContext ctx, Uri originalRequestUrl, bool checkForceAuthTokens = true) { + if (_umbracoContextAccessor.Value.Application.IsConfigured == false + && _umbracoContextAccessor.Value.Application.DatabaseContext.IsDatabaseConfigured == false) + { + //Do not authenticate the request if we don't have a db and we are not configured - since we will never need + // to know a current user in this scenario - we treat it as a new install. Without this we can have some issues + // when people have older invalid cookies on the same domain since our user managers might attempt to lookup a user + // and we don't even have a db. + return false; + } + var request = ctx.Request; var httpCtx = ctx.TryGetHttpContext(); diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs index f90faae7d3..ebe457eeb8 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs @@ -44,7 +44,7 @@ namespace Umbraco.Web.Security.Identity CookieName = securitySection.AuthCookieName; CookieHttpOnly = true; CookieSecure = forceSsl ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; - CookiePath = "/"; + CookiePath = "/"; //Custom cookie manager so we can filter requests CookieManager = new BackOfficeCookieManager(new SingletonUmbracoContextAccessor(), explicitPaths); @@ -84,12 +84,11 @@ namespace Umbraco.Web.Security.Identity if (ticket.Properties.IsPersistent) { - cookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; + cookieOptions.Expires = expiresUtc.UtcDateTime; } return cookieOptions; } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Services/ApplicationTreeService.cs b/src/Umbraco.Web/Services/ApplicationTreeService.cs index 4d3c767a79..9bfcdc2cac 100644 --- a/src/Umbraco.Web/Services/ApplicationTreeService.cs +++ b/src/Umbraco.Web/Services/ApplicationTreeService.cs @@ -77,7 +77,7 @@ namespace Umbraco.Web.Services // based on the ApplicationTreeRegistrar - and as noted there this is not an ideal way to do things but were stuck like this // currently because of the legacy assemblies and types not in the Core. - //Get all the trees not registered in the config + //Get all the trees not registered in the config (those not matching by alias casing will be detected as "unregistered") var unregistered = _allAvailableTrees.Value .Where(x => list.Any(l => l.Alias == x.Alias) == false) .ToArray(); @@ -86,10 +86,34 @@ namespace Umbraco.Web.Services if (hasChanges == false) return false; - //add the unregistered ones to the list and re-save the file if any changes were found + //add or edit the unregistered ones and re-save the file if any changes were found var count = 0; foreach (var tree in unregistered) { + var existingElement = doc.Root.Elements("add").SingleOrDefault(x => + string.Equals(x.Attribute("alias").Value, tree.Alias, + StringComparison.InvariantCultureIgnoreCase) && + string.Equals(x.Attribute("application").Value, tree.ApplicationAlias, + StringComparison.InvariantCultureIgnoreCase)); + if (existingElement != null) + { + existingElement.SetAttributeValue("alias", tree.Alias); + } + else + { + if (tree.Title.IsNullOrWhiteSpace()) + { + doc.Root.Add(new XElement("add", + new XAttribute("initialize", tree.Initialize), + new XAttribute("sortOrder", tree.SortOrder), + new XAttribute("alias", tree.Alias), + new XAttribute("application", tree.ApplicationAlias), + new XAttribute("iconClosed", tree.IconClosed), + new XAttribute("iconOpen", tree.IconOpened), + new XAttribute("type", tree.Type))); + } + else + { doc.Root.Add(new XElement("add", new XAttribute("initialize", tree.Initialize), new XAttribute("sortOrder", tree.SortOrder), @@ -99,6 +123,9 @@ namespace Umbraco.Web.Services new XAttribute("iconClosed", tree.IconClosed), new XAttribute("iconOpen", tree.IconOpened), new XAttribute("type", tree.Type))); + } + + } count++; } @@ -113,10 +140,7 @@ namespace Umbraco.Web.Services } } - return list; - - }, new TimeSpan(0, 10, 0)); } @@ -288,7 +312,7 @@ namespace Umbraco.Web.Services //remove the cache now that it has changed SD: I'm leaving this here even though it // is taken care of by events as well, I think unit tests may rely on it being cleared here. - _cache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); + _cache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); } } } @@ -324,13 +348,15 @@ namespace Umbraco.Web.Services { list.Add(new ApplicationTree( addElement.Attribute("initialize") == null || Convert.ToBoolean(addElement.Attribute("initialize").Value), - addElement.Attribute("sortOrder") != null ? Convert.ToByte(addElement.Attribute("sortOrder").Value) : (byte)0, - addElement.Attribute("application").Value, - addElement.Attribute("alias").Value, - addElement.Attribute("title").Value, - addElement.Attribute("iconClosed").Value, - addElement.Attribute("iconOpen").Value, - addElement.Attribute("type").Value)); + addElement.Attribute("sortOrder") != null + ? Convert.ToByte(addElement.Attribute("sortOrder").Value) + : (byte)0, + (string)addElement.Attribute("application"), + (string)addElement.Attribute("alias"), + (string)addElement.Attribute("title"), + (string)addElement.Attribute("iconClosed"), + (string)addElement.Attribute("iconOpen"), + (string)addElement.Attribute("type"))); } } diff --git a/src/Umbraco.Web/Services/SectionService.cs b/src/Umbraco.Web/Services/SectionService.cs index 07da567636..2659364b98 100644 --- a/src/Umbraco.Web/Services/SectionService.cs +++ b/src/Umbraco.Web/Services/SectionService.cs @@ -135,7 +135,7 @@ namespace Umbraco.Web.Services //remove the cache so it gets re-read ... SD: I'm leaving this here even though it // is taken care of by events as well, I think unit tests may rely on it being cleared here. - _cache.ClearCacheItem(CacheKeys.ApplicationsCacheKey); + _cache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationsCacheKey); } } } diff --git a/src/Umbraco.Web/SingletonHttpContextAccessor.cs b/src/Umbraco.Web/SingletonHttpContextAccessor.cs new file mode 100644 index 0000000000..cdeafa48e1 --- /dev/null +++ b/src/Umbraco.Web/SingletonHttpContextAccessor.cs @@ -0,0 +1,12 @@ +using System.Web; + +namespace Umbraco.Web +{ + internal class SingletonHttpContextAccessor : IHttpContextAccessor + { + public HttpContextBase Value + { + get { return new HttpContextWrapper(HttpContext.Current); } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs index 2a61d4177d..6c15814b92 100644 --- a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs +++ b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using System.Web; using Newtonsoft.Json; using Umbraco.Core; @@ -6,6 +8,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Services; using Umbraco.Core.Sync; using Umbraco.Web.Routing; +using Umbraco.Web.Scheduling; namespace Umbraco.Web.Strategies { @@ -22,74 +25,137 @@ namespace Umbraco.Web.Strategies /// public sealed class ServerRegistrationEventHandler : ApplicationEventHandler { - private readonly object _locko = new object(); private DatabaseServerRegistrar _registrar; - private DateTime _lastUpdated = DateTime.MinValue; + private BackgroundTaskRunner _backgroundTaskRunner; + private bool _started = false; // bind to events protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { _registrar = ServerRegistrarResolver.Current.Registrar as DatabaseServerRegistrar; + _backgroundTaskRunner = new BackgroundTaskRunner( + new BackgroundTaskRunnerOptions { AutoStart = true }, + applicationContext.ProfilingLogger.Logger); + // only for the DatabaseServerRegistrar if (_registrar == null) return; + //We will start the whole process when a successful request is made UmbracoModule.RouteAttempt += UmbracoModuleRouteAttempt; } - // handles route attempts. + /// + /// Handle when a request is made + /// + /// + /// + /// + /// We require this because: + /// - ApplicationContext.UmbracoApplicationUrl is initialized by UmbracoModule in BeginRequest + /// - RegisterServer is called on UmbracoModule.RouteAttempt which is triggered in ProcessRequest + /// we are safe, UmbracoApplicationUrl has been initialized + /// private void UmbracoModuleRouteAttempt(object sender, RoutableAttemptEventArgs e) { - if (e.HttpContext.Request == null || e.HttpContext.Request.Url == null) return; - switch (e.Outcome) { case EnsureRoutableOutcome.IsRoutable: // front-end request RegisterServer(e); + //remove handler, we're done + UmbracoModule.RouteAttempt -= UmbracoModuleRouteAttempt; break; case EnsureRoutableOutcome.NotDocumentRequest: // anything else (back-end request, service...) //so it's not a document request, we'll check if it's a back office request if (e.HttpContext.Request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath)) + { RegisterServer(e); - break; - /* - case EnsureRoutableOutcome.NotReady: - case EnsureRoutableOutcome.NotConfigured: - case EnsureRoutableOutcome.NoContent: - default: - // otherwise, do nothing - break; - */ + //remove handler, we're done + UmbracoModule.RouteAttempt -= UmbracoModuleRouteAttempt; + } + break; } } - - // register current server (throttled). + private void RegisterServer(UmbracoRequestEventArgs e) { - lock (_locko) // ensure we trigger only once - { - var secondsSinceLastUpdate = DateTime.Now.Subtract(_lastUpdated).TotalSeconds; - if (secondsSinceLastUpdate < _registrar.Options.ThrottleSeconds) return; - _lastUpdated = DateTime.Now; - } + //only process once + if (_started) return; + _started = true; + + var serverAddress = e.UmbracoContext.Application.UmbracoApplicationUrl; var svc = e.UmbracoContext.Application.Services.ServerRegistrationService; - // because - // - ApplicationContext.UmbracoApplicationUrl is initialized by UmbracoModule in BeginRequest - // - RegisterServer is called on UmbracoModule.RouteAttempt which is triggered in ProcessRequest - // we are safe, UmbracoApplicationUrl has been initialized - var serverAddress = e.UmbracoContext.Application.UmbracoApplicationUrl; + //Perform the rest async, we don't want to block the startup sequence + // this will just reoccur on a background thread + _backgroundTaskRunner.Add(new TouchServerTask(_backgroundTaskRunner, + 15000, //delay before first execution + _registrar.Options.RecurringSeconds * 1000, //amount of ms between executions + svc, _registrar, serverAddress)); + } - try + private class TouchServerTask : RecurringTaskBase + { + private readonly IServerRegistrationService _svc; + private readonly DatabaseServerRegistrar _registrar; + private readonly string _serverAddress; + + /// + /// Initializes a new instance of the class. + /// + /// The task runner. + /// The delay. + /// The period. + /// + /// + /// + /// The task will repeat itself periodically. Use this constructor to create a new task. + public TouchServerTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + IServerRegistrationService svc, DatabaseServerRegistrar registrar, string serverAddress) + : base(runner, delayMilliseconds, periodMilliseconds) { - svc.TouchServer(serverAddress, svc.CurrentServerIdentity, _registrar.Options.StaleServerTimeout); + if (svc == null) throw new ArgumentNullException("svc"); + _svc = svc; + _registrar = registrar; + _serverAddress = serverAddress; } - catch (Exception ex) + + public override bool IsAsync { - LogHelper.Error("Failed to update server record in database.", ex); + get { return false; } + } + + public override bool RunsOnShutdown + { + get { return false; } + } + + /// + /// Runs the background task. + /// + /// A value indicating whether to repeat the task. + public override bool PerformRun() + { + try + { + _svc.TouchServer(_serverAddress, _svc.CurrentServerIdentity, _registrar.Options.StaleServerTimeout); + + return true; // repeat + } + catch (Exception ex) + { + LogHelper.Error("Failed to update server record in database.", ex); + + return false; // probably stop if we have an error + } + } + + public override Task PerformRunAsync(CancellationToken token) + { + throw new NotImplementedException(); } } } diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index 7a0f0466ec..83e715a7db 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -1,13 +1,10 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Net.Http.Formatting; -using System.Text; -using System.Threading.Tasks; using umbraco; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; using Umbraco.Web.Models.Trees; using Umbraco.Web.WebApi.Filters; using Umbraco.Core.Services; @@ -65,6 +62,8 @@ namespace Umbraco.Web.Trees { var menu = new MenuItemCollection(); + var enableInheritedDocumentTypes = UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes; + if (id == Constants.System.Root.ToInvariantString()) { //set the default to create @@ -72,7 +71,14 @@ namespace Umbraco.Web.Trees // root actions menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionImport.Instance.Alias)), true).ConvertLegacyMenuItem(new UmbracoEntity + { + Id = int.Parse(id), + Level = 1, + ParentId = Constants.System.Root, + Name = "" + }, "documenttypes", "settings"); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); return menu; } @@ -87,14 +93,45 @@ namespace Umbraco.Web.Trees if (container.HasChildren() == false) { //can delete doc type - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); } - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), hasSeparator: true); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); } else { - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), hasSeparator: true); + var ct = Services.ContentTypeService.GetContentType(int.Parse(id)); + IContentType parent = null; + parent = ct == null ? null : Services.ContentTypeService.GetContentType(ct.ParentId); + + if (enableInheritedDocumentTypes) + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + + //no move action if this is a child doc type + if (parent == null) + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), true); + } + } + else + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias))); + //no move action if this is a child doc type + if (parent == null) + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), true); + } + } + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionExport.Instance.Alias)), true).ConvertLegacyMenuItem(new UmbracoEntity + { + Id = int.Parse(id), + Level = 1, + ParentId = Constants.System.Root, + Name = "" + }, "documenttypes", "settings"); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); + if (enableInheritedDocumentTypes) + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); } return menu; diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index 0a8d745a45..975a66195e 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Trees Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)] - [Tree(Constants.Applications.Members, Constants.Trees.Members)] + [Tree(Constants.Applications.Members, Constants.Trees.Members, null, sortOrder: 0)] [PluginController("UmbracoTrees")] [CoreTree] public class MemberTreeController : TreeController diff --git a/src/Umbraco.Web/Trees/TreeController.cs b/src/Umbraco.Web/Trees/TreeController.cs index aad0d8330d..0f975a9860 100644 --- a/src/Umbraco.Web/Trees/TreeController.cs +++ b/src/Umbraco.Web/Trees/TreeController.cs @@ -18,25 +18,23 @@ namespace Umbraco.Web.Trees /// public abstract class TreeController : TreeControllerBase { - private readonly TreeAttribute _attribute; + private TreeAttribute _attribute; protected TreeController() { - //Locate the tree attribute - var treeAttributes = GetType() - .GetCustomAttributes(typeof(TreeAttribute), false) - .OfType() - .ToArray(); - - if (treeAttributes.Any() == false) - { - throw new InvalidOperationException("The Tree controller is missing the " + typeof(TreeAttribute).FullName + " attribute"); - } - - //assign the properties of this object to those of the metadata attribute - _attribute = treeAttributes.First(); + Initialize(); } + protected TreeController(UmbracoContext umbracoContext) : base(umbracoContext) + { + Initialize(); + } + + protected TreeController(UmbracoContext umbracoContext, UmbracoHelper umbracoHelper) : base(umbracoContext, umbracoHelper) + { + Initialize(); + } + /// /// The name to display on the root node /// @@ -68,5 +66,21 @@ namespace Umbraco.Web.Trees get { return _attribute.Alias; } } + private void Initialize() + { + //Locate the tree attribute + var treeAttributes = GetType() + .GetCustomAttributes(typeof(TreeAttribute), false) + .OfType() + .ToArray(); + + if (treeAttributes.Any() == false) + { + throw new InvalidOperationException("The Tree controller is missing the " + typeof(TreeAttribute).FullName + " attribute"); + } + + //assign the properties of this object to those of the metadata attribute + _attribute = treeAttributes.First(); + } } } diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 74b2ffc670..91191a165d 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -18,6 +18,18 @@ namespace Umbraco.Web.Trees [AngularJsonOnlyConfiguration] public abstract class TreeControllerBase : UmbracoAuthorizedApiController { + protected TreeControllerBase() + { + } + + protected TreeControllerBase(UmbracoContext umbracoContext) : base(umbracoContext) + { + } + + protected TreeControllerBase(UmbracoContext umbracoContext, UmbracoHelper umbracoHelper) : base(umbracoContext, umbracoHelper) + { + } + /// /// The method called to render the contents of the tree structure /// diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 9c4ea749b4..0314c65fae 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -9,16 +9,17 @@ 'lib/angular/1.1.5/angular-cookies.min.js', 'lib/angular/1.1.5/angular-mobile.js', 'lib/angular/1.1.5/angular-sanitize.min.js', - + 'lib/angular/angular-ui-sortable.js', 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', 'lib/ng-file-upload/ng-file-upload.min.js', + 'lib/angular-local-storage/angular-local-storage.min.js', 'lib/bootstrap/js/bootstrap.2.3.2.min.js', 'lib/bootstrap-tabdrop/bootstrap-tabdrop.js', 'lib/umbraco/Extensions.js', - + 'lib/umbraco/NamespaceManager.js', 'lib/umbraco/LegacyUmbClientMgr.js', 'lib/umbraco/LegacySpeechBubble.js', diff --git a/src/Umbraco.Web/UI/Pages/ClientTools.cs b/src/Umbraco.Web/UI/Pages/ClientTools.cs index 215b4aeeb4..058af5a727 100644 --- a/src/Umbraco.Web/UI/Pages/ClientTools.cs +++ b/src/Umbraco.Web/UI/Pages/ClientTools.cs @@ -1,5 +1,6 @@ using Umbraco.Core.IO; using System.Web.UI; +using Umbraco.Core; namespace Umbraco.Web.UI.Pages { @@ -41,7 +42,12 @@ namespace Umbraco.Web.UI.Pages public static string ChangeContentFrameUrl(string url) { return string.Format(ClientMgrScript + ".contentFrame('{0}');", url); } - public static string ChildNodeCreated = GetMainTree + ".childNodeCreated();"; + public static string ReloadContentFrameUrlIfPathLoaded(string url) + { + return string.Format(ClientMgrScript + ".reloadContentFrameUrlIfPathLoaded('{0}');", url); + } + public static string ReloadLocation { get { return string.Format(ClientMgrScript + ".reloadLocation();"); } } + public static string ChildNodeCreated = GetMainTree + ".childNodeCreated();"; public static string SyncTree { get { return GetMainTree + ".syncTree('{0}', {1});"; } } public static string ClearTreeCache { get { return GetMainTree + ".clearTreeCache();"; } } public static string CopyNode { get { return GetMainTree + ".copyNode('{0}', '{1}');"; } } @@ -140,23 +146,57 @@ namespace Umbraco.Web.UI.Pages //don't load if there is no url if (string.IsNullOrEmpty(url)) return this; - if (url.StartsWith("/") && !url.StartsWith(IOHelper.ResolveUrl(SystemDirectories.Umbraco))) - url = IOHelper.ResolveUrl(SystemDirectories.Umbraco) + "/" + url; - - if (url.Trim().StartsWith("~")) - url = IOHelper.ResolveUrl(url); + url = EnsureUmbracoUrl(url); RegisterClientScript(Scripts.ChangeContentFrameUrl(url)); return this; } - /// - /// Shows the dashboard for the given application - /// - /// - /// - public ClientTools ShowDashboard(string app) + /// + /// Reloads the content in the content frame if the specified URL is currently loaded + /// + /// + public ClientTools ReloadContentFrameUrlIfPathLoaded(string url) + { + if (string.IsNullOrEmpty(url)) return this; + + url = EnsureUmbracoUrl(url); + + RegisterClientScript(Scripts.ReloadContentFrameUrlIfPathLoaded(url)); + + return this; + } + + /// + /// Reloads location, refreshing what is in the content frame + /// + public ClientTools ReloadLocation() + { + RegisterClientScript(Scripts.ReloadLocation); + + return this; + } + + private string EnsureUmbracoUrl(string url) + { + if (url.StartsWith("/") && url.StartsWith(IOHelper.ResolveUrl(SystemDirectories.Umbraco)) == false) + { + url = IOHelper.ResolveUrl(SystemDirectories.Umbraco).EnsureEndsWith('/') + url; + } + + if (url.Trim().StartsWith("~")) + url = IOHelper.ResolveUrl(url); + + return url; + } + + /// + /// Shows the dashboard for the given application + /// + /// + /// + public ClientTools ShowDashboard(string app) { return ChangeContentFrameUrl(SystemDirectories.Umbraco + string.Format("/dashboard.aspx?app={0}", app)); } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index c2bad0a5ca..008d7fc1de 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -99,10 +99,13 @@ ..\packages\AutoMapper.4.1.1\lib\net45\AutoMapper.dll + False + False True ..\packages\ClientDependency.1.8.4\lib\net45\ClientDependency.Core.dll + False True @@ -114,6 +117,7 @@ ..\packages\Examine.0.1.68.0\lib\Examine.dll + False True @@ -148,6 +152,11 @@ False ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + + ..\packages\Markdown.1.14.4\lib\net45\MarkdownSharp.dll + False + True + False ..\packages\Microsoft.AspNet.Identity.Core.2.2.1\lib\net45\Microsoft.AspNet.Identity.Core.dll @@ -178,8 +187,9 @@ ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll - True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + False + True ..\packages\MiniProfiler.3.2.0.157\lib\net40\MiniProfiler.dll @@ -187,6 +197,7 @@ ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + False True @@ -230,6 +241,7 @@ ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll + False True @@ -246,6 +258,7 @@ ..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll + False True @@ -253,14 +266,17 @@ ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll + False True ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll + False True ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll + False True @@ -298,9 +314,10 @@ {511F6D8D-7717-440A-9A57-A507E9A8B27F} {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - + + ..\packages\UrlRewritingNet.UrlRewriter.2.0.7\lib\UrlRewritingNet.UrlRewriter.dll False - ..\packages\UrlRewritingNet.UrlRewriter.2.0.60829.1\lib\UrlRewritingNet.UrlRewriter.dll + False @@ -313,6 +330,12 @@ + + + + + + @@ -357,21 +380,35 @@ - + + + + + + + + + + + + + + + @@ -386,6 +423,7 @@ + @@ -710,6 +748,7 @@ + @@ -1896,7 +1935,6 @@ umbraco_org_umbraco_update_CheckForUpgrade - 11.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v11.0 diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 6804673484..099aa33b76 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web /// public class UmbracoContext : DisposableObject, IDisposeOnRequestEnd { - private const string HttpContextItemName = "Umbraco.Web.UmbracoContext"; + internal const string HttpContextItemName = "Umbraco.Web.UmbracoContext"; private static readonly object Locker = new object(); private bool _replacing; @@ -124,6 +124,7 @@ namespace Umbraco.Web if (umbracoSettings == null) throw new ArgumentNullException("umbracoSettings"); if (urlProviders == null) throw new ArgumentNullException("urlProviders"); + //if there's already a singleton, and we're not replacing then there's no need to ensure anything if (UmbracoContext.Current != null) { if (replaceContext == false) @@ -131,6 +132,39 @@ namespace Umbraco.Web UmbracoContext.Current._replacing = true; } + var umbracoContext = CreateContext(httpContext, applicationContext, webSecurity, umbracoSettings, urlProviders, preview); + + //assign the singleton + UmbracoContext.Current = umbracoContext; + return UmbracoContext.Current; + } + + /// + /// Creates a standalone UmbracoContext instance + /// + /// + /// + /// + /// + /// + /// + /// + /// A new instance of UmbracoContext + /// + public static UmbracoContext CreateContext( + HttpContextBase httpContext, + ApplicationContext applicationContext, + WebSecurity webSecurity, + IUmbracoSettingsSection umbracoSettings, + IEnumerable urlProviders, + bool? preview) + { + if (httpContext == null) throw new ArgumentNullException("httpContext"); + if (applicationContext == null) throw new ArgumentNullException("applicationContext"); + if (webSecurity == null) throw new ArgumentNullException("webSecurity"); + if (umbracoSettings == null) throw new ArgumentNullException("umbracoSettings"); + if (urlProviders == null) throw new ArgumentNullException("urlProviders"); + var umbracoContext = new UmbracoContext( httpContext, applicationContext, @@ -141,15 +175,15 @@ namespace Umbraco.Web // create the RoutingContext, and assign var routingContext = new RoutingContext( umbracoContext, - + //TODO: Until the new cache is done we can't really expose these to override/mock new Lazy>(() => ContentFinderResolver.Current.Finders), new Lazy(() => ContentLastChanceFinderResolver.Current.Finder), - + // create the nice urls provider // there's one per request because there are some behavior parameters that can be changed new Lazy( - () => new UrlProvider( + () => new UrlProvider( umbracoContext, umbracoSettings.WebRouting, urlProviders), @@ -158,9 +192,7 @@ namespace Umbraco.Web //assign the routing context back umbracoContext.RoutingContext = routingContext; - //assign the singleton - UmbracoContext.Current = umbracoContext; - return UmbracoContext.Current; + return umbracoContext; } /// @@ -459,17 +491,9 @@ namespace Umbraco.Web protected override void DisposeResources() { Security.DisposeIfDisposable(); - Security = null; - _umbracoContext = null; - //Before we set these to null but in fact these are application lifespan singletons so - //there's no reason we need to set them to null and this also caused a problem with packages - //trying to access the cache properties on RequestEnd. - //http://issues.umbraco.org/issue/U4-2734 - //http://our.umbraco.org/projects/developer-tools/301-url-tracker/version-2/44327-Issues-with-URL-Tracker-in-614 - //ContentCache = null; - //MediaCache = null; - //Application = null; + //If not running in a web ctx, ensure the thread based instance is nulled + _umbracoContext = null; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoContextExtensions.cs b/src/Umbraco.Web/UmbracoContextExtensions.cs index ec85b4dad6..8cd38df6d1 100644 --- a/src/Umbraco.Web/UmbracoContextExtensions.cs +++ b/src/Umbraco.Web/UmbracoContextExtensions.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Web; +using Umbraco.Core; using Umbraco.Core.Events; namespace Umbraco.Web @@ -11,6 +13,38 @@ namespace Umbraco.Web /// public static class UmbracoContextExtensions { + /// + /// tries to get the Umbraco context from the HttpContext + /// + /// + /// + /// + /// This is useful when working on async threads since the UmbracoContext is not copied over explicitly + /// + public static UmbracoContext GetUmbracoContext(this HttpContext http) + { + return GetUmbracoContext(new HttpContextWrapper(http)); + } + + /// + /// tries to get the Umbraco context from the HttpContext + /// + /// + /// + /// + /// This is useful when working on async threads since the UmbracoContext is not copied over explicitly + /// + public static UmbracoContext GetUmbracoContext(this HttpContextBase http) + { + if (http == null) throw new ArgumentNullException("http"); + + if (http.Items.Contains(UmbracoContext.HttpContextItemName)) + { + var umbCtx = http.Items[UmbracoContext.HttpContextItemName] as UmbracoContext; + return umbCtx; + } + return null; + } /// /// If there are event messages in the current request this will return them , otherwise it will return null diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index c44a65652b..eb7532bb0e 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -61,12 +61,12 @@ namespace Umbraco.Web legacyRequestInitializer.InitializeRequest(); // create the UmbracoContext singleton, one per request, and assign - // NOTE: we assign 'true' to ensure the context is replaced if it is already set (i.e. during app startup) + // NOTE: we assign 'true' to ensure the context is replaced if it is already set (i.e. during app startup) UmbracoContext.EnsureContext( - httpContext, - ApplicationContext.Current, - new WebSecurity(httpContext, ApplicationContext.Current), - true); + httpContext, + ApplicationContext.Current, + new WebSecurity(httpContext, ApplicationContext.Current), + true); } /// @@ -74,12 +74,12 @@ namespace Umbraco.Web /// /// /// - /// + /// /// This will check if we are trying to route to the default back office page (i.e. ~/Umbraco/ or ~/Umbraco or ~/Umbraco/Default ) - /// and ensure that the MVC handler executes for that. This is required because the route for /Umbraco will never execute because + /// and ensure that the MVC handler executes for that. This is required because the route for /Umbraco will never execute because /// files/folders exist there and we cannot set the RouteCollection.RouteExistingFiles = true since that will muck a lot of other things up. /// So we handle it here and explicitly execute the MVC controller. - /// + /// /// void ProcessRequest(HttpContextBase httpContext) { @@ -99,7 +99,7 @@ namespace Umbraco.Web { if (EnsureIsConfigured(httpContext, umbracoContext.OriginalRequestUrl)) { - RewriteToBackOfficeHandler(httpContext); + RewriteToBackOfficeHandler(httpContext); } return; } @@ -113,7 +113,7 @@ namespace Umbraco.Web { return; } - + httpContext.Trace.Write("UmbracoModule", "Umbraco request confirmed"); @@ -143,10 +143,10 @@ namespace Umbraco.Web #endregion #region Methods - + /// /// Checks the current request and ensures that it is routable based on the structure of the request and URI - /// + /// /// /// /// @@ -165,12 +165,12 @@ namespace Umbraco.Web else if (!EnsureIsReady(httpContext, uri)) { reason = EnsureRoutableOutcome.NotReady; - } + } // ensure Umbraco is properly configured to serve documents else if (!EnsureIsConfigured(httpContext, uri)) { reason = EnsureRoutableOutcome.NotConfigured; - } + } // ensure Umbraco has documents to serve else if (!EnsureHasContent(context, httpContext)) { @@ -214,7 +214,8 @@ namespace Umbraco.Web // if the path contains an extension that is not .aspx // then it cannot be a document request - if (maybeDoc && lpath.Contains('.') && !lpath.EndsWith(".aspx")) + var extension = Path.GetExtension(lpath); + if (maybeDoc && extension.IsNullOrWhiteSpace() == false && extension != ".aspx") maybeDoc = false; // at that point, either we have no extension, or it is .aspx @@ -255,7 +256,7 @@ namespace Umbraco.Web httpContext.Response.StatusCode = 503; var bootUrl = "~/config/splashes/booting.aspx"; - + httpContext.RewritePath(UriUtility.ToAbsolute(bootUrl) + "?url=" + HttpUtility.UrlEncode(uri.ToString())); return false; @@ -274,7 +275,7 @@ namespace Umbraco.Web return true; LogHelper.Warn("Umbraco has no content"); - + const string noContentUrl = "~/config/splashes/noNodes.aspx"; httpContext.RewritePath(UriUtility.ToAbsolute(noContentUrl)); @@ -331,7 +332,7 @@ namespace Umbraco.Web response.TrySkipIisCustomErrors = UmbracoConfig.For.UmbracoSettings().WebRouting.TrySkipIisCustomErrors; if (response.TrySkipIisCustomErrors == false) - LogHelper.Warn("Status code is 404 yet TrySkipIisCustomErrors is false - IIS will take over."); + LogHelper.Warn("Status code is 404 yet TrySkipIisCustomErrors is false - IIS will take over."); } if (pcr.ResponseStatusCode > 0) @@ -367,8 +368,8 @@ namespace Umbraco.Web // rewrite the path to the path of the handler (i.e. /umbraco/RenderMvc) context.RewritePath(rewritePath, "", "", false); - //if it is MVC we need to do something special, we are not using TransferRequest as this will - //require us to rewrite the path with query strings and then reparse the query strings, this would + //if it is MVC we need to do something special, we are not using TransferRequest as this will + //require us to rewrite the path with query strings and then reparse the query strings, this would //also mean that we need to handle IIS 7 vs pre-IIS 7 differently. Instead we are just going to create //an instance of the UrlRoutingModule and call it's PostResolveRequestCache method. This does: // * Looks up the route based on the new rewritten URL @@ -383,7 +384,7 @@ namespace Umbraco.Web /// /// Rewrites to the Umbraco handler - we always send the request via our MVC rendering engine, this will deal with /// requests destined for webforms. - /// + /// /// /// private static void RewriteToUmbracoHandler(HttpContextBase context, PublishedContentRequest pcr) @@ -400,8 +401,8 @@ namespace Umbraco.Web // rewrite the path to the path of the handler (i.e. /umbraco/RenderMvc) context.RewritePath(rewritePath, "", query, false); - //if it is MVC we need to do something special, we are not using TransferRequest as this will - //require us to rewrite the path with query strings and then reparse the query strings, this would + //if it is MVC we need to do something special, we are not using TransferRequest as this will + //require us to rewrite the path with query strings and then reparse the query strings, this would //also mean that we need to handle IIS 7 vs pre-IIS 7 differently. Instead we are just going to create //an instance of the UrlRoutingModule and call it's PostResolveRequestCache method. This does: // * Looks up the route based on the new rewritten URL @@ -413,7 +414,7 @@ namespace Umbraco.Web urlRouting.PostResolveRequestCache(context); } - + /// /// Any object that is in the HttpContext.Items collection that is IDisposable will get disposed on the end of the request /// @@ -425,7 +426,7 @@ namespace Umbraco.Web return; //get a list of keys to dispose - var keys = new HashSet(); + var keys = new HashSet(); foreach (DictionaryEntry i in http.Items) { if (i.Value is IDisposeOnRequestEnd || i.Key is IDisposeOnRequestEnd) @@ -460,7 +461,7 @@ namespace Umbraco.Web #region IHttpModule /// - /// Initialize the module, this will trigger for each new application + /// Initialize the module, this will trigger for each new application /// and there may be more than 1 application per application domain /// /// @@ -474,7 +475,7 @@ namespace Umbraco.Web }; //disable asp.net headers (security) - // This is the correct place to modify headers according to MS: + // This is the correct place to modify headers according to MS: // https://our.umbraco.org/forum/umbraco-7/using-umbraco-7/65241-Heap-error-from-header-manipulation?p=0#comment220889 app.PostReleaseRequestState += (sender, args) => { @@ -501,7 +502,7 @@ namespace Umbraco.Web app.EndRequest += (sender, args) => { - var httpContext = ((HttpApplication)sender).Context; + var httpContext = ((HttpApplication)sender).Context; if (UmbracoContext.Current != null && UmbracoContext.Current.IsFrontEndUmbracoRequest) { LogHelper.Debug( @@ -535,7 +536,7 @@ namespace Umbraco.Web { if (EndRequest != null) EndRequest(this, args); - } + } #endregion @@ -567,7 +568,7 @@ namespace Umbraco.Web } return allRoutes; - }); + }); /// /// This is used internally to track any registered callback paths for Identity providers. If the request path matches diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index c42b46fa41..629d69ed4a 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -1,7 +1,10 @@ using System; using System.Linq; +using System.Web; using System.Web.Mvc; using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models; using Umbraco.Web.Mvc; namespace Umbraco.Web @@ -12,6 +15,209 @@ namespace Umbraco.Web public static class UrlHelperRenderExtensions { + #region GetCropUrl + + /// + /// Gets the ImageProcessor Url of a media item by the crop alias (using default media item property alias of "umbracoFile") + /// + /// + /// + /// The IPublishedContent item. + /// + /// + /// The crop alias e.g. thumbnail + /// + /// + /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be + /// set to false if using the result of this method for CSS. + /// + /// + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent mediaItem, string cropAlias, bool htmlEncode = true) + { + var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + /// + /// Gets the ImageProcessor Url by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. + /// + /// + /// + /// The IPublishedContent item. + /// + /// + /// The property alias of the property containing the Json data e.g. umbracoFile + /// + /// + /// The crop alias e.g. thumbnail + /// + /// + /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be + /// set to false if using the result of this method for CSS. + /// + /// + /// The ImageProcessor.Web Url. + /// + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true) + { + var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + /// + /// Gets the ImageProcessor Url from the image path. + /// + /// + /// The IPublishedContent item. + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// Property alias of the property containing the Json data. + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters + /// + /// + /// Add a serialised date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// The further options. + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// + /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be + /// set to false if using the result of this method for CSS. + /// + /// + /// The . + /// + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, + IPublishedContent mediaItem, + int? width = null, + int? height = null, + string propertyAlias = Umbraco.Core.Constants.Conventions.Media.File, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true, + bool htmlEncode = true) + { + var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, + upScale); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + /// + /// Gets the ImageProcessor Url from the image path. + /// + /// + /// The image url. + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// The Json data from the Umbraco Core Image Cropper property editor + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters + /// + /// + /// Add a serialised date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// The further options. + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// + /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be + /// set to false if using the result of this method for CSS. + /// + /// + /// The . + /// + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, + string imageUrl, + int? width = null, + int? height = null, + string imageCropperValue = null, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true, + bool htmlEncode = true) + { + var url = imageUrl.GetCropUrl(width, height, imageCropperValue, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, + upScale); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + #endregion + /// /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified SurfaceController /// diff --git a/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs new file mode 100644 index 0000000000..ae10d073cd --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs @@ -0,0 +1,38 @@ +using System; +using System.Net.Http; +using System.Web.Http.Filters; +using Umbraco.Core; +using Umbraco.Web.Editors; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Used to emit outgoing editor model events + /// + internal sealed class OutgoingEditorModelEventAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) + { + if (actionExecutedContext.Response == null) return; + + var user = UmbracoContext.Current.Security.CurrentUser; + if (user == null) return; + + var objectContent = actionExecutedContext.Response.Content as ObjectContent; + if (objectContent != null) + { + var model = objectContent.Value; + + if (model != null) + { + EditorModelEventManager.EmitEvent(actionExecutedContext, new EditorModelEventArgs( + (dynamic)model, + UmbracoContext.Current)); + } + } + + base.OnActionExecuted(actionExecutedContext); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs index 8912ca68ca..c42881f52a 100644 --- a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs @@ -5,23 +5,13 @@ using Umbraco.Core.Security; namespace Umbraco.Web.WebApi.Filters { - [Obsolete("This is no longer used and will be removed from the codebase in the future, use OWIN IAuthenticationManager.SignOut instead")] + [Obsolete("This is no longer used and will be removed from the codebase in the future, use OWIN IAuthenticationManager.SignOut instead", true)] [EditorBrowsable(EditorBrowsableState.Never)] public sealed class UmbracoBackOfficeLogoutAttribute : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext context) - { - if (context.Response == null) return; - if (context.Response.IsSuccessStatusCode == false) return; - - //this clears out all of our cookies - context.Response.UmbracoLogoutWebApi(); - - //this calls the underlying owin sign out logic - which should call the - // auth providers middleware callbacks if using custom auth middleware - context.Request.TryGetOwinContext().Result.Authentication.SignOut( - Core.Constants.Security.BackOfficeAuthenticationType, - Core.Constants.Security.BackOfficeExternalAuthenticationType); + { + throw new NotSupportedException("This method is not supported and should not be used, it has been removed in Umbraco 7.4"); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/UmbracoApiController.cs b/src/Umbraco.Web/WebApi/UmbracoApiController.cs index d93c070cf4..b850532011 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiController.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.WebApi /// The base class for auto-routed API controllers for Umbraco /// public abstract class UmbracoApiController : UmbracoApiControllerBase - { + { protected UmbracoApiController() { } @@ -20,5 +20,9 @@ namespace Umbraco.Web.WebApi protected UmbracoApiController(UmbracoContext umbracoContext) : base(umbracoContext) { } + + protected UmbracoApiController(UmbracoContext umbracoContext, UmbracoHelper umbracoHelper) : base(umbracoContext, umbracoHelper) + { + } } } diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index ad29726d4e..924ec8bd2e 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -22,14 +22,16 @@ namespace Umbraco.Web.WebApi { protected UmbracoAuthorizedApiController() { - } - protected UmbracoAuthorizedApiController(UmbracoContext umbracoContext) - : base(umbracoContext) + protected UmbracoAuthorizedApiController(UmbracoContext umbracoContext) : base(umbracoContext) { } - + + protected UmbracoAuthorizedApiController(UmbracoContext umbracoContext, UmbracoHelper umbracoHelper) : base(umbracoContext, umbracoHelper) + { + } + private bool _userisValidated = false; /// diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 20cfc514fb..907a31cebe 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -13,6 +13,7 @@ using ClientDependency.Core.Config; using Examine; using Examine.Config; using LightInject; +using Examine.Providers; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; @@ -35,8 +36,10 @@ using Umbraco.Web.Security; using Umbraco.Web.UI.JavaScript; using Umbraco.Web.WebApi; using Umbraco.Core.Events; +using Umbraco.Core.Cache; using Umbraco.Core.Services; using Umbraco.Web.Services; +using Umbraco.Web.Editors; using Umbraco.Core.DependencyInjection; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using ProfilingViewEngine = Umbraco.Core.Profiling.ProfilingViewEngine; @@ -46,13 +49,13 @@ using TypeHelper = Umbraco.Core.TypeHelper; namespace Umbraco.Web { /// - /// A bootstrapper for the Umbraco application which initializes all objects including the Web portion of the application + /// A bootstrapper for the Umbraco application which initializes all objects including the Web portion of the application /// public class WebBootManager : CoreBootManager { private readonly bool _isForTesting; //NOTE: see the Initialize method for what this is used for - private readonly List _indexesToRebuild = new List(); + private static readonly List IndexesToRebuild = new List(); public WebBootManager(UmbracoApplicationBase umbracoApplication) : base(umbracoApplication) @@ -108,7 +111,7 @@ namespace Umbraco.Web { "compositeFileHandlerPath", ClientDependencySettings.Instance.CompositeFileHandlerPath } }); ClientDependencySettings.Instance.MvcRendererCollection.Add(renderer); - + // Disable the X-AspNetMvc-Version HTTP Header MvcHandler.DisableMvcResponseHeader = true; @@ -119,7 +122,7 @@ namespace Umbraco.Web } /// - /// Override this method in order to ensure that the UmbracoContext is also created, this can only be + /// Override this method in order to ensure that the UmbracoContext is also created, this can only be /// created after resolution is frozen! /// protected override void FreezeResolution() @@ -133,8 +136,8 @@ namespace Umbraco.Web httpContext, ApplicationContext, new WebSecurity(httpContext, ApplicationContext), - UmbracoConfig.For.UmbracoSettings(), - UrlProviderResolver.Current.Providers, + UmbracoConfig.For.UmbracoSettings(), + UrlProviderResolver.Current.Providers, false); } @@ -161,13 +164,13 @@ namespace Umbraco.Web base.Complete(afterComplete); - //Ok, now that everything is complete we'll check if we've stored any references to index that need rebuilding and run them + //Ok, now that everything is complete we'll check if we've stored any references to index that need rebuilding and run them // (see the initialize method for notes) - we'll ensure we remove the event handler too in case examine manager doesn't actually // initialize during startup, in which case we want it to rebuild the indexes itself. ExamineManager.Instance.BuildingEmptyIndexOnStartup -= OnInstanceOnBuildingEmptyIndexOnStartup; - if (_indexesToRebuild.Any()) + if (IndexesToRebuild.Any()) { - foreach (var indexer in _indexesToRebuild) + foreach (var indexer in IndexesToRebuild) { indexer.RebuildIndex(); } @@ -198,7 +201,19 @@ namespace Umbraco.Web protected override CacheHelper CreateApplicationCache() { //create a web-based cache helper - return new CacheHelper(); + var cacheHelper = new CacheHelper( + //we need to have the dep clone runtime cache provider to ensure + //all entities are cached properly (cloned in and cloned out) + new DeepCloneRuntimeCacheProvider(new HttpRuntimeCacheProvider(HttpRuntime.Cache)), + new StaticCacheProvider(), + //we have no request based cache when not running in web-based context + new NullCacheProvider(), + new IsolatedRuntimeCache(type => + //we need to have the dep clone runtime cache provider to ensure + //all entities are cached properly (cloned in and cloned out) + new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); + + return cacheHelper; } /// @@ -281,7 +296,7 @@ namespace Umbraco.Web { route.DataTokens = new RouteValueDictionary(); } - route.DataTokens.Add("umbraco", "api"); //ensure the umbraco token is set + route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "api"); //ensure the umbraco token is set } private void RouteLocalSurfaceController(Type controller, string umbracoPath) @@ -292,7 +307,7 @@ namespace Umbraco.Web umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}",//url to match new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, new[] { meta.ControllerNamespace }); //look in this namespace to create the controller - route.DataTokens.Add("umbraco", "surface"); //ensure the umbraco token is set + route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); //ensure the umbraco token is set route.DataTokens.Add("UseNamespaceFallback", false); //Don't look anywhere else except this namespace! //make it use our custom/special SurfaceMvcHandler route.RouteHandler = new SurfaceRouteHandler(); @@ -337,7 +352,7 @@ namespace Umbraco.Web } /// - /// Initializes all web based and core resolves + /// Initializes all web based and core resolves /// protected override void InitializeResolvers() { @@ -345,6 +360,8 @@ namespace Umbraco.Web XsltExtensionsResolver.Current = new XsltExtensionsResolver(ServiceProvider, ProfilingLogger.Logger, () => PluginManager.ResolveXsltExtensions()); + EditorValidationResolver.Current= new EditorValidationResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolveTypes()); + //set the default RenderMvcController DefaultRenderMvcControllerResolver.Current = new DefaultRenderMvcControllerResolver(typeof(RenderMvcController)); @@ -356,7 +373,7 @@ namespace Umbraco.Web //set the legacy one by default - this maintains backwards compat ServerMessengerResolver.Current.SetServerMessenger(new BatchedWebServiceServerMessenger(() => { - //we should not proceed to change this if the app/database is not configured since there will + //we should not proceed to change this if the app/database is not configured since there will // be no user, plus we don't need to have server messages sent if this is the case. if (ApplicationContext.IsConfigured && ApplicationContext.DatabaseContext.IsDatabaseConfigured) { @@ -384,49 +401,40 @@ namespace Umbraco.Web else { - // NOTE: This is IMPORTANT! ... we don't want to rebuild any index that is already flagged to be re-indexed - // on startup based on our _indexesToRebuild variable and how Examine auto-rebuilds when indexes are empty - // this callback is used below for the DatabaseServerMessenger startup options + //We are using a custom action here so we can check the examine settings value first, we don't want to + // put that check into the CreateIndexesOnColdBoot method because developers may choose to use this + // method directly and they will be in charge of this check if they need it Action rebuildIndexes = () => { - //If the developer has explicitly opted out of rebuilding indexes on startup then we + //If the developer has explicitly opted out of rebuilding indexes on startup then we // should adhere to that and not do it, this means that if they are load balancing things will be // out of sync if they are auto-scaling but there's not much we can do about that. if (ExamineSettings.Instance.RebuildOnAppStart == false) return; - if (_indexesToRebuild.Any()) + foreach (var indexer in GetIndexesForColdBoot()) { - var otherIndexes = ExamineManager.Instance.IndexProviderCollection.Except(_indexesToRebuild); - foreach (var otherIndex in otherIndexes) - { - otherIndex.RebuildIndex(); - } - } - else - { - //rebuild them all - ExamineManager.Instance.RebuildIndex(); + indexer.RebuildIndex(); } }; ServerMessengerResolver.Current.SetServerMessenger(new BatchedDatabaseServerMessenger( - ApplicationContext, - true, - //Default options for web including the required callbacks to build caches - new DatabaseServerMessengerOptions - { - //These callbacks will be executed if the server has not been synced - // (i.e. it is a new server or the lastsynced.txt file has been removed) - InitializingCallbacks = new Action[] + ApplicationContext, + true, + //Default options for web including the required callbacks to build caches + new DatabaseServerMessengerOptions { - //rebuild the xml cache file if the server is not synced - () => global::umbraco.content.Instance.RefreshContentFromDatabase(), - //rebuild indexes if the server is not synced - // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific - // indexes then they can adjust this logic themselves. - rebuildIndexes - } - })); + //These callbacks will be executed if the server has not been synced + // (i.e. it is a new server or the lastsynced.txt file has been removed) + InitializingCallbacks = new Action[] + { + //rebuild the xml cache file if the server is not synced + () => global::umbraco.content.Instance.RefreshContentFromDatabase(), + //rebuild indexes if the server is not synced + // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific + // indexes then they can adjust this logic themselves. + rebuildIndexes + } + })); } SurfaceControllerResolver.Current = new SurfaceControllerResolver( @@ -446,6 +454,7 @@ namespace Umbraco.Web // same for other converters PropertyValueConvertersResolver.Current.RemoveType(); PropertyValueConvertersResolver.Current.RemoveType(); + PropertyValueConvertersResolver.Current.RemoveType(); PublishedCachesResolver.Current = new PublishedCachesResolver(Container, typeof(PublishedCaches)); @@ -514,7 +523,7 @@ namespace Umbraco.Web ViewEngines.Engines.Add(new PluginViewEngine()); //set model binder - ModelBinders.Binders.Add(new KeyValuePair(typeof(RenderModel), new RenderModelBinder())); + ModelBinderProviders.BinderProviders.Add(new RenderModelBinder()); // is a provider ////add the profiling action filter //GlobalFilters.Filters.Add(new ProfilingActionFilter()); @@ -522,12 +531,42 @@ namespace Umbraco.Web GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(GlobalConfiguration.Configuration)); } + + + /// + /// The method used to create indexes on a cold boot + /// + /// + /// A cold boot is when the server determines it will not (or cannot) process instructions in the cache table and + /// will rebuild it's own caches itself. + /// + public static IEnumerable GetIndexesForColdBoot() + { + // NOTE: This is IMPORTANT! ... we don't want to rebuild any index that is already flagged to be re-indexed + // on startup based on our _indexesToRebuild variable and how Examine auto-rebuilds when indexes are empty. + // This callback is used above for the DatabaseServerMessenger startup options. + + // all indexes + IEnumerable indexes = ExamineManager.Instance.IndexProviderCollection; + + // except those that are already flagged + // and are processed in Complete() + if (IndexesToRebuild.Any()) + indexes = indexes.Except(IndexesToRebuild); + + // return + foreach (var index in indexes) + yield return index; + } + + private void OnInstanceOnBuildingEmptyIndexOnStartup(object sender, BuildingEmptyIndexOnStartupEventArgs args) { - //store the indexer that needs rebuilding because it's empty for when the boot process + //store the indexer that needs rebuilding because it's empty for when the boot process // is complete and cancel this current event so the rebuild process doesn't start right now. args.Cancel = true; - _indexesToRebuild.Add(args.Indexer); + IndexesToRebuild.Add((BaseIndexProvider)args.Indexer); } } } + diff --git a/src/Umbraco.Web/WebServices/SaveFileController.cs b/src/Umbraco.Web/WebServices/SaveFileController.cs index 80c394780c..ab14f80a53 100644 --- a/src/Umbraco.Web/WebServices/SaveFileController.cs +++ b/src/Umbraco.Web/WebServices/SaveFileController.cs @@ -156,8 +156,8 @@ namespace Umbraco.Web.WebServices { t = new Template(templateId) { - Text = templateName.CleanForXss('[', ']', '(', ')'), - Alias = templateAlias.CleanForXss('[', ']', '(', ')'), + Text = templateName.CleanForXss('[', ']', '(', ')', ':'), + Alias = templateAlias.CleanForXss('[', ']', '(', ')', ':'), Design = templateContents }; diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index ed81be3d76..b22330600c 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -11,6 +11,7 @@ + @@ -31,6 +32,6 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 6fa1e0fcea..a29f61c984 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -189,7 +189,7 @@ namespace umbraco // check if document *is* published, it could be unpublished by an event if (d.Published) { - var parentId = d.Level == 1 ? -1 : d.Parent.Id; + var parentId = d.Level == 1 ? -1 : d.ParentId; // fix sortOrder - see note in UpdateSortOrder var node = GetPreviewOrPublishedNode(d, xmlContentCopy, false); @@ -272,7 +272,7 @@ namespace umbraco ClearContextCache(); var cachedFieldKeyStart = string.Format("{0}{1}_", CacheKeys.ContentItemCacheKey, d.Id); - ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch(cachedFieldKeyStart); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(cachedFieldKeyStart); FireAfterUpdateDocumentCache(d, e); } diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs index b659ea42cc..39086ad1c7 100644 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ b/src/Umbraco.Web/umbraco.presentation/library.cs @@ -441,11 +441,11 @@ namespace umbraco { if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration > 0) { - var xml = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var xml = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( string.Format( "{0}_{1}_{2}", CacheKeys.MediaCacheKey, MediaId, Deep), - TimeSpan.FromSeconds(UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration), - () => GetMediaDo(MediaId, Deep)); + timeout: TimeSpan.FromSeconds(UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration), + getCacheItem: () => GetMediaDo(MediaId, Deep)); if (xml != null) { @@ -501,11 +501,11 @@ namespace umbraco { if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration > 0) { - var xml = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var xml = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( string.Format( "{0}_{1}", CacheKeys.MemberLibraryCacheKey, MemberId), - TimeSpan.FromSeconds(UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration), - () => GetMemberDo(MemberId)); + timeout: TimeSpan.FromSeconds(UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration), + getCacheItem: () => GetMemberDo(MemberId)); if (xml != null) { diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs index b1c002fe50..bcfaae4723 100644 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/macro.cs @@ -455,11 +455,11 @@ namespace umbraco } //insert the cache string result - ApplicationContext.Current.ApplicationCache.InsertCacheItem( + ApplicationContext.Current.ApplicationCache.RuntimeCache.InsertCacheItem( CacheKeys.MacroHtmlCacheKey + Model.CacheIdentifier, - CacheItemPriority.NotRemovable, - new TimeSpan(0, 0, Model.CacheDuration), - () => outputCacheString); + priority: CacheItemPriority.NotRemovable, + timeout: new TimeSpan(0, 0, Model.CacheDuration), + getCacheItem: () => outputCacheString); dateAddedCacheKey = CacheKeys.MacroHtmlDateAddedCacheKey + Model.CacheIdentifier; @@ -474,11 +474,11 @@ namespace umbraco else { //insert the cache control result - ApplicationContext.Current.ApplicationCache.InsertCacheItem( + ApplicationContext.Current.ApplicationCache.RuntimeCache.InsertCacheItem( CacheKeys.MacroControlCacheKey + Model.CacheIdentifier, - CacheItemPriority.NotRemovable, - new TimeSpan(0, 0, Model.CacheDuration), - () => new MacroCacheContent(macroControl, macroControl.ID)); + priority: CacheItemPriority.NotRemovable, + timeout: new TimeSpan(0, 0, Model.CacheDuration), + getCacheItem: () => new MacroCacheContent(macroControl, macroControl.ID)); dateAddedCacheKey = CacheKeys.MacroControlDateAddedCacheKey + Model.CacheIdentifier; @@ -487,11 +487,11 @@ namespace umbraco } //insert the date inserted (so we can check file modification date) - ApplicationContext.Current.ApplicationCache.InsertCacheItem( + ApplicationContext.Current.ApplicationCache.RuntimeCache.InsertCacheItem( dateAddedCacheKey, - CacheItemPriority.NotRemovable, - new TimeSpan(0, 0, Model.CacheDuration), - () => DateTime.Now); + priority: CacheItemPriority.NotRemovable, + timeout: new TimeSpan(0, 0, Model.CacheDuration), + getCacheItem: () => DateTime.Now); } @@ -524,7 +524,7 @@ namespace umbraco if (CacheMacroAsString(Model)) { - macroHtml = ApplicationContext.Current.ApplicationCache.GetCacheItem( + macroHtml = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( CacheKeys.MacroHtmlCacheKey + Model.CacheIdentifier); // FlorisRobbemont: @@ -550,7 +550,7 @@ namespace umbraco } else { - var cacheContent = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var cacheContent = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( CacheKeys.MacroControlCacheKey + Model.CacheIdentifier); if (cacheContent != null) @@ -614,7 +614,7 @@ namespace umbraco { if (MacroIsFileBased(model)) { - var cacheResult = ApplicationContext.Current.ApplicationCache.GetCacheItem(dateAddedKey); + var cacheResult = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem(dateAddedKey); if (cacheResult != null) { diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index ad92d154a1..a288866e7d 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -66,7 +66,7 @@ namespace umbraco var docParentId = -1; try { - docParentId = document.Parent.Id; + docParentId = document.ParentId; } catch (ArgumentException) { diff --git a/src/Umbraco.Web/umbraco.presentation/template.cs b/src/Umbraco.Web/umbraco.presentation/template.cs index 5967d40b31..68868e86fd 100644 --- a/src/Umbraco.Web/umbraco.presentation/template.cs +++ b/src/Umbraco.Web/umbraco.presentation/template.cs @@ -482,7 +482,7 @@ namespace umbraco { var tId = templateID; - var t = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var t = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem