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/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 686fe16faf..72733c5800 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -17,7 +17,6 @@ - @@ -31,14 +30,13 @@ - + - - + + - - + diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 4127ea702e..66e82ac488 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -16,8 +16,8 @@ umbraco - - + + 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/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index 1cf886b312..dfb9925840 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -7,8 +7,8 @@
-
- +
+
@@ -21,8 +21,10 @@ + + - + @@ -37,7 +39,7 @@ - + @@ -48,13 +50,15 @@ - + - + - + + + > @@ -63,8 +67,7 @@ - + @@ -78,6 +81,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -86,11 +139,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -125,6 +280,8 @@ + + @@ -149,16 +306,14 @@ - - - + - + @@ -171,7 +326,7 @@ - + @@ -180,7 +335,7 @@ - + @@ -191,12 +346,12 @@ - + - + @@ -208,16 +363,16 @@ - + - + - - + + @@ -225,19 +380,19 @@ - + - + - + - + - + \ No newline at end of file diff --git a/build/NuSpecs/tools/install.core.ps1 b/build/NuSpecs/tools/install.core.ps1 index ba6416028b..c4f213ba01 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 } @@ -36,16 +48,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 } @@ -59,6 +73,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 } @@ -72,6 +88,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 } @@ -82,6 +100,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/UmbracoVersion.txt b/build/UmbracoVersion.txt index 2a8a3618d4..4846f08fcf 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,3 +1,2 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) -7.4.0 -beta4 \ No newline at end of file +7.4.2 \ No newline at end of file diff --git a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj index 73983e7e30..86796e1dd7 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 f385151454..39a59f898c 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.4.0")] -[assembly: AssemblyInformationalVersion("7.4.0-beta4")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.4.2")] +[assembly: AssemblyInformationalVersion("7.4.2")] \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs index 861c6b803e..0ae721943d 100644 --- a/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs @@ -7,14 +7,22 @@ using Umbraco.Core.Models.EntityBase; 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 index 66d9b2ac25..1f51fc3ccc 100644 --- a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs @@ -11,35 +11,35 @@ namespace Umbraco.Core.Cache /// /// /// - internal class DefaultRepositoryCachePolicy : DisposableObject, IRepositoryCachePolicy + /// + /// 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; - protected IRuntimeCacheProvider Cache { get; private set; } - private Action _action; public DefaultRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) - { - if (cache == null) throw new ArgumentNullException("cache"); + : base(cache) + { if (options == null) throw new ArgumentNullException("options"); - - _options = options; - Cache = cache; + _options = options; } - public string GetCacheIdKey(object id) + protected string GetCacheIdKey(object id) { if (id == null) throw new ArgumentNullException("id"); return string.Format("{0}{1}", GetCacheTypeKey(), id); } - public string GetCacheTypeKey() + protected string GetCacheTypeKey() { return string.Format("uRepo_{0}_", typeof(TEntity).Name); } - public void CreateOrUpdate(TEntity entity, Action persistMethod) + public override void CreateOrUpdate(TEntity entity, Action persistMethod) { if (entity == null) throw new ArgumentNullException("entity"); if (persistMethod == null) throw new ArgumentNullException("persistMethod"); @@ -54,7 +54,9 @@ namespace Umbraco.Core.Cache //just to be safe, we cannot cache an item without an identity if (entity.HasIdentity) { - Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity); + Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity, + timeout: TimeSpan.FromMinutes(5), + isSliding: true); } //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared @@ -79,24 +81,29 @@ namespace Umbraco.Core.Cache } } - public void Remove(TEntity entity, Action persistMethod) + public override void Remove(TEntity entity, Action persistMethod) { if (entity == null) throw new ArgumentNullException("entity"); if (persistMethod == null) throw new ArgumentNullException("persistMethod"); - persistMethod(entity); - - //set the disposal action - var cacheKey = GetCacheIdKey(entity.Id); - SetCacheAction(() => + try { - Cache.ClearCacheItem(cacheKey); - //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.ClearCacheItem(GetCacheTypeKey()); - }); + 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 TEntity Get(TId id, Func getFromRepo) + public override TEntity Get(TId id, Func getFromRepo) { if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); @@ -113,13 +120,13 @@ namespace Umbraco.Core.Cache return entity; } - public TEntity Get(TId id) + public override TEntity Get(TId id) { var cacheKey = GetCacheIdKey(id); return Cache.GetCacheItem(cacheKey); } - public bool Exists(TId id, Func getFromRepo) + public override bool Exists(TId id, Func getFromRepo) { if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); @@ -128,7 +135,7 @@ namespace Umbraco.Core.Cache return fromCache != null || getFromRepo(id); } - public virtual TEntity[] GetAll(TId[] ids, Func> getFromRepo) + public override TEntity[] GetAll(TId[] ids, Func> getFromRepo) { if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); @@ -182,7 +189,7 @@ namespace Umbraco.Core.Cache /// Looks up the zero count cache, must return null if it doesn't exist /// /// - protected virtual bool HasZeroCountCache() + protected bool HasZeroCountCache() { var zeroCount = Cache.GetCacheItem(GetCacheTypeKey()); return (zeroCount != null && zeroCount.Any() == false); @@ -192,24 +199,13 @@ namespace Umbraco.Core.Cache /// Performs the lookup for all entities of this type from the cache /// /// - protected virtual TEntity[] GetAllFromCache() + protected TEntity[] GetAllFromCache() { var allEntities = Cache.GetCacheItemsByKeySearch(GetCacheTypeKey()) .WhereNotNull() .ToArray(); return allEntities.Any() ? allEntities : new TEntity[] {}; - } - - /// - /// The disposal performs the caching - /// - protected override void DisposeResources() - { - if (_action != null) - { - _action(); - } - } + } /// /// Sets the action to execute on disposal for a single entity @@ -225,7 +221,9 @@ namespace Umbraco.Core.Cache //just to be safe, we cannot cache an item without an identity if (entity.HasIdentity) { - Cache.InsertCacheItem(cacheKey, () => entity); + Cache.InsertCacheItem(cacheKey, () => entity, + timeout: TimeSpan.FromMinutes(5), + isSliding: true); } }); } @@ -244,6 +242,7 @@ namespace Umbraco.Core.Cache { //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 @@ -256,20 +255,14 @@ namespace Umbraco.Core.Cache //just to be safe, we cannot cache an item without an identity if (localCopy.HasIdentity) { - Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => localCopy); + Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => localCopy, + timeout: TimeSpan.FromMinutes(5), + isSliding: true); } } } }); } - - /// - /// Sets the action to execute on disposal - /// - /// - protected void SetCacheAction(Action action) - { - _action = action; - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs index 40b100ef67..9b37d1861f 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -11,46 +11,190 @@ namespace Umbraco.Core.Cache /// /// /// - internal class FullDataSetRepositoryCachePolicy : DefaultRepositoryCachePolicy + internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IAggregateRoot { - public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache) : base(cache, - new RepositoryCachePolicyOptions - { - //Definitely allow zero'd cache entires since this is a full set, in many cases there will be none, - // and we must cache this! - GetAllCacheAllowZeroCount = true - }) + 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 override void SetCacheAction(string cacheKey, TEntity entity) + protected void SetCacheAction(string cacheKey, TEntity entity) { - //do nothing + //No-op } /// /// Sets the action to execute on disposal for an entity collection /// - /// /// - protected override void SetCacheAction(TId[] ids, TEntity[] entityCollection) + protected void SetCacheAction(TEntity[] entityCollection) { - //for this type of caching policy, we don't want to cache any GetAll request containing specific Ids - if (ids.Any()) return; - //set the disposal action SetCacheAction(() => { //We want to cache the result as a single collection - Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection)); + + if (_expires) + { + Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection), + timeout: TimeSpan.FromMinutes(5), + isSliding: true); + } + else + { + Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection)); + } }); } @@ -58,7 +202,7 @@ namespace Umbraco.Core.Cache /// Looks up the zero count cache, must return null if it doesn't exist /// /// - protected override bool HasZeroCountCache() + protected bool HasZeroCountCache() { if (_hasZeroCountCache.HasValue) return _hasZeroCountCache.Value; @@ -71,14 +215,15 @@ namespace Umbraco.Core.Cache /// This policy will cache the full data set as a single collection /// /// - protected override TEntity[] GetAllFromCache() + 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 index 75bdae7e83..e4addcf355 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache @@ -11,15 +13,21 @@ namespace Umbraco.Core.Cache where TEntity : class, IAggregateRoot { private readonly IRuntimeCacheProvider _runtimeCache; - - public FullDataSetRepositoryCachePolicyFactory(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; + _runtimeCache = runtimeCache; + _getEntityId = getEntityId; + _getAllFromRepo = getAllFromRepo; + _expires = expires; } public virtual IRepositoryCachePolicy CreatePolicy() { - return new FullDataSetRepositoryCachePolicy(_runtimeCache); + 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 index 97844933b7..215487c3be 100644 --- a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs @@ -10,9 +10,7 @@ namespace Umbraco.Core.Cache TEntity Get(TId id, Func getFromRepo); TEntity Get(TId id); bool Exists(TId id, Func getFromRepo); - - string GetCacheIdKey(object id); - string GetCacheTypeKey(); + void CreateOrUpdate(TEntity entity, Action persistMethod); void Remove(TEntity entity, Action persistMethod); TEntity[] GetAll(TId[] ids, Func> getFromRepo); 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/Cache/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs index 9566cd6e7f..28ac4ee2d1 100644 --- a/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Cache protected override void SetCacheAction(TId[] ids, TEntity[] entityCollection) { - //do nothing + //no-op } } } \ No newline at end of file diff --git a/src/Umbraco.Core/CacheHelper.cs b/src/Umbraco.Core/CacheHelper.cs index 4b009e5f86..0dc5f5b00f 100644 --- a/src/Umbraco.Core/CacheHelper.cs +++ b/src/Umbraco.Core/CacheHelper.cs @@ -290,7 +290,7 @@ namespace Umbraco.Core TimeSpan timeout, Func getCacheItem) { - var cache = RuntimeCache as HttpRuntimeCacheProvider; + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); if (cache != null) { var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); @@ -314,7 +314,7 @@ namespace Umbraco.Core CacheDependency cacheDependency, Func getCacheItem) { - var cache = RuntimeCache as HttpRuntimeCacheProvider; + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); if (cache != null) { var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), null, false, priority, null, cacheDependency); @@ -374,7 +374,7 @@ namespace Umbraco.Core TimeSpan timeout, Func getCacheItem) { - var cache = RuntimeCache as HttpRuntimeCacheProvider; + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); if (cache != null) { cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, null, cacheDependency); @@ -400,7 +400,7 @@ namespace Umbraco.Core TimeSpan? timeout, Func getCacheItem) { - var cache = RuntimeCache as HttpRuntimeCacheProvider; + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); if (cache != null) { cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); @@ -409,6 +409,20 @@ namespace Umbraco.Core } #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 index 365bf53b06..5067562aa7 100644 --- a/src/Umbraco.Core/Collections/DeepCloneableList.cs +++ b/src/Umbraco.Core/Collections/DeepCloneableList.cs @@ -14,18 +14,24 @@ namespace Umbraco.Core.Collections /// internal class DeepCloneableList : List, IDeepCloneable, IRememberBeingDirty { - /// - /// Initializes a new instance of the class that is empty and has the default initial capacity. - /// - public DeepCloneableList() + private readonly ListCloneBehavior _listCloneBehavior; + + public DeepCloneableList(ListCloneBehavior listCloneBehavior) { + _listCloneBehavior = listCloneBehavior; + } + + public DeepCloneableList(IEnumerable collection, ListCloneBehavior listCloneBehavior) : base(collection) + { + _listCloneBehavior = listCloneBehavior; } /// - /// Initializes a new instance of the class that contains elements copied from the specified collection and has sufficient capacity to accommodate the number of elements copied. + /// Default behavior is CloneOnce /// - /// The collection whose elements are copied to the new list. is null. - public DeepCloneableList(IEnumerable collection) : base(collection) + /// + public DeepCloneableList(IEnumerable collection) + : this(collection, ListCloneBehavior.CloneOnce) { } @@ -35,20 +41,47 @@ namespace Umbraco.Core.Collections /// public object DeepClone() { - var newList = new DeepCloneableList(); - foreach (var item in this) + switch (_listCloneBehavior) { - var dc = item as IDeepCloneable; - if (dc != null) - { - newList.Add((T) dc.DeepClone()); - } - else - { - newList.Add(item); - } + 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(); } - return newList; } public bool IsDirty() 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/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 4039ab7db0..c424d1fc21 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.4.0"); + private static readonly Version Version = new Version("7.4.2"); /// /// Gets the current version of Umbraco. @@ -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 "beta4"; } } + 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 d2dea09698..4da740f458 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -136,7 +136,10 @@ namespace Umbraco.Core { try { - x.OnApplicationInitialized(UmbracoApplication, ApplicationContext); + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationInitialized", x.GetType()))) + { + x.OnApplicationInitialized(UmbracoApplication, ApplicationContext); + } } catch (Exception ex) { @@ -299,7 +302,10 @@ namespace Umbraco.Core { try { - x.OnApplicationStarting(UmbracoApplication, ApplicationContext); + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationStarting", x.GetType()))) + { + x.OnApplicationStarting(UmbracoApplication, ApplicationContext); + } } catch (Exception ex) { @@ -350,7 +356,10 @@ namespace Umbraco.Core { try { - x.OnApplicationStarted(UmbracoApplication, ApplicationContext); + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationStarted", x.GetType()))) + { + x.OnApplicationStarted(UmbracoApplication, ApplicationContext); + } } catch (Exception ex) { 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/Dynamics/ExtensionMethodFinder.cs b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs index 4430d1d74e..9772aaf5bd 100644 --- a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs +++ b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs @@ -5,8 +5,10 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Linq.Expressions; +using System.Text; using System.Web.Services.Description; using Umbraco.Core.Cache; +using Umbraco.Core.Logging; namespace Umbraco.Core.Dynamics { @@ -20,6 +22,20 @@ namespace Umbraco.Core.Dynamics /// private static readonly ConcurrentDictionary, MethodInfo[]> MethodCache = new ConcurrentDictionary, MethodInfo[]>(); + private static IEnumerable GetTypes(Assembly a) + { + try + { + return TypeFinder.GetTypesWithFormattedException(a); + } + catch (ReflectionTypeLoadException ex) + { + // is this going to flood the log? + LogHelper.Error(typeof (ExtensionMethodFinder), "Failed to get types.", ex); + return Enumerable.Empty(); + } + } + /// /// Returns the enumerable of all extension method info's in the app domain = USE SPARINGLY!!! /// @@ -36,7 +52,7 @@ namespace Umbraco.Core.Dynamics // assemblies that contain extension methods .Where(a => a.IsDefined(typeof (ExtensionAttribute), false)) // types that contain extension methods - .SelectMany(a => a.GetTypes() + .SelectMany(a => GetTypes(a) .Where(t => t.IsDefined(typeof (ExtensionAttribute), false) && t.IsSealed && t.IsGenericType == false && t.IsNested == false)) // actual extension methods .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public) @@ -45,9 +61,9 @@ namespace Umbraco.Core.Dynamics .Concat(typeof (Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)) //If we don't do this then we'll be scanning all assemblies each time! .ToArray(), - + //only cache for 5 minutes - timeout: TimeSpan.FromMinutes(5), + timeout: TimeSpan.FromMinutes(5), //each time this is accessed it will be for 5 minutes longer isSliding:true); @@ -57,7 +73,7 @@ namespace Umbraco.Core.Dynamics /// Returns all extension methods found matching the definition /// /// - /// The runtime cache is used to temporarily cache all extension methods found in the app domain so that + /// The runtime cache is used to temporarily cache all extension methods found in the app domain so that /// while we search for individual extension methods, the process will be reasonably 'quick'. We then statically /// cache the MethodInfo's that we are looking for and then the runtime cache will expire and give back all that memory. /// @@ -78,7 +94,7 @@ namespace Umbraco.Core.Dynamics { var candidates = GetAllExtensionMethodsInAppDomain(runtimeCache); - // filter by name + // filter by name var filtr1 = candidates.Where(m => m.Name == name); // filter by args count @@ -102,7 +118,7 @@ namespace Umbraco.Core.Dynamics return filtr3.ToArray(); }); - + } private static MethodInfo DetermineMethodFromParams(IEnumerable methods, Type genericType, IEnumerable args) @@ -123,12 +139,12 @@ namespace Umbraco.Core.Dynamics types = method.GetParameters().Select(pi => pi.ParameterType).Skip(1) }); - //This type comparer will check + //This type comparer will check var typeComparer = new DelegateEqualityComparer( - //Checks if the argument type passed in can be assigned from the parameter type in the method. For + //Checks if the argument type passed in can be assigned from the parameter type in the method. For // example, if the argument type is HtmlHelper but the method parameter type is HtmlHelper then // it will match because the argument is assignable to that parameter type and will be able to execute - TypeHelper.IsTypeAssignableFrom, + TypeHelper.IsTypeAssignableFrom, //This will not ever execute but if it does we need to get the hash code of the string because the hash // code of a type is random type => type.FullName.GetHashCode()); @@ -159,7 +175,7 @@ namespace Umbraco.Core.Dynamics .ToArray(); var methods = GetAllExtensionMethods(runtimeCache, thisType, name, args.Length).ToArray(); - + return DetermineMethodFromParams(methods, genericType, args); } } diff --git a/src/Umbraco.Core/Events/MigrationEventArgs.cs b/src/Umbraco.Core/Events/MigrationEventArgs.cs index 7eed61484b..008e50d2ee 100644 --- a/src/Umbraco.Core/Events/MigrationEventArgs.cs +++ b/src/Umbraco.Core/Events/MigrationEventArgs.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using Semver; +using Umbraco.Core.Configuration; using Umbraco.Core.Persistence.Migrations; namespace Umbraco.Core.Events @@ -13,23 +14,49 @@ namespace Umbraco.Core.Events /// /// /// + /// /// /// - public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion, bool canCancel) - : base(eventObject, canCancel) - { - ConfiguredSemVersion = configuredVersion; - TargetSemVersion = targetVersion; - } + public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) + : this(eventObject, null, configuredVersion, targetVersion, productName, canCancel) + { } - [Obsolete("Use constructor accepting UmbracoVersion instances instead")] + /// + /// Constructor accepting multiple migrations that are used in the migration runner + /// + /// + /// + /// + /// + [Obsolete("Use constructor accepting a product name instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion, bool canCancel) + : this(eventObject, null, configuredVersion, targetVersion, GlobalSettings.UmbracoMigrationName, canCancel) + { } + + [Obsolete("Use constructor accepting SemVersion instances and a product name instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public MigrationEventArgs(IList eventObject, Version configuredVersion, Version targetVersion, bool canCancel) - : base(eventObject, canCancel) - { - ConfiguredSemVersion = new SemVersion(configuredVersion); - TargetSemVersion = new SemVersion(targetVersion); - } + : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), GlobalSettings.UmbracoMigrationName, canCancel) + { } + + /// + /// Constructor accepting multiple migrations that are used in the migration runner + /// + /// + /// + /// + /// + /// + /// + internal MigrationEventArgs(IList eventObject, MigrationContext migrationContext, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) + : base(eventObject, canCancel) + { + MigrationContext = migrationContext; + ConfiguredSemVersion = configuredVersion; + TargetSemVersion = targetVersion; + ProductName = productName; + } /// /// Constructor accepting multiple migrations that are used in the migration runner @@ -39,12 +66,15 @@ namespace Umbraco.Core.Events /// /// /// + [Obsolete("Use constructor accepting a product name instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] internal MigrationEventArgs(IList eventObject, MigrationContext migrationContext, SemVersion configuredVersion, SemVersion targetVersion, bool canCancel) : base(eventObject, canCancel) { MigrationContext = migrationContext; ConfiguredSemVersion = configuredVersion; TargetSemVersion = targetVersion; + ProductName = GlobalSettings.UmbracoMigrationName; } /// @@ -53,21 +83,28 @@ namespace Umbraco.Core.Events /// /// /// - public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion) - : base(eventObject) - { - ConfiguredSemVersion = configuredVersion; - TargetSemVersion = targetVersion; - } + /// + public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion, string productName) + : this(eventObject, null, configuredVersion, targetVersion, productName, false) + { } - [Obsolete("Use constructor accepting UmbracoVersion instances instead")] + /// + /// Constructor accepting multiple migrations that are used in the migration runner + /// + /// + /// + /// + [Obsolete("Use constructor accepting a product name instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion) + : this(eventObject, null, configuredVersion, targetVersion, GlobalSettings.UmbracoMigrationName, false) + { } + + [Obsolete("Use constructor accepting SemVersion instances and a product name instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public MigrationEventArgs(IList eventObject, Version configuredVersion, Version targetVersion) - : base(eventObject) - { - ConfiguredSemVersion = new SemVersion(configuredVersion); - TargetSemVersion = new SemVersion(targetVersion); - } + : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), GlobalSettings.UmbracoMigrationName, false) + { } /// /// Returns all migrations that were used in the migration runner @@ -95,6 +132,8 @@ namespace Umbraco.Core.Events public SemVersion TargetSemVersion { get; private set; } + public string ProductName { get; private set; } + internal MigrationContext MigrationContext { get; private set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index b6021e6538..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 diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 7218a2421d..a0305d2cfb 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -487,11 +487,8 @@ namespace Umbraco.Core.Models var oldPropertyGroup = PropertyGroups.FirstOrDefault(x => x.PropertyTypes.Any(y => y.Alias == propertyTypeAlias)); - // reset PropertyGroupId, which will be re-evaluated when the content type - // is saved - what is important is group.PropertyTypes - see code in - // ContentTypeBaseRepository.PersistUpdatedBaseContentType - propertyType.PropertyGroupId = new Lazy(() => default(int)); - propertyType.ResetDirtyProperties(); // PropertyGroupId must not be dirty + // set new group + propertyType.PropertyGroupId = newPropertyGroup == null ? null : new Lazy(() => newPropertyGroup.Id, false); // remove from old group, if any - add to new group, if any if (oldPropertyGroup != null) @@ -540,7 +537,7 @@ namespace Umbraco.Core.Models // re-assign the group's properties to no group foreach (var property in group.PropertyTypes) { - property.PropertyGroupId = new Lazy(() => 0); + property.PropertyGroupId = null; _propertyTypes.Add(property); } 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/IMediaType.cs b/src/Umbraco.Core/Models/IMediaType.cs index 3934d7a40f..29e4b665ba 100644 --- a/src/Umbraco.Core/Models/IMediaType.cs +++ b/src/Umbraco.Core/Models/IMediaType.cs @@ -7,6 +7,12 @@ namespace Umbraco.Core.Models /// + /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset + /// + /// + /// + IMediaType DeepCloneWithResetIdentities(string newAlias); } } \ 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 c8e2915afd..8596cce910 100644 --- a/src/Umbraco.Core/Models/MediaType.cs +++ b/src/Umbraco.Core/Models/MediaType.cs @@ -38,5 +38,30 @@ namespace Umbraco.Core.Models : base(parent, alias) { } + + /// + /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset + /// + /// + public IMediaType DeepCloneWithResetIdentities(string alias) + { + var clone = (MediaType)DeepClone(); + clone.Alias = alias; + clone.Key = Guid.Empty; + foreach (var propertyGroup in clone.PropertyGroups) + { + propertyGroup.ResetIdentity(); + propertyGroup.ResetDirtyProperties(false); + } + foreach (var propertyType in clone.PropertyTypes) + { + propertyType.ResetIdentity(); + propertyType.ResetDirtyProperties(false); + } + + clone.ResetIdentity(); + clone.ResetDirtyProperties(false); + return clone; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 7f22b65c8c..0649801a0c 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -221,8 +221,9 @@ namespace Umbraco.Core.Models } /// - /// Gets or Sets the PropertyGroup's Id for which this PropertyType belongs + /// Gets or sets the identifier of the PropertyGroup this PropertyType belongs to. /// + /// For generic properties, the value is null. [DataMember] internal Lazy PropertyGroupId { diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index 5f30c08ce7..d05960b08f 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Web.Caching; -using System.Web.UI; using Umbraco.Core.Cache; namespace Umbraco.Core.Models.PublishedContent @@ -16,6 +13,7 @@ namespace Umbraco.Core.Models.PublishedContent public class PublishedContentType { private readonly PublishedPropertyType[] _propertyTypes; + private readonly HashSet _compositionAliases; // fast alias-to-index xref containing both the raw alias and its lowercase version private readonly Dictionary _indexes = new Dictionary(); @@ -27,6 +25,7 @@ namespace Umbraco.Core.Models.PublishedContent { Id = contentType.Id; Alias = contentType.Alias; + _compositionAliases = new HashSet(contentType.CompositionAliases(), StringComparer.InvariantCultureIgnoreCase); _propertyTypes = contentType.CompositionPropertyTypes .Select(x => new PublishedPropertyType(this, x)) .ToArray(); @@ -34,10 +33,11 @@ namespace Umbraco.Core.Models.PublishedContent } // internal so it can be used for unit tests - internal PublishedContentType(int id, string alias, IEnumerable propertyTypes) + internal PublishedContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes) { Id = id; Alias = alias; + _compositionAliases = new HashSet(compositionAliases, StringComparer.InvariantCultureIgnoreCase); _propertyTypes = propertyTypes.ToArray(); foreach (var propertyType in _propertyTypes) propertyType.ContentType = this; @@ -45,8 +45,8 @@ namespace Umbraco.Core.Models.PublishedContent } // create detached content type - ie does not match anything in the DB - internal PublishedContentType(string alias, IEnumerable propertyTypes) - : this (0, alias, propertyTypes) + internal PublishedContentType(string alias, IEnumerable compositionAliases, IEnumerable propertyTypes) + : this(0, alias, compositionAliases, propertyTypes) { } private void InitializeIndexes() @@ -63,6 +63,7 @@ namespace Umbraco.Core.Models.PublishedContent public int Id { get; private set; } public string Alias { get; private set; } + public HashSet CompositionAliases { get { return _compositionAliases; } } #endregion @@ -113,10 +114,25 @@ namespace Umbraco.Core.Models.PublishedContent internal static void ClearContentType(int id) { Logging.LogHelper.Debug("Clear content type w/id {0}.", () => id); - // requires a predicate because the key does not contain the ID - // faster than key strings comparisons anyway + + // we don't support "get all" at the moment - so, cheating + var all = ApplicationContext.Current.ApplicationCache.StaticCache.GetCacheItemsByKeySearch("PublishedContentType_").ToArray(); + + // the one we want to clear + var clr = all.FirstOrDefault(x => x.Id == id); + if (clr == null) return; + + // those that have that one in their composition aliases + // note: CompositionAliases contains all recursive aliases + var oth = all.Where(x => x.CompositionAliases.InvariantContains(clr.Alias)).Select(x => x.Id); + + // merge ids + var ids = oth.Concat(new[] { clr.Id }).ToArray(); + + // clear them all at once + // we don't support "clear many at once" at the moment - so, cheating ApplicationContext.Current.ApplicationCache.StaticCache.ClearCacheObjectTypes( - (key, value) => value.Id == id); + (key, value) => ids.Contains(value.Id)); } internal static void ClearDataType(int id) diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index f4b1597a7d..1875ae03b3 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; @@ -230,13 +233,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/Persistence/Factories/MemberTypeReadOnlyFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs index 93ad99a5c1..eebbc34eda 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs @@ -164,7 +164,7 @@ namespace Umbraco.Core.Persistence.Factories Name = typeDto.Name, SortOrder = typeDto.SortOrder, ValidationRegExp = typeDto.ValidationRegExp, - PropertyGroupId = new Lazy(() => default(int)), + PropertyGroupId = null, CreateDate = dto.CreateDate, UpdateDate = dto.CreateDate }; diff --git a/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs b/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs index 234cd67386..081865b9a1 100644 --- a/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs +++ b/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Text; @@ -27,18 +28,21 @@ namespace Umbraco.Core.Persistence.Migrations private readonly IMigration[] _migrations; [Obsolete("Use the ctor that specifies all dependencies instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public MigrationRunner(Version currentVersion, Version targetVersion, string productName) : this(LoggerResolver.Current.Logger, currentVersion, targetVersion, productName) { } [Obsolete("Use the ctor that specifies all dependencies instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public MigrationRunner(ILogger logger, Version currentVersion, Version targetVersion, string productName) : this(logger, currentVersion, targetVersion, productName, null) { } [Obsolete("Use the ctor that specifies all dependencies instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public MigrationRunner(ILogger logger, Version currentVersion, Version targetVersion, string productName, params IMigration[] migrations) : this(ApplicationContext.Current.Services.MigrationEntryService, logger, new SemVersion(currentVersion), new SemVersion(targetVersion), productName, migrations) { @@ -92,7 +96,7 @@ namespace Umbraco.Core.Persistence.Migrations : OrderedDowngradeMigrations(foundMigrations).ToList(); - if (Migrating.IsRaisedEventCancelled(new MigrationEventArgs(migrations, _currentVersion, _targetVersion, true), this)) + if (Migrating.IsRaisedEventCancelled(new MigrationEventArgs(migrations, _currentVersion, _targetVersion, _productName, true), this)) { _logger.Warn("Migration was cancelled by an event"); return false; @@ -121,7 +125,7 @@ namespace Umbraco.Core.Persistence.Migrations throw; } - Migrated.RaiseEvent(new MigrationEventArgs(migrations, migrationContext, _currentVersion, _targetVersion, false), this); + Migrated.RaiseEvent(new MigrationEventArgs(migrations, migrationContext, _currentVersion, _targetVersion, _productName, false), this); return true; } @@ -289,7 +293,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); @@ -308,4 +312,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 830adfd7fb..684196c2e6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs @@ -37,9 +37,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(); @@ -71,13 +71,16 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven xml = new XmlDocument(); xml.LoadXml(data.Text); } - catch (Exception ex) + 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); @@ -91,11 +94,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); @@ -110,9 +113,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 ff71aec142..32c81700a8 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs @@ -33,36 +33,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 @@ -71,7 +75,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); } @@ -79,5 +83,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 d55ad8fc14..88f90639d1 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/Querying/BaseExpressionHelper.cs b/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs index f3cbe5a583..4679b9b2ad 100644 --- a/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs +++ b/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs @@ -128,6 +128,34 @@ namespace Umbraco.Core.Persistence.Querying right = Visit(b.Right); } } + else if (operand == "=") + { + // deal with (x == true|false) - most common + var constRight = b.Right as ConstantExpression; + if (constRight != null && constRight.Type == typeof (bool)) + return ((bool) constRight.Value) ? VisitNotNot(b.Left) : VisitNot(b.Left); + right = Visit(b.Right); + + // deal with (true|false == x) - why not + var constLeft = b.Left as ConstantExpression; + if (constLeft != null && constLeft.Type == typeof (bool)) + return ((bool) constLeft.Value) ? VisitNotNot(b.Right) : VisitNot(b.Right); + left = Visit(b.Left); + } + else if (operand == "<>") + { + // deal with (x != true|false) - most common + var constRight = b.Right as ConstantExpression; + if (constRight != null && constRight.Type == typeof(bool)) + return ((bool) constRight.Value) ? VisitNot(b.Left) : VisitNotNot(b.Left); + right = Visit(b.Right); + + // deal with (true|false != x) - why not + var constLeft = b.Left as ConstantExpression; + if (constLeft != null && constLeft.Type == typeof(bool)) + return ((bool) constLeft.Value) ? VisitNot(b.Right) : VisitNotNot(b.Right); + left = Visit(b.Left); + } else { left = Visit(b.Left); @@ -152,7 +180,7 @@ namespace Umbraco.Core.Persistence.Querying case "COALESCE": return string.Format("{0}({1},{2})", operand, left, right); default: - return left + " " + operand + " " + right; + return "(" + left + " " + operand + " " + right + ")"; } } @@ -231,25 +259,47 @@ namespace Umbraco.Core.Persistence.Querying switch (u.NodeType) { case ExpressionType.Not: - var o = Visit(u.Operand); - - //use a Not equal operator instead of <> since we don't know that <> works in all sql servers - - switch (u.Operand.NodeType) - { - case ExpressionType.MemberAccess: - //In this case it wil be a false property , i.e. x => !Trashed - SqlParameters.Add(true); - return string.Format("NOT ({0} = @0)", o); - default: - //In this case it could be anything else, such as: x => !x.Path.StartsWith("-20") - return string.Format("NOT ({0})", o); - } + return VisitNot(u.Operand); default: return Visit(u.Operand); } } + private string VisitNot(Expression exp) + { + var o = Visit(exp); + + // use a "NOT (...)" syntax instead of "<>" since we don't know whether "<>" works in all sql servers + // also, x.StartsWith(...) translates to "x LIKE '...%'" which we cannot "<>" and have to "NOT (...") + + switch (exp.NodeType) + { + case ExpressionType.MemberAccess: + // false property , i.e. x => !Trashed + SqlParameters.Add(true); + return string.Format("NOT ({0} = @{1})", o, SqlParameters.Count - 1); + default: + // could be anything else, such as: x => !x.Path.StartsWith("-20") + return "NOT (" + o + ")"; + } + } + + private string VisitNotNot(Expression exp) + { + var o = Visit(exp); + + switch (exp.NodeType) + { + case ExpressionType.MemberAccess: + // true property, i.e. x => Trashed + SqlParameters.Add(true); + return string.Format("({0} = @{1})", o, SqlParameters.Count - 1); + default: + // could be anything else, such as: x => x.Path.StartsWith("-20") + return o; + } + } + protected virtual string VisitNewArray(NewArrayExpression na) { 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 cc3fb37aa6..7d3557a81d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -156,7 +156,7 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsContentXml WHERE nodeId = @Id", "DELETE FROM cmsContent WHERE nodeId = @Id", "DELETE FROM umbracoAccess WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" + "DELETE FROM umbracoNode WHERE id = @Id" }; return list; } @@ -689,8 +689,7 @@ namespace Umbraco.Core.Persistence.Repositories public int CountPublished() { var sql = GetBaseQuery(true).Where(x => x.Trashed == false) - .Where(x => x.Published == true) - .Where(x => x.Newest == true); + .Where(x => x.Published == true); return Database.ExecuteScalar(sql); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index fd061dbda9..dc85ad3a33 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -32,12 +32,12 @@ namespace Umbraco.Core.Persistence.Repositories { protected ContentTypeBaseRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) - { + { } - + public IEnumerable> Move(TEntity toMove, EntityContainer container) { - var parentId = -1; + var parentId = Constants.System.Root; if (container != null) { // Check on paths @@ -54,38 +54,36 @@ namespace Umbraco.Core.Persistence.Repositories new MoveEventInfo(toMove, toMove.Path, parentId) }; - var origPath = toMove.Path; - //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; - - //set the updated path - toMove.Path = string.Concat(container == null ? parentId.ToInvariantString() : container.Path, ",", toMove.Id); - - //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, update in order of level - var descendants = this.GetByQuery( - new Query().Where(type => type.Path.StartsWith(origPath + ","))); + var descendants = GetByQuery(new Query().Where(type => type.Path.StartsWith(toMovePath))); + var paths = new Dictionary(); + paths[toMove.Id] = toMove.Path; - var lastParent = toMove; foreach (var descendant in descendants.OrderBy(x => x.Level)) { moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - descendant.ParentId = lastParent.Id; - descendant.Path = string.Concat(lastParent.Path, ",", descendant.Id); + descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; + descendant.Level += levelDelta; - //schedule it for updating in the transaction AddOrUpdate(descendant); - - lastParent = descendant; } return moveInfo; } - /// /// Returns the content type ids that match the query /// @@ -287,7 +285,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) { @@ -411,7 +409,7 @@ AND umbracoNode.id <> @id", AssignDataTypeFromPropertyEditor(propertyType); } - //validate the alias! + //validate the alias! ValidateAlias(propertyType); var propertyTypeDto = propertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType); @@ -585,14 +583,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); @@ -600,16 +597,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()) @@ -624,12 +620,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 @@ -651,25 +646,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 @@ -682,13 +674,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; @@ -705,11 +696,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(); @@ -721,19 +710,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, @@ -742,40 +726,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) { @@ -783,95 +750,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 @@ -879,8 +869,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 @@ -893,33 +883,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) { @@ -928,170 +900,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 && isDefaultTemplate.Value && 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, @@ -1109,14 +1050,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 @@ -1126,11 +1067,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 @@ -1271,5 +1212,19 @@ AND umbracoNode.id <> @id", { return PerformExists(id); } + + public string GetUniqueAlias(string alias) + { + // alias is unique accross ALL content types! + var aliasColumn = SqlSyntax.GetQuotedColumnName("alias"); + var aliases = Database.Fetch(@"SELECT cmsContentType." + aliasColumn + @" FROM cmsContentType +INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id +WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", + new { pattern = alias + "%", objectType = NodeObjectTypeId }); + var i = 1; + string test; + while (aliases.Contains(test = alias + i)) i++; + return test; + } } -} \ 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 af2c1778c4..b7b4ddd583 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -37,7 +37,10 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), + //allow this cache to expire + expires:true)); } } @@ -51,27 +54,29 @@ namespace Umbraco.Core.Persistence.Repositories { if (ids.Any()) { - return ContentTypeQueryMapper.GetContentTypes(ids, Database, SqlSyntax, this, _templateRepository); - } - else - { - var sql = new Sql().Select("id").From(SqlSyntax).Where(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(x => x.Text, SqlSyntax); + 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); } /// @@ -182,6 +187,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); } @@ -284,9 +305,6 @@ namespace Umbraco.Core.Persistence.Repositories else { return GetAll(); - //var sql = new Sql().Select("id").From(SqlSyntax).Where(dto => dto.NodeObjectType == NodeObjectTypeId); - //var allIds = Database.Fetch(sql).ToArray(); - //return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs index 6abab73dd7..7b6cc162a8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs @@ -29,7 +29,8 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs index 5e8a6c2b5f..49d52ffffa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs @@ -131,6 +131,8 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistDeletedItem(EntityContainer entity) { + EnsureContainerType(entity); + var nodeDto = Database.FirstOrDefault(new Sql().Select("*") .From(SqlSyntax) .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); @@ -160,6 +162,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"); @@ -219,6 +223,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"); @@ -268,5 +274,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/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs index 49520387cd..61d83645b3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs @@ -32,5 +32,13 @@ namespace Umbraco.Core.Persistence.Repositories /// /// IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); + + /// + /// Derives a unique alias from an existing alias. + /// + /// The original alias. + /// The original alias with a number appended to it, so that it is unique. + /// /// Unique accross all content, media and member types. + string GetUniqueAlias(string alias); } } \ 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 1cec8005c9..7f2f76e541 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs @@ -15,5 +15,13 @@ namespace Umbraco.Core.Persistence.Repositories IEnumerable GetByQuery(IQuery query); IEnumerable> Move(IMediaType toMove, EntityContainer container); + + /// + /// Derives a unique alias from an existing alias. + /// + /// The original alias. + /// The original alias with a number appended to it, so that it is unique. + /// Unique accross all content, media and member types. + string GetUniqueAlias(string alias); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs index 6fc9bd5ebc..f9a8e59cfa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs @@ -30,7 +30,8 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index 225aa759d1..50a89bfd65 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -34,44 +34,46 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); + 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(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(x => x.Text, SqlSyntax); + 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); } /// @@ -178,9 +180,6 @@ namespace Umbraco.Core.Persistence.Repositories else { return GetAll(); - //var sql = new Sql().Select("id").From(SqlSyntax).Where(dto => dto.NodeObjectType == NodeObjectTypeId); - //var allIds = Database.Fetch(sql).ToArray(); - //return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index 97c744c43e..b35c793400 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -33,29 +33,17 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), + //allow this cache to expire + expires: true)); } } - - #region Overrides of RepositoryBase - + protected override IMemberType PerformGet(int id) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - sql.OrderByDescending(x => x.NodeId, SqlSyntax); - - 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) @@ -63,10 +51,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(x => x.NodeId); + sql.OrderByDescending(x => x.NodeId, SqlSyntax); var dtos = Database.Fetch( @@ -82,7 +71,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(x => x.SortOrder); + .OrderBy(x => x.SortOrder, SqlSyntax); var dtos = Database.Fetch( @@ -90,11 +79,7 @@ namespace Umbraco.Core.Persistence.Repositories return BuildFromDtos(dtos); } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - + protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); @@ -168,11 +153,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); @@ -243,8 +224,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. @@ -282,9 +261,6 @@ namespace Umbraco.Core.Persistence.Repositories else { return GetAll(); - //var sql = new Sql().Select("id").From(SqlSyntax).Where(dto => dto.NodeObjectType == NodeObjectTypeId); - //var allIds = Database.Fetch(sql).ToArray(); - //return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs index 1086b9cee0..22fad9d99b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs @@ -26,7 +26,8 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index f6bcc46d06..5534a9ea40 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -82,7 +82,11 @@ namespace Umbraco.Core.Persistence.Repositories { } - + + protected virtual TId GetEntityId(TEntity entity) + { + return (TId)(object)entity.Id; + } /// /// The runtime cache used for this repo by default is the isolated cache for this type @@ -179,7 +183,8 @@ namespace Umbraco.Core.Persistence.Repositories using (var p = CachePolicyFactory.CreatePolicy()) { - return p.GetAll(ids, PerformGetAll); + var result = p.GetAll(ids, PerformGetAll); + return result; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index cf5c883dc0..0242d9d222 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -51,7 +51,8 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); } } @@ -492,7 +493,7 @@ namespace Umbraco.Core.Persistence.Repositories if (aliases.Any() == false) return base.GetAll(); //return from base.GetAll, this is all cached - return base.GetAll().Where(x => aliases.Contains(x.Alias)); + return base.GetAll().Where(x => aliases.InvariantContains(x.Alias)); } public IEnumerable GetChildren(int masterTemplateId) @@ -505,7 +506,7 @@ namespace Umbraco.Core.Persistence.Repositories var parent = all.FirstOrDefault(x => x.Id == masterTemplateId); if (parent == null) return Enumerable.Empty(); - var children = all.Where(x => x.MasterTemplateAlias == parent.Alias); + var children = all.Where(x => x.MasterTemplateAlias.InvariantEquals(parent.Alias)); return children; } @@ -514,7 +515,7 @@ namespace Umbraco.Core.Persistence.Repositories //return from base.GetAll, this is all cached return base.GetAll().Where(x => alias.IsNullOrWhiteSpace() ? x.MasterTemplateAlias.IsNullOrWhiteSpace() - : x.MasterTemplateAlias == alias); + : x.MasterTemplateAlias.InvariantEquals(alias)); } public IEnumerable GetDescendants(int masterTemplateId) @@ -549,7 +550,7 @@ namespace Umbraco.Core.Persistence.Repositories var descendants = new List(); if (alias.IsNullOrWhiteSpace() == false) { - var parent = all.FirstOrDefault(x => x.Alias == alias); + var parent = all.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); if (parent == null) return Enumerable.Empty(); //recursively add all children AddChildren(all, descendants, parent.Alias); @@ -569,7 +570,7 @@ namespace Umbraco.Core.Persistence.Repositories private void AddChildren(ITemplate[] all, List descendants, string masterAlias) { - var c = all.Where(x => x.MasterTemplateAlias == masterAlias).ToArray(); + var c = all.Where(x => x.MasterTemplateAlias.InvariantEquals(masterAlias)).ToArray(); descendants.AddRange(c); if (c.Any() == false) return; //recurse through all children @@ -590,7 +591,7 @@ namespace Umbraco.Core.Persistence.Repositories //first get all template objects 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; @@ -599,11 +600,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); @@ -615,7 +616,7 @@ namespace Umbraco.Core.Persistence.Repositories 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; } @@ -747,7 +748,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); @@ -777,7 +778,7 @@ namespace Umbraco.Core.Persistence.Repositories private bool AliasAlreadExists(ITemplate template) { - var sql = GetBaseQuery(true).Where(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/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index b151f84c08..c18a5f8477 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -28,6 +28,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax GuidColumnDefinition = "char(36)"; DefaultValueFormat = "DEFAULT {0}"; + + InitColumnTypeMap(); } public override IEnumerable GetTablesInSchema(Database db) diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 9541a1cac0..8fc1150409 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 { @@ -111,7 +111,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); } @@ -121,6 +123,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/Profiling/StartupWebProfilerProvider.cs b/src/Umbraco.Core/Profiling/StartupWebProfilerProvider.cs deleted file mode 100644 index 16ce638c0e..0000000000 --- a/src/Umbraco.Core/Profiling/StartupWebProfilerProvider.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Threading; -using System.Web; -using StackExchange.Profiling; - -namespace Umbraco.Core.Profiling -{ - /// - /// Allows us to profile items during app startup - before an HttpRequest is created - /// - internal class StartupWebProfilerProvider : WebRequestProfilerProvider - { - public StartupWebProfilerProvider() - { - _startupPhase = StartupPhase.Boot; - //create the startup profiler - _startupProfiler = new MiniProfiler("http://localhost/umbraco-startup", ProfileLevel.Verbose) - { - Name = "StartupProfiler" - }; - } - - private MiniProfiler _startupProfiler; - private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); - - private enum StartupPhase - { - None = 0, - Boot = 1, - Request = 2 - } - - private volatile StartupPhase _startupPhase; - - public void BootComplete() - { - using (new ReadLock(_locker)) - { - if (_startupPhase != StartupPhase.Boot) return; - } - - using (var l = new UpgradeableReadLock(_locker)) - { - if (_startupPhase == StartupPhase.Boot) - { - l.UpgradeToWriteLock(); - - ////Now we need to transfer some information from our startup phase to the normal - ////web request phase to output the startup profiled information. - ////Stop our internal startup profiler, this will write out it's results to storage. - //StopProfiler(_startupProfiler); - //SaveProfiler(_startupProfiler); - - _startupPhase = StartupPhase.Request; - } - } - } - - public override void Stop(bool discardResults) - { - using (new ReadLock(_locker)) - { - if (_startupPhase == StartupPhase.None) - { - base.Stop(discardResults); - return; - } - } - - using (var l = new UpgradeableReadLock(_locker)) - { - if (_startupPhase > 0 && base.GetCurrentProfiler() == null) - { - l.UpgradeToWriteLock(); - - _startupPhase = StartupPhase.None; - - if (HttpContext.Current != null) - { - HttpContext.Current.Items[":mini-profiler:"] = _startupProfiler; - base.Stop(discardResults); - _startupProfiler = null; - } - } - else - { - base.Stop(discardResults); - } - } - } - - public override MiniProfiler Start(ProfileLevel level) - { - using (new ReadLock(_locker)) - { - if (_startupPhase > 0 && base.GetCurrentProfiler() == null) - { - SetProfilerActive(_startupProfiler); - return _startupProfiler; - } - - return base.Start(level); - } - } - - public override MiniProfiler GetCurrentProfiler() - { - using (new ReadLock(_locker)) - { - if (_startupPhase > 0) - { - try - { - var current = base.GetCurrentProfiler(); - if (current == null) return _startupProfiler; - } - catch - { - return _startupProfiler; - } - } - - return base.GetCurrentProfiler(); - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Profiling/WebProfiler.cs b/src/Umbraco.Core/Profiling/WebProfiler.cs index 7e2cf49313..00d088bca7 100644 --- a/src/Umbraco.Core/Profiling/WebProfiler.cs +++ b/src/Umbraco.Core/Profiling/WebProfiler.cs @@ -12,24 +12,15 @@ namespace Umbraco.Core.Profiling /// internal class WebProfiler : IProfiler { - private StartupWebProfilerProvider _startupWebProfilerProvider; /// /// Constructor - /// + /// + /// + /// Binds to application events to enable the MiniProfiler + /// internal WebProfiler() { - //setup some defaults - MiniProfiler.Settings.SqlFormatter = new SqlServerFormatter(); - MiniProfiler.Settings.StackMaxLength = 5000; - - //At this point we know that we've been constructed during app startup, there won't be an HttpRequest in the HttpContext - // since it hasn't started yet. So we need to do some hacking to enable profiling during startup. - _startupWebProfilerProvider = new StartupWebProfilerProvider(); - //this should always be the case during startup, we'll need to set a custom profiler provider - MiniProfiler.Settings.ProfilerProvider = _startupWebProfilerProvider; - - //Binds to application events to enable the MiniProfiler with a real HttpRequest UmbracoApplicationBase.ApplicationInit += UmbracoApplicationApplicationInit; } @@ -62,12 +53,7 @@ namespace Umbraco.Core.Profiling /// void UmbracoApplicationEndRequest(object sender, EventArgs e) { - if (_startupWebProfilerProvider != null) - { - Stop(); - _startupWebProfilerProvider = null; - } - else if (CanPerformProfilingAction(sender)) + if (CanPerformProfilingAction(sender)) { Stop(); } @@ -80,11 +66,6 @@ namespace Umbraco.Core.Profiling /// void UmbracoApplicationBeginRequest(object sender, EventArgs e) { - if (_startupWebProfilerProvider != null) - { - _startupWebProfilerProvider.BootComplete(); - } - if (CanPerformProfilingAction(sender)) { Start(); @@ -143,7 +124,9 @@ namespace Umbraco.Core.Profiling /// Start the profiler /// public void Start() - { + { + MiniProfiler.Settings.SqlFormatter = new SqlServerFormatter(); + MiniProfiler.Settings.StackMaxLength = 5000; MiniProfiler.Start(); } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs index c81f243a32..a962923a0c 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs @@ -8,6 +8,7 @@ namespace Umbraco.Core.PropertyEditors /// /// Manages the list of IPropertyEditorValueConverter's /// + [Obsolete("IPropertyEditorValueConverter is obsolete, but we need to support them until v8, to resolve the correct converter types use: PropertyValueConvertersResolver")] internal sealed class PropertyEditorValueConvertersResolver : ManyObjectsResolverBase { /// 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..524663c9c5 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -103,10 +103,18 @@ namespace Umbraco.Core.Security var backOfficeIdentity = http.User.Identity as UmbracoBackOfficeIdentity; if (backOfficeIdentity != null) return backOfficeIdentity; + //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that + var claimsPrincipal = http.User as ClaimsPrincipal; + if (claimsPrincipal != null) + { + backOfficeIdentity = claimsPrincipal.Identities.OfType().FirstOrDefault(); + if (backOfficeIdentity != null) return backOfficeIdentity; + } + //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session var claimsIdentity = http.User.Identity as ClaimsIdentity; if (claimsIdentity != null && claimsIdentity.IsAuthenticated) - { + { try { return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); @@ -167,74 +175,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/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 680692ae4f..e7298a5c6e 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -910,7 +910,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> @@ -957,7 +957,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); } } @@ -1080,7 +1080,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(asArray, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } } using (new WriteLock(Locker)) @@ -1124,7 +1124,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); } } @@ -1147,7 +1147,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 @@ -1178,7 +1178,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); } } @@ -1227,6 +1227,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()) @@ -2043,7 +2050,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(content, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } } @@ -2075,7 +2082,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); } } diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 493768b368..34dcdc01a9 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -29,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); @@ -44,8 +44,9 @@ 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, Constants.ObjectTypes.DocumentTypeContainerGuid)) { @@ -57,20 +58,32 @@ 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, Constants.ObjectTypes.MediaTypeContainerGuid)) { @@ -82,42 +95,83 @@ 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.DocumentTypeContainerGuid, "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.MediaTypeContainerGuid, "media type", userId); + return SaveContainer( + SavingMediaTypeContainer, SavedMediaTypeContainer, + container, Constants.ObjectTypes.MediaTypeContainerGuid, "media type", userId); } - private void SaveContainer(EntityContainer container, Guid containerObjectType, string objectTypeName, int userId) + private Attempt SaveContainer( + TypedEventHandler> savingEvent, + TypedEventHandler> savedEvent, + EntityContainer container, + Guid containerObjectType, + string objectTypeName, int userId) { + var evtMsgs = EventMessagesFactory.Get(); + if (container.ContainedObjectType != containerObjectType) - throw new InvalidOperationException("Not a " + objectTypeName + " container."); + { + 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, 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) @@ -175,7 +229,7 @@ namespace Umbraco.Core.Services public EntityContainer GetContentTypeContainer(Guid containerId) { - return GetContainer(containerId, Constants.ObjectTypes.DocumentTypeGuid); + return GetContainer(containerId, Constants.ObjectTypes.DocumentTypeContainerGuid); } public IEnumerable GetContentTypeContainers(int[] containerIds) @@ -199,14 +253,14 @@ namespace Umbraco.Core.Services .Where(x => x != int.MinValue && x != contentType.Id) .ToArray(); - return GetContentTypeContainers(ancestorIds); + 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 containerObjectType) { var uow = UowProvider.GetUnitOfWork(); @@ -226,28 +280,54 @@ namespace Umbraco.Core.Services } } - public void DeleteContentTypeContainer(int containerId, int userId = 0) + 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 == 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, Constants.ObjectTypes.MediaTypeContainerGuid)) { var container = repo.Get(containerId); - if (container == null) 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 ? } } @@ -300,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); @@ -334,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); @@ -360,7 +440,7 @@ namespace Umbraco.Core.Services //set to root clone.ParentId = -1; } - + Save(clone); return clone; } @@ -425,7 +505,7 @@ namespace Umbraco.Core.Services public IEnumerable GetAllContentTypes(IEnumerable ids) { using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) - { + { return repository.GetAll(ids.ToArray()); } } @@ -519,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) { @@ -528,10 +608,10 @@ namespace Umbraco.Core.Services if (typedContentService != null) { typedContentService.RebuildXmlStructures(toUpdate.Select(x => x.Id).ToArray()); - } + } } } - + } public int CountContentTypes() @@ -581,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))); @@ -604,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; @@ -624,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()); } } @@ -636,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)) @@ -666,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)) @@ -702,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(); @@ -735,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)) @@ -761,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 /// @@ -894,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)) @@ -949,7 +1039,7 @@ namespace Umbraco.Core.Services var moveInfo = new List>(); var uow = UowProvider.GetUnitOfWork(); - using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) { try @@ -977,6 +1067,92 @@ namespace Umbraco.Core.Services new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); } + public Attempt> CopyMediaType(IMediaType toCopy, int containerId) + { + var evtMsgs = EventMessagesFactory.Get(); + + IMediaType copy; + var uow = UowProvider.GetUnitOfWork(); + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) + using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) + { + try + { + if (containerId > 0) + { + var container = containerRepository.Get(containerId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); + } + var alias = repository.GetUniqueAlias(toCopy.Alias); + copy = toCopy.DeepCloneWithResetIdentities(alias); + copy.Name = copy.Name + " (copy)"; // might not be unique + + // if it has a parent, and the parent is a content type, unplug composition + // all other compositions remain in place in the copied content type + if (copy.ParentId > 0) + { + var parent = repository.Get(copy.ParentId); + if (parent != null) + copy.RemoveContentType(parent.Alias); + } + + copy.ParentId = containerId; + repository.AddOrUpdate(copy); + } + catch (DataOperationException ex) + { + return Attempt.Fail(new OperationStatus(null, ex.Operation, evtMsgs)); + } + uow.Commit(); + } + + return Attempt.Succeed(new OperationStatus(copy, MoveOperationStatusType.Success, evtMsgs)); + } + + public Attempt> CopyContentType(IContentType toCopy, int containerId) + { + var evtMsgs = EventMessagesFactory.Get(); + + IContentType copy; + var uow = UowProvider.GetUnitOfWork(); + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) + { + try + { + if (containerId > 0) + { + var container = containerRepository.Get(containerId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); + } + var alias = repository.GetUniqueAlias(toCopy.Alias); + copy = toCopy.DeepCloneWithResetIdentities(alias); + copy.Name = copy.Name + " (copy)"; // might not be unique + + // if it has a parent, and the parent is a content type, unplug composition + // all other compositions remain in place in the copied content type + if (copy.ParentId > 0) + { + var parent = repository.Get(copy.ParentId); + if (parent != null) + copy.RemoveContentType(parent.Alias); + } + + copy.ParentId = containerId; + repository.AddOrUpdate(copy); + } + catch (DataOperationException ex) + { + return Attempt.Fail(new OperationStatus(null, ex.Operation, evtMsgs)); + } + uow.Commit(); + } + + return Attempt.Succeed(new OperationStatus(copy, MoveOperationStatusType.Success, evtMsgs)); + } + /// /// Saves a single object /// @@ -984,7 +1160,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)) @@ -996,7 +1172,7 @@ namespace Umbraco.Core.Services mediaType.CreatorId = userId; repository.AddOrUpdate(mediaType); uow.Commit(); - + } UpdateContentXmlStructure(mediaType); @@ -1035,7 +1211,7 @@ namespace Umbraco.Core.Services } //save it all in one go - uow.Commit(); + uow.Commit(); } UpdateContentXmlStructure(asArray.Cast().ToArray()); @@ -1053,7 +1229,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)) { @@ -1083,7 +1259,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)) { @@ -1105,7 +1281,7 @@ namespace Umbraco.Core.Services } Audit(AuditType.Delete, string.Format("Delete MediaTypes performed by user"), userId, -1); - } + } } /// @@ -1172,19 +1348,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 /// @@ -1194,7 +1380,7 @@ namespace Umbraco.Core.Services /// Occurs after Delete /// public static event TypedEventHandler> DeletedMediaType; - + /// /// Occurs before Save /// diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index a397975f04..035cfd0ab6 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -29,8 +29,9 @@ 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, Constants.ObjectTypes.DataTypeContainerGuid)) { @@ -42,15 +43,26 @@ 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 ? } } @@ -107,31 +119,65 @@ namespace Umbraco.Core.Services } } - public void SaveContainer(EntityContainer container, int userId = 0) + public Attempt SaveContainer(EntityContainer container, int userId = 0) { - if (container.ContainedObjectType != Constants.ObjectTypes.DataTypeGuid) - throw new InvalidOperationException("Not a data type container."); + 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, 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, Constants.ObjectTypes.DataTypeContainerGuid)) { var container = repo.Get(containerId); - if (container == null) 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 ? } } @@ -537,6 +583,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 69c6708496..1e7a95d6fd 100644 --- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Net.Http.Formatting; using System.Web; using System.Xml; using System.Xml.Linq; @@ -268,11 +267,15 @@ namespace Umbraco.Core.Services structure.Add(new XElement("MediaType", allowedType.Alias)); } - var genericProperties = new XElement("GenericProperties"); + var genericProperties = new XElement("GenericProperties"); // actually, all of them foreach (var propertyType in mediaType.PropertyTypes) { var definition = dataTypeService.GetDataTypeDefinitionById(propertyType.DataTypeDefinitionId); - var propertyGroup = mediaType.PropertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value); + + var propertyGroup = propertyType.PropertyGroupId == null // true generic property + ? null + : mediaType.PropertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value); + var genericProperty = new XElement("GenericProperty", new XElement("Name", propertyType.Name), new XElement("Alias", propertyType.Alias), @@ -382,14 +385,14 @@ namespace Umbraco.Core.Services structure.Add(new XElement("DocumentType", allowedType.Alias)); } - var genericProperties = new XElement("GenericProperties"); + var genericProperties = new XElement("GenericProperties"); // actually, all of them foreach (var propertyType in contentType.PropertyTypes) { var definition = dataTypeService.GetDataTypeDefinitionById(propertyType.DataTypeDefinitionId); - var propertyGroup = propertyType.PropertyGroupId == null - ? null - : contentType.PropertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value); + var propertyGroup = propertyType.PropertyGroupId == null // true generic property + ? null + : contentType.PropertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value); var genericProperty = new XElement("GenericProperty", new XElement("Name", propertyType.Name), diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index 3bb173345a..1dae926c6a 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -255,7 +255,12 @@ namespace Umbraco.Core.Services /// public Attempt> CreateTemplateForContentType(string contentTypeAlias, string contentTypeName, int userId = 0) { - var template = new Template(contentTypeName, contentTypeAlias); + var template = new Template(contentTypeName, + //NOTE: We are NOT passing in the content type alias here, we want to use it's name since we don't + // want to save template file names as camelCase, the Template ctor will clean the alias as + // `alias.ToCleanString(CleanStringType.UnderscoreAlias)` which has been the default. + // This fixes: http://issues.umbraco.org/issue/U4-7953 + contentTypeName); var evtMsgs = EventMessagesFactory.Get(); diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs index 3c6b8404b5..2dcdf01291 100644 --- a/src/Umbraco.Core/Services/IContentTypeService.cs +++ b/src/Umbraco.Core/Services/IContentTypeService.cs @@ -21,10 +21,11 @@ namespace Umbraco.Core.Services /// Attempt ValidateComposition(IContentTypeComposition compo); - Attempt CreateContentTypeContainer(int parentId, string name, int userId = 0); - Attempt CreateMediaTypeContainer(int parentId, string name, int userId = 0); - void SaveContentTypeContainer(EntityContainer container, int userId = 0); - void SaveMediaTypeContainer(EntityContainer container, int userId = 0); + Attempt> CreateContentTypeContainer(int parentId, string name, int userId = 0); + Attempt> CreateMediaTypeContainer(int parentId, string name, int userId = 0); + Attempt SaveContentTypeContainer(EntityContainer container, int userId = 0); + Attempt SaveMediaTypeContainer(EntityContainer container, int userId = 0); + EntityContainer GetContentTypeContainer(int containerId); EntityContainer GetContentTypeContainer(Guid containerId); IEnumerable GetContentTypeContainers(int[] containerIds); @@ -35,8 +36,8 @@ namespace Umbraco.Core.Services IEnumerable GetMediaTypeContainers(int[] containerIds); IEnumerable GetMediaTypeContainers(string folderName, int level); IEnumerable GetMediaTypeContainers(IMediaType mediaType); - void DeleteMediaTypeContainer(int folderId, int userId = 0); - void DeleteContentTypeContainer(int containerId, int userId = 0); + Attempt DeleteMediaTypeContainer(int folderId, int userId = 0); + Attempt DeleteContentTypeContainer(int containerId, int userId = 0); /// /// Gets all property type aliases. @@ -290,5 +291,7 @@ namespace Umbraco.Core.Services Attempt> MoveMediaType(IMediaType toMove, int containerId); Attempt> MoveContentType(IContentType toMove, int containerId); + Attempt> CopyMediaType(IMediaType toCopy, int containerId); + Attempt> CopyContentType(IContentType toCopy, int containerId); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index a94628aa87..9e119cd28a 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -10,14 +10,14 @@ namespace Umbraco.Core.Services /// public interface IDataTypeService : IService { - Attempt CreateContainer(int parentId, string name, int userId = 0); - void SaveContainer(EntityContainer container, int userId = 0); + Attempt> CreateContainer(int parentId, string name, int userId = 0); + Attempt SaveContainer(EntityContainer container, int userId = 0); EntityContainer GetContainer(int containerId); EntityContainer GetContainer(Guid containerId); IEnumerable GetContainers(string folderName, int level); IEnumerable GetContainers(IDataTypeDefinition dataTypeDefinition); IEnumerable GetContainers(int[] containerIds); - void DeleteContainer(int containerId, int userId = 0); + Attempt DeleteContainer(int containerId, int userId = 0); /// /// Gets a by its Name diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 40f8c86ced..83c815c78d 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using Umbraco.Core.Configuration; using System.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.DatabaseModelDefinitions; diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 19bf01f991..5240c5c9b3 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -750,7 +750,7 @@ namespace Umbraco.Core.Services if (Deleting.IsRaisedEventCancelled( new DeleteEventArgs(media, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } //Delete children before deleting the 'possible parent' @@ -775,7 +775,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Delete, "Delete Media performed by user", userId, media.Id); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } /// @@ -794,7 +794,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(media, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } } @@ -819,7 +819,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Save, "Save Media performed by user", userId, media.Id); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } /// @@ -839,7 +839,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(asArray, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } } @@ -867,7 +867,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Save, "Save Media items performed by user", userId, -1); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } /// @@ -969,7 +969,7 @@ namespace Umbraco.Core.Services if (Trashing.IsRaisedEventCancelled( new MoveEventArgs(new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia)), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } var moveInfo = new List> @@ -1011,7 +1011,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Move, "Move Media to Recycle Bin performed by user", userId, media.Id); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } /// diff --git a/src/Umbraco.Core/Services/OperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus.cs index 31b684c4f1..1561eacbda 100644 --- a/src/Umbraco.Core/Services/OperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus.cs @@ -45,14 +45,25 @@ namespace Umbraco.Core.Services #region Static Helper methods - internal static OperationStatus Cancelled(EventMessages eventMessages) + internal static Attempt Exception(EventMessages eventMessages, Exception ex) { - return new OperationStatus(OperationStatusType.FailedCancelledByEvent, eventMessages); + eventMessages.Add(new EventMessage("", ex.Message, EventMessageType.Error)); + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedExceptionThrown, eventMessages), ex); } - internal static OperationStatus Success(EventMessages eventMessages) + internal static Attempt Cancelled(EventMessages eventMessages) { - return new OperationStatus(OperationStatusType.Success, eventMessages); + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, eventMessages)); + } + + internal static Attempt Success(EventMessages eventMessages) + { + return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, eventMessages)); + } + + internal static Attempt NoOperation(EventMessages eventMessages) + { + return Attempt.Succeed(new OperationStatus(OperationStatusType.NoOperation, eventMessages)); } #endregion diff --git a/src/Umbraco.Core/Services/OperationStatusType.cs b/src/Umbraco.Core/Services/OperationStatusType.cs index 14f24c5c4e..85ec4a4746 100644 --- a/src/Umbraco.Core/Services/OperationStatusType.cs +++ b/src/Umbraco.Core/Services/OperationStatusType.cs @@ -12,11 +12,21 @@ namespace Umbraco.Core.Services /// The saving was successful. /// Success = 0, - + /// /// The saving has been cancelled by a 3rd party add-in /// - FailedCancelledByEvent = 14 + FailedCancelledByEvent = 14, + + /// + /// Failed, an exception was thrown/handled + /// + FailedExceptionThrown = 15, + + /// + /// When no operation is executed because it was not needed (i.e. deleting an item that doesn't exist) + /// + NoOperation = 100, //TODO: In the future, we might need to add more operations statuses, potentially like 'FailedByPermissions', etc... } diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index 43651dd18b..4d8a2773ba 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -365,8 +365,7 @@ namespace Umbraco.Core.Services var dependencies = new HashSet(); //Add the Master as a dependency - if (infoElement.Element("Master") != null && - string.IsNullOrEmpty(infoElement.Element("Master").Value) == false) + if (string.IsNullOrEmpty((string)infoElement.Element("Master")) == false) { dependencies.Add(infoElement.Element("Master").Value); } @@ -480,7 +479,7 @@ namespace Umbraco.Core.Services _logger.Error("Could not create folder: " + rootFolder, tryCreateFolder.Exception); throw tryCreateFolder.Exception; } - var rootFolderId = tryCreateFolder.Result; + var rootFolderId = tryCreateFolder.Result.Entity.Id; current = _contentTypeService.GetContentTypeContainer(rootFolderId); } @@ -514,7 +513,7 @@ namespace Umbraco.Core.Services _logger.Error("Could not create folder: " + folderName, tryCreateFolder.Exception); throw tryCreateFolder.Exception; } - return _contentTypeService.GetContentTypeContainer(tryCreateFolder.Result); + return _contentTypeService.GetContentTypeContainer(tryCreateFolder.Result.Entity.Id); } private IContentType CreateContentTypeFromXml(XElement documentType) @@ -627,7 +626,7 @@ namespace Umbraco.Core.Services contentType.AllowedTemplates = allowedTemplates; } - if (string.IsNullOrEmpty(defaultTemplateElement.Value) == false) + if (string.IsNullOrEmpty((string)defaultTemplateElement) == false) { var defaultTemplate = _fileService.GetTemplate(defaultTemplateElement.Value.ToSafeAlias()); if (defaultTemplate != null) @@ -698,7 +697,7 @@ namespace Umbraco.Core.Services : _dataTypeService.GetDataTypeDefinitionByPropertyEditorAlias(propertyEditorAlias); if (dataTypeDefinitions != null && dataTypeDefinitions.Any()) { - dataTypeDefinition = dataTypeDefinitions.First(); + dataTypeDefinition = dataTypeDefinitions.FirstOrDefault(); } } else if (legacyPropertyEditorId != Guid.Empty && dataTypeDefinition.ControlId != legacyPropertyEditorId) @@ -706,7 +705,7 @@ namespace Umbraco.Core.Services var dataTypeDefinitions = _dataTypeService.GetDataTypeDefinitionByControlId(legacyPropertyEditorId); if (dataTypeDefinitions != null && dataTypeDefinitions.Any()) { - dataTypeDefinition = dataTypeDefinitions.First(); + dataTypeDefinition = dataTypeDefinitions.FirstOrDefault(); } } else if (dataTypeDefinition.PropertyEditorAlias != propertyEditorAlias) @@ -714,7 +713,7 @@ namespace Umbraco.Core.Services var dataTypeDefinitions = _dataTypeService.GetDataTypeDefinitionByPropertyEditorAlias(propertyEditorAlias); if (dataTypeDefinitions != null && dataTypeDefinitions.Any()) { - dataTypeDefinition = dataTypeDefinitions.First(); + dataTypeDefinition = dataTypeDefinitions.FirstOrDefault(); } } @@ -741,13 +740,13 @@ namespace Umbraco.Core.Services var propertyType = new PropertyType(dataTypeDefinition, property.Element("Alias").Value) { Name = property.Element("Name").Value, - Description = property.Element("Description") != null ? property.Element("Description").Value : null, + Description = (string)property.Element("Description"), Mandatory = property.Element("Mandatory") != null ? property.Element("Mandatory").Value.ToLowerInvariant().Equals("true") : false, - ValidationRegExp = property.Element("Validation") != null ? property.Element("Validation").Value : null, + ValidationRegExp = (string)property.Element("Validation"), SortOrder = sortOrder }; - var tab = property.Element("Tab").Value; + var tab = (string)property.Element("Tab"); if (string.IsNullOrEmpty(tab)) { contentType.AddPropertyType(propertyType); @@ -797,14 +796,14 @@ namespace Umbraco.Core.Services using (var repository = _repositoryFactory.CreateContentTypeRepository(_uowProvider.GetUnitOfWork())) { var query = Query.Builder.Where(x => x.Alias == contentTypeAlias); - var types = repository.GetByQuery(query); + var types = repository.GetByQuery(query).ToArray(); - if (!types.Any()) + if (types.Any() == false) throw new Exception( string.Format("No ContentType matching the passed in Alias: '{0}' was found", contentTypeAlias)); - var contentType = types.First(); + var contentType = types.FirstOrDefault(); if (contentType == null) throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null", @@ -889,7 +888,7 @@ namespace Umbraco.Core.Services { var dataTypeDefinitionName = dataTypeElement.Attribute("Name").Value; - var legacyPropertyEditorId = Guid.Empty; + Guid legacyPropertyEditorId; Guid.TryParse(dataTypeElement.Attribute("Id").Value, out legacyPropertyEditorId); var dataTypeDefinitionId = new Guid(dataTypeElement.Attribute("Definition").Value); @@ -982,7 +981,7 @@ namespace Umbraco.Core.Services _logger.Error("Could not create folder: " + rootFolder, tryCreateFolder.Exception); throw tryCreateFolder.Exception; } - current = _dataTypeService.GetContainer(tryCreateFolder.Result); + current = _dataTypeService.GetContainer(tryCreateFolder.Result.Entity.Id); } importedFolders.Add(name, current.Id); @@ -1015,7 +1014,7 @@ namespace Umbraco.Core.Services _logger.Error("Could not create folder: " + folderName, tryCreateFolder.Exception); throw tryCreateFolder.Exception; } - return _dataTypeService.GetContainer(tryCreateFolder.Result); + return _dataTypeService.GetContainer(tryCreateFolder.Result.Entity.Id); } private void SavePrevaluesFromXml(List dataTypes, IEnumerable dataTypeElements) @@ -1026,23 +1025,30 @@ namespace Umbraco.Core.Services if (prevaluesElement == null) continue; var dataTypeDefinitionName = dataTypeElement.Attribute("Name").Value; - var dataTypeDefinition = dataTypes.First(x => x.Name == dataTypeDefinitionName); + var dataTypeDefinition = dataTypes.FirstOrDefault(x => x.Name == dataTypeDefinitionName); - var valuesWithoutKeys = prevaluesElement.Elements("PreValue") - .Where(x => ((string)x.Attribute("Alias")).IsNullOrWhiteSpace()) - .Select(x => x.Attribute("Value").Value); + if (dataTypeDefinition != null) + { + var valuesWithoutKeys = prevaluesElement.Elements("PreValue") + .Where(x => ((string) x.Attribute("Alias")).IsNullOrWhiteSpace()) + .Select(x => x.Attribute("Value").Value); - var valuesWithKeys = prevaluesElement.Elements("PreValue") - .Where(x => ((string)x.Attribute("Alias")).IsNullOrWhiteSpace() == false) - .ToDictionary( - key => (string)key.Attribute("Alias"), - val => new PreValue((string)val.Attribute("Value"))); + var valuesWithKeys = prevaluesElement.Elements("PreValue") + .Where(x => ((string) x.Attribute("Alias")).IsNullOrWhiteSpace() == false) + .ToDictionary( + key => (string) key.Attribute("Alias"), + val => new PreValue((string) val.Attribute("Value"))); - //save the values with keys - _dataTypeService.SavePreValues(dataTypeDefinition, valuesWithKeys); + //save the values with keys + _dataTypeService.SavePreValues(dataTypeDefinition, valuesWithKeys); - //save the values without keys (this is legacy) - _dataTypeService.SavePreValues(dataTypeDefinition.Id, valuesWithoutKeys); + //save the values without keys (this is legacy) + _dataTypeService.SavePreValues(dataTypeDefinition.Id, valuesWithoutKeys); + } + else + { + _logger.Warn("No data type found with name " + dataTypeDefinitionName + " data type pre-values will not be saved"); + } } } @@ -1329,31 +1335,31 @@ namespace Umbraco.Core.Services //Following xml elements are treated as nullable properties var useInEditorElement = macroElement.Element("useInEditor"); var useInEditor = false; - if (useInEditorElement != null && string.IsNullOrEmpty(useInEditorElement.Value) == false) + if (useInEditorElement != null && string.IsNullOrEmpty((string)useInEditorElement) == false) { useInEditor = bool.Parse(useInEditorElement.Value); } var cacheDurationElement = macroElement.Element("refreshRate"); var cacheDuration = 0; - if (cacheDurationElement != null && string.IsNullOrEmpty(cacheDurationElement.Value) == false) + if (cacheDurationElement != null && string.IsNullOrEmpty((string)cacheDurationElement) == false) { cacheDuration = int.Parse(cacheDurationElement.Value); } var cacheByMemberElement = macroElement.Element("cacheByMember"); var cacheByMember = false; - if (cacheByMemberElement != null && string.IsNullOrEmpty(cacheByMemberElement.Value) == false) + if (cacheByMemberElement != null && string.IsNullOrEmpty((string)cacheByMemberElement) == false) { cacheByMember = bool.Parse(cacheByMemberElement.Value); } var cacheByPageElement = macroElement.Element("cacheByPage"); var cacheByPage = false; - if (cacheByPageElement != null && string.IsNullOrEmpty(cacheByPageElement.Value) == false) + if (cacheByPageElement != null && string.IsNullOrEmpty((string)cacheByPageElement) == false) { cacheByPage = bool.Parse(cacheByPageElement.Value); } var dontRenderElement = macroElement.Element("dontRender"); var dontRender = true; - if (dontRenderElement != null && string.IsNullOrEmpty(dontRenderElement.Value) == false) + if (dontRenderElement != null && string.IsNullOrEmpty((string)dontRenderElement) == false) { dontRender = bool.Parse(dontRenderElement.Value); } @@ -1526,23 +1532,20 @@ namespace Umbraco.Core.Services var dependencies = new List(); var elementCopy = tempElement; //Ensure that the Master of the current template is part of the import, otherwise we ignore this dependency as part of the dependency sorting. - if (elementCopy.Element("Master") != null && - string.IsNullOrEmpty(elementCopy.Element("Master").Value) == false && - templateElements.Any(x => x.Element("Alias").Value == elementCopy.Element("Master").Value)) + if (string.IsNullOrEmpty((string)elementCopy.Element("Master")) == false && + templateElements.Any(x => (string)x.Element("Alias") == (string)elementCopy.Element("Master"))) { - dependencies.Add(elementCopy.Element("Master").Value); + dependencies.Add((string)elementCopy.Element("Master")); } - else if (elementCopy.Element("Master") != null && - string.IsNullOrEmpty(elementCopy.Element("Master").Value) == false && - templateElements.Any(x => x.Element("Alias").Value == elementCopy.Element("Master").Value) == - false) + else if (string.IsNullOrEmpty((string)elementCopy.Element("Master")) == false && + templateElements.Any(x => (string)x.Element("Alias") == (string)elementCopy.Element("Master")) == false) { - _logger.Info(string.Format("Template '{0}' has an invalid Master '{1}', so the reference has been ignored.", elementCopy.Element("Alias").Value, elementCopy.Element("Master").Value)); + _logger.Info(string.Format("Template '{0}' has an invalid Master '{1}', so the reference has been ignored.", (string)elementCopy.Element("Alias"), (string)elementCopy.Element("Master"))); } var field = new TopologicalSorter.DependencyField { - Alias = elementCopy.Element("Alias").Value, + Alias = (string)elementCopy.Element("Alias"), Item = new Lazy(() => elementCopy), DependsOn = dependencies.ToArray() }; @@ -1564,7 +1567,7 @@ namespace Umbraco.Core.Services var existingTemplate = _fileService.GetTemplate(alias) as Template; var template = existingTemplate ?? new Template(path, templateName, alias); template.Content = design; - if (masterElement != null && string.IsNullOrEmpty(masterElement.Value) == false) + if (masterElement != null && string.IsNullOrEmpty((string)masterElement) == false) { template.MasterTemplateAlias = masterElement.Value; var masterTemplate = templates.FirstOrDefault(x => x.Alias == masterElement.Value); diff --git a/src/Umbraco.Core/Services/PublicAccessService.cs b/src/Umbraco.Core/Services/PublicAccessService.cs index bb966309dc..c2eb536bca 100644 --- a/src/Umbraco.Core/Services/PublicAccessService.cs +++ b/src/Umbraco.Core/Services/PublicAccessService.cs @@ -183,7 +183,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(entry, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } repo.AddOrUpdate(entry); @@ -191,7 +191,7 @@ namespace Umbraco.Core.Services uow.Commit(); Saved.RaiseEvent(new SaveEventArgs(entry, false, evtMsgs), this); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } } @@ -207,7 +207,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(entry, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } var uow = UowProvider.GetUnitOfWork(); @@ -218,7 +218,7 @@ namespace Umbraco.Core.Services } Saved.RaiseEvent(new SaveEventArgs(entry, false, evtMsgs), this); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } /// @@ -232,7 +232,7 @@ namespace Umbraco.Core.Services new DeleteEventArgs(entry, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } var uow = UowProvider.GetUnitOfWork(); @@ -243,7 +243,7 @@ namespace Umbraco.Core.Services } Deleted.RaiseEvent(new DeleteEventArgs(entry, false, evtMsgs), this); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } /// diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index b6d6d7ce27..5aae1dbd4a 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -41,6 +41,16 @@ namespace Umbraco.Core ToCSharpEscapeChars[escape[0]] = escape[1]; } + /// + /// Removes new lines and tabs + /// + /// + /// + internal static string StripWhitespace(this string txt) + { + return Regex.Replace(txt, @"\s", string.Empty); + } + internal static string StripFileExtension(this string fileName) { //filenames cannot contain line breaks diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index a4b4d35d10..d8d828b23e 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -60,7 +60,7 @@ namespace Umbraco.Core.Sync protected override bool RequiresDistributed(IEnumerable servers, ICacheRefresher refresher, MessageType dispatchType) { - // we don't care if there's servers listed or not, + // we don't care if there's servers listed or not, // if distributed call is enabled we will make the call return _initialized && DistributedEnabled; } @@ -139,18 +139,43 @@ namespace Umbraco.Core.Sync { if (_released) return; + var coldboot = false; if (_lastId < 0) // never synced before { - // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new + // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new // server and it will need to rebuild it's own caches, eg Lucene or the xml cache file. - _logger.Warn("No last synced Id found, this generally means this is a new server/install. The server will rebuild its caches and indexes and then adjust it's last synced id to the latest found in the database and will start maintaining cache updates based on that id"); + _logger.Warn("No last synced Id found, this generally means this is a new server/install." + + " The server will build its caches and indexes, and then adjust its last synced Id to the latest found in" + + " the database and maintain cache updates based on that Id."); + coldboot = true; + } + else + { + //check for how many instructions there are to process + var count = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); + if (count > _options.MaxProcessingInstructionCount) + { + //too many instructions, proceed to cold boot + _logger.Warn("The instruction count ({0}) exceeds the specified MaxProcessingInstructionCount ({1})." + + " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id" + + " to the latest found in the database and maintain cache updates based on that Id.", + () => count, () => _options.MaxProcessingInstructionCount); + + coldboot = true; + } + } + + if (coldboot) + { // go get the last id in the db and store it // note: do it BEFORE initializing otherwise some instructions might get lost // when doing it before, some instructions might run twice - not an issue - var lastId = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction"); - if (lastId > 0) - SaveLastSynced(lastId); + var maxId = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction"); + + //if there is a max currently, or if we've never synced + if (maxId > 0 || _lastId < 0) + SaveLastSynced(maxId); // execute initializing callbacks if (_options.InitializingCallbacks != null) @@ -169,13 +194,13 @@ namespace Umbraco.Core.Sync { lock (_locko) { - if (_syncing) + if (_syncing) return; if (_released) return; - if ((DateTime.UtcNow - _lastSync).Seconds <= _options.ThrottleSeconds) + if ((DateTime.UtcNow - _lastSync).TotalSeconds <= _options.ThrottleSeconds) return; _syncing = true; @@ -213,9 +238,9 @@ namespace Umbraco.Core.Sync private void ProcessDatabaseInstructions() { // NOTE - // we 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that + // we 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that // would be a good idea since instructions could keep getting added and then all other threads will probably get stuck from serving requests - // (depending on what the cache refreshers are doing). I think it's best we do the one time check, process them and continue, if there are + // (depending on what the cache refreshers are doing). I think it's best we do the one time check, process them and continue, if there are // pending requests after being processed, they'll just be processed on the next poll. // // FIXME not true if we're running on a background thread, assuming we can? @@ -281,7 +306,7 @@ namespace Umbraco.Core.Sync /// Remove old instructions from the database /// /// - /// Always leave the last (most recent) record in the db table, this is so that not all instructions are removed which would cause + /// Always leave the last (most recent) record in the db table, this is so that not all instructions are removed which would cause /// the site to cold boot if there's been no instruction activity for more than DaysToRetainInstructions. /// See: http://issues.umbraco.org/issue/U4-7643#comment=67-25085 /// @@ -290,15 +315,15 @@ namespace Umbraco.Core.Sync var pruneDate = DateTime.UtcNow.AddDays(-_options.DaysToRetainInstructions); var sqlSyntax = _appContext.DatabaseContext.SqlSyntax; - //NOTE: this query could work on SQL server and MySQL: + //NOTE: this query could work on SQL server and MySQL: /* SELECT id FROM umbracoCacheInstruction - WHERE utcStamp < getdate() + WHERE utcStamp < getdate() AND id <> (SELECT MAX(id) FROM umbracoCacheInstruction) */ // However, this will not work on SQLCE and in fact it will be slower than the query we are - // using if the SQL server doesn't perform it's own query optimizations (i.e. since the above + // using if the SQL server doesn't perform it's own query optimizations (i.e. since the above // query could actually execute a sub query for every row found). So we've had to go with an // inner join which is faster and works on SQLCE but it's uglier to read. @@ -331,9 +356,9 @@ namespace Umbraco.Core.Sync var dtos = _appContext.DatabaseContext.Database.Fetch(sql); if (dtos.Count == 0) - _lastId = -1; + _lastId = -1; } - + /// /// Reads the last-synced id from file into memory. /// @@ -502,4 +527,3 @@ namespace Umbraco.Core.Sync #endregion } } - \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs index f1bebce10b..7559c37813 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; namespace Umbraco.Core.Sync -{ +{ /// /// Provides options to the . /// @@ -14,9 +14,15 @@ namespace Umbraco.Core.Sync public DatabaseServerMessengerOptions() { DaysToRetainInstructions = 2; // 2 days - ThrottleSeconds = 5; // 5 seconds + ThrottleSeconds = 5; // 5 second + MaxProcessingInstructionCount = 1000; } + /// + /// The maximum number of instructions that can be processed at startup; otherwise the server cold-boots (rebuilds its caches). + /// + public int MaxProcessingInstructionCount { get; set; } + /// /// A list of callbacks that will be invoked if the lastsynced.txt file does not exist. /// diff --git a/src/Umbraco.Core/TypeFinder.cs b/src/Umbraco.Core/TypeFinder.cs index e99a491939..f970cf225b 100644 --- a/src/Umbraco.Core/TypeFinder.cs +++ b/src/Umbraco.Core/TypeFinder.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core /// /// A utility class to find all classes of a certain type by reflection in the current bin folder - /// of the web application. + /// of the web application. /// public static class TypeFinder { @@ -34,7 +34,7 @@ namespace Umbraco.Core /// This is a modified version of: http://www.dominicpettifer.co.uk/Blog/44/how-to-get-a-reference-to-all-assemblies-in-the--bin-folder /// /// - /// We do this because we cannot use AppDomain.Current.GetAssemblies() as this will return only assemblies that have been + /// We do this because we cannot use AppDomain.Current.GetAssemblies() as this will return only assemblies that have been /// loaded in the CLR, not all assemblies. /// See these threads: /// http://issues.umbraco.org/issue/U5-198 @@ -136,7 +136,7 @@ namespace Umbraco.Core }); /// - /// Return a list of found local Assemblies excluding the known assemblies we don't want to scan + /// Return a list of found local Assemblies excluding the known assemblies we don't want to scan /// and exluding the ones passed in and excluding the exclusion list filter, the results of this are /// cached for perforance reasons. /// @@ -190,6 +190,7 @@ namespace Umbraco.Core /// /// /// NOTE the comma vs period... comma delimits the name in an Assembly FullName property so if it ends with comma then its an exact name match + /// NOTE this means that "foo." will NOT exclude "foo.dll" but only "foo.*.dll" /// internal static readonly string[] KnownAssemblyExclusionFilter = new[] { @@ -214,7 +215,7 @@ namespace Umbraco.Core "RouteDebugger,", "SqlCE4Umbraco,", "umbraco.datalayer,", - "umbraco.interfaces,", + "umbraco.interfaces,", //"umbraco.providers,", //"Umbraco.Web.UI,", "umbraco.webservices", @@ -230,9 +231,9 @@ namespace Umbraco.Core "AutoMapper,", "AutoMapper.", "AzureDirectory,", - "itextsharp,", + "itextsharp,", "UrlRewritingNet.", - "HtmlAgilityPack,", + "HtmlAgilityPack,", "MiniProfiler,", "Moq,", "nunit.framework,", @@ -371,7 +372,7 @@ namespace Umbraco.Core var assemblyList = assemblies.ToArray(); - //find all assembly references that are referencing the attribute type's assembly since we + //find all assembly references that are referencing the attribute type's assembly since we //should only be scanning those assemblies because any other assembly will definitely not //contain a class that has this attribute. var referencedAssemblies = TypeHelper.GetReferencedAssemblies(attributeType, assemblyList); @@ -418,7 +419,7 @@ namespace Umbraco.Core foreach (var subTypesInAssembly in allAttributeTypes.GroupBy(x => x.Assembly)) { - //So that we are not scanning too much, we need to group the sub types: + //So that we are not scanning too much, we need to group the sub types: // * if there is more than 1 sub type in the same assembly then we should only search on the 'lowest base' type. // * We should also not search for sub types if the type is sealed since you cannot inherit from a sealed class // * We should not search for sub types if the type is static since you cannot inherit from them. @@ -489,7 +490,7 @@ namespace Umbraco.Core /// /// Finds types that are assignable from the assignTypeFrom parameter and will scan for these types in the assembly - /// list passed in, however we will only scan assemblies that have a reference to the assignTypeFrom Type or any type + /// list passed in, however we will only scan assemblies that have a reference to the assignTypeFrom Type or any type /// deriving from the base type. /// /// @@ -513,7 +514,7 @@ namespace Umbraco.Core var assemblyList = assemblies.ToArray(); - //find all assembly references that are referencing the current type's assembly since we + //find all assembly references that are referencing the current type's assembly since we //should only be scanning those assemblies because any other assembly will definitely not //contain sub type's of the one we're currently looking for var referencedAssemblies = TypeHelper.GetReferencedAssemblies(assignTypeFrom, assemblyList); @@ -560,7 +561,7 @@ namespace Umbraco.Core foreach (var subTypesInAssembly in allSubTypes.GroupBy(x => x.Assembly)) { - //So that we are not scanning too much, we need to group the sub types: + //So that we are not scanning too much, we need to group the sub types: // * if there is more than 1 sub type in the same assembly then we should only search on the 'lowest base' type. // * We should also not search for sub types if the type is sealed since you cannot inherit from a sealed class // * We should not search for sub types if the type is static since you cannot inherit from them. @@ -602,39 +603,69 @@ namespace Umbraco.Core return foundAssignableTypes; } - private static IEnumerable GetTypesWithFormattedException(Assembly a) + internal static IEnumerable GetTypesWithFormattedException(Assembly a) { //if the assembly is dynamic, do not try to scan it if (a.IsDynamic) return Enumerable.Empty(); + var getAll = a.GetCustomAttribute() == null; + try { //we need to detect if an assembly is partially trusted, if so we cannot go interrogating all of it's types //only its exported types, otherwise we'll get exceptions. - if (a.GetCustomAttribute() == null) - { - return a.GetTypes(); - } - else - { - return a.GetExportedTypes(); - } + return getAll ? a.GetTypes() : a.GetExportedTypes(); } - catch (ReflectionTypeLoadException ex) + catch (TypeLoadException ex) // GetExportedTypes *can* throw TypeLoadException! { var sb = new StringBuilder(); - sb.AppendLine("Could not load types from assembly " + a.FullName + ", errors:"); - foreach (var loaderException in ex.LoaderExceptions.WhereNotNull()) - { - sb.AppendLine("Exception: " + loaderException); - } - throw new ReflectionTypeLoadException(ex.Types, ex.LoaderExceptions, sb.ToString()); + AppendCouldNotLoad(sb, a, getAll); + AppendLoaderException(sb, ex); + + // rethrow as ReflectionTypeLoadException (for consistency) with new message + throw new ReflectionTypeLoadException(new Type[0], new Exception[] { ex }, sb.ToString()); + } + catch (ReflectionTypeLoadException rex) // GetTypes throws ReflectionTypeLoadException + { + var sb = new StringBuilder(); + AppendCouldNotLoad(sb, a, getAll); + foreach (var loaderException in rex.LoaderExceptions.WhereNotNull()) + AppendLoaderException(sb, loaderException); + + // rethrow with new message + throw new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); } } + private static void AppendCouldNotLoad(StringBuilder sb, Assembly a, bool getAll) + { + sb.Append("Could not load "); + sb.Append(getAll ? "all" : "exported"); + sb.Append(" types from \""); + sb.Append(a.FullName); + sb.AppendLine("\" due to LoaderExceptions, skipping:"); + } + + private static void AppendLoaderException(StringBuilder sb, Exception loaderException) + { + sb.Append(". "); + sb.Append(loaderException.GetType().FullName); + + var tloadex = loaderException as TypeLoadException; + if (tloadex != null) + { + sb.Append(" on "); + sb.Append(tloadex.TypeName); + } + + sb.Append(": "); + sb.Append(loaderException.Message); + sb.AppendLine(); + } + #endregion - + public static Type GetTypeByName(string typeName) { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 4d44f812bf..f329fab881 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -102,28 +102,16 @@ - + False - ..\packages\SqlServerCE.4.0.0.0\lib\System.Data.SqlServerCe.dll + ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.dll - + False - ..\packages\SqlServerCE.4.0.0.0\lib\System.Data.SqlServerCe.Entity.dll + ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.Entity.dll - - ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll - True - - - ..\packages\Microsoft.AspNet.WebApi.Client.4.0.30506.0\lib\net40\System.Net.Http.Formatting.dll - False - - - ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll - True - @@ -175,6 +163,7 @@ + @@ -188,6 +177,7 @@ + @@ -319,6 +309,7 @@ + @@ -486,10 +477,11 @@ - + + @@ -501,6 +493,7 @@ + @@ -522,6 +515,7 @@ + @@ -1413,7 +1407,9 @@ - + + Designer + @@ -1429,11 +1425,6 @@ - - - - - + + + + + + +

Controller example

+
+	(function () {
+		"use strict";
+
+		function Controller() {
+
+            var vm = this;
+
+            vm.id = 1;
+            vm.name = "My Parent element";
+            vm.icon = "icon-document";
+            vm.selectedChildren = [];
+            vm.availableChildren = [
+                {
+                    id: 1,
+                    alias: "item1",
+                    name: "Item 1",
+                    icon: "icon-document"
+                },
+                {
+                    id: 2,
+                    alias: "item2",
+                    name: "Item 2",
+                    icon: "icon-document"
+                }
+            ];
+
+            vm.addChild = addChild;
+            vm.removeChild = removeChild;
+
+            function addChild($event) {
+                vm.overlay = {
+                    view: "itempicker",
+                    title: "Choose child",
+                    availableItems: vm.availableChildren,
+                    selectedItems: vm.selectedChildren,
+                    event: $event,
+                    show: true,
+                    submit: function(model) {
+
+                        // add selected child
+                        vm.selectedChildren.push(model.selectedItem);
+
+                        // close overlay
+                        vm.overlay.show = false;
+                        vm.overlay = null;
+                    }
+                };
+            }
+
+            function removeChild($index) {
+                vm.selectedChildren.splice($index, 1);
+            }
+
+        }
+
+		angular.module("umbraco").controller("My.Controller", Controller);
+
+	})();
+
+ +@param {array} selectedChildren (binding): Array of selected children. +@param {array} availableChildren (binding: Array of items available for selection. +@param {string} parentName (binding): The parent name. +@param {string} parentIcon (binding): The parent icon. +@param {number} parentId (binding): The parent id. +@param {callback} onRemove (binding): Callback when the remove button is clicked on an item. +

The callback returns:

+
    +
  • child: The selected item.
  • +
  • $index: The selected item index.
  • +
+@param {callback} onAdd (binding): Callback when the add button is clicked. +

The callback returns:

+
    +
  • $event: The select event.
  • +
+**/ + (function() { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js index df3e431620..1dcccda481 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js @@ -1,3 +1,69 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbConfirmAction +@restrict E +@scope + +@description +

Use this directive to toggle a confirmation prompt for an action. +The prompt consists of a checkmark and a cross to confirm or cancel the action. +The prompt can be opened in four direction up, down, left or right.

+ +

Markup example

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

Controller example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.promptIsVisible = false;
+
+            vm.confirmAction = confirmAction;
+            vm.showPrompt = showPrompt;
+            vm.hidePrompt = hidePrompt;
+
+            function confirmAction() {
+                // confirm logic here
+            }
+
+            function showPrompt() {
+                vm.promptIsVisible = true;
+            }
+
+            function hidePrompt() {
+                vm.promptIsVisible = false;
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {string} direction The direction the prompt opens ("up", "down", "left", "right"). +@param {callback} onConfirm Callback when the checkmark is clicked. +@param {callback} onCancel Callback when the cross is clicked. +**/ + (function() { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcontentgrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcontentgrid.directive.js index d993880e8c..3994770c8e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcontentgrid.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcontentgrid.directive.js @@ -1,3 +1,108 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbContentGrid +@restrict E +@scope + +@description +Use this directive to generate a list of content items presented as a flexbox grid. + +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.contentItems = [
+                {
+                    "name": "Cape",
+                    "published": true,
+                    "icon": "icon-document",
+                    "updateDate": "15-02-2016",
+                    "owner": "Mr. Batman",
+                    "selected": false
+                },
+                {
+                    "name": "Utility Belt",
+                    "published": true,
+                    "icon": "icon-document",
+                    "updateDate": "15-02-2016",
+                    "owner": "Mr. Batman",
+                    "selected": false
+                },
+                {
+                    "name": "Cave",
+                    "published": true,
+                    "icon": "icon-document",
+                    "updateDate": "15-02-2016",
+                    "owner": "Mr. Batman",
+                    "selected": false
+                }
+            ];
+            vm.includeProperties = [
+                {
+                  "alias": "updateDate",
+                  "header": "Last edited"
+                },
+                {
+                  "alias": "owner",
+                  "header": "Created by"
+                }
+            ];
+
+            vm.clickItem = clickItem;
+            vm.selectItem = selectItem;
+
+
+            function clickItem(item, $event, $index){
+                // do magic here
+            }
+
+            function selectItem(item, $event, $index) {
+                // set item.selected = true; to select the item
+                // do magic here
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {array} content (binding): Array of content items. +@param {array=} contentProperties (binding): Array of content item properties to include in the item. If left empty the item will only show the item icon and name. +@param {callback=} onClick (binding): Callback method to handle click events on the content item. +

The callback returns:

+
    +
  • item: The clicked item
  • +
  • $event: The select event
  • +
  • $index: The item index
  • +
+@param {callback=} onClickName (binding): Callback method to handle click events on the checkmark icon. +

The callback returns:

+
    +
  • item: The selected item
  • +
  • $event: The select event
  • +
  • $index: The item index
  • +
+**/ + (function() { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbemptystate.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbemptystate.directive.js index 6f5cbd2166..b37895637b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbemptystate.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbemptystate.directive.js @@ -1,3 +1,29 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEmptyState +@restrict E +@scope + +@description +Use this directive to show an empty state message. + +

Markup example

+
+    
+ + + // Empty state content + + +
+
+ +@param {string=} size Set the size of the text ("small", "large"). +@param {string=} position Set the position of the text ("center"). +**/ + (function() { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbfoldergrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbfoldergrid.directive.js index 9f18f91e26..812038b51a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbfoldergrid.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbfoldergrid.directive.js @@ -1,3 +1,83 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbFolderGrid +@restrict E +@scope + +@description +Use this directive to generate a list of folders presented as a flexbox grid. + +

Markup example

+
+    
+ + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller(myService) {
+
+            var vm = this;
+            vm.folders = [
+                {
+                    "name": "Folder 1",
+                    "icon": "icon-folder",
+                    "selected": false
+                },
+                {
+                    "name": "Folder 2",
+                    "icon": "icon-folder",
+                    "selected": false
+                }
+
+            ];
+
+            vm.clickFolder = clickFolder;
+            vm.selectFolder = selectFolder;
+
+            myService.getFolders().then(function(folders){
+                vm.folders = folders;
+            });
+
+            function clickFolder(folder){
+                // Execute when clicking folder name/link
+            }
+
+            function selectFolder(folder, event, index) {
+                // Execute when clicking folder
+                // set folder.selected = true; to show checkmark icon
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {array} folders (binding): Array of folders +@param {callback=} onClick (binding): Callback method to handle click events on the folder. +

The callback returns:

+
    +
  • folder: The selected folder
  • +
+@param {callback=} onSelect (binding): Callback method to handle click events on the checkmark icon. +

The callback returns:

+
    +
  • folder: The selected folder
  • +
  • $event: The select event
  • +
  • $index: The folder index
  • +
+**/ + (function() { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 81c6a6612e..caa79439be 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -1,10 +1,13 @@ (function() { 'use strict'; - function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout) { + function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService) { function link(scope, el, attr, ctrl) { + var validationTranslated = ""; + var tabNoSortOrderTranslated = ""; + scope.sortingMode = false; scope.toolbar = []; scope.sortableOptionsGroup = {}; @@ -25,6 +28,16 @@ // add init tab addInitGroup(scope.model.groups); + activateFirstGroup(scope.model.groups); + + // localize texts + localizationService.localize("validation_validation").then(function(value) { + validationTranslated = value; + }); + + localizationService.localize("contentTypeEditor_tabHasNoSortOrder").then(function(value) { + tabNoSortOrderTranslated = value; + }); } function setSortingOptions() { @@ -142,14 +155,14 @@ return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) { _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) { - //reset first + //reset first current.allowed = true; //see if this list item is found in the response (allowed) list var found = _.find(filteredAvailableCompositeTypes, function (f) { return current.contentType.alias === f.contentType.alias; }); - //allow if the item was found in the response (allowed) list - + //allow if the item was found in the response (allowed) list - // and ensure its set to allowed if it is currently checked, // DO not allow if it's a locked content type. current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 && @@ -170,7 +183,7 @@ } function setupAvailableContentTypesModel(result) { - scope.compositionsDialogModel.availableCompositeContentTypes = result; + scope.compositionsDialogModel.availableCompositeContentTypes = result; //iterate each one and set it up _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) { //enable it if it's part of the selected model @@ -202,13 +215,30 @@ scope.toggleSortingMode = function(tool) { - scope.sortingMode = !scope.sortingMode; + if (scope.sortingMode === true) { - if(scope.sortingMode === true) { - scope.sortingButtonKey = "general_reorderDone"; - } else { - scope.sortingButtonKey = "general_reorder"; - } + var sortOrderMissing = false; + + for (var i = 0; i < scope.model.groups.length; i++) { + var group = scope.model.groups[i]; + if (group.tabState !== "init" && group.sortOrder === undefined) { + sortOrderMissing = true; + group.showSortOrderMissing = true; + notificationsService.error(validationTranslated + ": " + group.name + " " + tabNoSortOrderTranslated); + } + } + + if (!sortOrderMissing) { + scope.sortingMode = false; + scope.sortingButtonKey = "general_reorder"; + } + + } else { + + scope.sortingMode = true; + scope.sortingButtonKey = "general_reorderDone"; + + } }; @@ -248,7 +278,7 @@ // submit overlay if no compositions has been removed // or the action has been confirmed } else { - + // make sure that all tabs has an init property if (scope.model.groups.length !== 0) { angular.forEach(scope.model.groups, function(group) { @@ -394,8 +424,12 @@ } }; - scope.changeSortOrderValue = function() { - scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder'); + scope.changeSortOrderValue = function(group) { + + if (group.sortOrder !== undefined) { + group.showSortOrderMissing = false; + } + scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder'); }; function addInitGroup(groups) { @@ -422,6 +456,15 @@ return groups; } + function activateFirstGroup(groups) { + if (groups && groups.length > 0) { + var firstGroup = groups[0]; + if(!firstGroup.tabState || firstGroup.tabState === "inActive") { + firstGroup.tabState = "active"; + } + } + } + /* ---------- PROPERTIES ---------- */ scope.addProperty = function(property, group) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbkeyboardshortcutsoverview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbkeyboardshortcutsoverview.directive.js index 127bf38f49..1bbfb850d9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbkeyboardshortcutsoverview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbkeyboardshortcutsoverview.directive.js @@ -1,3 +1,112 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbkeyboardShortcutsOverview +@restrict E +@scope + +@description + +

Use this directive to show an overview of keyboard shortcuts in an editor. +The directive will render an overview trigger wich shows how the overview is opened. +When this combination is hit an overview is opened with shortcuts based on the model sent to the directive.

+ +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.keyboardShortcutsOverview = [
+                {
+                    "name": "Sections",
+                    "shortcuts": [
+                        {
+                            "description": "Navigate sections",
+                            "keys": [
+                                {"key": "1"},
+                                {"key": "4"}
+                            ],
+                            "keyRange": true
+                        }
+                    ]
+                },
+                {
+                    "name": "Design",
+                    "shortcuts": [
+                        {
+                            "description": "Add tab",
+                            "keys": [
+                                {"key": "alt"},
+                                {"key": "shift"},
+                                {"key": "t"}
+                            ]
+                        }
+                    ]
+                }
+            ];
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +

Model description

+
    +
  • + name + (string) - + Sets the shortcut section name. +
  • +
  • + shortcuts + (array) - + Array of available shortcuts in the section. +
  • +
      +
    • + description + (string) - + Short description of the shortcut. +
    • +
    • + keys + (array) - + Array of keys in the shortcut. +
    • +
        +
      • + key + (string) - + The invidual key in the shortcut. +
      • +
      +
    • + keyRange + (boolean) - + Set to true to show a key range. It combines the shortcut keys with "-" instead of "+". +
    • +
    +
+ +@param {object} model keyboard shortcut model. See description and example above. +**/ + (function() { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js index efa95f1fbd..0671770796 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js @@ -1,3 +1,50 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbLoadIndicator +@restrict E + +@description +Use this directive to generate a loading indicator. + +

Markup example

+
+    
+ + + + +
+

{{content}}

+
+ +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller(myService) {
+
+            var vm = this;
+
+            vm.content = "";
+            vm.loading = true;
+
+            myService.getContent().then(function(content){
+                vm.content = content;
+                vm.loading = false;
+            });
+
+        }
+½
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+**/ + (function() { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js index 870a7d45d6..cc5a1eb2b1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js @@ -1,35 +1,54 @@ /** -* @ngdoc directive -* @name umbraco.directives.directive:umbContentName -* @restrict E -* @function -* @description -* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form. +@ngdoc directive +@name umbraco.directives.directive:umbLockedField +@restrict E +@scope + +@description +Use this directive to render a value with a lock next to it. When the lock is clicked the value gets unlocked and can be edited. + +

Markup example

+
+	
+ + + + +
+
+ +

Controller example

+
+	(function () {
+		"use strict";
+
+		function Controller() {
+
+			var vm = this;
+			vm.value = "My locked text";
+
+        }
+
+		angular.module("umbraco").controller("My.Controller", Controller);
+
+	})();
+
+ +@param {string} ngModel (binding): The locked text. +@param {boolean=} locked (binding): true by default. Set to false to unlock the text. +@param {string=} placeholderText (binding): If ngModel is empty this text will be shown. +@param {string=} regexValidation (binding): Set a regex expression for validation of the field. +@param {string=} serverValidationField (attribute): Set a server validation field. **/ + (function() { 'use strict'; function LockedFieldDirective($timeout, localizationService) { function link(scope, el, attr, ngModelCtrl) { - - //watch the ngModel so we can manually update the textbox view value when it changes - // this ensures that the normal flow (i.e. a user editing the text box) occurs so that - // the parsers, validators and viewchangelisteners execute - scope.$watch("ngModel", function (newValue, oldValue) { - if (newValue !== oldValue) { - //Hack: in order for the pipeline to execute for setViewValue, the underlying $modelValue cannot - // match the value being set with the newValue, so we'll se it to undefined first. - // We could avoid this hack by setting the ngModel of the lockedField input field to a custom - // scope object, but that would mean we'd have to watch that value too in order to set the outer - // ngModelCtrl.$modelValue. It's seems like less overhead to just do this and not have 2x watches. - scope.lockedFieldForm.lockedField.$modelValue = undefined; - scope.lockedFieldForm.lockedField.$setViewValue(newValue); - scope.lockedFieldForm.lockedField.$render(); - } - }); - - var input = el.find('.umb-locked-field__input'); function activate() { @@ -57,36 +76,14 @@ scope.lock = function() { scope.locked = true; - input.unbind("blur"); }; scope.unlock = function() { scope.locked = false; - autoFocusField(); }; - function autoFocusField() { - - var onBlurHandler = function() { - scope.$apply(function(){ - scope.lock(); - }); - }; - - $timeout(function() { - input.focus(); - input.select(); - input.on("blur", onBlurHandler); - }); - - } - activate(); - scope.$on('$destroy', function() { - input.unbind('blur'); - }); - } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js index 5175dd3b4d..2e95b0c96b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js @@ -1,167 +1,278 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbMediaGrid +@restrict E +@scope + +@description +Use this directive to generate a thumbnail grid of media items. + +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.mediaItems = [];
+
+            vm.clickItem = clickItem;
+            vm.clickItemName = clickItemName;
+
+            myService.getMediaItems().then(function (mediaItems) {
+                vm.mediaItems = mediaItems;
+            });
+
+            function clickItem(item, $event, $index){
+                // do magic here
+            }
+
+            function clickItemName(item, $event, $index) {
+                // set item.selected = true; to select the item
+                // do magic here
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {array} items (binding): Array of media items. +@param {callback=} onDetailsHover (binding): Callback method when the details icon is hovered. +

The callback returns:

+
    +
  • item: The hovered item
  • +
  • $event: The hover event
  • +
  • hover: Boolean to tell if the item is hovered or not
  • +
+@param {callback=} onClick (binding): Callback method to handle click events on the media item. +

The callback returns:

+
    +
  • item: The clicked item
  • +
  • $event: The click event
  • +
  • $index: The item index
  • +
+@param {callback=} onClickName (binding): Callback method to handle click events on the media item name. +

The callback returns:

+
    +
  • item: The clicked item
  • +
  • $event: The click event
  • +
  • $index: The item index
  • +
+@param {string=} filterBy (binding): String to filter media items by +@param {string=} itemMaxWidth (attribute): Sets a max width on the media item thumbnails. +@param {string=} itemMaxHeight (attribute): Sets a max height on the media item thumbnails. +@param {string=} itemMinWidth (attribute): Sets a min width on the media item thumbnails. +@param {string=} itemMinHeight (attribute): Sets a min height on the media item thumbnails. + +**/ + (function() { - 'use strict'; + 'use strict'; - function MediaGridDirective($filter, mediaHelper) { + function MediaGridDirective($filter, mediaHelper) { - function link(scope, el, attr, ctrl) { + function link(scope, el, attr, ctrl) { - var itemDefaultHeight = 200; - var itemDefaultWidth = 200; - var itemMaxWidth = 200; - var itemMaxHeight = 200; + var itemDefaultHeight = 200; + var itemDefaultWidth = 200; + var itemMaxWidth = 200; + var itemMaxHeight = 200; + var itemMinWidth = 125; + var itemMinHeight = 125; - function activate() { + function activate() { - for (var i = 0; scope.items.length > i; i++) { - var item = scope.items[i]; - setItemData(item); - setOriginalSize(item, itemMaxHeight); - } + if (scope.itemMaxWidth) { + itemMaxWidth = scope.itemMaxWidth; + } - if(scope.items.length > 0) { - setFlexValues(scope.items); - } + if (scope.itemMaxHeight) { + itemMaxHeight = scope.itemMaxHeight; + } - } + if (scope.itemMinWidth) { + itemMinWidth = scope.itemMinWidth; + } - function setItemData(item) { - item.isFolder = !mediaHelper.hasFilePropertyType(item); - if(!item.isFolder){ - item.thumbnail = mediaHelper.resolveFile(item, true); - item.image = mediaHelper.resolveFile(item, false); - } - } + if (scope.itemMinWidth) { + itemMinHeight = scope.itemMinHeight; + } - function setOriginalSize(item, maxHeight) { + for (var i = 0; scope.items.length > i; i++) { + var item = scope.items[i]; + setItemData(item); + setOriginalSize(item, itemMaxHeight); + } - //set to a square by default - item.width = itemDefaultWidth; - item.height = itemDefaultHeight; - item.aspectRatio = 1; - - var widthProp = _.find(item.properties, function(v) { return (v.alias === "umbracoWidth"); }); - - if (widthProp && widthProp.value) { - item.width = parseInt(widthProp.value, 10); - if (isNaN(item.width)) { - item.width = itemDefaultWidth; - } - } - - var heightProp = _.find(item.properties, function(v) { return (v.alias === "umbracoHeight"); }); - - if (heightProp && heightProp.value) { - item.height = parseInt(heightProp.value, 10); - if (isNaN(item.height)) { - item.height = itemDefaultWidth; - } - } - - item.aspectRatio = item.width / item.height; - - // set max width and height - // landscape - if(item.aspectRatio >= 1) { - if(item.width > itemMaxWidth) { - item.width = itemMaxWidth; - item.height = itemMaxWidth / item.aspectRatio; - } - // portrait - } else { - if(item.height > itemMaxHeight) { - item.height = itemMaxHeight; - item.width = itemMaxHeight * item.aspectRatio; - } - } - - } - - function setFlexValues(mediaItems) { - - var flexSortArray = mediaItems; - var smallestImageWidth = null; - var widestImageAspectRatio = null; - - // sort array after image width with the widest image first - flexSortArray = $filter('orderBy')(flexSortArray, 'width', true); - - // find widest image aspect ratio - widestImageAspectRatio = flexSortArray[0].aspectRatio; - - // find smallest image width - smallestImageWidth = flexSortArray[flexSortArray.length - 1].width; - - for (var i = 0; flexSortArray.length > i; i++) { - - var mediaItem = flexSortArray[i]; - var flex = 1 / (widestImageAspectRatio / mediaItem.aspectRatio); - - if (flex === 0) { - flex = 1; - } - - var imageMinWidth = smallestImageWidth * flex; - - var flexStyle = { - "flex": flex + " 1 " + imageMinWidth + "px", - "max-width": mediaItem.width + "px", - "min-width": "125px" - }; - - mediaItem.flexStyle = flexStyle; + if (scope.items.length > 0) { + setFlexValues(scope.items); + } } - } - - scope.clickItem = function(item, $event, $index) { - if(scope.onClick) { - scope.onClick(item, $event, $index); + function setItemData(item) { + item.isFolder = !mediaHelper.hasFilePropertyType(item); + if (!item.isFolder) { + item.thumbnail = mediaHelper.resolveFile(item, true); + item.image = mediaHelper.resolveFile(item, false); + } } - }; - scope.clickItemName = function(item, $event, $index) { - if(scope.onClickName) { - scope.onClickName(item, $event, $index); - $event.stopPropagation(); + function setOriginalSize(item, maxHeight) { + + //set to a square by default + item.width = itemDefaultWidth; + item.height = itemDefaultHeight; + item.aspectRatio = 1; + + var widthProp = _.find(item.properties, function(v) { + return (v.alias === "umbracoWidth"); + }); + + if (widthProp && widthProp.value) { + item.width = parseInt(widthProp.value, 10); + if (isNaN(item.width)) { + item.width = itemDefaultWidth; + } + } + + var heightProp = _.find(item.properties, function(v) { + return (v.alias === "umbracoHeight"); + }); + + if (heightProp && heightProp.value) { + item.height = parseInt(heightProp.value, 10); + if (isNaN(item.height)) { + item.height = itemDefaultWidth; + } + } + + item.aspectRatio = item.width / item.height; + + // set max width and height + // landscape + if (item.aspectRatio >= 1) { + if (item.width > itemMaxWidth) { + item.width = itemMaxWidth; + item.height = itemMaxWidth / item.aspectRatio; + } + // portrait + } else { + if (item.height > itemMaxHeight) { + item.height = itemMaxHeight; + item.width = itemMaxHeight * item.aspectRatio; + } + } + } - }; - scope.hoverItemDetails = function(item, $event, hover) { - if(scope.onDetailsHover) { - scope.onDetailsHover(item, $event, hover); + function setFlexValues(mediaItems) { + + var flexSortArray = mediaItems; + var smallestImageWidth = null; + var widestImageAspectRatio = null; + + // sort array after image width with the widest image first + flexSortArray = $filter('orderBy')(flexSortArray, 'width', true); + + // find widest image aspect ratio + widestImageAspectRatio = flexSortArray[0].aspectRatio; + + // find smallest image width + smallestImageWidth = flexSortArray[flexSortArray.length - 1].width; + + for (var i = 0; flexSortArray.length > i; i++) { + + var mediaItem = flexSortArray[i]; + var flex = 1 / (widestImageAspectRatio / mediaItem.aspectRatio); + + if (flex === 0) { + flex = 1; + } + + var imageMinFlexWidth = smallestImageWidth * flex; + + var flexStyle = { + "flex": flex + " 1 " + imageMinFlexWidth + "px", + "max-width": mediaItem.width + "px", + "min-width": itemMinWidth + "px", + "min-height": itemMinHeight + "px" + }; + + mediaItem.flexStyle = flexStyle; + + } + } - }; - var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue){ - if(angular.isArray(newValue)) { - activate(); - } - }); + scope.clickItem = function(item, $event, $index) { + if (scope.onClick) { + scope.onClick(item, $event, $index); + } + }; - scope.$on('$destroy', function(){ - unbindItemsWatcher(); - }); + scope.clickItemName = function(item, $event, $index) { + if (scope.onClickName) { + scope.onClickName(item, $event, $index); + $event.stopPropagation(); + } + }; - } + scope.hoverItemDetails = function(item, $event, hover) { + if (scope.onDetailsHover) { + scope.onDetailsHover(item, $event, hover); + } + }; - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-media-grid.html', - scope: { - items: '=', - onDetailsHover: "=", - onClick: '=', - onClickName: "=", - filterBy: "=" - }, - link: link - }; + var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue) { + if (angular.isArray(newValue)) { + activate(); + } + }); - return directive; - } + scope.$on('$destroy', function() { + unbindItemsWatcher(); + }); - angular.module('umbraco.directives').directive('umbMediaGrid', MediaGridDirective); + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-media-grid.html', + scope: { + items: '=', + onDetailsHover: "=", + onClick: '=', + onClickName: "=", + filterBy: "=", + itemMaxWidth: "@", + itemMaxHeight: "@", + itemMinWidth: "@", + itemMinHeight: "@" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbMediaGrid', MediaGridDirective); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js index 18dd616f75..fe753171e1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js @@ -1,3 +1,88 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbPagination +@restrict E +@scope + +@description +Use this directive to generate a pagination. + +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.pagination = {
+                pageNumber: 1,
+                totalPages: 10
+            }
+
+            vm.nextPage = nextPage;
+            vm.prevPage = prevPage;
+            vm.goToPage = goToPage;
+
+            function nextPage(pageNumber) {
+                // do magic here
+                console.log(pageNumber);
+                alert("nextpage");
+            }
+
+            function prevPage(pageNumber) {
+                // do magic here
+                console.log(pageNumber);
+                alert("prevpage");
+            }
+
+            function goToPage(pageNumber) {
+                // do magic here
+                console.log(pageNumber);
+                alert("go to");
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {number} pageNumber (binding): Current page number. +@param {number} totalPages (binding): The total number of pages. +@param {callback} onNext (binding): Callback method to go to the next page. +

The callback returns:

+
    +
  • pageNumber: The page number
  • +
+@param {callback=} onPrev (binding): Callback method to go to the previous page. +

The callback returns:

+
    +
  • pageNumber: The page number
  • +
+@param {callback=} onGoToPage (binding): Callback method to go to a specific page. +

The callback returns:

+
    +
  • pageNumber: The page number
  • +
+**/ + (function() { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js index 727dc5ec94..cee705b8e8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js @@ -1,3 +1,41 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbStickyBar +@restrict A + +@description +Use this directive make an element sticky and follow the page when scrolling. + +

Markup example

+
+    
+ +
+
+ +
+
+ +

CSS example

+
+    .my-sticky-bar {
+        padding: 15px 0;
+        background: #000000;
+        position: relative;
+        top: 0;
+    }
+
+    .my-sticky-bar.-umb-sticky-bar {
+        top: 100px;
+    }
+
+ +@param {string} scrollableContainer Set the class (".element") or the id ("#element") of the scrollable container element. +**/ + (function() { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js index fa57647a78..fe80d915da 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js @@ -51,13 +51,14 @@ templateUrl: 'views/components/umb-table.html', scope: { items: '=', - itemProperties: "=", + itemProperties: '=', + allowSelectAll: '=', onSelect: '=', - onClick: "=", - onSelectAll: "=", - onSelectedAll: "=", - onSortingDirection: "=", - onSort:"=" + onClick: '=', + onSelectAll: '=', + onSelectedAll: '=', + onSortingDirection: '=', + onSort: '=' }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtooltip.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtooltip.directive.js index 176e1681b8..d3bda41d43 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtooltip.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtooltip.directive.js @@ -1,3 +1,71 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbTooltip +@restrict E +@scope + +@description +Use this directive to render a tooltip. + +

Markup example

+
+    
+ +
+ Hover me +
+ + + // tooltip content here + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.tooltip = {
+                show: false,
+                event: null
+            };
+
+            vm.mouseOver = mouseOver;
+            vm.mouseLeave = mouseLeave;
+
+            function mouseOver($event) {
+                vm.tooltip = {
+                    show: true,
+                    event: $event
+                };
+            }
+
+            function mouseLeave() {
+                vm.tooltip = {
+                    show: false,
+                    event: null
+                };
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+
+    })();
+
+ +@param {string} event Set the $event from the target element to position the tooltip relative to the mouse cursor. +**/ + (function() { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js index d33a8edb48..e0678c1065 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js @@ -1,6 +1,6 @@ /** * @ngdoc directive -* @name umbraco.directives.directive:umbContentName +* @name umbraco.directives.directive:umbFileDropzone * @restrict E * @function * @description diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbsinglefileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbsinglefileupload.directive.js index b8123b9202..df4f68950e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbsinglefileupload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbsinglefileupload.directive.js @@ -1,6 +1,6 @@ /** * @ngdoc directive -* @name umbraco.directives.directive:umbFileUpload +* @name umbraco.directives.directive:umbSingleFileUpload * @function * @restrict A * @scope diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/umbsetdirtyonchange.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/umbsetdirtyonchange.directive.js new file mode 100644 index 0000000000..37a773ae97 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/umbsetdirtyonchange.directive.js @@ -0,0 +1,29 @@ +(function() { + 'use strict'; + + function SetDirtyOnChange() { + + function link(scope, el, attr, ctrl) { + + var initValue = attr.umbSetDirtyOnChange; + + attr.$observe("umbSetDirtyOnChange", function (newValue) { + if(newValue !== initValue) { + ctrl.$setDirty(); + } + }); + + } + + var directive = { + require: "^form", + restrict: 'A', + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbSetDirtyOnChange', SetDirtyOnChange); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valHighlight.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valHighlight.directive.js index 599cda766c..2afd75eb29 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valHighlight.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valHighlight.directive.js @@ -8,9 +8,9 @@ function valHighlight($timeout) { return { restrict: "A", link: function (scope, element, attrs, ctrl) { - + attrs.$observe("valHighlight", function (newVal) { - if (newVal === true) { + if (newVal === "true") { element.addClass("highlight-error"); $timeout(function () { //set the bound scope property to false @@ -25,4 +25,4 @@ function valHighlight($timeout) { } }; } -angular.module('umbraco.directives.validation').directive("valHighlight", valHighlight); \ No newline at end of file +angular.module('umbraco.directives.validation').directive("valHighlight", valHighlight); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valregex.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valregex.directive.js index bc69d1cd02..7bc3c6b877 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valregex.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valregex.directive.js @@ -14,6 +14,7 @@ function valRegex() { var flags = ""; var regex; + var eventBindings = []; attrs.$observe("valRegexFlags", function (newVal) { if (newVal) { @@ -38,6 +39,12 @@ function valRegex() { } }); + eventBindings.push(scope.$watch('ngModel', function(newValue, oldValue){ + if(newValue && newValue !== oldValue) { + patternValidator(newValue); + } + })); + var patternValidator = function (viewValue) { if (regex) { //NOTE: we don't validate on empty values, use required validator for that @@ -58,8 +65,14 @@ function valRegex() { } }; - ctrl.$parsers.push(patternValidator); + scope.$on('$destroy', function(){ + // unbind watchers + for(var e in eventBindings) { + eventBindings[e](); + } + }); + } }; } -angular.module('umbraco.directives.validation').directive("valRegex", valRegex); \ No newline at end of file +angular.module('umbraco.directives.validation').directive("valRegex", valRegex); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valserverfield.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valserverfield.directive.js index 6fe2dfdf08..46fbaf93dd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valserverfield.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valserverfield.directive.js @@ -12,6 +12,7 @@ function valServerField(serverValidationManager) { link: function (scope, element, attr, ctrl) { var fieldName = null; + var eventBindings = []; attr.$observe("valServerField", function (newVal) { if (newVal && fieldName === null) { @@ -22,11 +23,11 @@ function valServerField(serverValidationManager) { // resubmitted. So once a field is changed that has a server error assigned to it // we need to re-validate it for the server side validator so the user can resubmit // the form. Of course normal client-side validators will continue to execute. - ctrl.$viewChangeListeners.push(function () { + eventBindings.push(scope.$watch('ngModel', function(newValue){ if (ctrl.$invalid) { ctrl.$setValidity('valServerField', true); } - }); + })); //subscribe to the server validation changes serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) { @@ -48,10 +49,17 @@ function valServerField(serverValidationManager) { element.bind('$destroy', function () { serverValidationManager.unsubscribe(null, fieldName); }); - } }); + + scope.$on('$destroy', function(){ + // unbind watchers + for(var e in eventBindings) { + eventBindings[e](); + } + }); + } }; } -angular.module('umbraco.directives.validation').directive("valServerField", valServerField); \ No newline at end of file +angular.module('umbraco.directives.validation').directive("valServerField", valServerField); diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/externalLoginInfo.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/externalLoginInfo.mocks.js new file mode 100644 index 0000000000..f054c8493e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/externalLoginInfo.mocks.js @@ -0,0 +1,8 @@ +angular.module("umbraco.mocks").factory('externalLoginInfo', + function () { + return { + errors: [], + providers: [] + }; + } +); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js index aefb678ffe..6365606a5a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js @@ -716,7 +716,7 @@ angular.module('umbraco.mocks'). "user_administrators": "Administrator", "user_categoryField": "Category field", "user_changePassword": "Change Your Password", - "user_newPassword": "Change Your Password", + "user_newPassword": "New password", "user_confirmNewPassword": "Confirm new password", "user_changePasswordDescription": "You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button", "user_contentChannel": "Content Channel", @@ -730,6 +730,7 @@ angular.module('umbraco.mocks'). "user_mediastartnode": "Start Node in Media Library", "user_modules": "Sections", "user_noConsole": "Disable Umbraco Access", + "user_oldPassword": "Old password", "user_password": "Password", "user_resetPassword": "Reset password", "user_passwordChanged": "Your password has been changed!", diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 86584d41f7..2cf7127707 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -42,6 +42,15 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { return { + getRecycleBin: function() { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for content recycle bin'); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#sort @@ -500,6 +509,16 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to check permission for item ' + id); }, + getPermissions: function (nodeIds) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetPermissions"), + nodeIds), + 'Failed to get permissions'); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#save diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index c3e1368efe..4c7b6f916e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -24,29 +24,18 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { filterPropertyTypes = []; } - var query = ""; - _.each(filterContentTypes, function (item) { - query += "filterContentTypes=" + item + "&"; - }); - // if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error - if (filterContentTypes.length === 0) { - query += "filterContentTypes=&"; - } - _.each(filterPropertyTypes, function (item) { - query += "filterPropertyTypes=" + item + "&"; - }); - // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error - if (filterPropertyTypes.length === 0) { - query += "filterPropertyTypes=&"; - } - query += "contentTypeId=" + contentTypeId; - + var query = { + contentTypeId: contentTypeId, + filterContentTypes: filterContentTypes, + filterPropertyTypes: filterPropertyTypes + }; + return umbRequestHelper.resourcePromise( - $http.get( + $http.post( umbRequestHelper.getApiUrl( "contentTypeApiBaseUrl", - "GetAvailableCompositeContentTypes", - query)), + "GetAvailableCompositeContentTypes"), + query), 'Failed to retrieve data for content type id ' + contentTypeId); }, @@ -212,9 +201,9 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * .then(function() { * alert("node was moved"); * }, function(err){ - * alert("node didnt move:" + err.data.Message); + * alert("node didnt move:" + err.data.Message); * }); - * + * * @param {Object} args arguments object * @param {Int} args.idd the ID of the node to move * @param {Int} args.parentId the ID of the parent node to move to @@ -241,6 +230,26 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to move content'); }, + copy: function(args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCopy"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to copy content'); + }, + createContainer: function(parentId, name) { return umbRequestHelper.resourcePromise( @@ -248,7 +257,7 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to create a folder under parent id ' + parentId); } - + }; } angular.module('umbraco.resources').factory('contentTypeResource', contentTypeResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index f6443735cf..9a2310299f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -22,6 +22,15 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { return { + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for media recycle bin'); + }, + /** * @ngdoc method * @name umbraco.resources.mediaResource#sort diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index 17a96821fd..117edef77f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -24,29 +24,18 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { filterPropertyTypes = []; } - var query = ""; - _.each(filterContentTypes, function (item) { - query += "filterContentTypes=" + item + "&"; - }); - // if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error - if (filterContentTypes.length === 0) { - query += "filterContentTypes=&"; - } - _.each(filterPropertyTypes, function (item) { - query += "filterPropertyTypes=" + item + "&"; - }); - // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error - if (filterPropertyTypes.length === 0) { - query += "filterPropertyTypes=&"; - } - query += "contentTypeId=" + contentTypeId; + var query = { + contentTypeId: contentTypeId, + filterContentTypes: filterContentTypes, + filterPropertyTypes: filterPropertyTypes + }; return umbRequestHelper.resourcePromise( - $http.get( + $http.post( umbRequestHelper.getApiUrl( "mediaTypeApiBaseUrl", - "GetAvailableCompositeMediaTypes", - query)), + "GetAvailableCompositeMediaTypes"), + query), 'Failed to retrieve data for content type id ' + contentTypeId); }, @@ -64,7 +53,7 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * .then(function(array) { * $scope.type = type; * }); - * + * * @param {Int} mediaId id of the media item to retrive allowed child types for * @returns {Promise} resourcePromise object. * @@ -156,9 +145,9 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * .then(function() { * alert("node was moved"); * }, function(err){ - * alert("node didnt move:" + err.data.Message); + * alert("node didnt move:" + err.data.Message); * }); - * + * * @param {Object} args arguments object * @param {Int} args.idd the ID of the node to move * @param {Int} args.parentId the ID of the parent node to move to @@ -185,6 +174,26 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to move content'); }, + copy: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostCopy"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to copy content'); + }, + createContainer: function(parentId, name) { return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js index ea4bee718f..73b6394b0a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js @@ -16,7 +16,7 @@ function treeResource($q, $http, umbRequestHelper) { /** internal method to get the tree menu url */ function getTreeMenuUrl(node) { if (!node.menuUrl) { - throw "No menuUrl property found on the tree node, cannot load menu"; + return null; } return node.menuUrl; } @@ -26,10 +26,16 @@ function treeResource($q, $http, umbRequestHelper) { /** Loads in the data to display the nodes menu */ loadMenu: function (node) { - - return umbRequestHelper.resourcePromise( - $http.get(getTreeMenuUrl(node)), - "Failed to retrieve data for a node's menu " + node.id); + var treeMenuUrl = getTreeMenuUrl(node); + if (treeMenuUrl !== undefined && treeMenuUrl !== null && treeMenuUrl.length > 0) { + return umbRequestHelper.resourcePromise( + $http.get(getTreeMenuUrl(node)), + "Failed to retrieve data for a node's menu " + node.id); + } else { + return $q.reject({ + errorMsg: "No tree menu url defined for node " + node.id + }); + } }, /** Loads in the data to display the nodes for an application */ diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 13936a6733..b1464e5c9d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -55,7 +55,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica var deferred = $q.defer(); - if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, statusMessage: args.statusMessage })) { + if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, statusMessage: args.statusMessage, action: args.action })) { args.scope.busy = true; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js index 61cb0a7317..5c3e6eb4c8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js @@ -53,8 +53,8 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje if (modelsBuilderEnabled && modelsResource) { modelsResource.getModelsOutOfDateStatus().then(function(result) { - //Generate models buttons should be enabled if its not 100 - deferred.resolve(result.status !== 100); + //Generate models buttons should be enabled if it is 0 + deferred.resolve(result.status === 0); }); } else { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index 4b5521b8db..9dfa440a0e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -50,7 +50,7 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati } //the first thing any form must do is broadcast the formSubmitting event - args.scope.$broadcast("formSubmitting", { scope: args.scope }); + args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action }); //then check if the form is valid if (!args.skipValidation) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js index a9fb9eed84..f66b0b0ed0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js @@ -1,270 +1,323 @@ // This service was based on OpenJS library available in BSD License // http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php -angular.module('umbraco.services') -.factory('keyboardService', ['$window', '$timeout', function ($window, $timeout) { - var keyboardManagerService = {}; - var defaultOpt = { - 'type': 'keydown', - 'propagate': false, - 'inputDisabled': false, - 'target': $window.document, - 'keyCode': false - }; - var isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0; +function keyboardService($window, $timeout) { + + var keyboardManagerService = {}; + + var defaultOpt = { + 'type': 'keydown', + 'propagate': false, + 'inputDisabled': false, + 'target': $window.document, + 'keyCode': false + }; - // Store all keyboard combination shortcuts - keyboardManagerService.keyboardEvent = {}; + // Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken + var shift_nums = { + "`": "~", + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + ";": ":", + "'": "\"", + ",": "<", + ".": ">", + "/": "?", + "\\": "|" + }; + // Special Keys - and their codes + var special_keys = { + 'esc': 27, + 'escape': 27, + 'tab': 9, + 'space': 32, + 'return': 13, + 'enter': 13, + 'backspace': 8, - // Add a new keyboard combination shortcut - keyboardManagerService.bind = function (label, callback, opt) { + 'scrolllock': 145, + 'scroll_lock': 145, + 'scroll': 145, + 'capslock': 20, + 'caps_lock': 20, + 'caps': 20, + 'numlock': 144, + 'num_lock': 144, + 'num': 144, - //replace ctrl key with meta key - if(isMac && label !== "ctrl+space"){ - label = label.replace("ctrl","meta"); - } + 'pause': 19, + 'break': 19, - var fct, elt, code, k; - // Initialize opt object - opt = angular.extend({}, defaultOpt, opt); - label = label.toLowerCase(); - elt = opt.target; - if(typeof opt.target === 'string'){ - elt = document.getElementById(opt.target); - } + 'insert': 45, + 'home': 36, + 'delete': 46, + 'end': 35, + 'pageup': 33, + 'page_up': 33, + 'pu': 33, - fct = function (e) { - e = e || $window.event; + 'pagedown': 34, + 'page_down': 34, + 'pd': 34, - // Disable event handler when focus input and textarea - if (opt['inputDisabled']) { - var elt; - if (e.target){ - elt = e.target; - }else if (e.srcElement){ - elt = e.srcElement; - } + 'left': 37, + 'up': 38, + 'right': 39, + 'down': 40, - if (elt.nodeType === 3){elt = elt.parentNode;} - if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA'){return;} - } + 'f1': 112, + 'f2': 113, + 'f3': 114, + 'f4': 115, + 'f5': 116, + 'f6': 117, + 'f7': 118, + 'f8': 119, + 'f9': 120, + 'f10': 121, + 'f11': 122, + 'f12': 123 + }; - // Find out which key is pressed - if (e.keyCode){ - code = e.keyCode; - }else if (e.which){ - code = e.which; - } + var isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0; - var character = String.fromCharCode(code).toLowerCase(); + // The event handler for bound element events + function eventHandler(e) { + e = e || $window.event; - if (code === 188){character = ",";} // If the user presses , when the type is onkeydown - if (code === 190){character = ".";} // If the user presses , when the type is onkeydown + var code, k; - var keys = label.split("+"); - // Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked - var kp = 0; - // Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken - var shift_nums = { - "`":"~", - "1":"!", - "2":"@", - "3":"#", - "4":"$", - "5":"%", - "6":"^", - "7":"&", - "8":"*", - "9":"(", - "0":")", - "-":"_", - "=":"+", - ";":":", - "'":"\"", - ",":"<", - ".":">", - "/":"?", - "\\":"|" - }; - // Special Keys - and their codes - var special_keys = { - 'esc':27, - 'escape':27, - 'tab':9, - 'space':32, - 'return':13, - 'enter':13, - 'backspace':8, + // Find out which key is pressed + if (e.keyCode) + { + code = e.keyCode; + } + else if (e.which) { + code = e.which; + } - 'scrolllock':145, - 'scroll_lock':145, - 'scroll':145, - 'capslock':20, - 'caps_lock':20, - 'caps':20, - 'numlock':144, - 'num_lock':144, - 'num':144, + var character = String.fromCharCode(code).toLowerCase(); - 'pause':19, - 'break':19, + if (code === 188){character = ",";} // If the user presses , when the type is onkeydown + if (code === 190){character = ".";} // If the user presses , when the type is onkeydown - 'insert':45, - 'home':36, - 'delete':46, - 'end':35, + var propagate = true; - 'pageup':33, - 'page_up':33, - 'pu':33, + //Now we need to determine which shortcut this event is for, we'll do this by iterating over each + //registered shortcut to find the match. We use Find here so that the loop exits as soon + //as we've found the one we're looking for + _.find(_.keys(keyboardManagerService.keyboardEvent), function(key) { - 'pagedown':34, - 'page_down':34, - 'pd':34, + var shortcutLabel = key; + var shortcutVal = keyboardManagerService.keyboardEvent[key]; - 'left':37, - 'up':38, - 'right':39, - 'down':40, + // Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked + var kp = 0; - 'f1':112, - 'f2':113, - 'f3':114, - 'f4':115, - 'f5':116, - 'f6':117, - 'f7':118, - 'f8':119, - 'f9':120, - 'f10':121, - 'f11':122, - 'f12':123 - }; - // Some modifiers key - var modifiers = { - shift: { - wanted: false, - pressed: e.shiftKey ? true : false - }, - ctrl : { - wanted: false, - pressed: e.ctrlKey ? true : false - }, - alt : { - wanted: false, - pressed: e.altKey ? true : false - }, - meta : { //Meta is Mac specific - wanted: false, - pressed: e.metaKey ? true : false - } - }; - // Foreach keys in label (split on +) - var l = keys.length; - for (var i = 0; i < l; i++) { + // Some modifiers key + var modifiers = { + shift: { + wanted: false, + pressed: e.shiftKey ? true : false + }, + ctrl: { + wanted: false, + pressed: e.ctrlKey ? true : false + }, + alt: { + wanted: false, + pressed: e.altKey ? true : false + }, + meta: { //Meta is Mac specific + wanted: false, + pressed: e.metaKey ? true : false + } + }; - var k=keys[i]; - switch (k) { - case 'ctrl': - case 'control': - kp++; - modifiers.ctrl.wanted = true; - break; - case 'shift': - case 'alt': - case 'meta': - kp++; - modifiers[k].wanted = true; - break; - } + var keys = shortcutLabel.split("+"); + var opt = shortcutVal.opt; + var callback = shortcutVal.callback; - if (k.length > 1) { // If it is a special key - if(special_keys[k] === code){ - kp++; - } + // Foreach keys in label (split on +) + var l = keys.length; + for (var i = 0; i < l; i++) { - } else if (opt['keyCode']) { // If a specific key is set into the config - if (opt['keyCode'] === code) { - kp++; - } + var k = keys[i]; + switch (k) { + case 'ctrl': + case 'control': + kp++; + modifiers.ctrl.wanted = true; + break; + case 'shift': + case 'alt': + case 'meta': + kp++; + modifiers[k].wanted = true; + break; + } - } else { // The special keys did not match - if(character === k) { - kp++; - }else { - if(shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase - character = shift_nums[character]; - if(character === k){ - kp++; - } - } - } - } + if (k.length > 1) { // If it is a special key + if (special_keys[k] === code) { + kp++; + } + } + else if (opt['keyCode']) { // If a specific key is set into the config + if (opt['keyCode'] === code) { + kp++; + } + } + else { // The special keys did not match + if (character === k) { + kp++; + } + else { + if (shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase + character = shift_nums[character]; + if (character === k) { + kp++; + } + } + } + } - } //for end + } //for end - if(kp === keys.length && - modifiers.ctrl.pressed === modifiers.ctrl.wanted && - modifiers.shift.pressed === modifiers.shift.wanted && - modifiers.alt.pressed === modifiers.alt.wanted && - modifiers.meta.pressed === modifiers.meta.wanted) { - $timeout(function() { - callback(e); - }, 1); + if (kp === keys.length && + modifiers.ctrl.pressed === modifiers.ctrl.wanted && + modifiers.shift.pressed === modifiers.shift.wanted && + modifiers.alt.pressed === modifiers.alt.wanted && + modifiers.meta.pressed === modifiers.meta.wanted) { - if(!opt['propagate']) { // Stop the event - // e.cancelBubble is supported by IE - this will kill the bubbling process. - e.cancelBubble = true; - e.returnValue = false; + //found the right callback! - // e.stopPropagation works in Firefox. - if (e.stopPropagation) { - e.stopPropagation(); - e.preventDefault(); - } - return false; - } - } - }; - // Store shortcut - keyboardManagerService.keyboardEvent[label] = { - 'callback': fct, - 'target': elt, - 'event': opt['type'] - }; + // Disable event handler when focus input and textarea + if (opt['inputDisabled']) { + var elt; + if (e.target) { + elt = e.target; + } else if (e.srcElement) { + elt = e.srcElement; + } - //Attach the function with the event - if(elt.addEventListener){ - elt.addEventListener(opt['type'], fct, false); - }else if(elt.attachEvent){ - elt.attachEvent('on' + opt['type'], fct); - }else{ - elt['on' + opt['type']] = fct; - } - }; - // Remove the shortcut - just specify the shortcut and I will remove the binding - keyboardManagerService.unbind = function (label) { - label = label.toLowerCase(); - var binding = keyboardManagerService.keyboardEvent[label]; - delete(keyboardManagerService.keyboardEvent[label]); + if (elt.nodeType === 3) { elt = elt.parentNode; } + if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA') { + //This exits the Find loop + return true; + } + } - if(!binding){return;} + $timeout(function () { + callback(e); + }, 1); - var type = binding['event'], + if (!opt['propagate']) { // Stop the event + propagate = false; + } + + //This exits the Find loop + return true; + } + + //we haven't found one so continue looking + return false; + + }); + + // Stop the event if required + if (!propagate) { + // e.cancelBubble is supported by IE - this will kill the bubbling process. + e.cancelBubble = true; + e.returnValue = false; + + // e.stopPropagation works in Firefox. + if (e.stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } + return false; + } + } + + // Store all keyboard combination shortcuts + keyboardManagerService.keyboardEvent = {}; + + // Add a new keyboard combination shortcut + keyboardManagerService.bind = function (label, callback, opt) { + + //replace ctrl key with meta key + if(isMac && label !== "ctrl+space"){ + label = label.replace("ctrl","meta"); + } + + var elt; + // Initialize opt object + opt = angular.extend({}, defaultOpt, opt); + label = label.toLowerCase(); + elt = opt.target; + if(typeof opt.target === 'string'){ + elt = document.getElementById(opt.target); + } + + //Ensure we aren't double binding to the same element + type otherwise we'll end up multi-binding + // and raising events for now reason. So here we'll check if the event is already registered for the element + var boundValues = _.values(keyboardManagerService.keyboardEvent); + var found = _.find(boundValues, function (i) { + return i.target === elt && i.event === opt['type']; + }); + + // Store shortcut + keyboardManagerService.keyboardEvent[label] = { + 'callback': callback, + 'target': elt, + 'opt': opt + }; + + if (!found) { + //Attach the function with the event + if (elt.addEventListener) { + elt.addEventListener(opt['type'], eventHandler, false); + } else if (elt.attachEvent) { + elt.attachEvent('on' + opt['type'], eventHandler); + } else { + elt['on' + opt['type']] = eventHandler; + } + } + + }; + // Remove the shortcut - just specify the shortcut and I will remove the binding + keyboardManagerService.unbind = function (label) { + label = label.toLowerCase(); + var binding = keyboardManagerService.keyboardEvent[label]; + delete(keyboardManagerService.keyboardEvent[label]); + + if(!binding){return;} + + var type = binding['event'], elt = binding['target'], callback = binding['callback']; - if(elt.detachEvent){ - elt.detachEvent('on' + type, callback); - }else if(elt.removeEventListener){ - elt.removeEventListener(type, callback, false); - }else{ - elt['on'+type] = false; - } - }; - // + if(elt.detachEvent){ + elt.detachEvent('on' + type, callback); + }else if(elt.removeEventListener){ + elt.removeEventListener(type, callback, false); + }else{ + elt['on'+type] = false; + } + }; + // - return keyboardManagerService; -}]); \ No newline at end of file + return keyboardManagerService; +} angular.module('umbraco.services').factory('keyboardService', ['$window', '$timeout', keyboardService]); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 428b9f4323..e211218ce1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -1,288 +1,319 @@ -(function() { - 'use strict'; +(function () { + 'use strict'; - function listViewHelper(localStorageService) { + function listViewHelper(localStorageService) { - var firstSelectedIndex = 0; - var localStorageKey = "umblistViewLayout"; + var firstSelectedIndex = 0; + var localStorageKey = "umblistViewLayout"; - function getLayout(nodeId, availableLayouts) { + function getLayout(nodeId, availableLayouts) { - var storedLayouts = []; + var storedLayouts = []; - if(localStorageService.get(localStorageKey)) { - storedLayouts = localStorageService.get(localStorageKey); - } - - if (storedLayouts && storedLayouts.length > 0) { - for (var i = 0; storedLayouts.length > i; i++) { - var layout = storedLayouts[i]; - if (layout.nodeId === nodeId) { - return setLayout(nodeId, layout, availableLayouts); - } - } - - } - - return getFirstAllowedLayout(availableLayouts); - - } - - function setLayout(nodeId, selectedLayout, availableLayouts) { - - var activeLayout = {}; - var layoutFound = false; - - for (var i = 0; availableLayouts.length > i; i++) { - var layout = availableLayouts[i]; - if (layout.path === selectedLayout.path) { - activeLayout = layout; - layout.active = true; - layoutFound = true; - } else { - layout.active = false; - } - } - - if(!layoutFound) { - activeLayout = getFirstAllowedLayout(availableLayouts); - } - - saveLayoutInLocalStorage(nodeId, activeLayout); - - return activeLayout; - - } - - function saveLayoutInLocalStorage(nodeId, selectedLayout) { - var layoutFound = false; - var storedLayouts = []; - - if(localStorageService.get(localStorageKey)) { - storedLayouts = localStorageService.get(localStorageKey); - } - - if(storedLayouts.length > 0) { - for(var i = 0; storedLayouts.length > i; i++) { - var layout = storedLayouts[i]; - if(layout.nodeId === nodeId) { - layout.path = selectedLayout.path; - layoutFound = true; - } - } - } - - if(!layoutFound) { - var storageObject = { - "nodeId": nodeId, - "path": selectedLayout.path - }; - storedLayouts.push(storageObject); - } - - localStorageService.set(localStorageKey, storedLayouts); - - } - - function getFirstAllowedLayout(layouts) { - - var firstAllowedLayout = {}; - - for (var i = 0; layouts.length > i; i++) { - var layout = layouts[i]; - if (layout.selected === true) { - firstAllowedLayout = layout; - break; + if (localStorageService.get(localStorageKey)) { + storedLayouts = localStorageService.get(localStorageKey); } - } - return firstAllowedLayout; - } + if (storedLayouts && storedLayouts.length > 0) { + for (var i = 0; storedLayouts.length > i; i++) { + var layout = storedLayouts[i]; + if (layout.nodeId === nodeId) { + return setLayout(nodeId, layout, availableLayouts); + } + } - function selectHandler(selectedItem, selectedIndex, items, selection, $event) { + } - var start = 0; - var end = 0; - var item = null; + return getFirstAllowedLayout(availableLayouts); - if ($event.shiftKey === true) { + } - if(selectedIndex > firstSelectedIndex) { + function setLayout(nodeId, selectedLayout, availableLayouts) { - start = firstSelectedIndex; - end = selectedIndex; + var activeLayout = {}; + var layoutFound = false; - for (; end >= start; start++) { - item = items[start]; - selectItem(item, selection); - } + for (var i = 0; availableLayouts.length > i; i++) { + var layout = availableLayouts[i]; + if (layout.path === selectedLayout.path) { + activeLayout = layout; + layout.active = true; + layoutFound = true; + } else { + layout.active = false; + } + } + + if (!layoutFound) { + activeLayout = getFirstAllowedLayout(availableLayouts); + } + + saveLayoutInLocalStorage(nodeId, activeLayout); + + return activeLayout; + + } + + function saveLayoutInLocalStorage(nodeId, selectedLayout) { + var layoutFound = false; + var storedLayouts = []; + + if (localStorageService.get(localStorageKey)) { + storedLayouts = localStorageService.get(localStorageKey); + } + + if (storedLayouts.length > 0) { + for (var i = 0; storedLayouts.length > i; i++) { + var layout = storedLayouts[i]; + if (layout.nodeId === nodeId) { + layout.path = selectedLayout.path; + layoutFound = true; + } + } + } + + if (!layoutFound) { + var storageObject = { + "nodeId": nodeId, + "path": selectedLayout.path + }; + storedLayouts.push(storageObject); + } + + localStorageService.set(localStorageKey, storedLayouts); + + } + + function getFirstAllowedLayout(layouts) { + + var firstAllowedLayout = {}; + + for (var i = 0; layouts.length > i; i++) { + var layout = layouts[i]; + if (layout.selected === true) { + firstAllowedLayout = layout; + break; + } + } + + return firstAllowedLayout; + } + + function selectHandler(selectedItem, selectedIndex, items, selection, $event) { + + var start = 0; + var end = 0; + var item = null; + + if ($event.shiftKey === true) { + + if (selectedIndex > firstSelectedIndex) { + + start = firstSelectedIndex; + end = selectedIndex; + + for (; end >= start; start++) { + item = items[start]; + selectItem(item, selection); + } + + } else { + + start = firstSelectedIndex; + end = selectedIndex; + + for (; end <= start; start--) { + item = items[start]; + selectItem(item, selection); + } + + } } else { - start = firstSelectedIndex; - end = selectedIndex; + if (selectedItem.selected) { + deselectItem(selectedItem, selection); + } else { + selectItem(selectedItem, selection); + } - for (; end <= start; start--) { - item = items[start]; - selectItem(item, selection); - } + firstSelectedIndex = selectedIndex; } - } else { + } - if(selectedItem.selected) { - deselectItem(selectedItem, selection); - } else { - selectItem(selectedItem, selection); + function selectItem(item, selection) { + var isSelected = false; + for (var i = 0; selection.length > i; i++) { + var selectedItem = selection[i]; + if (item.id === selectedItem.id) { + isSelected = true; + } + } + if (!isSelected) { + selection.push({ id: item.id }); + item.selected = true; + } + } + + function deselectItem(item, selection) { + for (var i = 0; selection.length > i; i++) { + var selectedItem = selection[i]; + if (item.id === selectedItem.id) { + selection.splice(i, 1); + item.selected = false; + } + } + } + + function clearSelection(items, folders, selection) { + + var i = 0; + + selection.length = 0; + + if (angular.isArray(items)) { + for (i = 0; items.length > i; i++) { + var item = items[i]; + item.selected = false; + } } - firstSelectedIndex = selectedIndex; - - } - - } - - function selectItem(item, selection) { - var isSelected = false; - for (var i = 0; selection.length > i; i++) { - var selectedItem = selection[i]; - if (item.id === selectedItem.id) { - isSelected = true; + if (angular.isArray(items)) { + for (i = 0; folders.length > i; i++) { + var folder = folders[i]; + folder.selected = false; + } } - } - if(!isSelected) { - selection.push({id: item.id}); - item.selected = true; - } - } + } - function deselectItem(item, selection) { - for (var i = 0; selection.length > i; i++) { - var selectedItem = selection[i]; - if (item.id === selectedItem.id) { - selection.splice(i, 1); - item.selected = false; + function selectAllItems(items, selection, $event) { + + var checkbox = $event.target; + var clearSelection = false; + + if (!angular.isArray(items)) { + return; } - } - } - function clearSelection(items, folders, selection) { + selection.length = 0; - var i = 0; + for (var i = 0; i < items.length; i++) { - selection.length = 0; + var item = items[i]; + + if (checkbox.checked) { + selection.push({ id: item.id }); + } else { + clearSelection = true; + } + + item.selected = checkbox.checked; - if(angular.isArray(items)) { - for(i = 0; items.length > i; i++) { - var item = items[i]; - item.selected = false; } - } - if(angular.isArray(items)) { - for(i = 0; folders.length > i; i++) { - var folder = folders[i]; - folder.selected = false; + if (clearSelection) { + selection.length = 0; } - } - } - function selectAllItems(items, selection, $event) { + } - var checkbox = $event.target; - var clearSelection = false; + function isSelectedAll(items, selection) { - if (!angular.isArray(items)) { - return; - } + var numberOfSelectedItem = 0; - selection.length = 0; + for (var itemIndex = 0; items.length > itemIndex; itemIndex++) { + var item = items[itemIndex]; - for (var i = 0; i < items.length; i++) { + for (var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) { + var selectedItem = selection[selectedIndex]; - var item = items[i]; + if (item.id === selectedItem.id) { + numberOfSelectedItem++; + } + } - if (checkbox.checked) { - selection.push({id: item.id}); - } else { - clearSelection = true; - } + } - item.selected = checkbox.checked; + if (numberOfSelectedItem === items.length) { + return true; + } - } + } - if (clearSelection) { - selection.length = 0; - } + function setSortingDirection(col, direction, options) { + return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction; + } - } + function setSorting(field, allow, options) { + if (allow) { + options.orderBy = field; - function isSelectedAll(items, selection) { + if (options.orderDirection === "desc") { + options.orderDirection = "asc"; + } else { + options.orderDirection = "desc"; + } + } + } + + //This takes in a dictionary of Ids with Permissions and determines + // the intersect of all permissions to return an object representing the + // listview button permissions + function getButtonPermissions(unmergedPermissions, currentIdsWithPermissions) { + + if (currentIdsWithPermissions == null) { + currentIdsWithPermissions = {}; + } - var numberOfSelectedItem = 0; + //merge the newly retrieved permissions to the main dictionary + _.each(unmergedPermissions, function (value, key, list) { + currentIdsWithPermissions[key] = value; + }); - for(var itemIndex = 0; items.length > itemIndex; itemIndex++) { - var item = items[itemIndex]; + //get the intersect permissions + var arr = []; + _.each(currentIdsWithPermissions, function (value, key, list) { + arr.push(value); + }); - for(var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) { - var selectedItem = selection[selectedIndex]; + //we need to use 'apply' to call intersection with an array of arrays, + //see: http://stackoverflow.com/a/16229480/694494 + var intersectPermissions = _.intersection.apply(_, arr); - if(item.id === selectedItem.id) { - numberOfSelectedItem++; - } - } + return { + canCopy: _.contains(intersectPermissions, 'O'), //Magic Char = O + canCreate: _.contains(intersectPermissions, 'C'), //Magic Char = C + canDelete: _.contains(intersectPermissions, 'D'), //Magic Char = D + canMove: _.contains(intersectPermissions, 'M'), //Magic Char = M + canPublish: _.contains(intersectPermissions, 'U'), //Magic Char = U + canUnpublish: _.contains(intersectPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) + }; + } - } + var service = { + getLayout: getLayout, + getFirstAllowedLayout: getFirstAllowedLayout, + setLayout: setLayout, + saveLayoutInLocalStorage: saveLayoutInLocalStorage, + selectHandler: selectHandler, + selectItem: selectItem, + deselectItem: deselectItem, + clearSelection: clearSelection, + selectAllItems: selectAllItems, + isSelectedAll: isSelectedAll, + setSortingDirection: setSortingDirection, + setSorting: setSorting, + getButtonPermissions: getButtonPermissions + }; - if(numberOfSelectedItem === items.length) { - return true; - } + return service; - } + } - function setSortingDirection(col, direction, options) { - return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction; - } - - - function setSorting(field, allow, options) { - if (allow) { - options.orderBy = field; - - if (options.orderDirection === "desc") { - options.orderDirection = "asc"; - } else { - options.orderDirection = "desc"; - } - } - } - - - - var service = { - getLayout: getLayout, - getFirstAllowedLayout: getFirstAllowedLayout, - setLayout: setLayout, - saveLayoutInLocalStorage: saveLayoutInLocalStorage, - selectHandler: selectHandler, - selectItem: selectItem, - deselectItem: deselectItem, - clearSelection: clearSelection, - selectAllItems: selectAllItems, - isSelectedAll: isSelectedAll, - setSortingDirection: setSortingDirection, - setSorting: setSorting - }; - - return service; - - } - - - angular.module('umbraco.services').factory('listViewHelper', listViewHelper); + angular.module('umbraco.services').factory('listViewHelper', listViewHelper); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index 554790b6d0..9fbf2947af 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -21,6 +21,7 @@ function packageHelper(assetsService, treeService, eventsService, $templateCache } angular.module('umbraco.services').factory('packageHelper', packageHelper); +//TODO: I believe this is obsolete function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, mediaHelper, umbRequestHelper) { return { /** sets the image's url, thumbnail and if its a folder */ @@ -319,7 +320,6 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me } }; } - angular.module("umbraco.services").factory("umbPhotoFolderHelper", umbPhotoFolderHelper); /** diff --git a/src/Umbraco.Web.UI.Client/src/less/application/grid.less b/src/Umbraco.Web.UI.Client/src/less/application/grid.less index 7029cc2a93..02f56e4046 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/grid.less @@ -92,17 +92,30 @@ body { } #search-form { - display: block; - margin: 0px; - z-index: 100; - position: absolute; - top: 0; - left: 0; - right: 0; -} + display: block; + margin: 0; + z-index: 100; + position: absolute; + top: 0; + left: 0; + right: 0; + overflow: hidden; -#search-form form{ - margin-top: 17px; + .form-search { + display: -ms-flexbox; + display: -webkit-box; + display: -webkit-flex; + display: flex; + margin-top: 17px; + + .umb-search-field { + width: auto; + min-width: 160px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + } + } } #navigation { diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 1400547383..9d01cae4b9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -106,6 +106,8 @@ @import "components/overlays/umb-overlay-backdrop.less"; @import "components/umb-grid.less"; @import "components/umb-empty-state.less"; +@import "components/umb-property-editor.less"; +@import "components/umb-iconpicker.less"; @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index 81fc540f9e..84b96292c2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -49,9 +49,10 @@ // Disabled state &.disabled, - &[disabled] { + &[disabled], + &:disabled:hover { cursor: default; - background-image: none; + border-color: @btnBorder; .opacity(65); .box-shadow(none); } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/card.less b/src/Umbraco.Web.UI.Client/src/less/components/card.less index 6e1603b3d8..85a076636f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/card.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/card.less @@ -37,7 +37,7 @@ .umb-card-icons{ text-align: center; - vertical-align: center; + vertical-align: middle; display: block; list-style: none; margin: 0; @@ -88,8 +88,7 @@ display: flex; flex-flow: row wrap; justify-content: flex-start; - } - +} .umb-card-grid li { padding: 5px; @@ -98,31 +97,42 @@ text-align: center; width: 100px; - height: 100px; + height: 105px; box-sizing: border-box; - } +} - .umb-card-grid li.-four-in-row { - flex: 0 0 25%; - } +.umb-card-grid li.-four-in-row { + flex: 0 0 25%; +} - .umb-card-grid li.-three-in-row { - flex: 0 0 33.33%; - } +.umb-card-grid li.-three-in-row { + flex: 0 0 33.33%; +} -.umb-card-grid li:hover, .umb-card-grid li:hover * { - background: @blue; - color: white; - cursor: pointer; - } +.umb-card-grid .umb-card-grid-item { + display: block; + width: 100%; + height: 100%; +} + + + .umb-card-grid .umb-card-grid-item:hover, + .umb-card-grid .umb-card-grid-item:focus, + .umb-card-grid .umb-card-grid-item:hover > *, + .umb-card-grid .umb-card-grid-item:focus > * { + background: @blue; + color: white; + cursor: pointer; + outline: none; +} .umb-card-grid a { color: #222; text-decoration: none; } -.umb-card-grid i{ +.umb-card-grid i { font-size: 30px; line-height: 50px; display: block; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 0d68eeb0dd..3a5367c7c9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -6,13 +6,15 @@ justify-content: space-between; margin-top: -30px; position: relative; + top: 0; } .umb-editor-sub-header.-umb-sticky-bar { - box-shadow: - 0 5px 0 rgba(0, 0, 0, 0.08), - 0 1px 0 rgba(0, 0, 0, 0.16); + box-shadow: 0 5px 0 rgba(0, 0, 0, 0.08), 0 1px 0 rgba(0, 0, 0, 0.16); transition: box-shadow 1s; + top: 100px; + transform: translate(0, 50%); + } .umb-group-builder__property-preview .umb-editor-sub-header { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index 105b107cf1..0af0555b89 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -189,7 +189,9 @@ /*ensures dialogs doesnt have side-by-side labels*/ .umb-overlay .control-label, -.umb-overlay .form-horizontal .control-label { +.umb-overlay .form-horizontal .control-label, +.form-horizontal .umb-overlay .control-label + { width: 100%; display: block; box-sizing: border-box; @@ -198,6 +200,8 @@ } -.umb-overlay .controls-row { +.umb-overlay .controls-row, +.umb-overlay .form-horizontal .controls, +.form-horizontal .umb-overlay .controls { margin-left: 0 !important; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index d61f5dece2..46a74f7d8b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -21,7 +21,6 @@ } .umb-group-builder__group.-inherited { - background: #FDFDFD; border-color: #F2F2F2; animation: fadeIn 0.5s; } @@ -90,7 +89,6 @@ font-weight: bold; display: flex; align-items: center; - margin-right: 10px; } .umb-group-builder__group-title-icon { @@ -109,7 +107,6 @@ .umb-group-builder__group-title.-inherited { border-color: #F2F2F2; - background: #FDFDFD; } input.umb-group-builder__group-title-input { @@ -134,6 +131,15 @@ input.umb-group-builder__group-title-input { display: inline-block; position: relative; top: 2px; + margin-left: 10px; +} + +.umb-group-builder__group-sort-order { + display: flex; + align-items: center; + margin-left: 10px; + position: relative; + top: 2px; } input.umb-group-builder__group-sort-value { @@ -141,10 +147,7 @@ input.umb-group-builder__group-sort-value { padding: 0px 0 0px 5px; width: 40px; margin-bottom: 0; - margin-right: 10px; border-color: #d9d9d9; - position: relative; - top: 2px; } /* ---------- PROPERTIES ---------- */ @@ -182,7 +185,6 @@ input.umb-group-builder__group-sort-value { .umb-group-builder__property.-inherited { border: transparent; - background: #FDFDFD; animation: fadeIn 0.5s; } @@ -192,7 +194,6 @@ input.umb-group-builder__group-sort-value { .umb-group-builder__property.-locked { border: transparent; - background: #FDFDFD; animation: fadeIn 0.5s; } @@ -233,7 +234,9 @@ input.umb-group-builder__group-sort-value { .umb-group-builder__property-meta-alias { font-size: 10px; color: @gray; - word-wrap: break-word; + word-break: break-word; + line-height: 1.5; + margin-bottom: 5px; } .umb-group-builder__property-meta-label textarea { @@ -257,6 +260,7 @@ input.umb-group-builder__group-sort-value { .umb-group-builder__property-meta-description textarea { font-size: 12px; + line-height: 1.5; color: @grayDark; margin-bottom: 0; padding: 0; @@ -275,6 +279,7 @@ input.umb-group-builder__group-sort-value { overflow: hidden; position: relative; padding: 35px 10px 25px 10px; + background: url("../img/checkered-background-20.png"); } .umb-group-builder__property-preview:hover { @@ -293,18 +298,6 @@ input.umb-group-builder__group-sort-value { display:none !important; } -.umb-group-builder__property-preview-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 10; - border-radius: 10px; - background: url("../img/checkered-background.png"); - opacity: 0.2; -} - .umb-group-builder__property-preview-label { font-size: 11px; position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less new file mode 100644 index 0000000000..e0cdb7cac5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less @@ -0,0 +1,46 @@ +.umb-iconpicker { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + margin: 0; +} + +.umb-iconpicker-item { + display: flex; + flex-direction: row; + flex: 0 0 14.28%; + justify-content: center; + align-items: center; + + margin-bottom: 0; + + overflow: hidden; +} + +.umb-iconpicker-item a { + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; + + padding: 15px 0; + + text-decoration: none; +} + +.umb-iconpicker-item a:hover, +.umb-iconpicker-item a:focus{ + background: darken(@grayLighter, 1%); + outline: none; +} + +.umb-iconpicker-item a:active { + background: darken(@grayLighter, 4%); +} + +.umb-iconpicker-item i { + font-size: 30px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-keyboard-shortcuts-overview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-keyboard-shortcuts-overview.less index ab82eeec85..c603a3c7cd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-keyboard-shortcuts-overview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-keyboard-shortcuts-overview.less @@ -16,8 +16,23 @@ .umb-keyboard-shortcuts-overview__overlay-close { position: absolute; - top: 10px; - right: 10px; + top: 20px; + right: 20px; + font-size: 25px; + border-radius: 20px; + width: 35px; + height: 35px; + display: flex; + justify-content: center; + align-items: center; + background-color: transparent; + line-height: 1em; +} + +.umb-keyboard-shortcuts-overview__overlay-close:hover { + background-color: @blue; + color: #ffffff; + text-decoration: none; } .umb-keyboard-shortcuts-overview__overlay-content { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less index 43cb5a7f6e..0f2305bd90 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -9,17 +9,24 @@ } .umb-media-grid__item { - margin: 10px; - position: relative; - overflow: hidden; - cursor: pointer; - display: flex; - align-content: center; - align-items: center; - align-self: stretch; - box-shadow: 0 1px 1px 0 rgba(0,0,0,.2); - justify-content: center; - transition: box-shadow 100ms; + display: flex; + flex-direction: column; + flex-wrap: wrap; + + justify-content: center; + + align-content: center; + align-items: center; + align-self: stretch; + + margin: 10px; + position: relative; + overflow: hidden; + cursor: pointer; + + box-shadow: 0 1px 1px 0 rgba(0,0,0,.2); + + transition: box-shadow 100ms; } .umb-media-grid__item.-file { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less new file mode 100644 index 0000000000..cbea6987e7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less @@ -0,0 +1,3 @@ +.umb-property-editor.-not-clickable { + pointer-events: none; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index 631d7f5730..e94aa81898 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -198,6 +198,7 @@ input.umb-table__input { padding: 6px 2px; text-align: left; + overflow:hidden; } .umb-table-cell > * { diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 960ec8a7e2..f9dedf5902 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -16,18 +16,18 @@ label small, .guiDialogTiny { font-size: 11px } -label.control-label { +label.control-label, .control-label { padding: 0 10px 0 0 !important; font-weight: bold; - } + .umb-status-label{ color: @gray !important; } -.controls-row label{padding: 0 10px 0 10px; vertical-align: center} +.controls-row label{padding: 0 10px 0 10px; vertical-align: middle;} @@ -164,7 +164,7 @@ textarea { // Identify controls by their labels label { - display: block; + display: inline-block; margin-bottom: 5px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index 7e5645d6cd..0d41eed050 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -78,3 +78,94 @@ iframe, .content-column-body { .icon-chevron-down:before { content: "\e0c9"; } + + +/* Styling for validation in Public Access */ + +.pa-umb-overlay { + -webkit-font-smoothing: antialiased; + font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.pa-umb-overlay + .pa-umb-overlay { + padding-top: 30px; + border-top: 1px solid @grayLight; +} + +.pa-select-type { + display: flex; + flex-wrap: nowrap; + flex-direction: row; + justify-content: center; + align-items: flex-start; + + margin-top: 15px; +} + +.pa-select-type label { + padding: 0 20px; +} + +.pa-access-header { + font-weight: bold; + margin: 0 0 3px 0; + padding-bottom: 0; +} + +.pa-access-description { + color: #b3b3b3; + margin: 0; +} + +.pa-validation-message { + padding: 6px 12px !important; + margin: 5px 0 0 0 !important; + display: inline-block; +} + +.pa-select-pages label { + margin: 0; + font-size: 15px; +} + +.pa-select-pages label + .controls-row { + padding-top: 0; +} + +.pa-select-pages .umb-detail { + font-size: 13px; + margin: 2px 0 5px; +} + +.pa-choose-page a { + color: @blue; + font-size: 15px; +} + +.pa-choose-page a:hover, .pa-choose-page a:active, .pa-choose-page a:focus { + color: @blueDark; + text-decoration: none; +} + +.pa-choose-page a:before { + content:"+"; + margin-right: 3px; + font-weight: bold; +} + +.pa-choose-page .treePickerTitle { + font-weight: bold; + font-size: 13px; + font-style: italic; + background: whitesmoke; + padding: 3px 5px; + color: grey; + + border-bottom: none; +} + + +.pa-form + .pa-form { + margin-top: 10px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/installer.less b/src/Umbraco.Web.UI.Client/src/less/installer.less index 574fde27a9..aa8bbccfef 100644 --- a/src/Umbraco.Web.UI.Client/src/less/installer.less +++ b/src/Umbraco.Web.UI.Client/src/less/installer.less @@ -38,7 +38,7 @@ body { line-height: @baseLineHeight; color: @textColor; - vertical-align: center; + vertical-align: middle; text-align: center; } diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 4502527ce8..00cdd4f9b1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -139,8 +139,11 @@ h5.-black { } /* LABELS*/ -.umb-control-group label.control-label { - text-align: left +.umb-control-group label.control-label, .umb-control-group .control-label { + text-align: left; +} +.umb-control-group label.control-label > div > label { + padding-left: 0; } .umb-control-group label .help-block, .umb-control-group label small { @@ -150,7 +153,7 @@ h5.-black { padding-top: 5px; } .umb-nolabel .controls { - margin-left: 0px; + margin-left: 0; } .controls-row { @@ -159,11 +162,15 @@ h5.-black { } .umb-user-panel .controls-row { - margin-left: 0px; + margin-left: 0; } .controls-row label { - display: inline-block + display: inline-block; +} + +.controls-row > div > label { + padding-left: 0; } .block-form .controls-row { @@ -171,10 +178,24 @@ h5.-black { padding-top: 0; } -.hidelabel > div > .controls-row, .hidelabel > .controls-row, .hidelabel .controls { - padding: 0px; +.hidelabel > div > .controls-row, .hidelabel > .controls-row, .hidelabel > div > .controls { + padding: 0; border: none; - margin: 0px !important; + margin: 0 !important; +} + +.controls-row > .vertical-align-items { + display: flex; + align-items: center; +} + +.controls-row > .vertical-align-items > input.umb-editor-tiny { + margin-left: 5px; + margin-right: 5px; +} + +.controls-row > .vertical-align-items > input.umb-editor-tiny:first-child { + margin-left: 0; } .thumbnails .selected { diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index 38df48a457..8e986b5b00 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -161,7 +161,7 @@ // Mixin for form field states -//SD: I've had to modify this slightly to work nicely with angular validation , note the +//SD: I've had to modify this slightly to work nicely with angular validation , note the // additional targetting of the ng-invalid class. .formFieldState(@textColor: #555, @borderColor: #ccc, @backgroundColor: #f5f5f5) { // Set the text color @@ -522,10 +522,14 @@ *background-color: @endColor; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ .reset-filter(); + // button states + &:hover, &:focus, &:active { + background-color: darken(@startColor, 2%); + } + // in these cases the gradient won't cover the background, so we override &:hover, &:focus, &:active, &.active, &.disabled, &[disabled] { color: @textColor; - background-color: @endColor; *background-color: darken(@endColor, 5%); } diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index 79f3797197..081a194b7e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -9,13 +9,41 @@ color: @white; position: absolute; z-index: 10000; - top: 0px; - left: 0px; - margin: 0 !Important; + top: 0; + left: 0; + margin: 0 !important; padding: 0; border-radius: 0; } + +@media only screen and (-webkit-min-device-pixel-ratio: 1.5), + only screen and (min-resolution: 144dpi) +{ + .login-overlay { + background-image: url(../img/application/logo@2x.png) !important; + } +} + + +@media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (min-resolution: 192dpi) +{ + .login-overlay { + background-image: url(../img/application/logo@2x.png) !important; + } +} + + +@media only screen and (-webkit-min-device-pixel-ratio: 3), + only screen and (min-resolution: 3dppx), + only screen and (min-resolution: 350dpi) +{ + .login-overlay { + background-image: url(../img/application/logo@3x.png) !important; + } +} + .login-overlay .umb-modalcolumn { background: none; border: none; @@ -62,7 +90,7 @@ } #hrOr hr { - margin: 0px; + margin: 0; border: none; background-color: @gray; height: 1px; diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index b3cb034596..f274740020 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -81,6 +81,9 @@ div.umb-codeeditor .umb-btn-toolbar { font-size:16px !important; } +/* pre-value editor */ +.rte-editor-preval .control-group .controls > div > label .mce-ico { line-height: 20px; } + // // Color picker @@ -200,8 +203,11 @@ ul.color-picker li a { border: 1px solid #f8f8f8; } + .umb-sortable-thumbnails li:hover a { - display: block; + display: flex; + justify-content: center; + align-items: center; } .umb-sortable-thumbnails li img { @@ -228,6 +234,43 @@ ul.color-picker li a { display: block; } +.umb-sortable-thumbnails .umb-sortable-thumbnails__actions { + position: absolute; + bottom: 10px; + right: 10px; + text-decoration: none; + display: flex; + flex-direction: row; + opacity: 0; + visibility: hidden; +} + +.umb-sortable-thumbnails li:hover .umb-sortable-thumbnails__actions { + opacity: 1; + visibility: visible; +} + +.umb-sortable-thumbnails .umb-sortable-thumbnails__action { + font-size: 16px; + background: white; + height: 25px; + width: 25px; + border-radius: 15px; + color: @grayDarker; + display: flex; + justify-content: center; + align-items: center; + margin-left: 5px; +} + +.umb-sortable-thumbnails .umb-sortable-thumbnails__action.-red { + color: red; +} + +.umb-sortable-thumbnails .umb-sortable-thumbnails__action:hover { + text-decoration: none; +} + // // Cropper @@ -235,11 +278,12 @@ ul.color-picker li a { .umb-cropper{ position: relative; - padding: 10px; } .umb-cropper img, .umb-cropper-gravity img{ - position: absolute; + position: relative; + max-width: 100%; + height: auto; top: 0; left: 0; } @@ -253,19 +297,22 @@ ul.color-picker li a { left: 0; cursor: move; z-index: 6001; + position: absolute; } .umb-cropper .viewport{ overflow: hidden; position: relative; margin: auto; + max-width: 100%; + height: auto; } .umb-cropper-gravity .viewport{ overflow: hidden; position: relative; - width: 400px; - height: 300px + width: 100%; + height: 100%; } @@ -295,27 +342,45 @@ ul.color-picker li a { opacity: 0.8; } -.umb-cropper-gravity .overlay i{ +.umb-cropper-gravity .overlay i { font-size: 26px; line-height: 26px; opacity: 0.8 !important; } -.umb-cropper .crop-container{ +.umb-cropper .crop-container { text-align: center; } -.umb-cropper .crop-slider{ - vertical-align: middle; - padding: 10px 50px 10px 50px; +.umb-cropper .crop-slider { + padding: 10px; border-top: 1px solid @grayLighter; margin-top: 10px; + + display: flex; + align-items: center; + justify-content: center; + + flex-wrap: wrap; + + @media (min-width: 769px) { + padding: 10px 50px 10px 50px; + } } -.umb-cropper .crop-slider i{color: @gray;} -.umb-cropper .crop-slider input{ - margin-top: 7px; - width: 320px; +.umb-cropper .crop-slider i { + color: @gray; + flex: 0 0 25px; + padding: 0 5px; + box-sizing: border-box; +} + +.umb-cropper .crop-slider i:first-of-type { + text-align: right; +} + +.umb-cropper .crop-slider input { + flex: 0 1 auto; } .umb-cropper-gravity .viewport, .umb-cropper-gravity, .umb-cropper-imageholder { display: inline-block; @@ -332,55 +397,98 @@ ul.color-picker li a { } .gravity-container .viewport { - width: 494px; + max-width: 600px; + } + + .gravity-container .viewport:hover { + cursor: pointer; + } + + .imagecropper { + display: flex; + align-items: flex-start; + flex-direction: row; + + @media (max-width: 768px) { + flex-direction: column; + float: left; + max-width: 100%; + } + } + + .imagecropper .umb-cropper__container { + position: relative; + margin-bottom: 10px; + max-width: 100%; + border: 1px solid #f8f8f8; + + @media (min-width: 769px) { + width: 600px; + } + } + + .umb-close-cropper { + position: absolute; + top: 3px; + right: 3px; + cursor: pointer; + } + + .umb-close-cropper:hover { + opacity: .9; + background: @grayLighter; } .imagecropper .umb-sortable-thumbnails { - border-left: 2px solid #f8f8f8; - margin-left: 4px; - padding-left: 2px; - float: left; - width: 100px; + display: flex; + flex-direction: row; + flex-wrap: wrap; } .imagecropper .umb-sortable-thumbnails li { - display: block; - } + display: flex; + flex-direction: column; + justify-content: space-between; - .imagecropper .umb-sortable-thumbnails li.current a, .imagecropper .umb-sortable-thumbnails li.current a:hover { - background: #eeeeee; - text-decoration: none; - } - - .imagecropper .umb-sortable-thumbnails li:first-of-type { + padding: 8px; margin-top: 0; - padding-top: 0; } - .imagecropper .umb-sortable-thumbnails li .crop-name, .imagecropper .umb-sortable-thumbnails li .crop-size { + .imagecropper .umb-sortable-thumbnails li.current { + border-color: @grayLight; + background: @grayLighter; + color: @black; + cursor: pointer; + } + + .imagecropper .umb-sortable-thumbnails li:hover, + .imagecropper .umb-sortable-thumbnails li.current:hover { + border-color: @grayLight; + background: @grayLighter; + color: @black; + + cursor: pointer; + + opacity: .95; + } + + .imagecropper .umb-sortable-thumbnails li .crop-name, + .imagecropper .umb-sortable-thumbnails li .crop-size { display: block; text-align: left; - font-size: 11px; + font-size: 13px; + line-height: 1; + } + + .imagecropper .umb-sortable-thumbnails li .crop-name { + font-weight: bold; + margin: 10px 0 5px; } .imagecropper .umb-sortable-thumbnails li .crop-size { font-size: 10px; font-style: italic; - color: #666; - } - - .imagecropper .umb-sortable-thumbnails li a { - display: block; - padding: 5px; - } - - .imagecropper .umb-sortable-thumbnails li a:hover { - background: #f8f8f8; - text-decoration: none; - } - - .imagecropper .umb-sortable-thumbnails li a:hover .crop-text { - text-decoration: none; + margin: 0 0 5px; } .btn-crop-delete { @@ -388,20 +496,6 @@ ul.color-picker li a { text-align: left; } - .imagecropper .umb-sortable-thumbnails.many { - width: 210px; - } - - .imagecropper .umb-sortable-thumbnails.many li { - width: 85px; - float: left; - } - - .imagecropper .umb-sortable-thumbnails.many li:nth-child(2) { - margin-top: 0; - padding-top: 0; - } - // // folder-browser // -------------------------------------------------- @@ -644,3 +738,10 @@ ul.color-picker li a { height: 100%; min-height:200px; } + +// +// Nested boolean (e.g. list view bulk action permissions) +// ---------------------=====----------------------------- +.umb-nested-boolean label {margin-bottom: 8px; float: left; width: 320px;} +.umb-nested-boolean label span {float: left; width: 80%;} +.umb-nested-boolean label input[type='checkbox'] {margin-right: 10px; float: left;} diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index f57feeb2ad..c2db58a99e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -19,7 +19,8 @@ ul.sections li { } ul.sections li [class^="icon-"]:before, -ul.sections li [class*=" icon-"]:before{ +ul.sections li [class*=" icon-"]:before, +ul.sections li img.icon-section { font-size: 30px; margin: 1px 0 0 0; opacity: 0.4; @@ -29,7 +30,8 @@ ul.sections li [class*=" icon-"]:before{ } ul.sections:hover li [class^="icon-"]:before, -ul.sections:hover li [class*=" icon-"]:before { +ul.sections:hover li [class*=" icon-"]:before, +ul.sections:hover li img.icon-section { opacity: 1 } diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 970317fca9..5bddbd8022 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -119,7 +119,7 @@ @btnWarningBackgroundHighlight: @orange; @btnDangerBackground: #ee5f5b; -@btnDangerBackgroundHighlight: #bd362f; +@btnDangerBackgroundHighlight: #ee5f5b; @btnInverseBackground: #444; @btnInverseBackgroundHighlight: @grayDarker; diff --git a/src/Umbraco.Web.UI.Client/src/loader.dev.js b/src/Umbraco.Web.UI.Client/src/loader.dev.js index 2e164f0eef..6fa09f00d9 100644 --- a/src/Umbraco.Web.UI.Client/src/loader.dev.js +++ b/src/Umbraco.Web.UI.Client/src/loader.dev.js @@ -12,6 +12,10 @@ LazyLoad.js( 'lib/angular/angular-ui-sortable.js', + 'lib/angular/1.1.5/angular-mocks.js', + 'lib/angular/1.1.5/angular-mobile.min.js', + 'lib/underscore/underscore-min.js', + 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', 'lib/bootstrap/js/bootstrap.2.3.2.min.js', diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.html index bcf7fe3247..260d8ff85c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.html @@ -2,55 +2,48 @@
@@ -91,19 +100,21 @@ - + - - + + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html index b944d6e450..05eb30c242 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html @@ -1,62 +1,62 @@ -
-
- -
- - - - - - - - - -
+
+
+ +
+ + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html index 26d9fc75d7..fe13267be0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html @@ -8,7 +8,7 @@
- + {{label}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-view.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-view.html index a10ff503b9..f99603afa4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-view.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-view.html @@ -1,4 +1,4 @@ -
-
+
- +
- +
- +
-
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-thumbnail.html b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-thumbnail.html index 424f00cc9a..2f3861ae6c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-thumbnail.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-thumbnail.html @@ -1,4 +1,5 @@ -
+
{{}} -
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html index 4a15fc81c0..9bc0839122 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html @@ -1,12 +1,12 @@
-
+

{{model.title}}

{{model.subtitle}}

-
+
@@ -44,8 +44,9 @@ + type="button" + disabled="!directive.enableConfirmButton" + action="submitForm(model)">
@@ -64,10 +65,11 @@ label-key="{{model.submitButtonLabelKey}}" label="{{model.submitButtonLabel}}" ng-if="model.submit && model.hideSubmitButton !== true" - type="submit"> + type="button" + action="submitForm(model)">
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property-editor.html index 80e854e9c2..bbf70f7a8e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property-editor.html @@ -1,2 +1,3 @@ - +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-results.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-results.html index e277ec63a8..97f08fe359 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-results.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-results.html @@ -1,19 +1,23 @@ -
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 06ed26a155..567d00c871 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -75,6 +75,7 @@ ng-disabled="tab.inherited" umb-auto-focus umb-auto-resize + on-focus="activateGroup(tab)" required val-server-field="{{'Groups[' + $index + '].Name'}}" /> @@ -84,7 +85,13 @@ - + +
+ +
+
+
+
@@ -100,10 +107,18 @@
@@ -31,7 +37,7 @@ files-uploaded="vm.onUploadComplete" accept="{{vm.acceptedFileTypes}}" max-file-size="{{vm.maxFileSize}}" - hide-dropzone="{{!vm.activeDrag && items.length > 0 || !items && options.filter }}" + hide-dropzone="{{ options.filter.length > 0 }}" compact="{{ items.length > 0 }}" files-queued="vm.onFilesQueue"> @@ -68,9 +74,9 @@ - Sorry, we can not find what you are looking for + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js index a8a1ac0766..e1754dd89d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js @@ -14,12 +14,15 @@ var vm = this; vm.nodeId = $scope.contentId; - vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); + //vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); + //instead of passing in a whitelist, we pass in a blacklist by adding ! to the ext + vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles).replace(/\./g, "!."); + vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; vm.activeDrag = false; vm.mediaDetailsTooltip = {}; vm.itemsWithoutFolders = []; - vm.isRecycleBin = $scope.contentId === '-21'; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; vm.dragEnter = dragEnter; vm.dragLeave = dragLeave; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.html index 24f1dabf33..96bebb7abf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.html @@ -14,7 +14,7 @@ files-uploaded="vm.onUploadComplete" accept="{{vm.acceptedFileTypes}}" max-file-size="{{vm.maxFileSize}}" - hide-dropzone="{{!vm.activeDrag && items.length > 0 || !items && options.filter }}" + hide-dropzone="{{options.filter.length > 0}}" compact="{{ items.length > 0 }}" files-queued="vm.onFilesQueue"> @@ -22,6 +22,7 @@ - - - -
- Sorry, we can not find what you are looking for + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js index 59327524d6..1d2d361f28 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js @@ -5,12 +5,13 @@ var vm = this; - vm.nodeId = $scope.contentId; - vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); + //vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); + //instead of passing in a whitelist, we pass in a blacklist by adding ! to the ext + vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles).replace(/./g, "!."); vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; vm.activeDrag = false; - vm.isRecycleBin = $scope.contentId === '-21'; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; vm.selectItem = selectItem; vm.clickItem = clickItem; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 0d42428334..59ea03aa91 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -1,4 +1,4 @@ -function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper) { +function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper, userService) { //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content // that isn't created yet, if we continue this will use the parent id in the route params which isn't what @@ -20,7 +20,8 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie getListResultsCallback = contentResource.getPagedResults; deleteItemCallback = contentResource.deleteByKey; getIdCallback = function(selected) { - return selected.key; + var selectedKey = getItemKey(selected.id); + return selectedKey; }; createEditUrlCallback = function(item) { return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.key + "?page=" + $scope.options.pageNumber + "&listName=" + $scope.contentId; @@ -57,8 +58,85 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie totalPages: 0, items: [] }; + + $scope.currentNodePermissions = {} + + //Just ensure we do have an editorState + if (editorState.current) { + //Fetch current node allowed actions for the current user + //This is the current node & not each individual child node in the list + var currentUserPermissions = editorState.current.allowedActions; + + //Create a nicer model rather than the funky & hard to remember permissions strings + $scope.currentNodePermissions = { + "canCopy": _.contains(currentUserPermissions, 'O'), //Magic Char = O + "canCreate": _.contains(currentUserPermissions, 'C'), //Magic Char = C + "canDelete": _.contains(currentUserPermissions, 'D'), //Magic Char = D + "canMove": _.contains(currentUserPermissions, 'M'), //Magic Char = M + "canPublish": _.contains(currentUserPermissions, 'U'), //Magic Char = U + "canUnpublish": _.contains(currentUserPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) + }; + } + + //when this is null, we don't check permissions + $scope.buttonPermissions = null; + + //When we are dealing with 'content', we need to deal with permissions on child nodes. + // Currently there is no real good way to + if ($scope.entityType === "content") { + + var idsWithPermissions = null; + + $scope.buttonPermissions = { + canCopy: true, + canCreate: true, + canDelete: true, + canMove: true, + canPublish: true, + canUnpublish: true + }; + + $scope.$watch(function() { + return $scope.selection.length; + }, function(newVal, oldVal) { + + if ((idsWithPermissions == null && newVal > 0) || (idsWithPermissions != null)) { + + //get all of the selected ids + var ids = _.map($scope.selection, function(i) { + return i.id.toString(); + }); + + //remove the dictionary items that don't have matching ids + var filtered = {}; + _.each(idsWithPermissions, function (value, key, list) { + if (_.contains(ids, key)) { + filtered[key] = value; + } + }); + idsWithPermissions = filtered; + + //find all ids that we haven't looked up permissions for + var existingIds = _.keys(idsWithPermissions); + var missingLookup = _.map(_.difference(ids, existingIds), function (i) { + return Number(i); + }); + + if (missingLookup.length > 0) { + contentResource.getPermissions(missingLookup).then(function(p) { + $scope.buttonPermissions = listViewHelper.getButtonPermissions(p, idsWithPermissions); + }); + } + else { + $scope.buttonPermissions = listViewHelper.getButtonPermissions({}, idsWithPermissions); + } + } + }); + + } $scope.options = { + displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, pageNumber: ($routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0) ? $routeParams.page : 1, filter: '', @@ -72,11 +150,11 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie layouts: $scope.model.config.layouts, activeLayout: listViewHelper.getLayout($routeParams.id, $scope.model.config.layouts) }, - allowBulkPublish: true, - allowBulkUnpublish: true, - allowBulkCopy: true, - allowBulkMove: true, - allowBulkDelete: true, + allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, + allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, + allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, + allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, + allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete }; //update all of the system includeProperties to enable sorting @@ -164,9 +242,8 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie getListResultsCallback(id, $scope.options).then(function(data) { $scope.actionInProgress = false; - $scope.listViewResultSet = data; - + //update all values for display if ($scope.listViewResultSet.items) { _.each($scope.listViewResultSet.items, function(e, index) { @@ -275,28 +352,28 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie if (!(result.data && angular.isArray(result.data.notifications))) showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); }); - }; + } $scope.delete = function () { applySelected( - function (selected, index) { return deleteItemCallback(getIdCallback(selected[index])) }, - function (count, total) { return "Deleted " + count + " out of " + total + " item" + (total > 1 ? "s" : "") }, - function (total) { return "Deleted " + total + " item" + (total > 1 ? "s" : "") }, + function (selected, index) { return deleteItemCallback(getIdCallback(selected[index])); }, + function (count, total) { return "Deleted " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Deleted " + total + " item" + (total > 1 ? "s" : ""); }, "Sure you want to delete?"); }; $scope.publish = function () { applySelected( function (selected, index) { return contentResource.publishById(getIdCallback(selected[index])); }, - function (count, total) { return "Published " + count + " out of " + total + " item" + (total > 1 ? "s" : "") }, - function (total) { return "Published " + total + " item" + (total > 1 ? "s" : "") }); + function (count, total) { return "Published " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Published " + total + " item" + (total > 1 ? "s" : ""); }); }; $scope.unpublish = function() { applySelected( function (selected, index) { return contentResource.unPublish(getIdCallback(selected[index])); }, - function (count, total) { return "Unpublished " + count + " out of " + total + " item" + (total > 1 ? "s" : "") }, - function (total) { return "Unpublished " + total + " item" + (total > 1 ? "s" : "") }); + function (count, total) { return "Unpublished " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Unpublished " + total + " item" + (total > 1 ? "s" : ""); }); }; $scope.move = function() { @@ -380,7 +457,7 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie } return value; - }; + } /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ function setPropertyValues(result) { @@ -415,14 +492,14 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie }); - }; + } function isDate(val) { if (angular.isString(val)) { return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); } return false; - }; + } function initView() { //default to root id if the id is undefined @@ -436,8 +513,17 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie $scope.contentId = id; $scope.isTrashed = id === "-20" || id === "-21"; + $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; + $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; + + $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || + $scope.options.allowBulkUnpublish || + $scope.options.allowBulkCopy || + $scope.options.allowBulkMove || + $scope.options.allowBulkDelete; + $scope.reloadView($scope.contentId); - }; + } function getLocalizedKey(alias) { @@ -465,6 +551,15 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie return alias; } + function getItemKey(itemId) { + for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { + var item = $scope.listViewResultSet.items[i]; + if (item.id === itemId) { + return item.key; + } + } + } + //GO! initView(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html index e1873907a1..904939b75f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html @@ -6,143 +6,143 @@
- + - + - - - - - - - - - - - {{ selectedItemsCount() }} of {{ listViewResultSet.items.length }} selected - - -
-
-
-
- -
- - - - - - - - - - - - -