diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..e99299d699 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root=true + +[*] +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.{cs,cshtml,csx,vb,vbx,vbhtml,fs,fsx,txt,ps1,sql}] +indent_size = 4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7465b3c452..616b50a38d 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,4 @@ build/ui-docs.zip build/csharp-docs.zip build/msbuild.log .vs/ +src/packages/ \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 3044444730..dc6e22edbf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,21 +2,23 @@ version: '{build}' shallow_clone: true build_script: - cmd: >- - cd build + SET SLN=%CD% + + SET SRC=%SLN%\src + + SET PACKAGES=%SRC%\packages + + CD build SET "release=" FOR /F "skip=1 delims=" %%i IN (UmbracoVersion.txt) DO IF NOT DEFINED release SET "release=%%i" - SET nuGetFolder=C:\Users\appveyor\.nuget\packages - - ..\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 - - IF EXIST ..\src\umbraco.businesslogic\packages.config ..\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 "Restoring NuGet into %PACKAGES%" + + %SRC%\.nuget\NuGet.exe sources Add -Name MyGetUmbracoCore -Source https://www.myget.org/F/umbracocore/api/v2/ >NUL + + %SRC%\.nuget\NuGet.exe restore %SRC%\umbraco.sln -Verbosity Quiet -NonInteractive -PackagesDirectory %PACKAGES% ECHO Building Release %release% build%APPVEYOR_BUILD_NUMBER% @@ -24,13 +26,12 @@ build_script: SET MSBUILD="C:\Program Files (x86)\MSBuild\14.0\Bin\MsBuild.exe" - XCOPY "..\src\Umbraco.Tests\unit-test-log4net.CI.config" "..\src\Umbraco.Tests\unit-test-log4net.config" /Y + XCOPY "%SRC%\Umbraco.Tests\unit-test-log4net.CI.config" "%SRC%\Umbraco.Tests\unit-test-log4net.config" /Y - %MSBUILD% "..\src\Umbraco.Tests\Umbraco.Tests.csproj" /consoleloggerparameters:Summary;ErrorsOnly + %MSBUILD% "%SLN%/src/Umbraco.Tests/Umbraco.Tests.csproj" /consoleloggerparameters:Summary;ErrorsOnly;WarningsOnly /p:NugetPackagesDirectory=%PACKAGES% - build.bat nopause %release% build%APPVEYOR_BUILD_NUMBER% + build.bat -integration -release:%release% -comment:build%APPVEYOR_BUILD_NUMBER% -nugetfolder:%PACKAGES% - ECHO %PATH% test: assemblies: src\Umbraco.Tests\bin\Debug\Umbraco.Tests.dll artifacts: diff --git a/build/Build.bat b/build/Build.bat index fc0d8a69ea..d20eef70fb 100644 --- a/build/Build.bat +++ b/build/Build.bat @@ -1,31 +1,95 @@ @ECHO OFF + +:: UMBRACO BUILD FILE + + +:: ensure we have UmbracoVersion.txt IF NOT EXIST UmbracoVersion.txt ( - ECHO UmbracoVersion.txt missing! - GOTO :showerror + ECHO UmbracoVersion.txt is missing! + GOTO error ) REM Get the version and comment from UmbracoVersion.txt lines 2 and 3 -SET "release=" -SET "comment=" -FOR /F "skip=1 delims=" %%i IN (UmbracoVersion.txt) DO IF NOT DEFINED release SET "release=%%i" -FOR /F "skip=2 delims=" %%i IN (UmbracoVersion.txt) DO IF NOT DEFINED comment SET "comment=%%i" +SET RELEASE= +SET COMMENT= +FOR /F "skip=1 delims=" %%i IN (UmbracoVersion.txt) DO IF NOT DEFINED RELEASE SET RELEASE=%%i +FOR /F "skip=2 delims=" %%i IN (UmbracoVersion.txt) DO IF NOT DEFINED COMMENT SET COMMENT=%%i -REM If there's arguments on the command line overrule UmbracoVersion.txt and use that as the version -IF [%2] NEQ [] (SET release=%2) -IF [%3] NEQ [] (SET comment=%3) ELSE (IF [%2] NEQ [] (SET "comment=")) +REM process args -REM Get the "is continuous integration" from the parameters -SET "isci=0" -IF [%1] NEQ [] (SET isci=1) +SET INTEGRATION=0 +SET nuGetFolder=%CD%\..\src\packages +SET SKIPNUGET=0 -SET version=%release% -IF [%comment%] EQU [] (SET version=%release%) ELSE (SET version=%release%-%comment%) +:processArgs -ECHO. -ECHO Building Umbraco %version% -ECHO. +:: grab the first parameter as a whole eg "/action:start" +:: end if no more parameter +SET SWITCHPARSE=%1 +IF [%SWITCHPARSE%] == [] goto endProcessArgs -ReplaceIISExpressPortNumber.exe ..\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj %release% +:: get switch and value +SET SWITCH= +SET VALUE= +FOR /F "tokens=1,* delims=: " %%a IN ("%SWITCHPARSE%") DO SET SWITCH=%%a& SET VALUE=%%b + +:: route arg +IF '%SWITCH%'=='/release' GOTO argRelease +IF '%SWITCH%'=='-release' GOTO argRelease +IF '%SWITCH%'=='/comment' GOTO argComment +IF '%SWITCH%'=='-comment' GOTO argComment +IF '%SWITCH%'=='/integration' GOTO argIntegration +IF '%SWITCH%'=='-integration' GOTO argIntegration +IF '%SWITCH%'=='/nugetfolder' GOTO argNugetFolder +IF '%SWITCH%'=='-nugetfolder' GOTO argNugetFolder +IF '%SWITCH%'=='/skipnuget' GOTO argSkipNuget +IF '%SWITCH%'=='-skipnuget' GOTO argSkipNuget +ECHO "Invalid switch %SWITCH%" +GOTO error + +:: handle each arg + +:argRelease +set RELEASE=%VALUE% +SHIFT +goto processArgs + +:argComment +SET COMMENT=%VALUE% +SHIFT +GOTO processArgs + +:argIntegration +SET INTEGRATION=1 +SHIFT +GOTO processArgs + +:argNugetFolder +SET nuGetFolder=%VALUE% +SHIFT +GOTO processArgs + +:argSkipNuget +SET SKIPNUGET=1 +SHIFT +GOTO processArgs + +:endProcessArgs + +REM run + +SET VERSION=%RELEASE% +IF [%COMMENT%] EQU [] (SET VERSION=%RELEASE%) ELSE (SET VERSION=%RELEASE%-%COMMENT%) + +ECHO ################################################################ +ECHO Building Umbraco %VERSION% +ECHO ################################################################ + +SET MSBUILDPATH=C:\Program Files (x86)\MSBuild\14.0\Bin +SET MSBUILD="%MSBUILDPATH%\MsBuild.exe" +SET PATH="%MSBUILDPATH%";%PATH% + +ReplaceIISExpressPortNumber.exe ..\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj %RELEASE% ECHO. ECHO Removing the belle build folder and bower_components folder to make sure everything is clean as a whistle @@ -35,10 +99,10 @@ RD ..\src\Umbraco.Web.UI.Client\bower_components /Q /S ECHO. ECHO Removing existing built files to make sure everything is clean as a whistle RMDIR /Q /S _BuildOutput -DEL /F /Q UmbracoCms.*.zip -DEL /F /Q UmbracoExamine.*.zip -DEL /F /Q UmbracoCms.*.nupkg -DEL /F /Q webpihash.txt +DEL /F /Q UmbracoCms.*.zip 2>NUL +DEL /F /Q UmbracoExamine.*.zip 2>NUL +DEL /F /Q UmbracoCms.*.nupkg 2>NUL +DEL /F /Q webpihash.txt 2>NUL ECHO. ECHO Making sure Git is in the path so that the build can succeed @@ -47,11 +111,21 @@ CALL InstallGit.cmd REM Adding the default Git path so that if it's installed it can actually be found REM This is necessary because SETLOCAL is on in InstallGit.cmd so that one might find Git, REM but the path setting is lost due to SETLOCAL -path=C:\Program Files (x86)\Git\cmd;C:\Program Files\Git\cmd;%PATH% +SET PATH="C:\Program Files (x86)\Git\cmd";"C:\Program Files\Git\cmd";%PATH% ECHO. ECHO Making sure we have a web.config -IF NOT EXIST %CD%\..\src\Umbraco.Web.UI\web.config COPY %CD%\..\src\Umbraco.Web.UI\web.Template.config %CD%\..\src\Umbraco.Web.UI\web.config +IF NOT EXIST "%CD%\..\src\Umbraco.Web.UI\web.config" COPY "%CD%\..\src\Umbraco.Web.UI\web.Template.config" "%CD%\..\src\Umbraco.Web.UI\web.config" + +ECHO. +ECHO Reporting NuGet version +"%CD%\..\src\.nuget\NuGet.exe" help | findstr "^NuGet Version:" + +ECHO. +ECHO Restoring NuGet packages +ECHO Into %nuGetFolder% +"%CD%\..\src\.nuget\NuGet.exe" restore "%CD%\..\src\umbraco.sln" -Verbosity Quiet -NonInteractive -PackagesDirectory "%nuGetFolder%" +IF ERRORLEVEL 1 GOTO :error ECHO. ECHO. @@ -60,13 +134,15 @@ ECHO This takes a few minutes and logging is set to report warnings ECHO and errors only so it might seems like nothing is happening for a while. ECHO You can check the msbuild.log file for progress. ECHO. -%windir%\Microsoft.NET\Framework\v4.0.30319\msbuild.exe "Build.proj" /p:BUILD_RELEASE=%release% /p:BUILD_COMMENT=%comment% /p:NugetPackagesDirectory=%nuGetFolder% /consoleloggerparameters:Summary;ErrorsOnly;WarningsOnly /fileLogger -IF ERRORLEVEL 1 GOTO :error +%MSBUILD% "Build.proj" /p:BUILD_RELEASE=%RELEASE% /p:BUILD_COMMENT=%COMMENT% /p:NugetPackagesDirectory="%nuGetFolder%" /consoleloggerparameters:Summary;ErrorsOnly /fileLogger +IF ERRORLEVEL 1 GOTO error ECHO. ECHO Setting node_modules folder to hidden to prevent VS13 from crashing on it while loading the websites project attrib +h ..\src\Umbraco.Web.UI.Client\node_modules +IF %SKIPNUGET% EQU 1 GOTO success + ECHO. ECHO Adding Web.config transform files to the NuGet package REN .\_BuildOutput\WebApp\Views\Web.config Web.config.transform @@ -74,22 +150,25 @@ REN .\_BuildOutput\WebApp\Xslt\Web.config Web.config.transform ECHO. ECHO Packing the NuGet release files -..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.Core.nuspec -Version %version% -Symbols -Verbosity quiet -..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.nuspec -Version %version% -Verbosity quiet -IF ERRORLEVEL 1 GOTO :error +..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.Core.nuspec -Version %VERSION% -Symbols -Verbosity quiet +..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.nuspec -Version %VERSION% -Verbosity quiet +IF ERRORLEVEL 1 GOTO error :success ECHO. ECHO No errors were detected! ECHO There may still be some in the output, which you would need to investigate. ECHO Warnings are usually normal. +ECHO. +ECHO. GOTO :EOF :error ECHO. ECHO Errors were detected! +ECHO. REM don't pause if continuous integration else the build server waits forever REM before cancelling the build (and, there is noone to read the output anyways) -IF isci NEQ 1 PAUSE +IF %INTEGRATION% NEQ 1 PAUSE diff --git a/build/Build.proj b/build/Build.proj index 9413b49d15..1f57ca5915 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -1,8 +1,8 @@ - @@ -35,9 +35,9 @@ { using (var cryptoProvider = new SHA1CryptoServiceProvider()) { - + var fileHash = cryptoProvider.ComputeHash(stream); - + using (TextWriter w = new StreamWriter(OutputFile, false)) { w.WriteLine(string.Join("", fileHash.Select(b => b.ToString("x2")))); @@ -52,9 +52,9 @@ - @@ -102,9 +102,9 @@ - @@ -155,7 +155,7 @@ - + @@ -231,7 +231,7 @@ DestinationFiles="@(CustomLanguageFiles->'$(ConfigsFolder)Lang\%(RecursiveDir)%(Filename)%(Extension)')" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" /> - + - - + - $(BUILD_RELEASE) @@ -307,34 +305,34 @@ ReplacementText="AssemblyFileVersion("$(BUILD_RELEASE)")"/> - - - - - - + - diff --git a/build/InstallGit.cmd b/build/InstallGit.cmd index e009e2594e..4daa2f45d9 100644 --- a/build/InstallGit.cmd +++ b/build/InstallGit.cmd @@ -1,18 +1,18 @@ @ECHO OFF SETLOCAL - :: SETLOCAL is on, so changes to the path not persist to the actual user's path +REM SETLOCAL is on, so changes to the path not persist to the actual user's path git.exe --version IF %ERRORLEVEL%==9009 GOTO :trydefaultpath +REM OK, DONE GOTO :EOF - :: Git is installed, no need to to anything else :trydefaultpath PATH=C:\Program Files (x86)\Git\cmd;C:\Program Files\Git\cmd;%PATH% git.exe --version IF %ERRORLEVEL%==9009 GOTO :showerror +REM OK, DONE GOTO :EOF - :: Git is installed, no need to to anything else :showerror ECHO Git is not in your path and could not be found in C:\Program Files (x86)\Git\cmd nor in C:\Program Files\Git\cmd diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index ab79494f71..47e10bbcef 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -14,66 +14,67 @@ Contains the core assemblies needed to run Umbraco Cms en-US umbraco - + + + + - + - + - - + - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index b56c8e6205..c9912aeb89 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -16,8 +16,10 @@ umbraco - - + + + + @@ -44,6 +46,7 @@ + diff --git a/build/NuSpecs/tools/Dashboard.config.install.xdt b/build/NuSpecs/tools/Dashboard.config.install.xdt index 8368870186..036beeba29 100644 --- a/build/NuSpecs/tools/Dashboard.config.install.xdt +++ b/build/NuSpecs/tools/Dashboard.config.install.xdt @@ -41,16 +41,6 @@ views/dashboard/developer/examinemanagement.html - - - views/dashboard/developer/healthcheck.html - - - - - views/dashboard/developer/redirecturls.html - -
@@ -80,4 +70,26 @@
+ +
+ + content + + + + views/dashboard/developer/redirecturls.html + + +
+ +
+ + developer + + + + views/dashboard/developer/healthcheck.html + + +
\ No newline at end of file diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt index e0d660a795..5c46ee20bf 100644 --- a/build/NuSpecs/tools/ReadmeUpgrade.txt +++ b/build/NuSpecs/tools/ReadmeUpgrade.txt @@ -8,8 +8,16 @@ ---------------------------------------------------- -Don't forget to build! +*** IMPORTANT NOTICE FOR 7.6 UPGRADES *** +Be sure to read the version specific upgrade information before proceeding: +https://our.umbraco.org/documentation/Getting-Started/Setup/Upgrading/version-specific#version-7-6-0 + +You will most likely need to make some changes to your web.config and you will need to be +aware of the breaking changes listed there to see if these affect your installation. + + +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 cbb60bb949..35c4c4846d 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -58,7 +58,7 @@ - + > @@ -330,6 +330,8 @@ + + @@ -337,7 +339,7 @@ - + @@ -349,7 +351,7 @@ - + @@ -383,6 +385,10 @@ + + + + diff --git a/build/NuSpecs/tools/cache.config.install.xdt b/build/NuSpecs/tools/cache.config.install.xdt new file mode 100644 index 0000000000..746e3c3298 --- /dev/null +++ b/build/NuSpecs/tools/cache.config.install.xdt @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build/NuSpecs/tools/install.core.ps1 b/build/NuSpecs/tools/install.core.ps1 index e2230e0c32..c53b2cd7e5 100644 --- a/build/NuSpecs/tools/install.core.ps1 +++ b/build/NuSpecs/tools/install.core.ps1 @@ -53,7 +53,6 @@ if ($project) { 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 $umbracoBinFolder "amd64" diff --git a/build/NuSpecs/tools/trees.config.install.xdt b/build/NuSpecs/tools/trees.config.install.xdt index a9fbf07762..7d41835fb1 100644 --- a/build/NuSpecs/tools/trees.config.install.xdt +++ b/build/NuSpecs/tools/trees.config.install.xdt @@ -34,10 +34,15 @@ xdt:Transform="SetAttributes()" /> - + xdt:Transform="SetAttributes()" /> + + + + @@ -84,9 +89,14 @@ - + + + + diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index 78653d30ea..6ef1d69bd9 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,2 +1,2 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) -7.5.7 \ No newline at end of file +7.6.0 \ No newline at end of file diff --git a/src/.nuget/NuGet.exe b/src/.nuget/NuGet.exe index 9ca66594f9..6804fb7da7 100644 Binary files a/src/.nuget/NuGet.exe and b/src/.nuget/NuGet.exe differ diff --git a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj index 0689a7a7d4..a5d86d5377 100644 --- a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj +++ b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -49,9 +49,11 @@ ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.dll + True ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.Entity.dll + True diff --git a/src/SQLCE4Umbraco/SqlCEHelper.cs b/src/SQLCE4Umbraco/SqlCEHelper.cs index ab6f686c21..a8567abd67 100644 --- a/src/SQLCE4Umbraco/SqlCEHelper.cs +++ b/src/SQLCE4Umbraco/SqlCEHelper.cs @@ -180,12 +180,16 @@ namespace SqlCE4Umbraco /// The return value of the command. protected override object ExecuteScalar(string commandText, SqlCeParameter[] parameters) { - #if DEBUG && DebugDataLayer +#if DEBUG && DebugDataLayer // Log Query Execution Trace.TraceInformation(GetType().Name + " SQL ExecuteScalar: " + commandText); - #endif - - return SqlCeApplicationBlock.ExecuteScalar(ConnectionString, CommandType.Text, commandText, parameters); +#endif + using (var cc = UseCurrentConnection) + { + return SqlCeApplicationBlock.ExecuteScalar( + (SqlCeConnection) cc.Connection, (SqlCeTransaction) cc.Transaction, + CommandType.Text, commandText, parameters); + } } /// @@ -198,12 +202,17 @@ namespace SqlCE4Umbraco /// protected override int ExecuteNonQuery(string commandText, SqlCeParameter[] parameters) { - #if DEBUG && DebugDataLayer +#if DEBUG && DebugDataLayer // Log Query Execution Trace.TraceInformation(GetType().Name + " SQL ExecuteNonQuery: " + commandText); - #endif +#endif - return SqlCeApplicationBlock.ExecuteNonQuery(ConnectionString, CommandType.Text, commandText, parameters); + using (var cc = UseCurrentConnection) + { + return SqlCeApplicationBlock.ExecuteNonQuery( + (SqlCeConnection) cc.Connection, (SqlCeTransaction) cc.Transaction, + CommandType.Text, commandText, parameters); + } } /// @@ -216,13 +225,17 @@ namespace SqlCE4Umbraco /// protected override IRecordsReader ExecuteReader(string commandText, SqlCeParameter[] parameters) { - #if DEBUG && DebugDataLayer +#if DEBUG && DebugDataLayer // Log Query Execution Trace.TraceInformation(GetType().Name + " SQL ExecuteReader: " + commandText); - #endif +#endif - return new SqlCeDataReaderHelper(SqlCeApplicationBlock.ExecuteReader(ConnectionString, CommandType.Text, - commandText, parameters)); + using (var cc = UseCurrentConnection) + { + return new SqlCeDataReaderHelper(SqlCeApplicationBlock.ExecuteReader( + (SqlCeConnection) cc.Connection, (SqlCeTransaction) cc.Transaction, + CommandType.Text, commandText, parameters)); + } } diff --git a/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs b/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs index 5cd9bc2140..2dd0f26e90 100644 --- a/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs +++ b/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Data.SqlServerCe; using System.Data; using System.Diagnostics; @@ -26,30 +24,61 @@ namespace SqlCE4Umbraco params SqlCeParameter[] commandParameters ) { - object retVal; - try { - using (SqlCeConnection conn = SqlCeContextGuardian.Open(connectionString)) + using (var conn = SqlCeContextGuardian.Open(connectionString)) { - using (SqlCeCommand cmd = new SqlCeCommand(commandText, conn)) - { - AttachParameters(cmd, commandParameters); - Debug.WriteLine("---------------------------------SCALAR-------------------------------------"); - Debug.WriteLine(commandText); - Debug.WriteLine("----------------------------------------------------------------------------"); - retVal = cmd.ExecuteScalar(); - } + return ExecuteScalarTry(conn, null, commandText, commandParameters); } - - return retVal; } catch (Exception ee) { - throw new SqlCeProviderException("Error running Scalar: \nSQL Statement:\n" + commandText + "\n\nException:\n" + ee.ToString()); + throw new SqlCeProviderException("Error running Scalar: \nSQL Statement:\n" + commandText + "\n\nException:\n" + ee); } } + public static object ExecuteScalar( + SqlCeConnection conn, SqlCeTransaction trx, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters) + { + try + { + return ExecuteScalarTry(conn, trx, commandText, commandParameters); + } + catch (Exception ee) + { + throw new SqlCeProviderException("Error running Scalar: \nSQL Statement:\n" + commandText + "\n\nException:\n" + ee); + } + } + + public static object ExecuteScalar( + SqlCeConnection conn, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters) + { + return ExecuteScalar(conn, null, commandType, commandText, commandParameters); + } + + private static object ExecuteScalarTry( + SqlCeConnection conn, SqlCeTransaction trx, + string commandText, + params SqlCeParameter[] commandParameters) + { + object retVal; + using (var cmd = trx == null ? new SqlCeCommand(commandText, conn) : new SqlCeCommand(commandText, conn, trx)) + { + AttachParameters(cmd, commandParameters); + Debug.WriteLine("---------------------------------SCALAR-------------------------------------"); + Debug.WriteLine(commandText); + Debug.WriteLine("----------------------------------------------------------------------------"); + retVal = cmd.ExecuteScalar(); + } + return retVal; + } + /// /// /// @@ -66,49 +95,10 @@ namespace SqlCE4Umbraco { try { - int rowsAffected; - using (SqlCeConnection conn = SqlCeContextGuardian.Open(connectionString)) + using (var conn = SqlCeContextGuardian.Open(connectionString)) { - // this is for multiple queries in the installer - if (commandText.Trim().StartsWith("!!!")) - { - commandText = commandText.Trim().Trim('!'); - string[] commands = commandText.Split('|'); - string currentCmd = String.Empty; - - foreach (string cmd in commands) - { - try - { - currentCmd = cmd; - if (!String.IsNullOrWhiteSpace(cmd)) - { - SqlCeCommand c = new SqlCeCommand(cmd, conn); - c.ExecuteNonQuery(); - } - } - catch (Exception e) - { - Debug.WriteLine("*******************************************************************"); - Debug.WriteLine(currentCmd); - Debug.WriteLine(e); - Debug.WriteLine("*******************************************************************"); - } - } - return 1; - } - else - { - Debug.WriteLine("----------------------------------------------------------------------------"); - Debug.WriteLine(commandText); - Debug.WriteLine("----------------------------------------------------------------------------"); - SqlCeCommand cmd = new SqlCeCommand(commandText, conn); - AttachParameters(cmd, commandParameters); - rowsAffected = cmd.ExecuteNonQuery(); - } + return ExecuteNonQueryTry(conn, null, commandText, commandParameters); } - - return rowsAffected; } catch (Exception ee) { @@ -116,6 +106,74 @@ namespace SqlCE4Umbraco } } + public static int ExecuteNonQuery( + SqlCeConnection conn, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters + ) + { + return ExecuteNonQuery(conn, null, commandType, commandText, commandParameters); + } + + public static int ExecuteNonQuery( + SqlCeConnection conn, SqlCeTransaction trx, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters + ) + { + try + { + return ExecuteNonQueryTry(conn, trx, commandText, commandParameters); + } + catch (Exception ee) + { + throw new SqlCeProviderException("Error running NonQuery: \nSQL Statement:\n" + commandText + "\n\nException:\n" + ee.ToString()); + } + } + + private static int ExecuteNonQueryTry( + SqlCeConnection conn, SqlCeTransaction trx, + string commandText, + params SqlCeParameter[] commandParameters) + { + // this is for multiple queries in the installer + if (commandText.Trim().StartsWith("!!!")) + { + commandText = commandText.Trim().Trim('!'); + var commands = commandText.Split('|'); + var currentCmd = string.Empty; + + foreach (var command in commands) + { + try + { + currentCmd = command; + if (string.IsNullOrWhiteSpace(command)) continue; + var c = trx == null ? new SqlCeCommand(command, conn) : new SqlCeCommand(command, conn, trx); + c.ExecuteNonQuery(); + } + catch (Exception e) + { + Debug.WriteLine("*******************************************************************"); + Debug.WriteLine(currentCmd); + Debug.WriteLine(e); + Debug.WriteLine("*******************************************************************"); + } + } + return 1; + } + + Debug.WriteLine("----------------------------------------------------------------------------"); + Debug.WriteLine(commandText); + Debug.WriteLine("----------------------------------------------------------------------------"); + var cmd = new SqlCeCommand(commandText, conn); + AttachParameters(cmd, commandParameters); + var rowsAffected = cmd.ExecuteNonQuery(); + return rowsAffected; + } + /// /// /// @@ -133,25 +191,8 @@ namespace SqlCE4Umbraco { try { - Debug.WriteLine("---------------------------------READER-------------------------------------"); - Debug.WriteLine(commandText); - Debug.WriteLine("----------------------------------------------------------------------------"); - SqlCeDataReader reader; - SqlCeConnection conn = SqlCeContextGuardian.Open(connectionString); - - try - { - SqlCeCommand cmd = new SqlCeCommand(commandText, conn); - AttachParameters(cmd, commandParameters); - reader = cmd.ExecuteReader(CommandBehavior.CloseConnection); - } - catch - { - conn.Close(); - throw; - } - - return reader; + var conn = SqlCeContextGuardian.Open(connectionString); + return ExecuteReaderTry(conn, null, commandText, commandParameters); } catch (Exception ee) { @@ -159,30 +200,71 @@ namespace SqlCE4Umbraco } } - public static bool VerifyConnection(string connectionString) + public static SqlCeDataReader ExecuteReader( + SqlCeConnection conn, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters + ) { - bool isConnected = false; - using (SqlCeConnection conn = SqlCeContextGuardian.Open(connectionString)) - { - isConnected = conn.State == ConnectionState.Open; - } - - return isConnected; + return ExecuteReader(conn, commandType, commandText, commandParameters); } - private static void AttachParameters(SqlCeCommand command, SqlCeParameter[] commandParameters) + public static SqlCeDataReader ExecuteReader( + SqlCeConnection conn, SqlCeTransaction trx, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters + ) { - foreach (SqlCeParameter parameter in commandParameters) + try + { + return ExecuteReaderTry(conn, trx, commandText, commandParameters); + } + catch (Exception ee) + { + throw new SqlCeProviderException("Error running Reader: \nSQL Statement:\n" + commandText + "\n\nException:\n" + ee.ToString()); + } + } + + private static SqlCeDataReader ExecuteReaderTry( + SqlCeConnection conn, SqlCeTransaction trx, + string commandText, + params SqlCeParameter[] commandParameters) + { + Debug.WriteLine("---------------------------------READER-------------------------------------"); + Debug.WriteLine(commandText); + Debug.WriteLine("----------------------------------------------------------------------------"); + + try + { + var cmd = trx == null ? new SqlCeCommand(commandText, conn) : new SqlCeCommand(commandText, conn, trx); + AttachParameters(cmd, commandParameters); + return cmd.ExecuteReader(); + } + catch + { + conn.Close(); + throw; + } + } + + public static bool VerifyConnection(string connectionString) + { + using (var conn = SqlCeContextGuardian.Open(connectionString)) + { + return conn.State == ConnectionState.Open; + } + } + + private static void AttachParameters(SqlCeCommand command, IEnumerable commandParameters) + { + foreach (var parameter in commandParameters) { if ((parameter.Direction == ParameterDirection.InputOutput) && (parameter.Value == null)) - { parameter.Value = DBNull.Value; - } command.Parameters.Add(parameter); } } - - - } } diff --git a/src/SQLCE4Umbraco/app.config b/src/SQLCE4Umbraco/app.config index 1f5a6442ad..fb4b506207 100644 --- a/src/SQLCE4Umbraco/app.config +++ b/src/SQLCE4Umbraco/app.config @@ -4,7 +4,7 @@ - + @@ -30,6 +30,10 @@ + + + + - \ No newline at end of file + diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 3a27c3ddb2..ad697d99bf 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -2,7 +2,7 @@ using System.Resources; [assembly: AssemblyCompany("Umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2016")] +[assembly: AssemblyCopyright("Copyright © Umbraco 2017")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.5.7")] -[assembly: AssemblyInformationalVersion("7.5.7")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.6.0")] +[assembly: AssemblyInformationalVersion("7.6.0")] \ No newline at end of file diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index e47ef04650..384ff6e34c 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -1,12 +1,11 @@ using System; using System.Configuration; using System.Threading; -using System.Threading.Tasks; -using System.Web; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.ObjectResolution; using Umbraco.Core.Profiling; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Sync; @@ -162,6 +161,11 @@ namespace Umbraco.Core /// public static ApplicationContext Current { get; internal set; } + /// + /// Gets the scope provider. + /// + internal IScopeProvider ScopeProvider { get { return _databaseContext == null ? null : _databaseContext.ScopeProvider; } } + /// /// Returns the application wide cache accessor /// @@ -296,7 +300,7 @@ namespace Umbraco.Core // if we have a db context available, if we don't then we are not installed anyways if (DatabaseContext.IsDatabaseConfigured && DatabaseContext.CanConnect) { - var found = Services.MigrationEntryService.FindEntry(GlobalSettings.UmbracoMigrationName, UmbracoVersion.GetSemanticVersion()); + var found = Services.MigrationEntryService.FindEntry(Constants.System.UmbracoMigrationName, UmbracoVersion.GetSemanticVersion()); if (found == null) { //we haven't executed this migration in this environment, so even though the config versions match, @@ -418,10 +422,17 @@ namespace Umbraco.Core this.ApplicationCache = null; if (_databaseContext != null) //need to check the internal field here { + if (_databaseContext.ScopeProvider.AmbientScope != null) + { + var scope = _databaseContext.ScopeProvider.AmbientScope; + scope.Dispose(); + } + /* if (DatabaseContext.IsDatabaseConfigured && DatabaseContext.Database != null) { DatabaseContext.Database.Dispose(); - } + } + */ } this.DatabaseContext = null; this.Services = null; diff --git a/src/Umbraco.Core/BindingRedirects.cs b/src/Umbraco.Core/BindingRedirects.cs new file mode 100644 index 0000000000..4fadb4db70 --- /dev/null +++ b/src/Umbraco.Core/BindingRedirects.cs @@ -0,0 +1,49 @@ +using System; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Web; +using Umbraco.Core; + +[assembly: PreApplicationStartMethod(typeof(BindingRedirects), "Initialize")] + +namespace Umbraco.Core +{ + /// + /// Manages any assembly binding redirects that cannot be done via config (i.e. unsigned --> signed assemblies) + /// + public sealed class BindingRedirects + { + public static void Initialize() + { + // this only gets called when an assembly can't be resolved + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + } + + private static readonly Regex Log4NetAssemblyPattern = new Regex("log4net, Version=([\\d\\.]+?), Culture=neutral, PublicKeyToken=\\w+$", RegexOptions.Compiled); + private const string Log4NetReplacement = "log4net, Version=2.0.8.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a"; + + /// + /// This is used to do an assembly binding redirect via code - normally required due to signature changes in assemblies + /// + /// + /// + /// + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + //log4net: + if (Log4NetAssemblyPattern.IsMatch(args.Name) && args.Name != Log4NetReplacement) + { + return Assembly.Load(Log4NetAssemblyPattern.Replace(args.Name, Log4NetReplacement)); + } + + //AutoMapper: + // ensure the assembly is indeed AutoMapper and that the PublicKeyToken is null before trying to Load again + // do NOT just replace this with 'return Assembly', as it will cause an infinite loop -> stackoverflow + if (args.Name.StartsWith("AutoMapper") && args.Name.EndsWith("PublicKeyToken=null")) + return Assembly.Load(args.Name.Replace(", PublicKeyToken=null", ", PublicKeyToken=be96cd2c38ef1005")); + + return null; + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/ByteArrayExtensions.cs b/src/Umbraco.Core/ByteArrayExtensions.cs new file mode 100644 index 0000000000..dacdd509ca --- /dev/null +++ b/src/Umbraco.Core/ByteArrayExtensions.cs @@ -0,0 +1,39 @@ +namespace Umbraco.Core +{ + public static class ByteArrayExtensions + { + private static readonly char[] BytesToHexStringLookup = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + public static string ToHexString(this byte[] bytes) + { + int i = 0, p = 0, bytesLength = bytes.Length; + var chars = new char[bytesLength * 2]; + while (i < bytesLength) + { + var b = bytes[i++]; + chars[p++] = BytesToHexStringLookup[b / 0x10]; + chars[p++] = BytesToHexStringLookup[b % 0x10]; + } + return new string(chars, 0, chars.Length); + } + + public static string ToHexString(this byte[] bytes, char separator, int blockSize, int blockCount) + { + int p = 0, bytesLength = bytes.Length, count = 0, size = 0; + var chars = new char[bytesLength * 2 + blockCount]; + for (var i = 0; i < bytesLength; i++) + { + var b = bytes[i++]; + chars[p++] = BytesToHexStringLookup[b / 0x10]; + chars[p++] = BytesToHexStringLookup[b % 0x10]; + if (count == blockCount) continue; + if (++size < blockSize) continue; + + chars[p++] = '/'; + size = 0; + count++; + } + return new string(chars, 0, chars.Length); + } + } +} diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 0c1a202b66..3b9ee9c63a 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -64,7 +64,8 @@ namespace Umbraco.Core.Cache [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string ContentTypePropertiesCacheKey = "ContentType_PropertyTypes_Content:"; - [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] + [Obsolete("No longer used and will be removed in v8")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string PropertyTypeCacheKey = "UmbracoPropertyTypeCache"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] diff --git a/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs index 0ae721943d..14fef80f0d 100644 --- a/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs @@ -26,6 +26,9 @@ namespace Umbraco.Core.Cache public DeepCloneRuntimeCacheProvider(IRuntimeCacheProvider innerProvider) { + if (innerProvider.GetType() == typeof(DeepCloneRuntimeCacheProvider)) + throw new InvalidOperationException("A " + typeof(DeepCloneRuntimeCacheProvider) + " cannot wrap another instance of " + typeof(DeepCloneRuntimeCacheProvider)); + InnerProvider = innerProvider; } @@ -105,9 +108,11 @@ namespace Umbraco.Core.Cache var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache if (value == null) return null; // do not store null values (backward compat) + //Clone/reset to go into the cache return CheckCloneableAndTracksChanges(value); }, timeout, isSliding, priority, removedCallback, dependentFiles); + //Clone/reset to go out of the cache return CheckCloneableAndTracksChanges(cached); } diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs index 1f51fc3ccc..57d12c4223 100644 --- a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs @@ -1,268 +1,250 @@ using System; using System.Collections.Generic; using System.Linq; -using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Cache { /// - /// The default cache policy for retrieving a single entity + /// Represents the default cache policy. /// - /// - /// + /// The type of the entity. + /// The type of the identifier. /// - /// 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. + /// The default cache policy caches entities with a 5 minutes sliding expiration. + /// Each entity is cached individually. + /// If options.GetAllCacheAllowZeroCount then a 'zero-count' array is cached when GetAll finds nothing. + /// If options.GetAllCacheValidateCount then we check against the db when getting many entities. /// internal class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IAggregateRoot { + private static readonly TEntity[] EmptyEntities = new TEntity[0]; // const private readonly RepositoryCachePolicyOptions _options; - + public DefaultRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) : base(cache) - { + { if (options == null) throw new ArgumentNullException("options"); - _options = options; + _options = options; } - protected string GetCacheIdKey(object id) + public override IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope) + { + return new ScopedRepositoryCachePolicy(this, runtimeCache, scope); + } + + protected string GetEntityCacheKey(object id) { if (id == null) throw new ArgumentNullException("id"); - - return string.Format("{0}{1}", GetCacheTypeKey(), id); + return GetEntityTypeCacheKey() + id; } - protected string GetCacheTypeKey() + protected string GetEntityTypeCacheKey() { return string.Format("uRepo_{0}_", typeof(TEntity).Name); } - public override void CreateOrUpdate(TEntity entity, Action persistMethod) + protected virtual void InsertEntity(string cacheKey, TEntity entity) + { + Cache.InsertCacheItem(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); + } + + protected virtual void InsertEntities(TId[] ids, TEntity[] entities) + { + if (ids.Length == 0 && entities.Length == 0 && _options.GetAllCacheAllowZeroCount) + { + // getting all of them, and finding nothing. + // if we can cache a zero count, cache an empty array, + // for as long as the cache is not cleared (no expiration) + Cache.InsertCacheItem(GetEntityTypeCacheKey(), () => EmptyEntities); + } + else + { + // individually cache each item + foreach (var entity in entities) + { + var capture = entity; + Cache.InsertCacheItem(GetEntityCacheKey(entity.Id), () => capture, TimeSpan.FromMinutes(5), true); + } + } + } + + /// + public override void Create(TEntity entity, Action persistNew) { if (entity == null) throw new ArgumentNullException("entity"); - if (persistMethod == null) throw new ArgumentNullException("persistMethod"); try { - persistMethod(entity); + persistNew(entity); - //set the disposal action - SetCacheAction(() => + // just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) { - //just to be safe, we cannot cache an item without an identity - if (entity.HasIdentity) - { - Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity, - timeout: TimeSpan.FromMinutes(5), - isSliding: true); - } - - //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.ClearCacheItem(GetCacheTypeKey()); - }); - + Cache.InsertCacheItem(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); + } + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetEntityTypeCacheKey()); } catch { - //set the disposal action - SetCacheAction(() => - { - //if an exception is thrown we need to remove the entry from cache, this is ONLY a work around because of the way - // that we cache entities: http://issues.umbraco.org/issue/U4-4259 - Cache.ClearCacheItem(GetCacheIdKey(entity.Id)); + // if an exception is thrown we need to remove the entry from cache, + // this is ONLY a work around because of the way + // that we cache entities: http://issues.umbraco.org/issue/U4-4259 + Cache.ClearCacheItem(GetEntityCacheKey(entity.Id)); + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetEntityTypeCacheKey()); - //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.ClearCacheItem(GetCacheTypeKey()); - }); - throw; } } - public override void Remove(TEntity entity, Action persistMethod) + /// + public override void Update(TEntity entity, Action persistUpdated) { if (entity == null) throw new ArgumentNullException("entity"); - if (persistMethod == null) throw new ArgumentNullException("persistMethod"); try { - persistMethod(entity); - } - finally - { - //set the disposal action - var cacheKey = GetCacheIdKey(entity.Id); - SetCacheAction(() => + persistUpdated(entity); + + // just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) { - Cache.ClearCacheItem(cacheKey); - //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.ClearCacheItem(GetCacheTypeKey()); - }); + Cache.InsertCacheItem(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); + } + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetEntityTypeCacheKey()); + } + catch + { + // if an exception is thrown we need to remove the entry from cache, + // this is ONLY a work around because of the way + // that we cache entities: http://issues.umbraco.org/issue/U4-4259 + Cache.ClearCacheItem(GetEntityCacheKey(entity.Id)); + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetEntityTypeCacheKey()); + + throw; } } - public override TEntity Get(TId id, Func getFromRepo) + /// + public override void Delete(TEntity entity, Action persistDeleted) { - if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); + if (entity == null) throw new ArgumentNullException("entity"); - var cacheKey = GetCacheIdKey(id); + try + { + persistDeleted(entity); + } + finally + { + // whatever happens, clear the cache + var cacheKey = GetEntityCacheKey(entity.Id); + Cache.ClearCacheItem(cacheKey); + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetEntityTypeCacheKey()); + } + } + + /// + public override TEntity Get(TId id, Func performGet, Func> performGetAll) + { + var cacheKey = GetEntityCacheKey(id); var fromCache = Cache.GetCacheItem(cacheKey); + + // if found in cache then return else fetch and cache if (fromCache != null) return fromCache; - - var entity = getFromRepo(id); + var entity = performGet(id); - //set the disposal action - SetCacheAction(cacheKey, entity); + if (entity != null && entity.HasIdentity) + InsertEntity(cacheKey, entity); return entity; } - public override TEntity Get(TId id) + /// + public override TEntity GetCached(TId id) { - var cacheKey = GetCacheIdKey(id); + var cacheKey = GetEntityCacheKey(id); return Cache.GetCacheItem(cacheKey); } - public override bool Exists(TId id, Func getFromRepo) + /// + public override bool Exists(TId id, Func performExists, Func> performGetAll) { - if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); - - var cacheKey = GetCacheIdKey(id); + // if found in cache the return else check + var cacheKey = GetEntityCacheKey(id); var fromCache = Cache.GetCacheItem(cacheKey); - return fromCache != null || getFromRepo(id); + return fromCache != null || performExists(id); } - public override TEntity[] GetAll(TId[] ids, Func> getFromRepo) + /// + public override TEntity[] GetAll(TId[] ids, Func> performGetAll) { - if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); - - if (ids.Any()) + if (ids.Length > 0) { - var entities = ids.Select(Get).ToArray(); - if (ids.Length.Equals(entities.Length) && entities.Any(x => x == null) == false) - return entities; + // try to get each entity from the cache + // if we can find all of them, return + var entities = ids.Select(GetCached).WhereNotNull().ToArray(); + if (ids.Length.Equals(entities.Length)) + return entities; // no need for null checks, we are not caching nulls } else { - var allEntities = GetAllFromCache(); - if (allEntities.Any()) + // get everything we have + var entities = Cache.GetCacheItemsByKeySearch(GetEntityTypeCacheKey()) + .ToArray(); // no need for null checks, we are not caching nulls + + if (entities.Length > 0) { + // if some of them were in the cache... if (_options.GetAllCacheValidateCount) { - //Get count of all entities of current type (TEntity) to ensure cached result is correct + // need to validate the count, get the actual count and return if ok var totalCount = _options.PerformCount(); - if (allEntities.Length == totalCount) - return allEntities; + if (entities.Length == totalCount) + return entities; } else { - return allEntities; + // no need to validate, just return what we have and assume it's all there is + return entities; } } else if (_options.GetAllCacheAllowZeroCount) { - //if the repository allows caching a zero count, then check the zero count cache - if (HasZeroCountCache()) - { - //there is a zero count cache so return an empty list - return new TEntity[] {}; - } + // if none of them were in the cache + // and we allow zero count - check for the special (empty) entry + var empty = Cache.GetCacheItem(GetEntityTypeCacheKey()); + if (empty != null) return empty; } } - //we need to do the lookup from the repo - var entityCollection = getFromRepo(ids) - //ensure we don't include any null refs in the returned collection! - .WhereNotNull() + // cache failed, get from repo and cache + var repoEntities = performGetAll(ids) + .WhereNotNull() // exclude nulls! + .Where(x => x.HasIdentity) // be safe, though would be weird... .ToArray(); - //set the disposal action - SetCacheAction(ids, entityCollection); + // note: if empty & allow zero count, will cache a special (empty) entry + InsertEntities(ids, repoEntities); - return entityCollection; + return repoEntities; } - /// - /// Looks up the zero count cache, must return null if it doesn't exist - /// - /// - protected bool HasZeroCountCache() + /// + public override void ClearAll() { - var zeroCount = Cache.GetCacheItem(GetCacheTypeKey()); - return (zeroCount != null && zeroCount.Any() == false); + Cache.ClearAllCache(); } - - /// - /// Performs the lookup for all entities of this type from the cache - /// - /// - protected TEntity[] GetAllFromCache() - { - var allEntities = Cache.GetCacheItemsByKeySearch(GetCacheTypeKey()) - .WhereNotNull() - .ToArray(); - return allEntities.Any() ? allEntities : new TEntity[] {}; - } - - /// - /// Sets the action to execute on disposal for a single entity - /// - /// - /// - protected virtual void SetCacheAction(string cacheKey, TEntity entity) - { - if (entity == null) return; - - SetCacheAction(() => - { - //just to be safe, we cannot cache an item without an identity - if (entity.HasIdentity) - { - Cache.InsertCacheItem(cacheKey, () => entity, - timeout: TimeSpan.FromMinutes(5), - isSliding: true); - } - }); - } - - /// - /// Sets the action to execute on disposal for an entity collection - /// - /// - /// - protected virtual void SetCacheAction(TId[] ids, TEntity[] entityCollection) - { - SetCacheAction(() => - { - //This option cannot execute if we are looking up specific Ids - if (ids.Any() == false && entityCollection.Length == 0 && _options.GetAllCacheAllowZeroCount) - { - //there was nothing returned but we want to cache a zero count result so add an TEntity[] to the cache - // to signify that there is a zero count cache - //NOTE: Don't set expiry/sliding for a zero count - Cache.InsertCacheItem(GetCacheTypeKey(), () => new TEntity[] {}); - } - else - { - //This is the default behavior, we'll individually cache each item so that if/when these items are resolved - // by id, they are returned from the already existing cache. - foreach (var entity in entityCollection.WhereNotNull()) - { - var localCopy = entity; - //just to be safe, we cannot cache an item without an identity - if (localCopy.HasIdentity) - { - Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => localCopy, - timeout: TimeSpan.FromMinutes(5), - isSliding: true); - } - } - } - }); - } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs deleted file mode 100644 index 5c02e41a48..0000000000 --- a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Umbraco.Core.Models.EntityBase; - -namespace Umbraco.Core.Cache -{ - /// - /// Creates cache policies - /// - /// - /// - internal class DefaultRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory - where TEntity : class, IAggregateRoot - { - private readonly IRuntimeCacheProvider _runtimeCache; - private readonly RepositoryCachePolicyOptions _options; - - public DefaultRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, RepositoryCachePolicyOptions options) - { - _runtimeCache = runtimeCache; - _options = options; - } - - public virtual IRepositoryCachePolicy CreatePolicy() - { - return new DefaultRepositoryCachePolicy(_runtimeCache, _options); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs index 9b37d1861f..41c0249877 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -3,227 +3,178 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Collections; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Cache { /// - /// A caching policy that caches an entire dataset as a single collection + /// Represents a caching policy that caches the entire entities set as a single collection. /// - /// - /// + /// The type of the entity. + /// The type of the identifier. + /// + /// Caches the entire set of entities as a single collection. + /// Used by Content-, Media- and MemberTypeRepository, DataTypeRepository, DomainRepository, + /// LanguageRepository, PublicAccessRepository, TemplateRepository... things that make sense to + /// keep as a whole in memory. + /// internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IAggregateRoot { - private readonly Func _getEntityId; - private readonly Func> _getAllFromRepo; + private readonly Func _entityGetId; private readonly bool _expires; - public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, Func getEntityId, Func> getAllFromRepo, bool expires) + public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, Func entityGetId, bool expires) : base(cache) { - _getEntityId = getEntityId; - _getAllFromRepo = getAllFromRepo; + _entityGetId = entityGetId; _expires = expires; } - private bool? _hasZeroCountCache; + public override IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope) + { + return new ScopedRepositoryCachePolicy(this, runtimeCache, scope); + } + protected static readonly TId[] EmptyIds = new TId[0]; // const - protected string GetCacheTypeKey() + protected string GetEntityTypeCacheKey() { return string.Format("uRepo_{0}_", typeof(TEntity).Name); } - public override void CreateOrUpdate(TEntity entity, Action persistMethod) + protected void InsertEntities(TEntity[] entities) { - if (entity == null) throw new ArgumentNullException("entity"); - if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + // cache is expected to be a deep-cloning cache ie it deep-clones whatever is + // IDeepCloneable when it goes in, and out. it also resets dirty properties, + // making sure that no 'dirty' entity is cached. + // + // this policy is caching the entire list of entities. to ensure that entities + // are properly deep-clones when cached, it uses a DeepCloneableList. however, + // we don't want to deep-clone *each* entity in the list when fetching it from + // cache as that would not be efficient for Get(id). so the DeepCloneableList is + // set to ListCloneBehavior.CloneOnce ie it will clone *once* when inserting, + // and then will *not* clone when retrieving. - try + if (_expires) { - persistMethod(entity); - - //set the disposal action - SetCacheAction(() => - { - //Clear all - Cache.ClearCacheItem(GetCacheTypeKey()); - }); + Cache.InsertCacheItem(GetEntityTypeCacheKey(), () => new DeepCloneableList(entities), TimeSpan.FromMinutes(5), true); } - catch + else { - //set the disposal action - SetCacheAction(() => - { - //Clear all - Cache.ClearCacheItem(GetCacheTypeKey()); - }); - throw; + Cache.InsertCacheItem(GetEntityTypeCacheKey(), () => new DeepCloneableList(entities)); } } - public override void Remove(TEntity entity, Action persistMethod) + /// + public override void Create(TEntity entity, Action persistNew) { if (entity == null) throw new ArgumentNullException("entity"); - if (persistMethod == null) throw new ArgumentNullException("persistMethod"); try { - persistMethod(entity); + persistNew(entity); } finally { - //set the disposal action - SetCacheAction(() => - { - //Clear all - Cache.ClearCacheItem(GetCacheTypeKey()); - }); + ClearAll(); } } - public override TEntity Get(TId id, Func getFromRepo) + /// + public override void Update(TEntity entity, Action persistUpdated) { - //Force get all with cache - var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); + if (entity == null) throw new ArgumentNullException("entity"); - //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()) + try { - return allEntities; + persistUpdated(entity); } - - //check the zero count cache - if (HasZeroCountCache()) + finally { - //there is a zero count cache so return an empty list - return new TEntity[] { }; + ClearAll(); } - - //we need to do the lookup from the repo - var entityCollection = getFromRepo(new TId[] { }) - //ensure we don't include any null refs in the returned collection! - .WhereNotNull() - .ToArray(); - - //set the disposal action - SetCacheAction(entityCollection); - - return entityCollection; } - /// - /// For this type of caching policy, we don't cache individual items - /// - /// - /// - protected void SetCacheAction(string cacheKey, TEntity entity) + /// + public override void Delete(TEntity entity, Action persistDeleted) { - //No-op - } + if (entity == null) throw new ArgumentNullException("entity"); - /// - /// Sets the action to execute on disposal for an entity collection - /// - /// - protected void SetCacheAction(TEntity[] entityCollection) - { - //set the disposal action - SetCacheAction(() => + try { - //We want to cache the result as a single collection - - if (_expires) - { - Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection), - timeout: TimeSpan.FromMinutes(5), - isSliding: true); - } - else - { - Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection)); - } - }); + persistDeleted(entity); + } + finally + { + ClearAll(); + } } - /// - /// Looks up the zero count cache, must return null if it doesn't exist - /// - /// - protected bool HasZeroCountCache() + /// + public override TEntity Get(TId id, Func performGet, Func> performGetAll) { - if (_hasZeroCountCache.HasValue) - return _hasZeroCountCache.Value; + // get all from the cache, then look for the entity + var all = GetAllCached(performGetAll); + var entity = all.FirstOrDefault(x => _entityGetId(x).Equals(id)); - _hasZeroCountCache = Cache.GetCacheItem>(GetCacheTypeKey()) != null; - return _hasZeroCountCache.Value; + // see note in InsertEntities - what we get here is the original + // cached entity, not a clone, so we need to manually ensure it is deep-cloned. + return entity == null ? null : (TEntity) entity.DeepClone(); } - /// - /// This policy will cache the full data set as a single collection - /// - /// - protected TEntity[] GetAllFromCache() + /// + public override TEntity GetCached(TId id) { - var found = Cache.GetCacheItem>(GetCacheTypeKey()); + // get all from the cache -- and only the cache, then look for the entity + var all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); + var entity = all == null ? null : all.FirstOrDefault(x => _entityGetId(x).Equals(id)); - //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(); + // see note in InsertEntities - what we get here is the original + // cached entity, not a clone, so we need to manually ensure it is deep-cloned. + return entity == null ? null : (TEntity)entity.DeepClone(); } + /// + public override bool Exists(TId id, Func performExits, Func> performGetAll) + { + // get all as one set, then look for the entity + var all = GetAllCached(performGetAll); + return all.Any(x => _entityGetId(x).Equals(id)); + } + + /// + public override TEntity[] GetAll(TId[] ids, Func> performGetAll) + { + // get all as one set, from cache if possible, else repo + var all = GetAllCached(performGetAll); + + // if ids have been specified, filter + if (ids.Length > 0) all = all.Where(x => ids.Contains(_entityGetId(x))); + + // and return + // see note in SetCacheActionToInsertEntities - what we get here is the original + // cached entities, not clones, so we need to manually ensure they are deep-cloned. + return all.Select(x => (TEntity) x.DeepClone()).ToArray(); + } + + // does NOT clone anything, so be nice with the returned values + private IEnumerable GetAllCached(Func> performGetAll) + { + // try the cache first + var all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); + if (all != null) return all.ToArray(); + + // else get from repo and cache + var entities = performGetAll(EmptyIds).WhereNotNull().ToArray(); + InsertEntities(entities); // may be an empty array... + return entities; + } + + /// + public override void ClearAll() + { + Cache.ClearCacheItem(GetEntityTypeCacheKey()); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs deleted file mode 100644 index e4addcf355..0000000000 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using Umbraco.Core.Models.EntityBase; - -namespace Umbraco.Core.Cache -{ - /// - /// Creates cache policies - /// - /// - /// - internal class FullDataSetRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory - where TEntity : class, IAggregateRoot - { - private readonly IRuntimeCacheProvider _runtimeCache; - private readonly Func _getEntityId; - private readonly Func> _getAllFromRepo; - private readonly bool _expires; - - public FullDataSetRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, Func getEntityId, Func> getAllFromRepo, bool expires) - { - _runtimeCache = runtimeCache; - _getEntityId = getEntityId; - _getAllFromRepo = getAllFromRepo; - _expires = expires; - } - - public virtual IRepositoryCachePolicy CreatePolicy() - { - return new FullDataSetRepositoryCachePolicy(_runtimeCache, _getEntityId, _getAllFromRepo, _expires); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs index e7f5d17b83..0e98cd5318 100644 --- a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs @@ -154,7 +154,7 @@ namespace Umbraco.Core.Cache value = result.Value; // will not throw (safe lazy) var eh = value as ExceptionHolder; - if (eh != null) throw eh.Exception; // throw once! + if (eh != null) throw new Exception("Exception while creating a value.", eh.Exception); // throw once! return value; } diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs index 215487c3be..eeb4f77de3 100644 --- a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs @@ -1,18 +1,97 @@ using System; using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Cache { - internal interface IRepositoryCachePolicy : IDisposable + internal interface IRepositoryCachePolicy where TEntity : class, IAggregateRoot { - TEntity Get(TId id, Func getFromRepo); - TEntity Get(TId id); - bool Exists(TId id, Func getFromRepo); - - void CreateOrUpdate(TEntity entity, Action persistMethod); - void Remove(TEntity entity, Action persistMethod); - TEntity[] GetAll(TId[] ids, Func> getFromRepo); + // note: + // at the moment each repository instance creates its corresponding cache policy instance + // we could reduce allocations by using static cache policy instances but then we would need + // to modify all methods here to pass the repository and cache eg: + // + // TEntity Get(TRepository repository, IRuntimeCacheProvider cache, TId id); + // + // it is not *that* complicated but then RepositoryBase needs to have a TRepository generic + // type parameter and it all becomes convoluted - keeping it simple for the time being. + + /// + /// Creates a scoped version of this cache policy. + /// + /// The global isolated runtime cache for this policy. + /// The scope. + /// When a policy is scoped, it means that it has been created with a scoped + /// isolated runtime cache, and now it needs to be wrapped into something that can apply + /// changes to the global isolated runtime cache. + IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope); + + /// + /// Gets an entity from the cache, else from the repository. + /// + /// The identifier. + /// The repository PerformGet method. + /// The repository PerformGetAll method. + /// The entity with the specified identifier, if it exits, else null. + /// First considers the cache then the repository. + TEntity Get(TId id, Func performGet, Func> performGetAll); + + /// + /// Gets an entity from the cache. + /// + /// The identifier. + /// The entity with the specified identifier, if it is in the cache already, else null. + /// Does not consider the repository at all. + TEntity GetCached(TId id); + + /// + /// Gets a value indicating whether an entity with a specified identifier exists. + /// + /// The identifier. + /// The repository PerformExists method. + /// The repository PerformGetAll method. + /// A value indicating whether an entity with the specified identifier exists. + /// First considers the cache then the repository. + bool Exists(TId id, Func performExists, Func> performGetAll); + + /// + /// Creates an entity. + /// + /// The entity. + /// The repository PersistNewItem method. + /// Creates the entity in the repository, and updates the cache accordingly. + void Create(TEntity entity, Action persistNew); + + /// + /// Updates an entity. + /// + /// The entity. + /// The reopsitory PersistUpdatedItem method. + /// Updates the entity in the repository, and updates the cache accordingly. + void Update(TEntity entity, Action persistUpdated); + + /// + /// Removes an entity. + /// + /// The entity. + /// The repository PersistDeletedItem method. + /// Removes the entity from the repository and clears the cache. + void Delete(TEntity entity, Action persistDeleted); + + /// + /// Gets entities. + /// + /// The identifiers. + /// The repository PerformGetAll method. + /// If is empty, all entities, else the entities with the specified identifiers. + /// Get all the entities. Either from the cache or the repository depending on the implementation. + TEntity[] GetAll(TId[] ids, Func> performGetAll); + + /// + /// Clears the entire cache. + /// + void ClearAll(); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs deleted file mode 100644 index 2d69704b63..0000000000 --- a/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Umbraco.Core.Models.EntityBase; - -namespace Umbraco.Core.Cache -{ - internal interface IRepositoryCachePolicyFactory where TEntity : class, IAggregateRoot - { - IRepositoryCachePolicy CreatePolicy(); - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/NoRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/NoRepositoryCachePolicy.cs new file mode 100644 index 0000000000..54891af48b --- /dev/null +++ b/src/Umbraco.Core/Cache/NoRepositoryCachePolicy.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Cache +{ + internal class NoRepositoryCachePolicy : IRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + private static readonly NoRepositoryCachePolicy StaticInstance = new NoRepositoryCachePolicy(); + + private NoRepositoryCachePolicy() + { } + + public static NoRepositoryCachePolicy Instance { get { return StaticInstance; } } + + public IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope) + { + throw new NotImplementedException(); + } + + public TEntity Get(TId id, Func performGet, Func> performGetAll) + { + return performGet(id); + } + + public TEntity GetCached(TId id) + { + return null; + } + + public bool Exists(TId id, Func performExists, Func> performGetAll) + { + return performExists(id); + } + + public void Create(TEntity entity, Action persistNew) + { + persistNew(entity); + } + + public void Update(TEntity entity, Action persistUpdated) + { + persistUpdated(entity); + } + + public void Delete(TEntity entity, Action persistDeleted) + { + persistDeleted(entity); + } + + public TEntity[] GetAll(TId[] ids, Func> performGetAll) + { + return performGetAll(ids).ToArray(); + } + + public void ClearAll() + { } + } +} diff --git a/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs deleted file mode 100644 index b24838bc3b..0000000000 --- a/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Umbraco.Core.Models.EntityBase; - -namespace Umbraco.Core.Cache -{ - /// - /// Creates cache policies - /// - /// - /// - internal class OnlySingleItemsRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory - where TEntity : class, IAggregateRoot - { - private readonly IRuntimeCacheProvider _runtimeCache; - private readonly RepositoryCachePolicyOptions _options; - - public OnlySingleItemsRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, RepositoryCachePolicyOptions options) - { - _runtimeCache = runtimeCache; - _options = options; - } - - public virtual IRepositoryCachePolicy CreatePolicy() - { - return new SingleItemsOnlyRepositoryCachePolicy(_runtimeCache, _options); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs index b939cd14e6..0b5d2b15c5 100644 --- a/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs @@ -1,48 +1,51 @@ using System; using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Cache { - internal abstract class RepositoryCachePolicyBase : DisposableObject, IRepositoryCachePolicy + /// + /// A base class for repository cache policies. + /// + /// The type of the entity. + /// The type of the identifier. + internal abstract class RepositoryCachePolicyBase : IRepositoryCachePolicy where TEntity : class, IAggregateRoot { - private Action _action; - protected RepositoryCachePolicyBase(IRuntimeCacheProvider cache) { - if (cache == null) throw new ArgumentNullException("cache"); - + if (cache == null) throw new ArgumentNullException("cache"); Cache = cache; } + public abstract IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope); + protected IRuntimeCacheProvider Cache { get; private set; } - /// - /// The disposal performs the caching - /// - protected override void DisposeResources() - { - if (_action != null) - { - _action(); - } - } + /// + public abstract TEntity Get(TId id, Func performGet, Func> performGetAll); - /// - /// Sets the action to execute on disposal - /// - /// - protected void SetCacheAction(Action action) - { - _action = action; - } + /// + public abstract TEntity GetCached(TId id); + + /// + public abstract bool Exists(TId id, Func performExists, Func> performGetAll); + + /// + public abstract void Create(TEntity entity, Action persistNew); + + /// + public abstract void Update(TEntity entity, Action persistUpdated); + + /// + public abstract void Delete(TEntity entity, Action persistDeleted); + + /// + public abstract TEntity[] GetAll(TId[] ids, Func> performGetAll); + + /// + public abstract void ClearAll(); - 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/RepositoryCachePolicyOptions.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs index e8c6ac02b0..14cef76db6 100644 --- a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs @@ -2,6 +2,9 @@ using System; namespace Umbraco.Core.Cache { + /// + /// Specifies how a repository cache policy should cache entities. + /// internal class RepositoryCachePolicyOptions { /// diff --git a/src/Umbraco.Core/Cache/ScopedRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/ScopedRepositoryCachePolicy.cs new file mode 100644 index 0000000000..f4244a49a1 --- /dev/null +++ b/src/Umbraco.Core/Cache/ScopedRepositoryCachePolicy.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Cache +{ + internal class ScopedRepositoryCachePolicy : IRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + private readonly IRepositoryCachePolicy _cachePolicy; + private readonly IRuntimeCacheProvider _globalIsolatedCache; + private readonly IScope _scope; + + public ScopedRepositoryCachePolicy(IRepositoryCachePolicy cachePolicy, IRuntimeCacheProvider globalIsolatedCache, IScope scope) + { + _cachePolicy = cachePolicy; + _globalIsolatedCache = globalIsolatedCache; + _scope = scope; + } + + public IRepositoryCachePolicy Scoped(IRuntimeCacheProvider runtimeCache, IScope scope) + { + throw new InvalidOperationException(); // obviously + } + + public TEntity Get(TId id, Func performGet, Func> performGetAll) + { + // loads into the local cache only, ok for now + return _cachePolicy.Get(id, performGet, performGetAll); + } + + public TEntity GetCached(TId id) + { + // loads into the local cache only, ok for now + return _cachePolicy.GetCached(id); + } + + public bool Exists(TId id, Func performExists, Func> performGetAll) + { + // loads into the local cache only, ok for now + return _cachePolicy.Exists(id, performExists, performGetAll); + } + + public void Create(TEntity entity, Action persistNew) + { + // writes into the local cache + _cachePolicy.Create(entity, persistNew); + } + + public void Update(TEntity entity, Action persistUpdated) + { + // writes into the local cache + _cachePolicy.Update(entity, persistUpdated); + } + + public void Delete(TEntity entity, Action persistDeleted) + { + // deletes the local cache + _cachePolicy.Delete(entity, persistDeleted); + } + + public TEntity[] GetAll(TId[] ids, Func> performGetAll) + { + // loads into the local cache only, ok for now + return _cachePolicy.GetAll(ids, performGetAll); + } + + public void ClearAll() + { + // clears the local cache + _cachePolicy.ClearAll(); + } + } +} diff --git a/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs index 28ac4ee2d1..7ba7d445fe 100644 --- a/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs @@ -1,24 +1,27 @@ -using System.Linq; -using Umbraco.Core.Collections; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache { /// - /// A caching policy that ignores all caches for GetAll - it will only cache calls for individual items + /// Represents a special policy that does not cache the result of GetAll. /// - /// - /// + /// The type of the entity. + /// The type of the identifier. + /// + /// Overrides the default repository cache policy and does not writes the result of GetAll + /// to cache, but only the result of individual Gets. It does read the cache for GetAll, though. + /// Used by DictionaryRepository. + /// internal class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy where TEntity : class, IAggregateRoot { - public SingleItemsOnlyRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) : base(cache, options) + public SingleItemsOnlyRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) + : base(cache, options) + { } + + protected override void InsertEntities(TId[] ids, TEntity[] entities) { - } - - protected override void SetCacheAction(TId[] ids, TEntity[] entityCollection) - { - //no-op + // nop } } } \ No newline at end of file diff --git a/src/Umbraco.Core/CacheHelper.cs b/src/Umbraco.Core/CacheHelper.cs index 0dc5f5b00f..dbf3f89714 100644 --- a/src/Umbraco.Core/CacheHelper.cs +++ b/src/Umbraco.Core/CacheHelper.cs @@ -21,6 +21,10 @@ namespace Umbraco.Core private static readonly ICacheProvider NullRequestCache = new NullCacheProvider(); private static readonly ICacheProvider NullStaticCache = new NullCacheProvider(); private static readonly IRuntimeCacheProvider NullRuntimeCache = new NullCacheProvider(); + private static readonly IsolatedRuntimeCache NullIsolatedCache = new IsolatedRuntimeCache(_ => NullRuntimeCache); + private static readonly CacheHelper NullCache = new CacheHelper(NullRuntimeCache, NullStaticCache, NullRequestCache, NullIsolatedCache); + + public static CacheHelper NoCache { get { return NullCache; } } /// /// Creates a cache helper with disabled caches @@ -31,7 +35,10 @@ namespace Umbraco.Core /// public static CacheHelper CreateDisabledCacheHelper() { - return new CacheHelper(NullRuntimeCache, NullStaticCache, NullRequestCache, new IsolatedRuntimeCache(t => NullRuntimeCache)); + // do *not* return NoCache + // NoCache is a special instance that is detected by RepositoryBase and disables all cache policies + // CreateDisabledCacheHelper is used in tests to use no cache, *but* keep all cache policies + return new CacheHelper(NullRuntimeCache, NullStaticCache, NullRequestCache, NullIsolatedCache); } /// diff --git a/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs b/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs new file mode 100644 index 0000000000..0bb9de6c86 --- /dev/null +++ b/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Umbraco.Core.CodeAnnotations +{ + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + internal class UmbracoUdiTypeAttribute : Attribute + { + public string UdiType { get; private set; } + + public UmbracoUdiTypeAttribute(string udiType) + { + UdiType = udiType; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/ContentXmlStorage.cs b/src/Umbraco.Core/Configuration/ContentXmlStorage.cs new file mode 100644 index 0000000000..7cbbc70675 --- /dev/null +++ b/src/Umbraco.Core/Configuration/ContentXmlStorage.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Core.Configuration +{ + internal enum ContentXmlStorage + { + Default, + AspNetTemp, + EnvironmentTemp + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/CoreDebug.cs b/src/Umbraco.Core/Configuration/CoreDebug.cs new file mode 100644 index 0000000000..74a28e7637 --- /dev/null +++ b/src/Umbraco.Core/Configuration/CoreDebug.cs @@ -0,0 +1,31 @@ +using System; + +namespace Umbraco.Core.Configuration +{ + internal static class CoreDebugExtensions + { + private static CoreDebug _coreDebug; + + public static CoreDebug CoreDebug(this UmbracoConfig config) + { + return _coreDebug ?? (_coreDebug = new CoreDebug()); + } + } + + internal class CoreDebug + { + public CoreDebug() + { + var appSettings = System.Configuration.ConfigurationManager.AppSettings; + LogUncompletedScopes = string.Equals("true", appSettings["Umbraco.CoreDebug.LogUncompletedScopes"], StringComparison.OrdinalIgnoreCase); + DumpOnTimeoutThreadAbort = string.Equals("true", appSettings["Umbraco.CoreDebug.DumpOnTimeoutThreadAbort"], StringComparison.OrdinalIgnoreCase); + } + + // when true, Scope logs the stack trace for any scope that gets disposed without being completed. + // this helps troubleshooting rogue scopes that we forget to complete + public bool LogUncompletedScopes { get; private set; } + // when true, the Logger creates a minidump of w3wp in ~/App_Data/MiniDump whenever it logs + // an error due to a ThreadAbortException that is due to a timeout. + public bool DumpOnTimeoutThreadAbort { get; private set; } + } +} diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 525bff2999..acbf0065c0 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -212,7 +212,7 @@ namespace Umbraco.Core.Configuration { get { - var settings = ConfigurationManager.ConnectionStrings[UmbracoConnectionName]; + var settings = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName]; var connectionString = string.Empty; if (settings != null) @@ -241,10 +241,7 @@ namespace Umbraco.Core.Configuration } } } - - //TODO: Move these to constants! - public const string UmbracoConnectionName = "umbracoDbDSN"; - public const string UmbracoMigrationName = "Umbraco"; + /// /// Gets or sets the configuration status. This will return the version number of the currently installed umbraco instance. @@ -520,12 +517,25 @@ namespace Umbraco.Core.Configuration } internal static bool ContentCacheXmlStoredInCodeGen + { + get { return ContentCacheXmlStorageLocation == ContentXmlStorage.AspNetTemp; } + } + + internal static ContentXmlStorage ContentCacheXmlStorageLocation { get { - //defaults to false - return ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLUseLocalTemp") - && bool.Parse(ConfigurationManager.AppSettings["umbracoContentXMLUseLocalTemp"]); //default to false + if (ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLStorage")) + { + return Enum.Parse(ConfigurationManager.AppSettings["umbracoContentXMLStorage"]); + } + if (ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLUseLocalTemp")) + { + return bool.Parse(ConfigurationManager.AppSettings["umbracoContentXMLUseLocalTemp"]) + ? ContentXmlStorage.AspNetTemp + : ContentXmlStorage.Default; + } + return ContentXmlStorage.Default; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index d743daca6a..51a39e15df 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -4,8 +4,7 @@ using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { - - internal class ContentElement : ConfigurationElement, IContentSection + internal class ContentElement : UmbracoConfigurationElement, IContentSection { [ConfigurationProperty("imaging")] internal ContentImagingElement Imaging @@ -22,25 +21,13 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("ResolveUrlsFromTextString")] internal InnerTextConfigurationElement ResolveUrlsFromTextString { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ResolveUrlsFromTextString"], - //set the default - false); - } + get { return GetOptionalTextElement("ResolveUrlsFromTextString", false); } } [ConfigurationProperty("UploadAllowDirectories")] internal InnerTextConfigurationElement UploadAllowDirectories { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["UploadAllowDirectories"], - //set the default - true); - } + get { return GetOptionalTextElement("UploadAllowDirectories", true); } } public IEnumerable Error404Collection @@ -63,121 +50,61 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("ensureUniqueNaming")] internal InnerTextConfigurationElement EnsureUniqueNaming { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ensureUniqueNaming"], - //set the default - true); - } + get { return GetOptionalTextElement("ensureUniqueNaming", true); } } [ConfigurationProperty("TidyEditorContent")] internal InnerTextConfigurationElement TidyEditorContent { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["TidyEditorContent"], - //set the default - false); - } + get { return GetOptionalTextElement("TidyEditorContent", false); } } [ConfigurationProperty("TidyCharEncoding")] internal InnerTextConfigurationElement TidyCharEncoding { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["TidyCharEncoding"], - //set the default - "UTF8"); - } + get { return GetOptionalTextElement("TidyCharEncoding", "UTF8"); } } [ConfigurationProperty("XmlCacheEnabled")] internal InnerTextConfigurationElement XmlCacheEnabled { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["XmlCacheEnabled"], - //set the default - true); - } + get { return GetOptionalTextElement("XmlCacheEnabled", true); } } [ConfigurationProperty("ContinouslyUpdateXmlDiskCache")] internal InnerTextConfigurationElement ContinouslyUpdateXmlDiskCache { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ContinouslyUpdateXmlDiskCache"], - //set the default - true); - } + get { return GetOptionalTextElement("ContinouslyUpdateXmlDiskCache", true); } } [ConfigurationProperty("XmlContentCheckForDiskChanges")] internal InnerTextConfigurationElement XmlContentCheckForDiskChanges { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["XmlContentCheckForDiskChanges"], - //set the default - false); - } + get { return GetOptionalTextElement("XmlContentCheckForDiskChanges", false); } } [ConfigurationProperty("EnableSplashWhileLoading")] internal InnerTextConfigurationElement EnableSplashWhileLoading { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["EnableSplashWhileLoading"], - //set the default - false); - } + get { return GetOptionalTextElement("EnableSplashWhileLoading", false); } } [ConfigurationProperty("PropertyContextHelpOption")] internal InnerTextConfigurationElement PropertyContextHelpOption { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["PropertyContextHelpOption"], - //set the default - "text"); - } + get { return GetOptionalTextElement("PropertyContextHelpOption", "text"); } } [ConfigurationProperty("UseLegacyXmlSchema")] internal InnerTextConfigurationElement UseLegacyXmlSchema { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["UseLegacyXmlSchema"], - //set the default - false); - } + get { return GetOptionalTextElement("UseLegacyXmlSchema", false); } } [ConfigurationProperty("ForceSafeAliases")] internal InnerTextConfigurationElement ForceSafeAliases { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ForceSafeAliases"], - //set the default - true); - } + get { return GetOptionalTextElement("ForceSafeAliases", true); } } [ConfigurationProperty("PreviewBadge")] @@ -185,123 +112,81 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["PreviewBadge"], - //set the default - @"In Preview Mode - click to end"); + return GetOptionalTextElement("PreviewBadge", @"In Preview Mode - click to end"); } } [ConfigurationProperty("UmbracoLibraryCacheDuration")] internal InnerTextConfigurationElement UmbracoLibraryCacheDuration { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["UmbracoLibraryCacheDuration"], - //set the default - 1800); - - } + get { return GetOptionalTextElement("UmbracoLibraryCacheDuration", 1800); } } [ConfigurationProperty("MacroErrors")] internal InnerTextConfigurationElement MacroErrors { - get - { - - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["MacroErrors"], - //set the default - MacroErrorBehaviour.Inline); - } + get { return GetOptionalTextElement("MacroErrors", MacroErrorBehaviour.Inline); } } [Obsolete("This is here so that if this config element exists we won't get a YSOD, it is not used whatsoever and will be removed in future versions")] [ConfigurationProperty("DocumentTypeIconList")] internal InnerTextConfigurationElement DocumentTypeIconList { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["DocumentTypeIconList"], - //set the default - IconPickerBehaviour.HideFileDuplicates); - } + get { return GetOptionalTextElement("DocumentTypeIconList", IconPickerBehaviour.HideFileDuplicates); } } [ConfigurationProperty("disallowedUploadFiles")] internal CommaDelimitedConfigurationElement DisallowedUploadFiles { - get - { - return new OptionalCommaDelimitedConfigurationElement( - (CommaDelimitedConfigurationElement)this["disallowedUploadFiles"], - //set the default - new[] { "ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd" }); - - } + get { return GetOptionalDelimitedElement("disallowedUploadFiles", new[] {"ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd"}); } } [ConfigurationProperty("cloneXmlContent")] internal InnerTextConfigurationElement CloneXmlContent { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["cloneXmlContent"], - //set the default - true); - } + get { return GetOptionalTextElement("cloneXmlContent", true); } } [ConfigurationProperty("GlobalPreviewStorageEnabled")] internal InnerTextConfigurationElement GlobalPreviewStorageEnabled { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["GlobalPreviewStorageEnabled"], - //set the default - false); - } + get { return GetOptionalTextElement("GlobalPreviewStorageEnabled", false); } } [ConfigurationProperty("defaultDocumentTypeProperty")] internal InnerTextConfigurationElement DefaultDocumentTypeProperty { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["defaultDocumentTypeProperty"], - //set the default - "Textstring"); - } + get { return GetOptionalTextElement("defaultDocumentTypeProperty", "Textstring"); } + } + + [ConfigurationProperty("showDeprecatedPropertyEditors")] + internal InnerTextConfigurationElement ShowDeprecatedPropertyEditors + { + get { return GetOptionalTextElement("showDeprecatedPropertyEditors", false); } } [ConfigurationProperty("EnableInheritedDocumentTypes")] internal InnerTextConfigurationElement EnableInheritedDocumentTypes { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement) this["EnableInheritedDocumentTypes"], - //set the default - true); - } + get { return GetOptionalTextElement("EnableInheritedDocumentTypes", true); } } [ConfigurationProperty("EnableInheritedMediaTypes")] internal InnerTextConfigurationElement EnableInheritedMediaTypes { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["EnableInheritedMediaTypes"], - //set the default - true); - } + get { return GetOptionalTextElement("EnableInheritedMediaTypes", true); } + } + + [ConfigurationProperty("EnablePropertyValueConverters")] + internal InnerTextConfigurationElement EnablePropertyValueConverters + { + get { return GetOptionalTextElement("EnablePropertyValueConverters", false); } + } + + [ConfigurationProperty("loginBackgroundImage")] + internal InnerTextConfigurationElement LoginBackgroundImage + { + get { return GetOptionalTextElement("loginBackgroundImage", string.Empty); } } string IContentSection.NotificationEmailAddress @@ -439,6 +324,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return DefaultDocumentTypeProperty; } } + bool IContentSection.ShowDeprecatedPropertyEditors + { + get { return ShowDeprecatedPropertyEditors; } + } + bool IContentSection.EnableInheritedDocumentTypes { get { return EnableInheritedDocumentTypes; } @@ -448,5 +338,14 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { return EnableInheritedMediaTypes; } } + bool IContentSection.EnablePropertyValueConverters + { + get { return EnablePropertyValueConverters; } + } + + string IContentSection.LoginBackgroundImage + { + get { return LoginBackgroundImage; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs index 0d14609caa..f60e964a04 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs @@ -3,42 +3,24 @@ using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class ContentScriptEditorElement : ConfigurationElement + internal class ContentScriptEditorElement : UmbracoConfigurationElement { [ConfigurationProperty("scriptFolderPath")] internal InnerTextConfigurationElement ScriptFolderPath { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["scriptFolderPath"], - //set the default - "/scripts"); - } + get { return GetOptionalTextElement("scriptFolderPath", "/scripts"); } } [ConfigurationProperty("scriptFileTypes")] internal OptionalCommaDelimitedConfigurationElement ScriptFileTypes { - get - { - return new OptionalCommaDelimitedConfigurationElement( - (OptionalCommaDelimitedConfigurationElement)this["scriptFileTypes"], - //set the default - new[] { "js", "xml" }); - } + get { return GetOptionalDelimitedElement("scriptFileTypes", new[] {"js", "xml"}); } } [ConfigurationProperty("scriptDisableEditor")] internal InnerTextConfigurationElement ScriptEditorDisable { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement) this["scriptDisableEditor"], - //set the default - false); - } + get { return GetOptionalTextElement("scriptDisableEditor", false); } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index 3d5e4435b6..d73b3b9e41 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -60,8 +60,18 @@ namespace Umbraco.Core.Configuration.UmbracoSettings string DefaultDocumentTypeProperty { get; } + /// + /// The default for this is false but if you would like deprecated property editors displayed + /// in the data type editor you can enable this + /// + bool ShowDeprecatedPropertyEditors { get; } + bool EnableInheritedDocumentTypes { get; } bool EnableInheritedMediaTypes { get; } + + bool EnablePropertyValueConverters { get; } + + string LoginBackgroundImage { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IRepository.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IRepository.cs index 052c23edd5..7559f090c0 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IRepository.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IRepository.cs @@ -8,6 +8,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings Guid Id { get; } string RepositoryUrl { get; } string WebServiceUrl { get; } - bool HasCustomWebServiceUrl { get; } + bool HasCustomWebServiceUrl { get; } + string RestApiUrl { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs index 0dfc4afc00..eafe43817d 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs @@ -2,7 +2,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class ImagingAutoFillUploadFieldElement : ConfigurationElement, IImagingAutoFillUploadField + internal class ImagingAutoFillUploadFieldElement : UmbracoConfigurationElement, IImagingAutoFillUploadField { /// /// Allow setting internally so we can create a default @@ -17,49 +17,25 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("widthFieldAlias")] internal InnerTextConfigurationElement WidthFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["widthFieldAlias"], - //set the default - "umbracoWidth"); - } + get { return GetOptionalTextElement("widthFieldAlias", "umbracoWidth"); } } [ConfigurationProperty("heightFieldAlias")] internal InnerTextConfigurationElement HeightFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["heightFieldAlias"], - //set the default - "umbracoHeight"); - } + get { return GetOptionalTextElement("heightFieldAlias", "umbracoHeight"); } } [ConfigurationProperty("lengthFieldAlias")] internal InnerTextConfigurationElement LengthFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["lengthFieldAlias"], - //set the default - "umbracoBytes"); - } + get { return GetOptionalTextElement("lengthFieldAlias", "umbracoBytes"); } } [ConfigurationProperty("extensionFieldAlias")] internal InnerTextConfigurationElement ExtensionFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["extensionFieldAlias"], - //set the default - "umbracoExtension"); - } + get { return GetOptionalTextElement("extensionFieldAlias", "umbracoExtension"); } } string IImagingAutoFillUploadField.Alias diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs index f95a9c7e76..39e6327b3a 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs @@ -3,67 +3,37 @@ using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class LoggingElement : ConfigurationElement, ILoggingSection + internal class LoggingElement : UmbracoConfigurationElement, ILoggingSection { [ConfigurationProperty("autoCleanLogs")] internal InnerTextConfigurationElement AutoCleanLogs { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["autoCleanLogs"], - //set the default - false); - } + get { return GetOptionalTextElement("autoCleanLogs", false); } } [ConfigurationProperty("enableLogging")] internal InnerTextConfigurationElement EnableLogging { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableLogging"], - //set the default - true); - } + get { return GetOptionalTextElement("enableLogging", true); } } [ConfigurationProperty("enableAsyncLogging")] internal InnerTextConfigurationElement EnableAsyncLogging { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableAsyncLogging"], - //set the default - true); - } + get { return GetOptionalTextElement("enableAsyncLogging", true); } } [ConfigurationProperty("cleaningMiliseconds")] internal InnerTextConfigurationElement CleaningMiliseconds { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["cleaningMiliseconds"], - //set the default - -1); - } + get { return GetOptionalTextElement("cleaningMiliseconds", -1); } } [ConfigurationProperty("maxLogAge")] internal InnerTextConfigurationElement MaxLogAge { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["maxLogAge"], - //set the default - -1); - } + get { return GetOptionalTextElement("maxLogAge", -1); } } [ConfigurationCollection(typeof(DisabledLogTypesCollection), AddItemName = "logTypeAlias")] diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs index 16eb943887..89e3f447ee 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs @@ -2,7 +2,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class NotificationsElement : ConfigurationElement + internal class NotificationsElement : UmbracoConfigurationElement { [ConfigurationProperty("email")] internal InnerTextConfigurationElement NotificationEmailAddress @@ -13,13 +13,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("disableHtmlEmail")] internal InnerTextConfigurationElement DisableHtmlEmail { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement) this["disableHtmlEmail"], - //set the default - false); - } + get { return GetOptionalTextElement("disableHtmlEmail", false); } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/RepositoryConfigExtensions.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/RepositoryConfigExtensions.cs new file mode 100644 index 0000000000..e2c4283dc6 --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/RepositoryConfigExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Linq; + +namespace Umbraco.Core.Configuration.UmbracoSettings +{ + public static class RepositoryConfigExtensions + { + //Our package repo + private static readonly Guid RepoGuid = new Guid("65194810-1f85-11dd-bd0b-0800200c9a66"); + + public static IRepository GetDefault(this IRepositoriesSection repos) + { + var found = repos.Repositories.FirstOrDefault(x => x.Id == RepoGuid); + if (found == null) + throw new InvalidOperationException("No default package repository found with id " + RepoGuid); + return found; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/RepositoryElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/RepositoryElement.cs index b7a1157c40..a249be2ee3 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/RepositoryElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/RepositoryElement.cs @@ -38,9 +38,16 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { var prop = Properties["webserviceurl"]; - var repoUrl = this[prop] as ConfigurationElement; - return (repoUrl != null && repoUrl.ElementInformation.IsPresent); + return (string) prop.DefaultValue != (string) this[prop]; } } + + [ConfigurationProperty("restapiurl", DefaultValue = "https://our.umbraco.org/webapi/packages/v1")] + public string RestApiUrl + { + get { return (string)base["restapiurl"]; } + set { base["restapiurl"] = value; } + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs index 9dc4a94824..779d33c8b8 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs @@ -5,30 +5,18 @@ using System.Collections.Generic; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class RequestHandlerElement : ConfigurationElement, IRequestHandlerSection + internal class RequestHandlerElement : UmbracoConfigurationElement, IRequestHandlerSection { [ConfigurationProperty("useDomainPrefixes")] public InnerTextConfigurationElement UseDomainPrefixes { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["useDomainPrefixes"], - //set the default - false); - } + get { return GetOptionalTextElement("useDomainPrefixes", false); } } [ConfigurationProperty("addTrailingSlash")] public InnerTextConfigurationElement AddTrailingSlash { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["addTrailingSlash"], - //set the default - true); - } + get { return GetOptionalTextElement("addTrailingSlash", true); } } private UrlReplacingElement _defaultUrlReplacing; diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs index f280b3e20c..ddb168ddbd 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs @@ -2,66 +2,36 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class SecurityElement : ConfigurationElement, ISecuritySection + internal class SecurityElement : UmbracoConfigurationElement, ISecuritySection { [ConfigurationProperty("keepUserLoggedIn")] internal InnerTextConfigurationElement KeepUserLoggedIn { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["keepUserLoggedIn"], - //set the default - true); - } + get { return GetOptionalTextElement("keepUserLoggedIn", true); } } [ConfigurationProperty("hideDisabledUsersInBackoffice")] internal InnerTextConfigurationElement HideDisabledUsersInBackoffice { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["hideDisabledUsersInBackoffice"], - //set the default - false); - } + get { return GetOptionalTextElement("hideDisabledUsersInBackoffice", false); } } [ConfigurationProperty("allowPasswordReset")] internal InnerTextConfigurationElement AllowPasswordReset { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["allowPasswordReset"], - //set the default - true); - } + get { return GetOptionalTextElement("allowPasswordReset", true); } } [ConfigurationProperty("authCookieName")] internal InnerTextConfigurationElement AuthCookieName { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["authCookieName"], - //set the default - Constants.Web.AuthCookieName); - } + get { return GetOptionalTextElement("authCookieName", Constants.Web.AuthCookieName); } } [ConfigurationProperty("authCookieDomain")] internal InnerTextConfigurationElement AuthCookieDomain { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["authCookieDomain"], - //set the default - null); - } + get { return GetOptionalTextElement("authCookieDomain", null); } } bool ISecuritySection.KeepUserLoggedIn diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs index c7eb659766..4e249df3a2 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs @@ -3,55 +3,31 @@ using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class TemplatesElement : ConfigurationElement, ITemplatesSection + internal class TemplatesElement : UmbracoConfigurationElement, ITemplatesSection { [ConfigurationProperty("useAspNetMasterPages")] internal InnerTextConfigurationElement UseAspNetMasterPages { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["useAspNetMasterPages"], - //set the default - true); - } + get { return GetOptionalTextElement("useAspNetMasterPages", true); } } [ConfigurationProperty("enableSkinSupport")] internal InnerTextConfigurationElement EnableSkinSupport { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableSkinSupport"], - //set the default - true); - } + get { return GetOptionalTextElement("enableSkinSupport", true); } } [ConfigurationProperty("defaultRenderingEngine", IsRequired = true)] internal InnerTextConfigurationElement DefaultRenderingEngine { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["defaultRenderingEngine"], - //set the default - RenderingEngine.Mvc); - } + get { return GetOptionalTextElement("defaultRenderingEngine", RenderingEngine.Mvc); } } [Obsolete("This has no affect and will be removed in future versions")] [ConfigurationProperty("enableTemplateFolders")] internal InnerTextConfigurationElement EnableTemplateFolders { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableTemplateFolders"], - //set the default - false); - } + get { return GetOptionalTextElement("enableTemplateFolders", false); } } bool ITemplatesSection.UseAspNetMasterPages diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoConfigurationElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoConfigurationElement.cs new file mode 100644 index 0000000000..063b5324d8 --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoConfigurationElement.cs @@ -0,0 +1,36 @@ +using System.Collections.Concurrent; +using System.Configuration; + +namespace Umbraco.Core.Configuration.UmbracoSettings +{ + /// + /// Base class with shared helper methods + /// + internal class UmbracoConfigurationElement : ConfigurationElement + { + /// + /// Used so the RawElement types are not re-created every time they are accessed + /// + private readonly ConcurrentDictionary _rawElements = new ConcurrentDictionary(); + + protected OptionalInnerTextConfigurationElement GetOptionalTextElement(string name, T defaultVal) + { + return (OptionalInnerTextConfigurationElement) _rawElements.GetOrAdd( + name, + s => new OptionalInnerTextConfigurationElement( + (InnerTextConfigurationElement) this[s], + //set the default + defaultVal)); + } + + protected OptionalCommaDelimitedConfigurationElement GetOptionalDelimitedElement(string name, string[] defaultVal) + { + return (OptionalCommaDelimitedConfigurationElement) _rawElements.GetOrAdd( + name, + s => new OptionalCommaDelimitedConfigurationElement( + (CommaDelimitedConfigurationElement) this[name], + //set the default + defaultVal)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 86c64872ff..952dd9a22a 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.5.7"); + private static readonly Version Version = new Version("7.6.0"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index fb1fb42044..fd9476a8ab 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -112,9 +112,15 @@ public const string Languages = "languages"; + public const string PartialViews = "partialViews"; + + public const string PartialViewMacros = "partialViewMacros"; + + public const string Scripts = "scripts"; + //TODO: Fill in the rest! } - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index d7f4576137..2e3d652d7e 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -4,7 +4,8 @@ using Umbraco.Core.Models; namespace Umbraco.Core { - public static partial class Constants + + public static partial class Constants { /// /// Defines the identifiers for property-type alias conventions that are used within the Umbraco core. diff --git a/src/Umbraco.Core/Constants-DatabaseProviders.cs b/src/Umbraco.Core/Constants-DatabaseProviders.cs new file mode 100644 index 0000000000..2a64ade081 --- /dev/null +++ b/src/Umbraco.Core/Constants-DatabaseProviders.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + public static class DatabaseProviders + { + public const string SqlCe = "System.Data.SqlServerCe.4.0"; + public const string SqlServer = "System.Data.SqlClient"; + public const string MySql = "MySql.Data.MySqlClient"; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-DeploySelector.cs b/src/Umbraco.Core/Constants-DeploySelector.cs new file mode 100644 index 0000000000..cd9c48a6f5 --- /dev/null +++ b/src/Umbraco.Core/Constants-DeploySelector.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + /// + /// Contains the valid selector values. + /// + public static class DeploySelector + { + public const string This = "this"; + public const string ThisAndChildren = "this-and-children"; + public const string ThisAndDescendants = "this-and-descendants"; + public const string ChildrenOfThis = "children"; + public const string DescendantsOfThis = "descendants"; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 3f9974166c..adc154174a 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -89,6 +89,11 @@ namespace Umbraco.Core /// public const string Media = "B796F64C-1F99-4FFB-B886-4BF4BC011A9C"; + /// + /// Guid for a Document object. + /// + public static readonly Guid MediaGuid = new Guid(Media); + /// /// Guid for the Media Recycle Bin. /// @@ -109,11 +114,21 @@ namespace Umbraco.Core /// public const string Member = "39EB0F98-B348-42A1-8662-E7EB18487560"; + /// + /// Guid for a Media Type object. + /// + public static readonly Guid MemberGuid = new Guid(Member); + /// /// Guid for a Member Group object. /// public const string MemberGroup = "366E63B9-880F-4E13-A61C-98069B029728"; + /// + /// Guid for a Member Group object. + /// + public static readonly Guid MemberGroupGuid = new Guid(MemberGroup); + /// /// Guid for a Member Type object. /// @@ -143,6 +158,11 @@ namespace Umbraco.Core /// public const string Template = "6FBDE604-4178-42CE-A10B-8A2600A2F07D"; + /// + /// Guid for a Template object. + /// + public static readonly Guid TemplateTypeGuid = new Guid(Template); + /// /// Guid for a Lock object. /// @@ -152,6 +172,46 @@ namespace Umbraco.Core /// Guid for a Lock object. /// public static readonly Guid LockObjectGuid = new Guid(LockObject); + + /// + /// Guid for a relation type. + /// + public const string RelationType = "B1988FAD-8675-4F47-915A-B3A602BC5D8D"; + + /// + /// Guid for a relation type. + /// + public static readonly Guid RelationTypeGuid = new Guid(RelationType); + + /// + /// Guid for a Forms Form. + /// + public const string FormsForm = "F5A9F787-6593-46F0-B8FF-BFD9BCA9F6BB"; + + /// + /// Guid for a Forms Form. + /// + public static readonly Guid FormsFormGuid = new Guid(FormsForm); + + /// + /// Guid for a Forms PreValue Source. + /// + public const string FormsPreValue = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; + + /// + /// Guid for a Forms PreValue Source. + /// + public static readonly Guid FormsPreValueGuid = new Guid(FormsPreValue); + + /// + /// Guid for a Forms DataSource. + /// + public const string FormsDataSource = "CFED6CE4-9359-443E-9977-9956FEB1D867"; + + /// + /// Guid for a Forms DataSource. + /// + public static readonly Guid FormsDataSourceGuid = new Guid(FormsDataSource); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 80f118b58e..eba5cef23d 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -42,10 +42,14 @@ namespace Umbraco.Core [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string ContentPicker = "158AA029-24ED-4948-939E-C3DA209E5FBA"; + + [Obsolete("This is an obsoleted content picker, use ContentPicker2Alias instead")] + public const string ContentPickerAlias = "Umbraco.ContentPickerAlias"; + /// /// Alias for the Content Picker datatype. /// - public const string ContentPickerAlias = "Umbraco.ContentPickerAlias"; + public const string ContentPicker2Alias = "Umbraco.ContentPicker2"; /// /// Guid for the Date datatype. @@ -192,11 +196,15 @@ namespace Umbraco.Core [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string MediaPicker = "EAD69342-F06D-4253-83AC-28000225583B"; + [Obsolete("This is an obsoleted picker, use MediaPicker2Alias instead")] + public const string MediaPickerAlias = "Umbraco.MediaPicker"; + /// /// Alias for the Media Picker datatype. /// - public const string MediaPickerAlias = "Umbraco.MediaPicker"; + public const string MediaPicker2Alias = "Umbraco.MediaPicker2"; + [Obsolete("This is an obsoleted picker, use MediaPicker2Alias instead")] public const string MultipleMediaPickerAlias = "Umbraco.MultipleMediaPicker"; /// @@ -205,26 +213,32 @@ namespace Umbraco.Core [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string MemberPicker = "39F533E4-0551-4505-A64B-E0425C5CE775"; + [Obsolete("This is an obsoleted picker, use MemberPicker2Alias instead")] + public const string MemberPickerAlias = "Umbraco.MemberPicker"; + /// /// Alias for the Member Picker datatype. /// - public const string MemberPickerAlias = "Umbraco.MemberPicker"; + public const string MemberPicker2Alias = "Umbraco.MemberPicker2"; /// /// Alias for the Member Group Picker datatype. /// public const string MemberGroupPickerAlias = "Umbraco.MemberGroupPicker"; - + /// /// Guid for the Multi-Node Tree Picker datatype /// [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string MultiNodeTreePicker = "7E062C13-7C41-4AD9-B389-41D88AEEF87C"; + [Obsolete("This is an obsoleted picker, use MultiNodeTreePicker2Alias instead")] + public const string MultiNodeTreePickerAlias = "Umbraco.MultiNodeTreePicker"; + /// /// Alias for the Multi-Node Tree Picker datatype /// - public const string MultiNodeTreePickerAlias = "Umbraco.MultiNodeTreePicker"; + public const string MultiNodeTreePicker2Alias = "Umbraco.MultiNodeTreePicker2"; /// /// Guid for the Multiple Textstring datatype. @@ -275,11 +289,14 @@ namespace Umbraco.Core /// [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string RelatedLinks = "71B8AD1A-8DC2-425C-B6B8-FAA158075E63"; + + [Obsolete("This is an obsoleted picker, use RelatedLinks2Alias instead")] + public const string RelatedLinksAlias = "Umbraco.RelatedLinks"; /// - /// Alias for the Related Links datatype. + /// Alias for the Related Links property editor. /// - public const string RelatedLinksAlias = "Umbraco.RelatedLinks"; + public const string RelatedLinks2Alias = "Umbraco.RelatedLinks2"; /// /// Guid for the Slider datatype. diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs new file mode 100644 index 0000000000..bd2e1c5acf --- /dev/null +++ b/src/Umbraco.Core/Constants-Security.cs @@ -0,0 +1,33 @@ +using System; +using System.ComponentModel; + +namespace Umbraco.Core +{ + public static partial class Constants + { + public static class Security + { + + public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; + public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; + public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; + public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; + public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; + + /// + /// The prefix used for external identity providers for their authentication type + /// + /// + /// By default we don't want to interfere with front-end external providers and their default setup, for back office the + /// providers need to be setup differently and each auth type for the back office will be prefixed with this value + /// + public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; + + public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; + public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; + public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; + public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index 4a30db9cd8..b38417c39b 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -26,15 +26,9 @@ public const int DefaultMediaListViewDataTypeId = -96; public const int DefaultMembersListViewDataTypeId = -97; - // identifiers for lock objects - public const int ServersLock = -331; - } - - public static class DatabaseProviders - { - public const string SqlCe = "System.Data.SqlServerCe.4.0"; - public const string SqlServer = "System.Data.SqlClient"; - public const string MySql = "MySql.Data.MySqlClient"; + public const string UmbracoConnectionName = "umbracoDbDSN"; + public const string UmbracoMigrationName = "Umbraco"; } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index f596820506..67b5698610 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -29,30 +29,6 @@ namespace Umbraco.Core public const string AuthCookieName = "UMB_UCONTEXT"; } - - public static class Security - { - - public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; - public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; - public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; - public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; - public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; - - /// - /// The prefix used for external identity providers for their authentication type - /// - /// - /// By default we don't want to interfere with front-end external providers and their default setup, for back office the - /// providers need to be setup differently and each auth type for the back office will be prefixed with this value - /// - public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; - - public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; - public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; - public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; - public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; - - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 4ef08bd02f..dcce15f419 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -28,6 +28,7 @@ using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Publishing; using Umbraco.Core.Macros; using Umbraco.Core.Manifest; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Sync; using Umbraco.Core.Strings; @@ -105,11 +106,14 @@ namespace Umbraco.Core LegacyParameterEditorAliasConverter.CreateMappingsForCoreEditors(); //create database and service contexts for the app context - var dbFactory = new DefaultDatabaseFactory(GlobalSettings.UmbracoConnectionName, ProfilingLogger.Logger); + var dbFactory = new DefaultDatabaseFactory(Constants.System.UmbracoConnectionName, ProfilingLogger.Logger); Database.Mapper = new PetaPocoMapper(); + var scopeProvider = new ScopeProvider(dbFactory); + dbFactory.ScopeProvider = scopeProvider; + var dbContext = new DatabaseContext( - dbFactory, + scopeProvider, ProfilingLogger.Logger, SqlSyntaxProviders.CreateDefault(ProfilingLogger.Logger)); @@ -117,7 +121,7 @@ namespace Umbraco.Core dbContext.Initialize(); //get the service context - var serviceContext = CreateServiceContext(dbContext, dbFactory); + var serviceContext = CreateServiceContext(dbContext, scopeProvider); //set property and singleton from response ApplicationContext.Current = ApplicationContext = CreateApplicationContext(dbContext, serviceContext); @@ -160,17 +164,15 @@ namespace Umbraco.Core /// Creates and returns the service context for the app /// /// - /// + /// /// - protected virtual ServiceContext CreateServiceContext(DatabaseContext dbContext, IDatabaseFactory dbFactory) + protected virtual ServiceContext CreateServiceContext(DatabaseContext dbContext, IScopeProvider scopeProvider) { //default transient factory var msgFactory = new TransientMessagesFactory(); return new ServiceContext( new RepositoryFactory(ApplicationCache, ProfilingLogger.Logger, dbContext.SqlSyntax, UmbracoConfig.For.UmbracoSettings()), - new PetaPocoUnitOfWorkProvider(dbFactory), - new FileUnitOfWorkProvider(), - new PublishingStrategy(msgFactory, ProfilingLogger.Logger), + new PetaPocoUnitOfWorkProvider(scopeProvider), ApplicationCache, ProfilingLogger.Logger, msgFactory); @@ -415,7 +417,7 @@ namespace Umbraco.Core if (currentTry == 5) { - throw new UmbracoStartupFailedException("Umbraco cannot start. A connection string is configured but the Umbraco cannot connect to the database."); + throw new UmbracoStartupFailedException("Umbraco cannot start. A connection string is configured but Umbraco cannot connect to the database."); } } diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index a6d6ca9e6e..1409016da6 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -4,7 +4,6 @@ using System.Data.SqlServerCe; using System.IO; using System.Linq; using System.Web; -using System.Web.Configuration; using System.Xml.Linq; using Semver; using Umbraco.Core.Configuration; @@ -14,6 +13,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Migrations; using Umbraco.Core.Persistence.Migrations.Initial; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; namespace Umbraco.Core @@ -26,31 +26,42 @@ namespace Umbraco.Core /// public class DatabaseContext { - private readonly IDatabaseFactory _factory; + internal readonly IScopeProviderInternal ScopeProvider; private readonly ILogger _logger; private readonly SqlSyntaxProviders _syntaxProviders; private bool _configured; - private readonly object _locker = new object(); private string _connectionString; private string _providerName; private DatabaseSchemaResult _result; - private DateTime? _connectionLastChecked = null; + private DateTime? _connectionLastChecked; /// /// The number of minutes to throttle the checks to CanConnect /// private const int ConnectionCheckMinutes = 1; + #region Compatibility with 7.5 + + // note: the ctors accepting IDatabaseFactory are here only for backward compatibility purpose + // + // problem: IDatabaseFactory2 adds the CreateNewDatabase() method which creates a new database + // 'cos IDatabaseFactory CreateDatabase() is supposed to also manage the ambient thing. We + // want to keep these ctors for backward compatibility reasons (in case ppl use them in tests) + // so we need to create a scope provider (else nothing would work) and so we need a IDatabaseFactory2, + // so...? + // solution: wrap IDatabaseFactory and pretend we have a IDatabaseFactory2, it *should* work in most + // cases but really, it depends on what ppl are doing in their tests... yet, cannot really see any + // other way to do it? + [Obsolete("Use the constructor specifying all dependencies instead")] public DatabaseContext(IDatabaseFactory factory) : this(factory, LoggerResolver.Current.Logger, new SqlSyntaxProviders(new ISqlSyntaxProvider[] { new MySqlSyntaxProvider(LoggerResolver.Current.Logger), - new SqlCeSyntaxProvider(), + new SqlCeSyntaxProvider(), new SqlServerSyntaxProvider() })) - { - } + { } /// /// Default constructor @@ -64,7 +75,11 @@ namespace Umbraco.Core if (logger == null) throw new ArgumentNullException("logger"); if (syntaxProviders == null) throw new ArgumentNullException("syntaxProviders"); - _factory = factory; + var asDbFactory2 = factory as IDatabaseFactory2; + ScopeProvider = asDbFactory2 == null + ? new ScopeProvider(new DatabaseFactoryWrapper(factory)) + : new ScopeProvider(asDbFactory2); + _logger = logger; _syntaxProviders = syntaxProviders; } @@ -81,7 +96,83 @@ namespace Umbraco.Core _providerName = providerName; SqlSyntax = sqlSyntax; SqlSyntaxContext.SqlSyntaxProvider = SqlSyntax; - _factory = factory; + + var asDbFactory2 = factory as IDatabaseFactory2; + ScopeProvider = asDbFactory2 == null + ? new ScopeProvider(new DatabaseFactoryWrapper(factory)) + : new ScopeProvider(asDbFactory2); + + _logger = logger; + _configured = true; + } + + private class DatabaseFactoryWrapper : IDatabaseFactory2 + { + private readonly IDatabaseFactory _factory; + + public DatabaseFactoryWrapper(IDatabaseFactory factory) + { + _factory = factory; + } + + public UmbracoDatabase CreateDatabase() + { + return _factory.CreateDatabase(); + } + + public UmbracoDatabase CreateNewDatabase() + { + return CreateDatabase(); + } + + public void Dispose() + { + _factory.Dispose(); + } + } + + #endregion + + [Obsolete("Use the constructor specifying all dependencies instead")] + internal DatabaseContext(IScopeProviderInternal scopeProvider) + : this(scopeProvider, LoggerResolver.Current.Logger, new SqlSyntaxProviders(new ISqlSyntaxProvider[] + { + new MySqlSyntaxProvider(LoggerResolver.Current.Logger), + new SqlCeSyntaxProvider(), + new SqlServerSyntaxProvider() + })) + { } + + /// + /// Default constructor + /// + /// + /// + /// + internal DatabaseContext(IScopeProviderInternal scopeProvider, ILogger logger, SqlSyntaxProviders syntaxProviders) + { + if (scopeProvider == null) throw new ArgumentNullException("scopeProvider"); + if (logger == null) throw new ArgumentNullException("logger"); + if (syntaxProviders == null) throw new ArgumentNullException("syntaxProviders"); + + ScopeProvider = scopeProvider; + _logger = logger; + _syntaxProviders = syntaxProviders; + } + + /// + /// Create a configured DatabaseContext + /// + /// + /// + /// + /// + internal DatabaseContext(IScopeProviderInternal scopeProvider, ILogger logger, ISqlSyntaxProvider sqlSyntax, string providerName) + { + _providerName = providerName; + SqlSyntax = sqlSyntax; + SqlSyntaxContext.SqlSyntaxProvider = SqlSyntax; + ScopeProvider = scopeProvider; _logger = logger; _configured = true; } @@ -89,16 +180,27 @@ namespace Umbraco.Core public ISqlSyntaxProvider SqlSyntax { get; private set; } /// - /// Gets the object for doing CRUD operations - /// against custom tables that resides in the Umbraco database. + /// Gets an "ambient" database for doing CRUD operations against custom tables that resides in the Umbraco database. /// /// - /// This should not be used for CRUD operations or queries against the - /// standard Umbraco tables! Use the Public services for that. + /// Should not be used for operation against standard Umbraco tables; as services should be used instead. + /// Gets or creates an "ambient" database that is either stored in http context + available for the whole + /// request + auto-disposed at the end of the request, or stored in call context if there is no http context - in which + /// case it *must* be explicitely disposed (which will remove it from call context). /// public virtual UmbracoDatabase Database { - get { return _factory.CreateDatabase(); } + get + { + if (IsDatabaseConfigured == false) + { + throw new InvalidOperationException("Cannot create a database instance, there is no available connection string"); + } + + return ScopeProvider.GetAmbientOrNoScope().Database; + //var scope = ScopeProvider.AmbientScope; + //return scope != null ? scope.Database : ScopeProvider.CreateNoScope().Database; + } } /// @@ -121,7 +223,7 @@ namespace Umbraco.Core //Don't check again if the timeout period hasn't elapsed //this ensures we don't keep checking the connection too many times in a row like during startup. - //Do check if the _connectionLastChecked is null which means we're just initializing or it could + //Do check if the _connectionLastChecked is null which means we're just initializing or it could //not connect last time it was checked. if ((_connectionLastChecked.HasValue && (DateTime.Now - _connectionLastChecked.Value).TotalMinutes > ConnectionCheckMinutes) || _connectionLastChecked.HasValue == false) @@ -157,14 +259,14 @@ namespace Umbraco.Core return _providerName; _providerName = Constants.DatabaseProviders.SqlServer; - if (ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName] != null) + if (ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName] != null) { - if (string.IsNullOrEmpty(ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName].ProviderName) == false) - _providerName = ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName].ProviderName; + if (string.IsNullOrEmpty(ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName].ProviderName) == false) + _providerName = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName].ProviderName; } else { - throw new InvalidOperationException("Can't find a connection string with the name '" + GlobalSettings.UmbracoConnectionName + "'"); + throw new NullReferenceException("Can't find a connection string with the name '" + Constants.System.UmbracoConnectionName + "'"); } return _providerName; } @@ -203,15 +305,10 @@ namespace Umbraco.Core var path = Path.Combine(GlobalSettings.FullpathToRoot, "App_Data", "Umbraco.sdf"); if (File.Exists(path) == false) { - var engine = new SqlCeEngine(connectionString); - engine.CreateDatabase(); - - // SD: Pretty sure this should be in a using clause but i don't want to cause unknown side-effects here - // since it's been like this for quite some time - //using (var engine = new SqlCeEngine(connectionString)) - //{ - // engine.CreateDatabase(); - //} + using (var engine = new SqlCeEngine(connectionString)) + { + engine.CreateDatabase(); + } } Initialize(providerName); @@ -251,14 +348,14 @@ namespace Umbraco.Core /// Database Password /// Type of the provider to be used (Sql, Sql Azure, Sql Ce, MySql) public void ConfigureDatabaseConnection(string server, string databaseName, string user, string password, string databaseProvider) - { + { string providerName; var connectionString = GetDatabaseConnectionString(server, databaseName, user, password, databaseProvider, out providerName); SaveConnectionString(connectionString, providerName); Initialize(providerName); } - + public string GetDatabaseConnectionString(string server, string databaseName, string user, string password, string databaseProvider, out string providerName) { providerName = Constants.DatabaseProviders.SqlServer; @@ -289,18 +386,18 @@ namespace Umbraco.Core public string GetIntegratedSecurityDatabaseConnectionString(string server, string databaseName) { - return String.Format("Server={0};Database={1};Integrated Security=true", server, databaseName); + return String.Format("Server={0};Database={1};Integrated Security=true", server, databaseName); } internal string BuildAzureConnectionString(string server, string databaseName, string user, string password) { if (server.Contains(".") && ServerStartsWithTcp(server) == false) server = string.Format("tcp:{0}", server); - + if (server.Contains(".") == false && ServerStartsWithTcp(server)) { - string serverName = server.Contains(",") - ? server.Substring(0, server.IndexOf(",", StringComparison.Ordinal)) + string serverName = server.Contains(",") + ? server.Substring(0, server.IndexOf(",", StringComparison.Ordinal)) : server; var portAddition = string.Empty; @@ -310,10 +407,10 @@ namespace Umbraco.Core server = string.Format("{0}.database.windows.net{1}", serverName, portAddition); } - + if (ServerStartsWithTcp(server) == false) server = string.Format("tcp:{0}.database.windows.net", server); - + if (server.Contains(",") == false) server = string.Format("{0},1433", server); @@ -350,23 +447,23 @@ namespace Umbraco.Core { //Set the connection string for the new datalayer var connectionStringSettings = string.IsNullOrEmpty(providerName) - ? new ConnectionStringSettings(GlobalSettings.UmbracoConnectionName, + ? new ConnectionStringSettings(Constants.System.UmbracoConnectionName, connectionString) - : new ConnectionStringSettings(GlobalSettings.UmbracoConnectionName, + : new ConnectionStringSettings(Constants.System.UmbracoConnectionName, connectionString, providerName); _connectionString = connectionString; _providerName = providerName; - + var fileName = IOHelper.MapPath(string.Format("{0}/web.config", SystemDirectories.Root)); var xml = XDocument.Load(fileName, LoadOptions.PreserveWhitespace); var connectionstrings = xml.Root.DescendantsAndSelf("connectionStrings").Single(); // Update connectionString if it exists, or else create a new appSetting for the given key and value - var setting = connectionstrings.Descendants("add").FirstOrDefault(s => s.Attribute("name").Value == GlobalSettings.UmbracoConnectionName); + var setting = connectionstrings.Descendants("add").FirstOrDefault(s => s.Attribute("name").Value == Constants.System.UmbracoConnectionName); if (setting == null) connectionstrings.Add(new XElement("add", - new XAttribute("name", GlobalSettings.UmbracoConnectionName), + new XAttribute("name", Constants.System.UmbracoConnectionName), new XAttribute("connectionString", connectionStringSettings), new XAttribute("providerName", providerName))); else @@ -391,23 +488,23 @@ namespace Umbraco.Core /// internal void Initialize() { - var databaseSettings = ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName]; + var databaseSettings = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName]; if (databaseSettings != null && string.IsNullOrWhiteSpace(databaseSettings.ConnectionString) == false && string.IsNullOrWhiteSpace(databaseSettings.ProviderName) == false) { var providerName = Constants.DatabaseProviders.SqlServer; string connString = null; - if (!string.IsNullOrEmpty(ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName].ProviderName)) + if (!string.IsNullOrEmpty(ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName].ProviderName)) { - providerName = ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName].ProviderName; - connString = ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName].ConnectionString; + providerName = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName].ProviderName; + connString = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName].ConnectionString; } Initialize(providerName, connString); - + } - else if (ConfigurationManager.AppSettings.ContainsKey(GlobalSettings.UmbracoConnectionName) && string.IsNullOrEmpty(ConfigurationManager.AppSettings[GlobalSettings.UmbracoConnectionName]) == false) + else if (ConfigurationManager.AppSettings.ContainsKey(Constants.System.UmbracoConnectionName) && string.IsNullOrEmpty(ConfigurationManager.AppSettings[Constants.System.UmbracoConnectionName]) == false) { //A valid connectionstring does not exist, but the legacy appSettings key was found, so we'll reconfigure the conn.string. - var legacyConnString = ConfigurationManager.AppSettings[GlobalSettings.UmbracoConnectionName]; + var legacyConnString = ConfigurationManager.AppSettings[Constants.System.UmbracoConnectionName]; if (legacyConnString.ToLowerInvariant().Contains("sqlce4umbraco")) { ConfigureEmbeddedDatabaseConnection(); @@ -438,8 +535,8 @@ namespace Umbraco.Core } //Remove the legacy connection string, so we don't end up in a loop if something goes wrong. - GlobalSettings.RemoveSetting(GlobalSettings.UmbracoConnectionName); - + GlobalSettings.RemoveSetting(Constants.System.UmbracoConnectionName); + } else { @@ -458,15 +555,15 @@ namespace Umbraco.Core { if (_syntaxProviders != null) { - SqlSyntax = _syntaxProviders.GetByProviderNameOrDefault(providerName); + SqlSyntax = _syntaxProviders.GetByProviderNameOrDefault(providerName); } else if (SqlSyntax == null) { throw new InvalidOperationException("No " + typeof(ISqlSyntaxProvider) + " specified or no " + typeof(SqlSyntaxProviders) + " instance specified"); } - + SqlSyntaxContext.SqlSyntaxProvider = SqlSyntax; - + _configured = true; } catch (Exception e) @@ -506,7 +603,7 @@ namespace Umbraco.Core } internal Result CreateDatabaseSchemaAndData(ApplicationContext applicationContext) - { + { try { var readyForInstall = CheckReadyForInstall(); @@ -524,7 +621,7 @@ namespace Umbraco.Core // If MySQL, we're going to ensure that database calls are maintaining proper casing as to remove the necessity for checks // for case insensitive queries. In an ideal situation (which is what we're striving for), all calls would be case sensitive. - /* + /* var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database); if (supportsCaseInsensitiveQueries == false) { @@ -542,9 +639,9 @@ namespace Umbraco.Core message = GetResultMessageForMySql(); var schemaResult = ValidateDatabaseSchema(); - + var installedSchemaVersion = schemaResult.DetermineInstalledVersion(); - + //If Configuration Status is empty and the determined version is "empty" its a new install - otherwise upgrade the existing if (string.IsNullOrEmpty(GlobalSettings.ConfigurationStatus) && installedSchemaVersion.Equals(new Version(0, 0, 0))) { @@ -563,9 +660,9 @@ namespace Umbraco.Core message = "

Upgrading database, this may take some time...

"; return new Result { - RequiresUpgrade = true, - Message = message, - Success = true, + RequiresUpgrade = true, + Message = message, + Success = true, Percentage = "30" }; } @@ -593,7 +690,7 @@ namespace Umbraco.Core _logger.Info("Database upgrade started"); var database = new UmbracoDatabase(_connectionString, ProviderName, _logger); - //var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database); + //var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database); var message = GetResultMessageForMySql(); @@ -601,32 +698,32 @@ namespace Umbraco.Core var installedSchemaVersion = new SemVersion(schemaResult.DetermineInstalledVersion()); - var installedMigrationVersion = schemaResult.DetermineInstalledVersionByMigrations(migrationEntryService); - + var installedMigrationVersion = schemaResult.DetermineInstalledVersionByMigrations(migrationEntryService); + var targetVersion = UmbracoVersion.Current; - + //In some cases - like upgrading from 7.2.6 -> 7.3, there will be no migration information in the database and therefore it will - // return a version of 0.0.0 and we don't necessarily want to run all migrations from 0 -> 7.3, so we'll just ensure that the + // return a version of 0.0.0 and we don't necessarily want to run all migrations from 0 -> 7.3, so we'll just ensure that the // migrations are run for the target version if (installedMigrationVersion == new SemVersion(new Version(0, 0, 0)) && installedSchemaVersion > new SemVersion(new Version(0, 0, 0))) { //set the installedMigrationVersion to be one less than the target so the latest migrations are guaranteed to execute installedMigrationVersion = new SemVersion(targetVersion.SubtractRevision()); } - + //Figure out what our current installed version is. If the web.config doesn't have a version listed, then we'll use the minimum - // version detected between the schema installed and the migrations listed in the migration table. + // version detected between the schema installed and the migrations listed in the migration table. // If there is a version in the web.config, we'll take the minimum between the listed migration in the db and what // is declared in the web.config. - + var currentInstalledVersion = string.IsNullOrEmpty(GlobalSettings.ConfigurationStatus) //Take the minimum version between the detected schema version and the installed migration version ? new[] {installedSchemaVersion, installedMigrationVersion}.Min() //Take the minimum version between the installed migration version and the version specified in the config : new[] { SemVersion.Parse(GlobalSettings.ConfigurationStatus), installedMigrationVersion }.Min(); - //Ok, another edge case here. If the current version is a pre-release, - // then we want to ensure all migrations for the current release are executed. + //Ok, another edge case here. If the current version is a pre-release, + // then we want to ensure all migrations for the current release are executed. if (currentInstalledVersion.Prerelease.IsNullOrWhiteSpace() == false) { currentInstalledVersion = new SemVersion(currentInstalledVersion.GetVersion().SubtractRevision()); @@ -634,7 +731,7 @@ namespace Umbraco.Core //DO the upgrade! - var runner = new MigrationRunner(migrationEntryService, _logger, currentInstalledVersion, UmbracoVersion.GetSemanticVersion(), GlobalSettings.UmbracoMigrationName); + var runner = new MigrationRunner(migrationEntryService, _logger, currentInstalledVersion, UmbracoVersion.GetSemanticVersion(), Constants.System.UmbracoMigrationName); var upgraded = runner.Execute(database, true); @@ -761,7 +858,7 @@ namespace Umbraco.Core { var datasource = dataSourcePart.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory").ToString()); var filePath = datasource.Replace("Data Source=", string.Empty); - sqlCeDatabaseExists = File.Exists(filePath); + sqlCeDatabaseExists = File.Exists(filePath); } } @@ -775,5 +872,30 @@ namespace Umbraco.Core return true; } + + /* + private class UsingDatabase : IDisposable + { + private readonly UmbracoDatabase _orig; + private readonly UmbracoDatabase _temp; + + public UsingDatabase(UmbracoDatabase orig, UmbracoDatabase temp) + { + _orig = orig; + _temp = temp; + } + + public void Dispose() + { + if (_temp != null) + { + _temp.Dispose(); + if (_orig != null) + DefaultDatabaseFactory.AttachAmbientDatabase(_orig); + } + GC.SuppressFinalize(this); + } + } + */ } } \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactBase.cs b/src/Umbraco.Core/Deploy/ArtifactBase.cs new file mode 100644 index 0000000000..a3fd160a49 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactBase.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Umbraco.Core.Deploy +{ + /// + /// Provides a base class to all artifacts. + /// + public abstract class ArtifactBase : IArtifact + where TUdi : Udi + { + protected ArtifactBase(TUdi udi, IEnumerable dependencies = null) + { + if (udi == null) + throw new ArgumentNullException("udi"); + Udi = udi; + Name = Udi.ToString(); + + Dependencies = dependencies ?? Enumerable.Empty(); + _checksum = new Lazy(GetChecksum); + } + + private readonly Lazy _checksum; + private IEnumerable _dependencies; + + protected abstract string GetChecksum(); + + #region Abstract implementation of IArtifactSignature + + Udi IArtifactSignature.Udi + { + get { return Udi; } + } + + public TUdi Udi { get; set; } + + [JsonIgnore] + public string Checksum + { + get { return _checksum.Value; } + } + + public IEnumerable Dependencies + { + get { return _dependencies; } + set { _dependencies = value.OrderBy(x => x.Udi); } + } + + #endregion + + public string Name { get; set; } + public string Alias { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDependency.cs b/src/Umbraco.Core/Deploy/ArtifactDependency.cs new file mode 100644 index 0000000000..41e349d636 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDependency.cs @@ -0,0 +1,40 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represents an artifact dependency. + /// + /// + /// Dependencies have an order property which indicates whether it must be respected when ordering artifacts. + /// Dependencies have a mode which can be Match or Exist depending on whether the checksum should match. + /// + public class ArtifactDependency + { + /// + /// Initializes a new instance of the ArtifactDependency class with an entity identifier and a mode. + /// + /// The entity identifier of the artifact that is a dependency. + /// A value indicating whether the dependency is ordering. + /// The dependency mode. + public ArtifactDependency(Udi udi, bool ordering, ArtifactDependencyMode mode) + { + Udi = udi; + Ordering = ordering; + Mode = mode; + } + + /// + /// Gets the entity id of the artifact that is a dependency. + /// + public Udi Udi { get; private set; } + + /// + /// Gets a value indicating whether the dependency is ordering. + /// + public bool Ordering { get; private set; } + + /// + /// Gets the dependency mode. + /// + public ArtifactDependencyMode Mode { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs new file mode 100644 index 0000000000..fd036f4628 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents a collection of distinct . + /// + /// The collection cannot contain duplicates and modes are properly managed. + public class ArtifactDependencyCollection : ICollection + { + private readonly Dictionary _dependencies + = new Dictionary(); + + public IEnumerator GetEnumerator() + { + return _dependencies.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(ArtifactDependency item) + { + if (_dependencies.ContainsKey(item.Udi)) + { + var exist = _dependencies[item.Udi]; + if (item.Mode == ArtifactDependencyMode.Exist || item.Mode == exist.Mode) + return; + } + + _dependencies[item.Udi] = item; + } + + public void Clear() + { + _dependencies.Clear(); + } + + public bool Contains(ArtifactDependency item) + { + return _dependencies.ContainsKey(item.Udi) && + (_dependencies[item.Udi].Mode == item.Mode || _dependencies[item.Udi].Mode == ArtifactDependencyMode.Match); + } + + public void CopyTo(ArtifactDependency[] array, int arrayIndex) + { + _dependencies.Values.CopyTo(array, arrayIndex); + } + + public bool Remove(ArtifactDependency item) + { + throw new NotSupportedException(); + } + + public int Count + { + get { return _dependencies.Count; } + } + + public bool IsReadOnly + { + get { return false; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs new file mode 100644 index 0000000000..7ee5c2f220 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Indicates the mode of the dependency. + /// + public enum ArtifactDependencyMode + { + /// + /// The dependency must match exactly. + /// + Match, + + /// + /// The dependency must exist. + /// + Exist + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDeployState.cs b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs new file mode 100644 index 0000000000..3723e483cb --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs @@ -0,0 +1,50 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represent the state of an artifact being deployed. + /// + public abstract class ArtifactDeployState + { + /// + /// Creates a new instance of the class from an artifact and an entity. + /// + /// The type of the artifact. + /// The type of the entity. + /// The artifact. + /// The entity. + /// The service connector deploying the artifact. + /// The next pass number. + /// A deploying artifact. + public static ArtifactDeployState Create(TArtifact art, TEntity entity, IServiceConnector connector, int nextPass) + where TArtifact : IArtifact + { + return new ArtifactDeployState(art, entity, connector, nextPass); + } + + /// + /// Gets the artifact. + /// + public IArtifact Artifact + { + get { return GetArtifactAsIArtifact(); } + } + + /// + /// Gets the artifact as an . + /// + /// The artifact, as an . + /// This is because classes that inherit from this class cannot override the Artifact property + /// with a property that specializes the return type, and so they need to 'new' the property. + protected abstract IArtifact GetArtifactAsIArtifact(); + + /// + /// Gets or sets the service connector in charge of deploying the artifact. + /// + public IServiceConnector Connector { get; set; } + + /// + /// Gets or sets the next pass number. + /// + public int NextPass { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs b/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs new file mode 100644 index 0000000000..b4d2be23cd --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs @@ -0,0 +1,48 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represent the state of an artifact being deployed. + /// + /// The type of the artifact. + /// The type of the entity. + public class ArtifactDeployState : ArtifactDeployState + where TArtifact : IArtifact + { + /// + /// Initializes a new instance of the class. + /// + public ArtifactDeployState() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The artifact. + /// The entity. + /// The service connector deploying the artifact. + /// The next pass number. + public ArtifactDeployState(TArtifact art, TEntity entity, IServiceConnector connector, int nextPass) + { + Artifact = art; + Entity = entity; + Connector = connector; + NextPass = nextPass; + } + + /// + /// Gets or sets the artifact. + /// + public new TArtifact Artifact { get; set; } + + /// + /// Gets or sets the entity. + /// + public TEntity Entity { get; set; } + + /// + protected sealed override IArtifact GetArtifactAsIArtifact() + { + return Artifact; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactSignature.cs b/src/Umbraco.Core/Deploy/ArtifactSignature.cs new file mode 100644 index 0000000000..4bea6a5ce8 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactSignature.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Deploy +{ + public sealed class ArtifactSignature : IArtifactSignature + { + public ArtifactSignature(Udi udi, string checksum, IEnumerable dependencies = null) + { + Udi = udi; + Checksum = checksum; + Dependencies = dependencies ?? Enumerable.Empty(); + } + + public Udi Udi { get; private set; } + + public string Checksum { get; private set; } + + public IEnumerable Dependencies { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/Difference.cs b/src/Umbraco.Core/Deploy/Difference.cs new file mode 100644 index 0000000000..90329afdd6 --- /dev/null +++ b/src/Umbraco.Core/Deploy/Difference.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Core.Deploy +{ + public class Difference + { + public Difference(string title, string text = null, string category = null) + { + Title = title; + Text = text; + Category = category; + } + + public string Title { get; set; } + public string Text { get; set; } + public string Category { get; set; } + + public override string ToString() + { + var s = Title; + if (!string.IsNullOrWhiteSpace(Category)) s += string.Format("[{0}]", Category); + if (!string.IsNullOrWhiteSpace(Text)) + { + if (s.Length > 0) s += ":"; + s += Text; + } + return s; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/Direction.cs b/src/Umbraco.Core/Deploy/Direction.cs new file mode 100644 index 0000000000..b0e1c1dc0a --- /dev/null +++ b/src/Umbraco.Core/Deploy/Direction.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Deploy +{ + public enum Direction + { + ToArtifact, + FromArtifact + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IArtifact.cs b/src/Umbraco.Core/Deploy/IArtifact.cs new file mode 100644 index 0000000000..653e6386bb --- /dev/null +++ b/src/Umbraco.Core/Deploy/IArtifact.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represents an artifact ie an object that can be transfered between environments. + /// + public interface IArtifact : IArtifactSignature + { + string Name { get; } + string Alias { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IArtifactSignature.cs b/src/Umbraco.Core/Deploy/IArtifactSignature.cs new file mode 100644 index 0000000000..83b112586b --- /dev/null +++ b/src/Umbraco.Core/Deploy/IArtifactSignature.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents the signature of an artifact. + /// + public interface IArtifactSignature + { + /// + /// Gets the entity unique identifier of this artifact. + /// + /// + /// The project identifier is independent from the state of the artifact, its data + /// values, dependencies, anything. It never changes and fully identifies the artifact. + /// What an entity uses as a unique identifier will influence what we can transfer + /// between environments. Eg content type "Foo" on one environment is not necessarily the + /// same as "Foo" on another environment, if guids are used as unique identifiers. What is + /// used should be documented for each entity, along with the consequences of the choice. + /// + Udi Udi { get; } + + /// + /// Gets the checksum of this artifact. + /// + /// + /// The checksum depends on the artifact's properties, and on the identifiers of all its dependencies, + /// but not on their checksums. So the checksum changes when any of the artifact's properties changes, + /// or when the list of dependencies changes. But not if one of these dependencies change. + /// It is assumed that checksum collisions cannot happen ie that no two different artifact's + /// states will ever produce the same checksum, so that if two artifacts have the same checksum then + /// they are identical. + /// + string Checksum { get; } + + /// + /// Gets the dependencies of this artifact. + /// + IEnumerable Dependencies { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IDeployContext.cs b/src/Umbraco.Core/Deploy/IDeployContext.cs new file mode 100644 index 0000000000..7d4066e015 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IDeployContext.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents a deployment context. + /// + public interface IDeployContext + { + /// + /// Gets the unique identifier of the deployment. + /// + Guid SessionId { get; } + + /// + /// Gets the file source. + /// + /// The file source is used to obtain files from the source environment. + IFileSource FileSource { get; } + + /// + /// Gets the next number in a numerical sequence. + /// + /// The next sequence number. + /// Can be used to uniquely number things during a deployment. + int NextSeq(); + + /// + /// Gets items. + /// + IDictionary Items { get; } + + /// + /// Gets item. + /// + /// The type of the item. + /// The key of the item. + /// The item with the specified key and type, if any, else null. + T Item(string key) where T : class; + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IFileSource.cs b/src/Umbraco.Core/Deploy/IFileSource.cs new file mode 100644 index 0000000000..7d13e0fbde --- /dev/null +++ b/src/Umbraco.Core/Deploy/IFileSource.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents a file source, ie a mean for a target environment involved in a + /// deployment to obtain the content of files being deployed. + /// + public interface IFileSource + { + /// + /// Gets the content of a file as a stream. + /// + /// A file entity identifier. + /// A stream with read access to the file content. + /// + /// Returns null if no content could be read. + /// The caller should ensure that the stream is properly closed/disposed. + /// + Stream GetFileStream(StringUdi udi); + + /// + /// Gets the content of a file as a stream. + /// + /// A file entity identifier. + /// A cancellation token. + /// A stream with read access to the file content. + /// + /// Returns null if no content could be read. + /// The caller should ensure that the stream is properly closed/disposed. + /// + Task GetFileStreamAsync(StringUdi udi, CancellationToken token); + + /// + /// Gets the content of a file as a string. + /// + /// A file entity identifier. + /// A string containing the file content. + /// Returns null if no content could be read. + string GetFileContent(StringUdi udi); + + /// + /// Gets the content of a file as a string. + /// + /// A file entity identifier. + /// A cancellation token. + /// A string containing the file content. + /// Returns null if no content could be read. + Task GetFileContentAsync(StringUdi udi, CancellationToken token); + + /// + /// Gets the length of a file. + /// + /// A file entity identifier. + /// The length of the file, or -1 if the file does not exist. + long GetFileLength(StringUdi udi); + + /// + /// Gets the length of a file. + /// + /// A file entity identifier. + /// A cancellation token. + /// The length of the file, or -1 if the file does not exist. + Task GetFileLengthAsync(StringUdi udi, CancellationToken token); + + /// + /// Gets files and store them using a file store. + /// + /// The udis of the files to get. + /// A collection of file types which can store the files. + void GetFiles(IEnumerable udis, IFileTypeCollection fileTypes); + + /// + /// Gets files and store them using a file store. + /// + /// The udis of the files to get. + /// A collection of file types which can store the files. + /// A cancellation token. + Task GetFilesAsync(IEnumerable udis, IFileTypeCollection fileTypes, CancellationToken token); + + ///// + ///// Gets the content of a file as a bytes array. + ///// + ///// A file entity identifier. + ///// A byte array containing the file content. + //byte[] GetFileBytes(StringUdi Udi); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IFileType.cs b/src/Umbraco.Core/Deploy/IFileType.cs new file mode 100644 index 0000000000..f7ab22ffae --- /dev/null +++ b/src/Umbraco.Core/Deploy/IFileType.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Core.Deploy +{ + public interface IFileType + { + Stream GetStream(StringUdi udi); + + Task GetStreamAsync(StringUdi udi, CancellationToken token); + + Stream GetChecksumStream(StringUdi udi); + + long GetLength(StringUdi udi); + + void SetStream(StringUdi udi, Stream stream); + + Task SetStreamAsync(StringUdi udi, Stream stream, CancellationToken token); + + bool CanSetPhysical { get; } + + void Set(StringUdi udi, string physicalPath, bool copy = false); + + // this is not pretty as *everywhere* in Deploy we take care of ignoring + // the physical path and always rely on Core's virtual IFileSystem but + // Cloud wants to add some of these files to Git and needs the path... + string GetPhysicalPath(StringUdi udi); + + string GetVirtualPath(StringUdi udi); + } +} diff --git a/src/Umbraco.Core/Deploy/IFileTypeCollection.cs b/src/Umbraco.Core/Deploy/IFileTypeCollection.cs new file mode 100644 index 0000000000..9e2ab137d1 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IFileTypeCollection.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Core.Deploy +{ + public interface IFileTypeCollection + { + IFileType this[string entityType] { get; } + + bool Contains(string entityType); + } +} diff --git a/src/Umbraco.Core/Deploy/IGridCellValueConnector.cs b/src/Umbraco.Core/Deploy/IGridCellValueConnector.cs new file mode 100644 index 0000000000..a4c5e7aeab --- /dev/null +++ b/src/Umbraco.Core/Deploy/IGridCellValueConnector.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Deploy +{ + /// + /// Defines methods that can convert a grid cell value to / from an environment-agnostic string. + /// + /// Grid cell values may contain values such as content identifiers, that would be local + /// to one environment, and need to be converted in order to be deployed. + public interface IGridCellValueConnector + { + /// + /// Gets a value indicating whether the connector supports a specified grid editor view. + /// + /// The grid editor view. It needs to be the view instead of the alias as the view is really what identifies what kind of connector should be used. Alias can be anything and you can have multiple different aliases using the same kind of view. + /// A value indicating whether the connector supports the grid editor view. + /// Note that can be string.Empty to indicate the "default" connector. + bool IsConnector(string view); + + /// + /// Gets the value to be deployed from the control value as a string. + /// + /// The control containing the value. + /// The property where the control is located. Do not modify - only used for context + /// The dependencies of the property. + /// The grid cell value to be deployed. + /// Note that + string GetValue(GridValue.GridControl gridControl, Property property, ICollection dependencies); + + /// + /// Allows you to modify the value of a control being deployed. + /// + /// The control being deployed. + /// The property where the is located. Do not modify - only used for context. + /// Follows the pattern of the property value connectors (). The SetValue method is used to modify the value of the . + /// Note that only the value should be modified - not the . + /// The should only be used to assist with context data relevant when setting the value. + void SetValue(GridValue.GridControl gridControl, Property property); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IImageSourceParser.cs b/src/Umbraco.Core/Deploy/IImageSourceParser.cs new file mode 100644 index 0000000000..d8e8d860ac --- /dev/null +++ b/src/Umbraco.Core/Deploy/IImageSourceParser.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Provides methods to parse image tag sources in property values. + /// + public interface IImageSourceParser + { + /// + /// Parses an Umbraco property value and produces an artifact property value. + /// + /// The property value. + /// A list of dependencies. + /// The parsed value. + /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies. + string ToArtifact(string value, ICollection dependencies); + + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// The artifact property value. + /// The parsed value. + /// Turns umb://media/... into /media/.... + string FromArtifact(string value); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ILocalLinkParser.cs b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs new file mode 100644 index 0000000000..c5906c2060 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Provides methods to parse local link tags in property values. + /// + public interface ILocalLinkParser + { + /// + /// Parses an Umbraco property value and produces an artifact property value. + /// + /// The property value. + /// A list of dependencies. + /// The parsed value. + /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the dependencies. + string ToArtifact(string value, ICollection dependencies); + + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// The artifact property value. + /// The parsed value. + /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}. + string FromArtifact(string value); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IMacroParser.cs b/src/Umbraco.Core/Deploy/IMacroParser.cs new file mode 100644 index 0000000000..9cde8ef8b6 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IMacroParser.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + public interface IMacroParser + { + /// + /// Parses an Umbraco property value and produces an artifact property value. + /// + /// Property value. + /// A list of dependencies. + /// Parsed value. + string ToArtifact(string value, ICollection dependencies); + + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// Artifact property value. + /// Parsed value. + string FromArtifact(string value); + + /// + /// Tries to replace the value of the attribute/parameter with a value containing a converted identifier. + /// + /// Value to attempt to convert + /// Alias of the editor used for the parameter + /// Collection to add dependencies to when performing ToArtifact + /// Indicates which action is being performed (to or from artifact) + /// Value with converted identifiers + string ReplaceAttributeValue(string value, string editorAlias, ICollection dependencies, Direction direction); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IPreValueConnector.cs b/src/Umbraco.Core/Deploy/IPreValueConnector.cs new file mode 100644 index 0000000000..4ef898cc74 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IPreValueConnector.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Defines methods that can convert a preValue to / from an environment-agnostic string. + /// + /// PreValues may contain values such as content identifiers, that would be local + /// to one environment, and need to be converted in order to be deployed. + public interface IPreValueConnector + { + /// + /// Gets the property editor aliases that the value converter supports by default. + /// + IEnumerable PropertyEditorAliases { get; } + + /// + /// Gets the environment-agnostic preValues corresponding to environment-specific preValues. + /// + /// The environment-specific preValues. + /// The dependencies. + /// + IDictionary ConvertToDeploy(IDictionary preValues, ICollection dependencies); + + /// + /// Gets the environment-specific preValues corresponding to environment-agnostic preValues. + /// + /// The environment-agnostic preValues. + /// + IDictionary ConvertToLocalEnvironment(IDictionary preValues); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IServiceConnector.cs b/src/Umbraco.Core/Deploy/IServiceConnector.cs new file mode 100644 index 0000000000..65553b991d --- /dev/null +++ b/src/Umbraco.Core/Deploy/IServiceConnector.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using umbraco.interfaces; +using Umbraco.Core; + +namespace Umbraco.Core.Deploy +{ + /// + /// Connects to an Umbraco service. + /// + public interface IServiceConnector : IDiscoverable + { + /// + /// Gets an artifact. + /// + /// The entity identifier of the artifact. + /// The corresponding artifact, or null. + IArtifact GetArtifact(Udi udi); + + /// + /// Gets an artifact. + /// + /// The entity. + /// The corresponding artifact. + IArtifact GetArtifact(object entity); + + /// + /// Initializes processing for an artifact. + /// + /// The artifact. + /// The deploy context. + /// The mapped artifact. + ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context); + + /// + /// Processes an artifact. + /// + /// The mapped artifact. + /// The deploy context. + /// The processing pass number. + void Process(ArtifactDeployState dart, IDeployContext context, int pass); + + /// + /// Explodes a range into udis. + /// + /// The range. + /// The list of udis where to add the new udis. + /// Also, it's cool to have a method named Explode. Kaboom! + void Explode(UdiRange range, List udis); + + /// + /// Gets a named range for a specified udi and selector. + /// + /// The udi. + /// The selector. + /// The named range for the specified udi and selector. + NamedUdiRange GetRange(Udi udi, string selector); + + /// + /// Gets a named range for specified entity type, identifier and selector. + /// + /// The entity type. + /// The identifier. + /// The selector. + /// The named range for the specified entity type, identifier and selector. + /// + /// This is temporary. At least we thought it would be, in sept. 2016. What day is it now? + /// At the moment our UI has a hard time returning proper udis, mainly because Core's tree do + /// not manage guids but only ints... so we have to provide a way to support it. The string id here + /// can be either a real string (for string udis) or an "integer as a string", using the value "-1" to + /// indicate the "root" i.e. an open udi. + /// + NamedUdiRange GetRange(string entityType, string sid, string selector); + + /// + /// Compares two artifacts. + /// + /// The first artifact. + /// The second artifact. + /// A collection of differences to append to, if not null. + /// A boolean value indicating whether the artifacts are identical. + /// ServiceConnectorBase{TArtifact} provides a very basic default implementation. + bool Compare(IArtifact art1, IArtifact art2, ICollection differences = null); + } + +} diff --git a/src/Umbraco.Core/Deploy/IValueConnector.cs b/src/Umbraco.Core/Deploy/IValueConnector.cs new file mode 100644 index 0000000000..a93e5a05a4 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IValueConnector.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Deploy +{ + /// + /// Defines methods that can convert a property value to / from an environment-agnostic string. + /// + /// Property values may contain values such as content identifiers, that would be local + /// to one environment, and need to be converted in order to be deployed. Connectors also deal + /// with serializing to / from string. + public interface IValueConnector + { + /// + /// Gets the property editor aliases that the value converter supports by default. + /// + IEnumerable PropertyEditorAliases { get; } + + /// + /// Gets the deploy property corresponding to a content property. + /// + /// The content property. + /// The content dependencies. + /// The deploy property value. + string GetValue(Property property, ICollection dependencies); + + /// + /// Sets a content property value using a deploy property. + /// + /// The content item. + /// The property alias. + /// The deploy property value. + void SetValue(IContentBase content, string alias, string value); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Diagnostics/MiniDump.cs b/src/Umbraco.Core/Diagnostics/MiniDump.cs new file mode 100644 index 0000000000..e8c2e82f94 --- /dev/null +++ b/src/Umbraco.Core/Diagnostics/MiniDump.cs @@ -0,0 +1,135 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Diagnostics +{ + // taken from https://blogs.msdn.microsoft.com/dondu/2010/10/24/writing-minidumps-in-c/ + // and https://blogs.msdn.microsoft.com/dondu/2010/10/31/writing-minidumps-from-exceptions-in-c/ + // which itself got it from http://blog.kalmbach-software.de/2008/12/13/writing-minidumps-in-c/ + + internal static class MiniDump + { + private static readonly object LockO = new object(); + + [Flags] + public enum Option : uint + { + // From dbghelp.h: + Normal = 0x00000000, + WithDataSegs = 0x00000001, + WithFullMemory = 0x00000002, + WithHandleData = 0x00000004, + FilterMemory = 0x00000008, + ScanMemory = 0x00000010, + WithUnloadedModules = 0x00000020, + WithIndirectlyReferencedMemory = 0x00000040, + FilterModulePaths = 0x00000080, + WithProcessThreadData = 0x00000100, + WithPrivateReadWriteMemory = 0x00000200, + WithoutOptionalData = 0x00000400, + WithFullMemoryInfo = 0x00000800, + WithThreadInfo = 0x00001000, + WithCodeSegs = 0x00002000, + WithoutAuxiliaryState = 0x00004000, + WithFullAuxiliaryState = 0x00008000, + WithPrivateWriteCopyMemory = 0x00010000, + IgnoreInaccessibleMemory = 0x00020000, + ValidTypeFlags = 0x0003ffff, + } + + //typedef struct _MINIDUMP_EXCEPTION_INFORMATION { + // DWORD ThreadId; + // PEXCEPTION_POINTERS ExceptionPointers; + // BOOL ClientPointers; + //} MINIDUMP_EXCEPTION_INFORMATION, *PMINIDUMP_EXCEPTION_INFORMATION; + [StructLayout(LayoutKind.Sequential, Pack = 4)] // Pack=4 is important! So it works also for x64! + public struct MiniDumpExceptionInformation + { + public uint ThreadId; + public IntPtr ExceptionPointers; + [MarshalAs(UnmanagedType.Bool)] + public bool ClientPointers; + } + + //BOOL + //WINAPI + //MiniDumpWriteDump( + // __in HANDLE hProcess, + // __in DWORD ProcessId, + // __in HANDLE hFile, + // __in MINIDUMP_TYPE DumpType, + // __in_opt PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + // __in_opt PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, + // __in_opt PMINIDUMP_CALLBACK_INFORMATION CallbackParam + // ); + + // Overload requiring MiniDumpExceptionInformation + [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, ref MiniDumpExceptionInformation expParam, IntPtr userStreamParam, IntPtr callbackParam); + + // Overload supporting MiniDumpExceptionInformation == NULL + [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam); + + [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)] + private static extern uint GetCurrentThreadId(); + + private static bool Write(SafeHandle fileHandle, Option options, bool withException = false) + { + var currentProcess = Process.GetCurrentProcess(); + var currentProcessHandle = currentProcess.Handle; + var currentProcessId = (uint)currentProcess.Id; + + MiniDumpExceptionInformation exp; + + exp.ThreadId = GetCurrentThreadId(); + exp.ClientPointers = false; + exp.ExceptionPointers = IntPtr.Zero; + + if (withException) + exp.ExceptionPointers = Marshal.GetExceptionPointers(); + + var bRet = exp.ExceptionPointers == IntPtr.Zero + ? MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint) options, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero) + : MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint) options, ref exp, IntPtr.Zero, IntPtr.Zero); + + return bRet; + } + + public static bool Dump(Option options = Option.WithFullMemory, bool withException = false) + { + lock (LockO) + { + // work around "stack trace is not available while minidump debugging", + // by making sure a local var (that we can inspect) contains the stack trace. + // getting the call stack before it is unwound would require a special exception + // filter everywhere in our code = not! + var stacktrace = withException ? Environment.StackTrace : string.Empty; + + var filepath = IOHelper.MapPath("~/App_Data/MiniDump"); + if (Directory.Exists(filepath) == false) + Directory.CreateDirectory(filepath); + + var filename = Path.Combine(filepath, string.Format("{0:yyyyMMddTHHmmss}.{1}.dmp", DateTime.UtcNow, Guid.NewGuid().ToString("N").Substring(0, 4))); + using (var stream = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.Write)) + { + return Write(stream.SafeFileHandle, options, withException); + } + } + } + + public static bool OkToDump() + { + lock (LockO) + { + var filepath = IOHelper.MapPath("~/App_Data/MiniDump"); + if (Directory.Exists(filepath) == false) return true; + var count = Directory.GetFiles(filepath, "*.dmp").Length; + return count < 8; + } + } + } +} diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index 58d4d453b7..e8565f3bc7 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; @@ -67,22 +68,15 @@ namespace Umbraco.Core } } - /// The for each. - /// The items. - /// The func. - /// item type - /// Result type - /// the Results + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use a normal foreach loop instead, this adds more allocations than necessary")] public static TResult[] ForEach(this IEnumerable items, Func func) { return items.Select(func).ToArray(); } - /// The for each. - /// The items. - /// The action. - /// Item type - /// list of TItem + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use a normal foreach loop instead, this adds more allocations than necessary")] public static IEnumerable ForEach(this IEnumerable items, Action action) { if (items != null) @@ -101,6 +95,7 @@ namespace Umbraco.Core /// The select child. /// Item type /// list of TItem + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Do not use, use SelectRecursive instead which has far less potential of re-iterating an iterator which may cause significantly more SQL queries")] public static IEnumerable FlattenList(this IEnumerable e, Func> f) { @@ -300,6 +295,5 @@ namespace Umbraco.Core return list1Groups.Count == list2Groups.Count && list1Groups.All(g => g.Count() == list2Groups[g.Key].Count()); } - } } diff --git a/src/Umbraco.Core/Events/CancellableEventArgs.cs b/src/Umbraco.Core/Events/CancellableEventArgs.cs index 72cef19f7a..a102ea66ef 100644 --- a/src/Umbraco.Core/Events/CancellableEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableEventArgs.cs @@ -10,8 +10,8 @@ namespace Umbraco.Core.Events /// Event args for that can support cancellation ///
[HostProtection(SecurityAction.LinkDemand, SharedState = true)] - public class CancellableEventArgs : EventArgs - { + public class CancellableEventArgs : EventArgs, IEquatable + { private bool _cancel; public CancellableEventArgs(bool canCancel, EventMessages messages, IDictionary additionalData) @@ -98,6 +98,36 @@ namespace Umbraco.Core.Events /// This allows for a bit of flexibility in our event raising - it's not pretty but we need to maintain backwards compatibility /// so we cannot change the strongly typed nature for some events. /// - public ReadOnlyDictionary AdditionalData { get; private set; } - } + public ReadOnlyDictionary AdditionalData { get; private set; } + + public bool Equals(CancellableEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(AdditionalData, other.AdditionalData); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((CancellableEventArgs) obj); + } + + public override int GetHashCode() + { + return (AdditionalData != null ? AdditionalData.GetHashCode() : 0); + } + + public static bool operator ==(CancellableEventArgs left, CancellableEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(CancellableEventArgs left, CancellableEventArgs right) + { + return !Equals(left, right); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs index a05f09ece5..63562eb53e 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs @@ -1,52 +1,177 @@ +using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Security.Permissions; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Events { - /// - /// Event args for a strongly typed object that can support cancellation - /// - /// - [HostProtection(SecurityAction.LinkDemand, SharedState = true)] - public class CancellableObjectEventArgs : CancellableEventArgs - { - public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + /// + /// Used as a base class for the generic type CancellableObjectEventArgs{T} so that we can get direct 'object' access to the underlying EventObject + /// + [HostProtection(SecurityAction.LinkDemand, SharedState = true)] + public abstract class CancellableObjectEventArgs : CancellableEventArgs + { + protected CancellableObjectEventArgs(object eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) : base(canCancel, messages, additionalData) - { + { EventObject = eventObject; } - public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages eventMessages) + protected CancellableObjectEventArgs(object eventObject, bool canCancel, EventMessages eventMessages) : base(canCancel, eventMessages) { EventObject = eventObject; } - public CancellableObjectEventArgs(T eventObject, EventMessages eventMessages) + protected CancellableObjectEventArgs(object eventObject, EventMessages eventMessages) : this(eventObject, true, eventMessages) { } + protected CancellableObjectEventArgs(object eventObject, bool canCancel) + : base(canCancel) + { + EventObject = eventObject; + } + + protected CancellableObjectEventArgs(object eventObject) + : this(eventObject, true) + { + } + + /// + /// Returns the object relating to the event + /// + /// + /// This is protected so that inheritors can expose it with their own name + /// + internal object EventObject { get; set; } + + } + + /// + /// Event args for a strongly typed object that can support cancellation + /// + /// + [HostProtection(SecurityAction.LinkDemand, SharedState = true)] + public class CancellableObjectEventArgs : CancellableObjectEventArgs, IEquatable> + { + public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) + { + } + + public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + } + + public CancellableObjectEventArgs(T eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } + public CancellableObjectEventArgs(T eventObject, bool canCancel) - : base(canCancel) - { - EventObject = eventObject; - } + : base(eventObject, canCancel) + { + } - public CancellableObjectEventArgs(T eventObject) - : this(eventObject, true) - { - } + public CancellableObjectEventArgs(T eventObject) + : base(eventObject) + { + } - /// - /// Returns the object relating to the event - /// - /// - /// This is protected so that inheritors can expose it with their own name - /// - protected T EventObject { get; set; } + /// + /// Returns the object relating to the event + /// + /// + /// This is protected so that inheritors can expose it with their own name + /// + protected new T EventObject + { + get { return (T) base.EventObject; } + set { base.EventObject = value; } + } - } + public bool Equals(CancellableObjectEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && EqualityComparer.Default.Equals(EventObject, other.EventObject); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((CancellableObjectEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ EqualityComparer.Default.GetHashCode(EventObject); + } + } + + public static bool operator ==(CancellableObjectEventArgs left, CancellableObjectEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(CancellableObjectEventArgs left, CancellableObjectEventArgs right) + { + return !Equals(left, right); + } + } + + [HostProtection(SecurityAction.LinkDemand, SharedState = true)] + public class CancellableEnumerableObjectEventArgs : CancellableObjectEventArgs>, IEquatable> + { + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) + { } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel) + : base(eventObject, canCancel) + { } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject) + : base(eventObject) + { } + + public bool Equals(CancellableEnumerableObjectEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return EventObject.SequenceEqual(other.EventObject); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((CancellableEnumerableObjectEventArgs)obj); + } + + public override int GetHashCode() + { + return HashCodeHelper.GetHashCode(EventObject); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/CopyEventArgs.cs b/src/Umbraco.Core/Events/CopyEventArgs.cs index 16f4fae982..65dc54cc88 100644 --- a/src/Umbraco.Core/Events/CopyEventArgs.cs +++ b/src/Umbraco.Core/Events/CopyEventArgs.cs @@ -1,6 +1,9 @@ +using System; +using System.Collections.Generic; + namespace Umbraco.Core.Events { - public class CopyEventArgs : CancellableObjectEventArgs + public class CopyEventArgs : CancellableObjectEventArgs, IEquatable> { public CopyEventArgs(TEntity original, TEntity copy, bool canCancel, int parentId) : base(original, canCancel) @@ -43,5 +46,42 @@ namespace Umbraco.Core.Events public int ParentId { get; private set; } public bool RelateToOriginal { get; set; } + + public bool Equals(CopyEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && EqualityComparer.Default.Equals(Copy, other.Copy) && ParentId == other.ParentId && RelateToOriginal == other.RelateToOriginal; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((CopyEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Copy); + hashCode = (hashCode * 397) ^ ParentId; + hashCode = (hashCode * 397) ^ RelateToOriginal.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(CopyEventArgs left, CopyEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(CopyEventArgs left, CopyEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/DeleteEventArgs.cs b/src/Umbraco.Core/Events/DeleteEventArgs.cs index 1025066bcc..d0a4f024e1 100644 --- a/src/Umbraco.Core/Events/DeleteEventArgs.cs +++ b/src/Umbraco.Core/Events/DeleteEventArgs.cs @@ -1,8 +1,14 @@ +using System; using System.Collections.Generic; +using System.Linq; namespace Umbraco.Core.Events { - public class DeleteEventArgs : CancellableObjectEventArgs> + [SupersedeEvent(typeof(SaveEventArgs<>))] + [SupersedeEvent(typeof(PublishEventArgs<>))] + [SupersedeEvent(typeof(MoveEventArgs<>))] + [SupersedeEvent(typeof(CopyEventArgs<>))] + public class DeleteEventArgs : CancellableEnumerableObjectEventArgs, IEquatable>, IDeletingMediaFilesEventArgs { /// /// Constructor accepting multiple entities that are used in the delete operation @@ -99,10 +105,43 @@ namespace Umbraco.Core.Events /// /// A list of media files that can be added to during a deleted operation for which Umbraco will ensure are removed /// - public List MediaFilesToDelete { get; private set; } + public List MediaFilesToDelete { get; private set; } + + public bool Equals(DeleteEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && MediaFilesToDelete.SequenceEqual(other.MediaFilesToDelete); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DeleteEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ MediaFilesToDelete.GetHashCode(); + } + } + + public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) + { + return !Equals(left, right); + } } - public class DeleteEventArgs : CancellableEventArgs + public class DeleteEventArgs : CancellableEventArgs, IEquatable { public DeleteEventArgs(int id, bool canCancel, EventMessages eventMessages) : base(canCancel, eventMessages) @@ -125,5 +164,38 @@ namespace Umbraco.Core.Events /// Gets the Id of the object being deleted. /// public int Id { get; private set; } + + public bool Equals(DeleteEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && Id == other.Id; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DeleteEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ Id; + } + } + + public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/DeleteRevisionsEventArgs.cs b/src/Umbraco.Core/Events/DeleteRevisionsEventArgs.cs index 4d53270608..1db1296640 100644 --- a/src/Umbraco.Core/Events/DeleteRevisionsEventArgs.cs +++ b/src/Umbraco.Core/Events/DeleteRevisionsEventArgs.cs @@ -2,7 +2,7 @@ using System; namespace Umbraco.Core.Events { - public class DeleteRevisionsEventArgs : DeleteEventArgs + public class DeleteRevisionsEventArgs : DeleteEventArgs, IEquatable { public DeleteRevisionsEventArgs(int id, bool canCancel, Guid specificVersion = default(Guid), bool deletePriorVersions = false, DateTime dateToRetain = default(DateTime)) : base(id, canCancel) @@ -31,5 +31,42 @@ namespace Umbraco.Core.Events { get { return SpecificVersion != default(Guid); } } + + public bool Equals(DeleteRevisionsEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && DateToRetain.Equals(other.DateToRetain) && DeletePriorVersions == other.DeletePriorVersions && SpecificVersion.Equals(other.SpecificVersion); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DeleteRevisionsEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ DateToRetain.GetHashCode(); + hashCode = (hashCode * 397) ^ DeletePriorVersions.GetHashCode(); + hashCode = (hashCode * 397) ^ SpecificVersion.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(DeleteRevisionsEventArgs left, DeleteRevisionsEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(DeleteRevisionsEventArgs left, DeleteRevisionsEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventDefinition.cs b/src/Umbraco.Core/Events/EventDefinition.cs new file mode 100644 index 0000000000..e861e9496e --- /dev/null +++ b/src/Umbraco.Core/Events/EventDefinition.cs @@ -0,0 +1,73 @@ +using System; + +namespace Umbraco.Core.Events +{ + internal class EventDefinition : EventDefinitionBase + { + private readonly EventHandler _trackedEvent; + private readonly object _sender; + private readonly EventArgs _args; + + public EventDefinition(EventHandler trackedEvent, object sender, EventArgs args, string eventName = null) + : base(sender, args, eventName) + { + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; + } + + public override void RaiseEvent() + { + if (_trackedEvent != null) + { + _trackedEvent(_sender, _args); + } + } + } + + internal class EventDefinition : EventDefinitionBase + { + private readonly EventHandler _trackedEvent; + private readonly object _sender; + private readonly TEventArgs _args; + + public EventDefinition(EventHandler trackedEvent, object sender, TEventArgs args, string eventName = null) + : base(sender, args, eventName) + { + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; + } + + public override void RaiseEvent() + { + if (_trackedEvent != null) + { + _trackedEvent(_sender, _args); + } + } + } + + internal class EventDefinition : EventDefinitionBase + { + private readonly TypedEventHandler _trackedEvent; + private readonly TSender _sender; + private readonly TEventArgs _args; + + public EventDefinition(TypedEventHandler trackedEvent, TSender sender, TEventArgs args, string eventName = null) + : base(sender, args, eventName) + { + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; + } + + public override void RaiseEvent() + { + if (_trackedEvent != null) + { + _trackedEvent(_sender, _args); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventDefinitionBase.cs b/src/Umbraco.Core/Events/EventDefinitionBase.cs new file mode 100644 index 0000000000..c0a061f80f --- /dev/null +++ b/src/Umbraco.Core/Events/EventDefinitionBase.cs @@ -0,0 +1,70 @@ +using System; +using System.Reflection; + +namespace Umbraco.Core.Events +{ + public abstract class EventDefinitionBase : IEventDefinition, IEquatable + { + protected EventDefinitionBase(object sender, object args, string eventName = null) + { + if (sender == null) throw new ArgumentNullException("sender"); + if (args == null) throw new ArgumentNullException("args"); + Sender = sender; + Args = args; + EventName = eventName; + + if (EventName.IsNullOrWhiteSpace()) + { + var findResult = EventNameExtractor.FindEvent(sender, args, + //don't match "Ing" suffixed names + exclude:EventNameExtractor.MatchIngNames); + + if (findResult.Success == false) + throw new AmbiguousMatchException("Could not automatically find the event name, the event name will need to be explicitly registered for this event definition. Error: " + findResult.Result.Error); + EventName = findResult.Result.Name; + } + } + + public object Sender { get; private set; } + public object Args { get; private set; } + public string EventName { get; private set; } + + public abstract void RaiseEvent(); + + public bool Equals(EventDefinitionBase other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Args.Equals(other.Args) && string.Equals(EventName, other.EventName) && Sender.Equals(other.Sender); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((EventDefinitionBase) obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Args.GetHashCode(); + hashCode = (hashCode * 397) ^ EventName.GetHashCode(); + hashCode = (hashCode * 397) ^ Sender.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(EventDefinitionBase left, EventDefinitionBase right) + { + return Equals(left, right); + } + + public static bool operator !=(EventDefinitionBase left, EventDefinitionBase right) + { + return Equals(left, right) == false; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventDefinitionFilter.cs b/src/Umbraco.Core/Events/EventDefinitionFilter.cs new file mode 100644 index 0000000000..4bbe75d10b --- /dev/null +++ b/src/Umbraco.Core/Events/EventDefinitionFilter.cs @@ -0,0 +1,24 @@ +namespace Umbraco.Core.Events +{ + /// + /// The filter used in the GetEvents method which determines + /// how the result list is filtered + /// + public enum EventDefinitionFilter + { + /// + /// Returns all events tracked + /// + All, + + /// + /// Deduplicates events and only returns the first duplicate instance tracked + /// + FirstIn, + + /// + /// Deduplicates events and only returns the last duplicate instance tracked + /// + LastIn + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventExtensions.cs b/src/Umbraco.Core/Events/EventExtensions.cs index 700a02457a..a366ad0cce 100644 --- a/src/Umbraco.Core/Events/EventExtensions.cs +++ b/src/Umbraco.Core/Events/EventExtensions.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Events { @@ -9,43 +12,40 @@ namespace Umbraco.Core.Events ///
public static class EventExtensions { - /// - /// Raises the event and returns a boolean value indicating if the event was cancelled - /// - /// - /// - /// - /// - /// - /// - public static bool IsRaisedEventCancelled( - this TypedEventHandler eventHandler, - TArgs args, - TSender sender) - where TArgs : CancellableEventArgs - { - if (eventHandler != null) - eventHandler(sender, args); + // keep these two for backward compatibility reasons but understand that + // they are *not* part of any scope / event dispatcher / anything... + /// + /// Raises a cancelable event and returns a value indicating whether the event should be cancelled. + /// + /// The type of the event source. + /// The type of the event data. + /// The event handler. + /// The event source. + /// The event data. + /// A value indicating whether the cancelable event should be cancelled + /// A cancelable event is raised by a component when it is about to perform an action that can be canceled. + public static bool IsRaisedEventCancelled(this TypedEventHandler eventHandler, TArgs args, TSender sender) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + eventHandler(sender, args); return args.Cancel; } - + /// - /// Raises the event + /// Raises an event. /// - /// - /// - /// - /// - /// - public static void RaiseEvent( - this TypedEventHandler eventHandler, - TArgs args, - TSender sender) - where TArgs : EventArgs - { - if (eventHandler != null) - eventHandler(sender, args); - } - } + /// The type of the event source. + /// The type of the event data. + /// The event handler. + /// The event source. + /// The event data. + public static void RaiseEvent(this TypedEventHandler eventHandler, TArgs args, TSender sender) + where TArgs : EventArgs + { + if (eventHandler == null) return; + eventHandler(sender, args); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/MessageType.cs b/src/Umbraco.Core/Events/EventMessageType.cs similarity index 100% rename from src/Umbraco.Core/Events/MessageType.cs rename to src/Umbraco.Core/Events/EventMessageType.cs diff --git a/src/Umbraco.Core/Events/EventNameExtractor.cs b/src/Umbraco.Core/Events/EventNameExtractor.cs new file mode 100644 index 0000000000..33137770d4 --- /dev/null +++ b/src/Umbraco.Core/Events/EventNameExtractor.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Umbraco.Core.Events +{ + /// + /// There is actually no way to discover an event name in c# at the time of raising the event. It is possible + /// to get the event name from the handler that is being executed based on the event being raised, however that is not + /// what we want in this case. We need to find the event name before it is being raised - you would think that it's possible + /// with reflection or anything but that is not the case, the delegate that defines an event has no info attached to it, it + /// is literally just an event. + /// + /// So what this does is take the sender and event args objects, looks up all public/static events on the sender that have + /// a generic event handler with generic arguments (but only) one, then we match the type of event arguments with the ones + /// being passed in. As it turns out, in our services this will work for the majority of our events! In some cases it may not + /// work and we'll have to supply a string but hopefully this saves a bit of magic strings. + /// + /// We can also write tests to validate these are all working correctly for all services. + /// + internal class EventNameExtractor + { + + /// + /// Finds the event name on the sender that matches the args type + /// + /// + /// + /// + /// A filter to exclude matched event names, this filter should return true to exclude the event name from being matched + /// + /// + /// null if not found or an ambiguous match + /// + public static Attempt FindEvent(Type senderType, Type argsType, Func exclude) + { + var found = MatchedEventNames.GetOrAdd(new Tuple(senderType, argsType), tuple => + { + var events = CandidateEvents.GetOrAdd(senderType, t => + { + return t.GetEvents(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + //we can only look for events handlers with generic types because that is the only + // way that we can try to find a matching event based on the arg type passed in + .Where(x => x.EventHandlerType.IsGenericType) + .Select(x => new EventInfoArgs(x, x.EventHandlerType.GetGenericArguments())) + //we are only looking for event handlers that have more than one generic argument + .Where(x => + { + if (x.GenericArgs.Length == 1) return true; + + //special case for our own TypedEventHandler + if (x.EventInfo.EventHandlerType.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) && x.GenericArgs.Length == 2) + { + return true; + } + + return false; + }) + .ToArray(); + }); + + return events.Where(x => + { + if (x.GenericArgs.Length == 1 && x.GenericArgs[0] == tuple.Item2) + return true; + + //special case for our own TypedEventHandler + if (x.EventInfo.EventHandlerType.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) + && x.GenericArgs.Length == 2 + && x.GenericArgs[1] == tuple.Item2) + { + return true; + } + + return false; + }).Select(x => x.EventInfo.Name).ToArray(); + }); + + var filtered = found.Where(x => exclude(x) == false).ToArray(); + + if (filtered.Length == 0) + return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.NoneFound)); + + if (filtered.Length == 1) + return Attempt.Succeed(new EventNameExtractorResult(filtered[0])); + + //there's more than one left so it's ambiguous! + return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.Ambiguous)); + } + + /// + /// Finds the event name on the sender that matches the args type + /// + /// + /// + /// + /// A filter to exclude matched event names, this filter should return true to exclude the event name from being matched + /// + /// + /// null if not found or an ambiguous match + /// + public static Attempt FindEvent(object sender, object args, Func exclude) + { + return FindEvent(sender.GetType(), args.GetType(), exclude); + } + + /// + /// Return true if the event is named with an ING name such as "Saving" or "RollingBack" + /// + /// + /// + internal static bool MatchIngNames(string eventName) + { + var splitter = new Regex(@"(? + /// Return true if the event is not named with an ING name such as "Saving" or "RollingBack" + ///
+ /// + /// + internal static bool MatchNonIngNames(string eventName) + { + var splitter = new Regex(@"(? + /// Used to cache all candidate events for a given type so we don't re-look them up + ///
+ private static readonly ConcurrentDictionary CandidateEvents = new ConcurrentDictionary(); + + /// + /// Used to cache all matched event names by (sender type + arg type) so we don't re-look them up + /// + private static readonly ConcurrentDictionary, string[]> MatchedEventNames = new ConcurrentDictionary, string[]>(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventNameExtractorError.cs b/src/Umbraco.Core/Events/EventNameExtractorError.cs new file mode 100644 index 0000000000..3b27db2a1a --- /dev/null +++ b/src/Umbraco.Core/Events/EventNameExtractorError.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Events +{ + internal enum EventNameExtractorError + { + NoneFound, + Ambiguous + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventNameExtractorResult.cs b/src/Umbraco.Core/Events/EventNameExtractorResult.cs new file mode 100644 index 0000000000..6213882d1f --- /dev/null +++ b/src/Umbraco.Core/Events/EventNameExtractorResult.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Events +{ + internal class EventNameExtractorResult + { + public EventNameExtractorError? Error { get; private set; } + public string Name { get; private set; } + + public EventNameExtractorResult(string name) + { + Name = name; + } + + public EventNameExtractorResult(EventNameExtractorError error) + { + Error = error; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ExportEventArgs.cs b/src/Umbraco.Core/Events/ExportEventArgs.cs index 161a073615..acbf920636 100644 --- a/src/Umbraco.Core/Events/ExportEventArgs.cs +++ b/src/Umbraco.Core/Events/ExportEventArgs.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Xml.Linq; namespace Umbraco.Core.Events { - public class ExportEventArgs : CancellableObjectEventArgs> + public class ExportEventArgs : CancellableObjectEventArgs>, IEquatable> { /// /// Constructor accepting a single entity instance @@ -48,5 +49,38 @@ namespace Umbraco.Core.Events /// Returns the xml relating to the export event /// public XElement Xml { get; private set; } + + public bool Equals(ExportEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && Equals(Xml, other.Xml); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ExportEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ (Xml != null ? Xml.GetHashCode() : 0); + } + } + + public static bool operator ==(ExportEventArgs left, ExportEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(ExportEventArgs left, ExportEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs b/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs new file mode 100644 index 0000000000..45681042ba --- /dev/null +++ b/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Events +{ + internal interface IDeletingMediaFilesEventArgs + { + List MediaFilesToDelete { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/IEventDefinition.cs b/src/Umbraco.Core/Events/IEventDefinition.cs new file mode 100644 index 0000000000..eb4fe65eb8 --- /dev/null +++ b/src/Umbraco.Core/Events/IEventDefinition.cs @@ -0,0 +1,13 @@ +using System; + +namespace Umbraco.Core.Events +{ + public interface IEventDefinition + { + object Sender { get; } + object Args { get; } + string EventName { get; } + + void RaiseEvent(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/IEventDispatcher.cs b/src/Umbraco.Core/Events/IEventDispatcher.cs new file mode 100644 index 0000000000..78f8ed3a4a --- /dev/null +++ b/src/Umbraco.Core/Events/IEventDispatcher.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Events +{ + /// + /// Dispatches events from within a scope. + /// + /// + /// The name of the event is auto-magically discovered by matching the sender type, args type, and + /// eventHandler type. If the match is not unique, then the name parameter must be used to specify the + /// name in an explicit way. + /// What happens when an event is dispatched depends on the scope settings. It can be anything from + /// "trigger immediately" to "just ignore". Refer to the scope documentation for more details. + /// + public interface IEventDispatcher + { + // not sure about the Dispatch & DispatchCancelable signatures at all for now + // nor about the event name thing, etc - but let's keep it like this + + /// + /// Dispatches a cancelable event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string name = null); + + /// + /// Dispatches a cancelable event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string name = null) + where TArgs : CancellableEventArgs; + + /// + /// Dispatches a cancelable event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string name = null) + where TArgs : CancellableEventArgs; + + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string name = null); + + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(EventHandler eventHandler, object sender, TArgs args, string name = null); + + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string name = null); + + /// + /// Notifies the dispatcher that the scope is exiting. + /// + /// A value indicating whether the scope completed. + void ScopeExit(bool completed); + + /// + /// Gets the collected events. + /// + /// The collected events. + IEnumerable GetEvents(EventDefinitionFilter filter); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ImportEventArgs.cs b/src/Umbraco.Core/Events/ImportEventArgs.cs index 3bdd6d6fcf..892149c0a2 100644 --- a/src/Umbraco.Core/Events/ImportEventArgs.cs +++ b/src/Umbraco.Core/Events/ImportEventArgs.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Xml.Linq; namespace Umbraco.Core.Events { - public class ImportEventArgs : CancellableObjectEventArgs> + public class ImportEventArgs : CancellableEnumerableObjectEventArgs, IEquatable> { /// /// Constructor accepting an XElement with the xml being imported @@ -46,5 +47,38 @@ namespace Umbraco.Core.Events /// Returns the xml relating to the import event /// public XElement Xml { get; private set; } + + public bool Equals(ImportEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && Equals(Xml, other.Xml); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ImportEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ (Xml != null ? Xml.GetHashCode() : 0); + } + } + + public static bool operator ==(ImportEventArgs left, ImportEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(ImportEventArgs left, ImportEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs index 8596629731..4477faea50 100644 --- a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs +++ b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Packaging.Models; namespace Umbraco.Core.Events { - internal class ImportPackageEventArgs : CancellableObjectEventArgs> + internal class ImportPackageEventArgs : CancellableEnumerableObjectEventArgs, IEquatable> { private readonly MetaData _packageMetaData; @@ -21,6 +22,45 @@ namespace Umbraco.Core.Events public MetaData PackageMetaData { get { return _packageMetaData; } + } + + public IEnumerable InstallationSummary + { + get { return EventObject; } + } + + public bool Equals(ImportPackageEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + //TODO: MetaData for package metadata has no equality operators :/ + return base.Equals(other) && _packageMetaData.Equals(other._packageMetaData); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ImportPackageEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ _packageMetaData.GetHashCode(); + } + } + + public static bool operator ==(ImportPackageEventArgs left, ImportPackageEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(ImportPackageEventArgs left, ImportPackageEventArgs right) + { + return !Equals(left, right); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/MigrationEventArgs.cs b/src/Umbraco.Core/Events/MigrationEventArgs.cs index 89dfe56294..8b8898e7d4 100644 --- a/src/Umbraco.Core/Events/MigrationEventArgs.cs +++ b/src/Umbraco.Core/Events/MigrationEventArgs.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Persistence.Migrations; namespace Umbraco.Core.Events { - public class MigrationEventArgs : CancellableObjectEventArgs> + public class MigrationEventArgs : CancellableObjectEventArgs>, IEquatable { /// /// Constructor accepting multiple migrations that are used in the migration runner @@ -31,13 +31,13 @@ namespace Umbraco.Core.Events [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) + : this(eventObject, null, configuredVersion, targetVersion, Constants.System.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) - : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), GlobalSettings.UmbracoMigrationName, canCancel) + : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), Constants.System.UmbracoMigrationName, canCancel) { } /// @@ -74,7 +74,7 @@ namespace Umbraco.Core.Events MigrationContext = migrationContext; ConfiguredSemVersion = configuredVersion; TargetSemVersion = targetVersion; - ProductName = GlobalSettings.UmbracoMigrationName; + ProductName = Constants.System.UmbracoMigrationName; } /// @@ -97,13 +97,13 @@ namespace Umbraco.Core.Events [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) + : this(eventObject, null, configuredVersion, targetVersion, Constants.System.UmbracoMigrationName, false) { } [Obsolete("Use constructor accepting SemVersion instances and a product name instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public MigrationEventArgs(IList eventObject, Version configuredVersion, Version targetVersion) - : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), GlobalSettings.UmbracoMigrationName, false) + : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), Constants.System.UmbracoMigrationName, false) { } /// @@ -141,5 +141,43 @@ namespace Umbraco.Core.Events public string ProductName { get; private set; } internal MigrationContext MigrationContext { get; private set; } + + public bool Equals(MigrationEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && ConfiguredSemVersion.Equals(other.ConfiguredSemVersion) && MigrationContext.Equals(other.MigrationContext) && string.Equals(ProductName, other.ProductName) && TargetSemVersion.Equals(other.TargetSemVersion); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((MigrationEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ ConfiguredSemVersion.GetHashCode(); + hashCode = (hashCode * 397) ^ MigrationContext.GetHashCode(); + hashCode = (hashCode * 397) ^ ProductName.GetHashCode(); + hashCode = (hashCode * 397) ^ TargetSemVersion.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(MigrationEventArgs left, MigrationEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(MigrationEventArgs left, MigrationEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/MoveEventArgs.cs b/src/Umbraco.Core/Events/MoveEventArgs.cs index 0f0a5183a9..228e1ca2f7 100644 --- a/src/Umbraco.Core/Events/MoveEventArgs.cs +++ b/src/Umbraco.Core/Events/MoveEventArgs.cs @@ -4,7 +4,7 @@ using System.Linq; namespace Umbraco.Core.Events { - public class MoveEventArgs : CancellableObjectEventArgs + public class MoveEventArgs : CancellableObjectEventArgs, IEquatable> { /// /// Constructor accepting a collection of MoveEventInfo objects @@ -123,5 +123,38 @@ namespace Umbraco.Core.Events /// [Obsolete("Retrieve the ParentId from the MoveInfoCollection property instead")] public int ParentId { get; private set; } + + public bool Equals(MoveEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && MoveInfoCollection.Equals(other.MoveInfoCollection); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((MoveEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ MoveInfoCollection.GetHashCode(); + } + } + + public static bool operator ==(MoveEventArgs left, MoveEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(MoveEventArgs left, MoveEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/MoveEventInfo.cs b/src/Umbraco.Core/Events/MoveEventInfo.cs index a74db7f36e..9e77971837 100644 --- a/src/Umbraco.Core/Events/MoveEventInfo.cs +++ b/src/Umbraco.Core/Events/MoveEventInfo.cs @@ -1,6 +1,9 @@ +using System; +using System.Collections.Generic; + namespace Umbraco.Core.Events { - public class MoveEventInfo + public class MoveEventInfo : IEquatable> { public MoveEventInfo(TEntity entity, string originalPath, int newParentId) { @@ -12,5 +15,41 @@ namespace Umbraco.Core.Events public TEntity Entity { get; set; } public string OriginalPath { get; set; } public int NewParentId { get; set; } + + public bool Equals(MoveEventInfo other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return EqualityComparer.Default.Equals(Entity, other.Entity) && NewParentId == other.NewParentId && string.Equals(OriginalPath, other.OriginalPath); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((MoveEventInfo) obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = EqualityComparer.Default.GetHashCode(Entity); + hashCode = (hashCode * 397) ^ NewParentId; + hashCode = (hashCode * 397) ^ OriginalPath.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(MoveEventInfo left, MoveEventInfo right) + { + return Equals(left, right); + } + + public static bool operator !=(MoveEventInfo left, MoveEventInfo right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/NewEventArgs.cs b/src/Umbraco.Core/Events/NewEventArgs.cs index acfd64e60d..415d82b90e 100644 --- a/src/Umbraco.Core/Events/NewEventArgs.cs +++ b/src/Umbraco.Core/Events/NewEventArgs.cs @@ -1,8 +1,10 @@ +using System; +using System.Collections.Generic; using Umbraco.Core.Models; namespace Umbraco.Core.Events { - public class NewEventArgs : CancellableObjectEventArgs + public class NewEventArgs : CancellableObjectEventArgs, IEquatable> { @@ -84,5 +86,42 @@ namespace Umbraco.Core.Events /// Gets or Sets the parent IContent object. /// public TEntity Parent { get; private set; } + + public bool Equals(NewEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && string.Equals(Alias, other.Alias) && EqualityComparer.Default.Equals(Parent, other.Parent) && ParentId == other.ParentId; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((NewEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ Alias.GetHashCode(); + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Parent); + hashCode = (hashCode * 397) ^ ParentId; + return hashCode; + } + } + + public static bool operator ==(NewEventArgs left, NewEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(NewEventArgs left, NewEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs b/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs new file mode 100644 index 0000000000..bc3709ea2a --- /dev/null +++ b/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Events +{ + /// + /// An IEventDispatcher that immediately raise all events. + /// + /// This means that events will be raised during the scope transaction, + /// whatever happens, and the transaction could roll back in the end. + internal class PassThroughEventDispatcher : IEventDispatcher + { + public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string eventName = null) + { + if (eventHandler == null) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string eventName = null) + { + if (eventHandler == null) return; + eventHandler(sender, args); + } + + public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string eventName = null) + { + if (eventHandler == null) return; + eventHandler(sender, args); + } + + public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) + { + if (eventHandler == null) return; + eventHandler(sender, args); + } + + public IEnumerable GetEvents(EventDefinitionFilter filter) + { + return Enumerable.Empty(); + } + + public void ScopeExit(bool completed) + { } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/PublishEventArgs.cs b/src/Umbraco.Core/Events/PublishEventArgs.cs index a791781617..10bf94146c 100644 --- a/src/Umbraco.Core/Events/PublishEventArgs.cs +++ b/src/Umbraco.Core/Events/PublishEventArgs.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Events { - public class PublishEventArgs : CancellableObjectEventArgs> + public class PublishEventArgs : CancellableEnumerableObjectEventArgs, IEquatable> { /// /// Constructor accepting multiple entities that are used in the publish operation @@ -101,5 +102,38 @@ namespace Umbraco.Core.Events } public bool IsAllRepublished { get; private set; } + + public bool Equals(PublishEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && IsAllRepublished == other.IsAllRepublished; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((PublishEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ IsAllRepublished.GetHashCode(); + } + } + + public static bool operator ==(PublishEventArgs left, PublishEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(PublishEventArgs left, PublishEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs index ca4bbd2719..c6d7c659b7 100644 --- a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs +++ b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Events { - public class RecycleBinEventArgs : CancellableEventArgs + public class RecycleBinEventArgs : CancellableEventArgs, IEquatable, IDeletingMediaFilesEventArgs { public RecycleBinEventArgs(Guid nodeObjectType, Dictionary> allPropertyData, bool emptiedSuccessfully) : base(false) @@ -97,6 +97,8 @@ namespace Umbraco.Core.Events /// public List Files { get; private set; } + public List MediaFilesToDelete { get { return Files; } } + /// /// Gets the list of all property data associated with a content id /// @@ -122,5 +124,44 @@ namespace Umbraco.Core.Events { get { return NodeObjectType == new Guid(Constants.ObjectTypes.Media); } } + + public bool Equals(RecycleBinEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && AllPropertyData.Equals(other.AllPropertyData) && Files.Equals(other.Files) && Ids.Equals(other.Ids) && NodeObjectType.Equals(other.NodeObjectType) && RecycleBinEmptiedSuccessfully == other.RecycleBinEmptiedSuccessfully; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((RecycleBinEventArgs) obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ AllPropertyData.GetHashCode(); + hashCode = (hashCode * 397) ^ Files.GetHashCode(); + hashCode = (hashCode * 397) ^ Ids.GetHashCode(); + hashCode = (hashCode * 397) ^ NodeObjectType.GetHashCode(); + hashCode = (hashCode * 397) ^ RecycleBinEmptiedSuccessfully.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(RecycleBinEventArgs left, RecycleBinEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(RecycleBinEventArgs left, RecycleBinEventArgs right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/RollbackEventArgs.cs b/src/Umbraco.Core/Events/RollbackEventArgs.cs index db9dded08c..cf2189e962 100644 --- a/src/Umbraco.Core/Events/RollbackEventArgs.cs +++ b/src/Umbraco.Core/Events/RollbackEventArgs.cs @@ -17,5 +17,7 @@ namespace Umbraco.Core.Events { get { return EventObject; } } + + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/SaveEventArgs.cs b/src/Umbraco.Core/Events/SaveEventArgs.cs index dafd326e1c..cd19038d8e 100644 --- a/src/Umbraco.Core/Events/SaveEventArgs.cs +++ b/src/Umbraco.Core/Events/SaveEventArgs.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; +using System.Linq; namespace Umbraco.Core.Events { - public class SaveEventArgs : CancellableObjectEventArgs> + public class SaveEventArgs : CancellableEnumerableObjectEventArgs { /// /// Constructor accepting multiple entities that are used in the saving operation diff --git a/src/Umbraco.Core/Events/ScopeEventDispatcher.cs b/src/Umbraco.Core/Events/ScopeEventDispatcher.cs new file mode 100644 index 0000000000..eb4f8ec34e --- /dev/null +++ b/src/Umbraco.Core/Events/ScopeEventDispatcher.cs @@ -0,0 +1,44 @@ +using Umbraco.Core.IO; + +namespace Umbraco.Core.Events +{ + /// + /// An IEventDispatcher that queues events, and raise them when the scope + /// exits and has been completed. + /// + internal class ScopeEventDispatcher : ScopeEventDispatcherBase + { + public ScopeEventDispatcher() + : base(true) + { } + + protected override void ScopeExitCompleted() + { + // processing only the last instance of each event... + // this is probably far from perfect, because if eg a content is saved in a list + // and then as a single content, the two events will probably not be de-duplicated, + // but it's better than nothing + + foreach (var e in GetEvents(EventDefinitionFilter.LastIn)) + { + e.RaiseEvent(); + + // separating concerns means that this should probably not be here, + // but then where should it be (without making things too complicated)? + var delete = e.Args as IDeletingMediaFilesEventArgs; + if (delete != null && delete.MediaFilesToDelete.Count > 0) + MediaFileSystem.DeleteMediaFiles(delete.MediaFilesToDelete); + } + } + + private MediaFileSystem _mediaFileSystem; + + private MediaFileSystem MediaFileSystem + { + get + { + return _mediaFileSystem ?? (_mediaFileSystem = FileSystemProviderManager.Current.MediaFileSystem); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs b/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs new file mode 100644 index 0000000000..c703a10cb4 --- /dev/null +++ b/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Events +{ + /// + /// An IEventDispatcher that queues events. + /// + /// + /// Can raise, or ignore, cancelable events, depending on option. + /// Implementations must override ScopeExitCompleted to define what + /// to do with the events when the scope exits and has been completed. + /// If the scope exits without being completed, events are ignored. + /// + public abstract class ScopeEventDispatcherBase : IEventDispatcher + { + //events will be enlisted in the order they are raised + private List _events; + private readonly bool _raiseCancelable; + + protected ScopeEventDispatcherBase(bool raiseCancelable) + { + _raiseCancelable = raiseCancelable; + } + + private List Events { get { return _events ?? (_events = new List()); } } + + public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string eventName = null) + { + if (eventHandler == null) return args.Cancel; + if (_raiseCancelable == false) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + if (_raiseCancelable == false) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + if (_raiseCancelable == false) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string eventName = null) + { + if (eventHandler == null) return; + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string eventName = null) + { + if (eventHandler == null) return; + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) + { + if (eventHandler == null) return; + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public IEnumerable GetEvents(EventDefinitionFilter filter) + { + if (_events == null) + return Enumerable.Empty(); + + switch (filter) + { + case EventDefinitionFilter.All: + return FilterSupersededAndUpdateToLatestEntity(_events); + case EventDefinitionFilter.FirstIn: + var l1 = new OrderedHashSet(); + foreach (var e in _events) + { + l1.Add(e); + } + return FilterSupersededAndUpdateToLatestEntity(l1); + case EventDefinitionFilter.LastIn: + var l2 = new OrderedHashSet(keepOldest: false); + foreach (var e in _events) + { + l2.Add(e); + } + return FilterSupersededAndUpdateToLatestEntity(l2); + default: + throw new ArgumentOutOfRangeException("filter", filter, null); + } + } + + private class EventDefinitionTypeData + { + public IEventDefinition EventDefinition { get; set; } + public Type EventArgType { get; set; } + public SupersedeEventAttribute[] SupersedeAttributes { get; set; } + } + + /// + /// This will iterate over the events (latest first) and filter out any events or entities in event args that are included + /// in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want + /// to raise the Saved event (well actually we just don't want to include it in the args for that saved event) + /// + /// + /// + private static IEnumerable FilterSupersededAndUpdateToLatestEntity(IReadOnlyList events) + { + //used to keep the 'latest' entity and associated event definition data + var allEntities = new List>(); + + //tracks all CancellableObjectEventArgs instances in the events which is the only type of args we can work with + var cancelableArgs = new List(); + + var result = new List(); + + //This will eagerly load all of the event arg types and their attributes so we don't have to continuously look this data up + var allArgTypesWithAttributes = events.Select(x => x.Args.GetType()) + .Distinct() + .ToDictionary(x => x, x => x.GetCustomAttributes(false).ToArray()); + + //Iterate all events and collect the actual entities in them and relates them to their corresponding EventDefinitionTypeData + //we'll process the list in reverse because events are added in the order they are raised and we want to filter out + //any entities from event args that are not longer relevant + //(i.e. if an item is Deleted after it's Saved, we won't include the item in the Saved args) + for (var index = events.Count - 1; index >= 0; index--) + { + var eventDefinition = events[index]; + + var argType = eventDefinition.Args.GetType(); + var attributes = allArgTypesWithAttributes[eventDefinition.Args.GetType()]; + + var meta = new EventDefinitionTypeData + { + EventDefinition = eventDefinition, + EventArgType = argType, + SupersedeAttributes = attributes + }; + + var args = eventDefinition.Args as CancellableObjectEventArgs; + if (args != null) + { + var list = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject); + + if (list == null) + { + //extract the event object + var obj = args.EventObject as IEntity; + if (obj != null) + { + //Now check if this entity already exists in other event args that supersede this current event arg type + if (IsFiltered(obj, meta, allEntities) == false) + { + //if it's not filtered we can adde these args to the response + cancelableArgs.Add(args); + result.Add(eventDefinition); + //track the entity + allEntities.Add(Tuple.Create(obj, meta)); + } + } + else + { + //Can't retrieve the entity so cant' filter or inspect, just add to the output + result.Add(eventDefinition); + } + } + else + { + var toRemove = new List(); + foreach (var entity in list) + { + //extract the event object + var obj = entity as IEntity; + if (obj != null) + { + //Now check if this entity already exists in other event args that supersede this current event arg type + if (IsFiltered(obj, meta, allEntities)) + { + //track it to be removed + toRemove.Add(obj); + } + else + { + //track the entity, it's not filtered + allEntities.Add(Tuple.Create(obj, meta)); + } + } + else + { + //we don't need to do anything here, we can't cast to IEntity so we cannot filter, so it will just remain in the list + } + } + + //remove anything that has been filtered + foreach (var entity in toRemove) + { + list.Remove(entity); + } + + //track the event and include in the response if there's still entities remaining in the list + if (list.Count > 0) + { + if (toRemove.Count > 0) + { + //re-assign if the items have changed + args.EventObject = list; + } + cancelableArgs.Add(args); + result.Add(eventDefinition); + } + } + } + else + { + //it's not a cancelable event arg so we just include it in the result + result.Add(eventDefinition); + } + } + + //Now we'll deal with ensuring that only the latest(non stale) entities are used throughout all event args + UpdateToLatestEntities(allEntities, cancelableArgs); + + //we need to reverse the result since we've been adding by latest added events first! + result.Reverse(); + + return result; + } + + private static void UpdateToLatestEntities(IEnumerable> allEntities, IEnumerable cancelableArgs) + { + //Now we'll deal with ensuring that only the latest(non stale) entities are used throughout all event args + + var latestEntities = new OrderedHashSet(keepOldest: true); + foreach (var entity in allEntities.OrderByDescending(entity => entity.Item1.UpdateDate)) + { + latestEntities.Add(entity.Item1); + } + + foreach (var args in cancelableArgs) + { + var list = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject); + if (list == null) + { + //try to find the args entity in the latest entity - based on the equality operators, this will + //match by Id since that is the default equality checker for IEntity. If one is found, than it is + //the most recent entity instance so update the args with that instance so we don't emit a stale instance. + var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, args.EventObject)); + if (foundEntity != null) + { + args.EventObject = foundEntity; + } + } + else + { + var updated = false; + + for (int i = 0; i < list.Count; i++) + { + //try to find the args entity in the latest entity - based on the equality operators, this will + //match by Id since that is the default equality checker for IEntity. If one is found, than it is + //the most recent entity instance so update the args with that instance so we don't emit a stale instance. + var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, list[i])); + if (foundEntity != null) + { + list[i] = foundEntity; + updated = true; + } + } + + if (updated) + { + args.EventObject = list; + } + } + } + } + + /// + /// This will check against all of the processed entity/events (allEntities) to see if this entity already exists in + /// event args that supersede the event args being passed in and if so returns true. + /// + /// + /// + /// + /// + private static bool IsFiltered( + IEntity entity, + EventDefinitionTypeData eventDef, + List> allEntities) + { + var argType = eventDef.EventDefinition.Args.GetType(); + + //check if the entity is found in any processed event data that could possible supersede this one + var foundByEntity = allEntities + .Where(x => x.Item2.SupersedeAttributes.Length > 0 + //if it's the same arg type than it cannot supersede + && x.Item2.EventArgType != argType + && Equals(x.Item1, entity)) + .ToArray(); + + //no args have been processed with this entity so it should not be filtered + if (foundByEntity.Length == 0) + return false; + + if (argType.IsGenericType) + { + var supercededBy = foundByEntity + .FirstOrDefault(x => + x.Item2.SupersedeAttributes.Any(y => + //if the attribute type is a generic type def then compare with the generic type def of the event arg + (y.SupersededEventArgsType.IsGenericTypeDefinition && y.SupersededEventArgsType == argType.GetGenericTypeDefinition()) + //if the attribute type is not a generic type def then compare with the normal type of the event arg + || (y.SupersededEventArgsType.IsGenericTypeDefinition == false && y.SupersededEventArgsType == argType))); + return supercededBy != null; + } + else + { + var supercededBy = foundByEntity + .FirstOrDefault(x => + x.Item2.SupersedeAttributes.Any(y => + //since the event arg type is not a generic type, then we just compare type 1:1 + y.SupersededEventArgsType == argType)); + return supercededBy != null; + } + } + + public void ScopeExit(bool completed) + { + if (_events == null) return; + if (completed) + ScopeExitCompleted(); + _events.Clear(); + } + + protected abstract void ScopeExitCompleted(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ScopeLifespanMessagesFactory.cs b/src/Umbraco.Core/Events/ScopeLifespanMessagesFactory.cs new file mode 100644 index 0000000000..710ee8a129 --- /dev/null +++ b/src/Umbraco.Core/Events/ScopeLifespanMessagesFactory.cs @@ -0,0 +1,58 @@ +using System; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Events +{ + /// + /// Stores the instance of EventMessages in the current scope. + /// + internal class ScopeLifespanMessagesFactory : IEventMessagesFactory + { + public const string ContextKey = "Umbraco.Core.Events.ScopeLifespanMessagesFactory"; + + private readonly IHttpContextAccessor _contextAccessor; + private readonly IScopeProviderInternal _scopeProvider; + + public static ScopeLifespanMessagesFactory Current { get; private set; } + + public ScopeLifespanMessagesFactory(IHttpContextAccessor contextAccesor, IScopeProvider scopeProvider) + { + if (contextAccesor == null) throw new ArgumentNullException("contextAccesor"); + if (scopeProvider == null) throw new ArgumentNullException("scopeProvider"); + if (scopeProvider is IScopeProviderInternal == false) throw new ArgumentException("Not IScopeProviderInternal.", "scopeProvider"); + _contextAccessor = contextAccesor; + _scopeProvider = (IScopeProviderInternal) scopeProvider; + Current = this; + } + + public EventMessages Get() + { + var messages = GetFromHttpContext(); + if (messages != null) return messages; + + var scope = _scopeProvider.GetAmbientOrNoScope(); + return scope.Messages; + } + + public EventMessages GetFromHttpContext() + { + if (_contextAccessor == null || _contextAccessor.Value == null) return null; + return (EventMessages)_contextAccessor.Value.Items[ContextKey]; + } + + public EventMessages TryGet() + { + var messages = GetFromHttpContext(); + if (messages != null) return messages; + + var scope = _scopeProvider.AmbientScope; + return scope == null ? null : scope.MessagesOrNull; + } + + public void Set(EventMessages messages) + { + if (_contextAccessor.Value == null) return; + _contextAccessor.Value.Items[ContextKey] = messages; + } + } +} diff --git a/src/Umbraco.Core/Events/SupersedeEventAttribute.cs b/src/Umbraco.Core/Events/SupersedeEventAttribute.cs new file mode 100644 index 0000000000..c7a14ea158 --- /dev/null +++ b/src/Umbraco.Core/Events/SupersedeEventAttribute.cs @@ -0,0 +1,20 @@ +using System; + +namespace Umbraco.Core.Events +{ + /// + /// This is used to know if the event arg attributed should supersede another event arg type when + /// tracking events for the same entity. If one event args supercedes another then the event args that have been superseded + /// will mean that the event will not be dispatched or the args will be filtered to exclude the entity. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + internal class SupersedeEventAttribute : Attribute + { + public Type SupersededEventArgsType { get; private set; } + + public SupersedeEventAttribute(Type supersededEventArgsType) + { + SupersededEventArgsType = supersededEventArgsType; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/TypedEventHandler.cs b/src/Umbraco.Core/Events/TypedEventHandler.cs index a8170190d4..113bb82f7e 100644 --- a/src/Umbraco.Core/Events/TypedEventHandler.cs +++ b/src/Umbraco.Core/Events/TypedEventHandler.cs @@ -2,6 +2,6 @@ using System; namespace Umbraco.Core.Events { - [Serializable] + [Serializable] public delegate void TypedEventHandler(TSender sender, TEventArgs e); } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs b/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs new file mode 100644 index 0000000000..324867a8f7 --- /dev/null +++ b/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Umbraco.Core.Packaging.Models; + +namespace Umbraco.Core.Events +{ + internal class UninstallPackageEventArgs : CancellableObjectEventArgs> + { + private readonly MetaData _packageMetaData; + + public UninstallPackageEventArgs(TEntity eventObject, bool canCancel) + : base(new[] { eventObject }, canCancel) + { + } + + public UninstallPackageEventArgs(TEntity eventObject, MetaData packageMetaData) + : base(new[] { eventObject }) + { + _packageMetaData = packageMetaData; + } + + public MetaData PackageMetaData + { + get { return _packageMetaData; } + } + + public IEnumerable UninstallationSummary + { + get { return EventObject; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Exceptions/ConnectionException.cs b/src/Umbraco.Core/Exceptions/ConnectionException.cs new file mode 100644 index 0000000000..3535dd52bf --- /dev/null +++ b/src/Umbraco.Core/Exceptions/ConnectionException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Umbraco.Core.Exceptions +{ + internal class ConnectionException : Exception + { + public ConnectionException(string message, Exception innerException) : base(message, innerException) + { + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Exceptions/DataOperationException.cs b/src/Umbraco.Core/Exceptions/DataOperationException.cs index 9a66e6a5be..60a3ccd4eb 100644 --- a/src/Umbraco.Core/Exceptions/DataOperationException.cs +++ b/src/Umbraco.Core/Exceptions/DataOperationException.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Exceptions public T Operation { get; private set; } public DataOperationException(T operation, string message) - :base(message) + : base(message) { Operation = operation; } diff --git a/src/Umbraco.Core/FileResources/Files.Designer.cs b/src/Umbraco.Core/FileResources/Files.Designer.cs index 456dae221f..500f9bf36c 100644 --- a/src/Umbraco.Core/FileResources/Files.Designer.cs +++ b/src/Umbraco.Core/FileResources/Files.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.0 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs new file mode 100644 index 0000000000..7a740ef58f --- /dev/null +++ b/src/Umbraco.Core/GuidUdi.cs @@ -0,0 +1,85 @@ +using System; +using System.ComponentModel; + +namespace Umbraco.Core +{ + /// + /// Represents a guid-based entity identifier. + /// + [TypeConverter(typeof(UdiTypeConverter))] + public class GuidUdi : Udi + { + /// + /// The guid part of the identifier. + /// + public Guid Guid { get; private set; } + + /// + /// Initializes a new instance of the GuidUdi class with an entity type and a guid. + /// + /// The entity type part of the udi. + /// The guid part of the udi. + public GuidUdi(string entityType, Guid guid) + : base(entityType, "umb://" + entityType + "/" + guid.ToString("N")) + { + Guid = guid; + } + + /// + /// Initializes a new instance of the GuidUdi class with an uri value. + /// + /// The uri value of the udi. + public GuidUdi(Uri uriValue) + : base(uriValue) + { + Guid = Guid.Parse(uriValue.AbsolutePath.TrimStart('/')); + } + + /// + /// Converts the string representation of an entity identifier into the equivalent GuidUdi instance. + /// + /// The string to convert. + /// A GuidUdi instance that contains the value that was parsed. + public new static GuidUdi Parse(string s) + { + var udi = Udi.Parse(s); + if (!(udi is GuidUdi)) + throw new FormatException("String \"" + s + "\" is not a guid entity id."); + return (GuidUdi)udi; + } + + public static bool TryParse(string s, out GuidUdi udi) + { + Udi tmp; + udi = null; + if (!TryParse(s, out tmp)) return false; + udi = tmp as GuidUdi; + return udi != null; + } + + public override bool Equals(object obj) + { + var other = obj as GuidUdi; + if (other == null) return false; + return EntityType == other.EntityType && Guid == other.Guid; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + public override bool IsRoot + { + get { return Guid == Guid.Empty; } + } + + /// + public GuidUdi EnsureClosed() + { + base.EnsureNotRoot(); + return this; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/HashCodeCombiner.cs b/src/Umbraco.Core/HashCodeCombiner.cs index b97a3cfacf..d3a55d5256 100644 --- a/src/Umbraco.Core/HashCodeCombiner.cs +++ b/src/Umbraco.Core/HashCodeCombiner.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; @@ -7,32 +6,32 @@ using System.Text; namespace Umbraco.Core { - /// - /// Used to create a hash code from multiple objects. - /// - /// - /// .Net has a class the same as this: System.Web.Util.HashCodeCombiner and of course it works for all sorts of things - /// which we've not included here as we just need a quick easy class for this in order to create a unique - /// hash of directories/files to see if they have changed. - /// - internal class HashCodeCombiner - { - private long _combinedHash = 5381L; + /// + /// Used to create a hash code from multiple objects. + /// + /// + /// .Net has a class the same as this: System.Web.Util.HashCodeCombiner and of course it works for all sorts of things + /// which we've not included here as we just need a quick easy class for this in order to create a unique + /// hash of directories/files to see if they have changed. + /// + internal class HashCodeCombiner + { + private long _combinedHash = 5381L; - internal void AddInt(int i) - { - _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; - } + internal void AddInt(int i) + { + _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; + } - internal void AddObject(object o) - { - AddInt(o.GetHashCode()); - } + internal void AddObject(object o) + { + AddInt(o.GetHashCode()); + } - internal void AddDateTime(DateTime d) - { - AddInt(d.GetHashCode()); - } + internal void AddDateTime(DateTime d) + { + AddInt(d.GetHashCode()); + } internal void AddString(string s) { @@ -40,61 +39,61 @@ namespace Umbraco.Core AddInt((StringComparer.InvariantCulture).GetHashCode(s)); } - internal void AddCaseInsensitiveString(string s) - { - if (s != null) - AddInt((StringComparer.InvariantCultureIgnoreCase).GetHashCode(s)); - } + internal void AddCaseInsensitiveString(string s) + { + if (s != null) + AddInt((StringComparer.InvariantCultureIgnoreCase).GetHashCode(s)); + } - internal void AddFileSystemItem(FileSystemInfo f) - { - //if it doesn't exist, don't proceed. - if (!f.Exists) - return; + internal void AddFileSystemItem(FileSystemInfo f) + { + //if it doesn't exist, don't proceed. + if (!f.Exists) + return; - AddCaseInsensitiveString(f.FullName); - AddDateTime(f.CreationTimeUtc); - AddDateTime(f.LastWriteTimeUtc); - - //check if it is a file or folder - var fileInfo = f as FileInfo; - if (fileInfo != null) - { - AddInt(fileInfo.Length.GetHashCode()); - } - - var dirInfo = f as DirectoryInfo; - if (dirInfo != null) - { - foreach (var d in dirInfo.GetFiles()) - { - AddFile(d); - } - foreach (var s in dirInfo.GetDirectories()) - { - AddFolder(s); - } - } - } + AddCaseInsensitiveString(f.FullName); + AddDateTime(f.CreationTimeUtc); + AddDateTime(f.LastWriteTimeUtc); - internal void AddFile(FileInfo f) - { - AddFileSystemItem(f); - } + //check if it is a file or folder + var fileInfo = f as FileInfo; + if (fileInfo != null) + { + AddInt(fileInfo.Length.GetHashCode()); + } - internal void AddFolder(DirectoryInfo d) - { - AddFileSystemItem(d); - } + var dirInfo = f as DirectoryInfo; + if (dirInfo != null) + { + foreach (var d in dirInfo.GetFiles()) + { + AddFile(d); + } + foreach (var s in dirInfo.GetDirectories()) + { + AddFolder(s); + } + } + } - /// - /// Returns the hex code of the combined hash code - /// - /// - internal string GetCombinedHashCode() - { - return _combinedHash.ToString("x", CultureInfo.InvariantCulture); - } + internal void AddFile(FileInfo f) + { + AddFileSystemItem(f); + } - } + internal void AddFolder(DirectoryInfo d) + { + AddFileSystemItem(d); + } + + /// + /// Returns the hex code of the combined hash code + /// + /// + internal string GetCombinedHashCode() + { + return _combinedHash.ToString("x", CultureInfo.InvariantCulture); + } + + } } diff --git a/src/Umbraco.Core/HashCodeHelper.cs b/src/Umbraco.Core/HashCodeHelper.cs new file mode 100644 index 0000000000..f0f281056d --- /dev/null +++ b/src/Umbraco.Core/HashCodeHelper.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; + +namespace Umbraco.Core +{ + /// + /// Borrowed from http://stackoverflow.com/a/2575444/694494 + /// + internal static class HashCodeHelper + { + public static int GetHashCode(T1 arg1, T2 arg2) + { + unchecked + { + return 31 * arg1.GetHashCode() + arg2.GetHashCode(); + } + } + + public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3) + { + unchecked + { + int hash = arg1.GetHashCode(); + hash = 31 * hash + arg2.GetHashCode(); + return 31 * hash + arg3.GetHashCode(); + } + } + + public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3, + T4 arg4) + { + unchecked + { + int hash = arg1.GetHashCode(); + hash = 31 * hash + arg2.GetHashCode(); + hash = 31 * hash + arg3.GetHashCode(); + return 31 * hash + arg4.GetHashCode(); + } + } + + public static int GetHashCode(T[] list) + { + unchecked + { + int hash = 0; + foreach (var item in list) + { + if (item == null) continue; + hash = 31 * hash + item.GetHashCode(); + } + return hash; + } + } + + public static int GetHashCode(IEnumerable list) + { + unchecked + { + int hash = 0; + foreach (var item in list) + { + if (item == null) continue; + hash = 31 * hash + item.GetHashCode(); + } + return hash; + } + } + + /// + /// Gets a hashcode for a collection for that the order of items + /// does not matter. + /// So {1, 2, 3} and {3, 2, 1} will get same hash code. + /// + public static int GetHashCodeForOrderNoMatterCollection( + IEnumerable list) + { + unchecked + { + int hash = 0; + int count = 0; + foreach (var item in list) + { + if (item == null) continue; + hash += item.GetHashCode(); + count++; + } + return 31 * hash + count.GetHashCode(); + } + } + + /// + /// Alternative way to get a hashcode is to use a fluent + /// interface like this:
+ /// return 0.CombineHashCode(field1).CombineHashCode(field2). + /// CombineHashCode(field3); + ///
+ public static int CombineHashCode(this int hashCode, T arg) + { + unchecked + { + return 31 * hashCode + arg.GetHashCode(); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/ICompletable.cs b/src/Umbraco.Core/ICompletable.cs new file mode 100644 index 0000000000..594d82b0ae --- /dev/null +++ b/src/Umbraco.Core/ICompletable.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Core +{ + public interface ICompletable : IDisposable + { + void Complete(); + } +} diff --git a/src/Umbraco.Core/IHttpContextAccessor.cs b/src/Umbraco.Core/IHttpContextAccessor.cs new file mode 100644 index 0000000000..a0873c78a9 --- /dev/null +++ b/src/Umbraco.Core/IHttpContextAccessor.cs @@ -0,0 +1,9 @@ +using System.Web; + +namespace Umbraco.Core +{ + public interface IHttpContextAccessor + { + HttpContextBase Value { get; } + } +} diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 2bdc6cc982..be09f1e310 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -37,8 +37,13 @@ namespace Umbraco.Core.IO throw new ArgumentException("Retries must be greater than zero"); } + // GetSize has been added to IFileSystem2 but not IFileSystem public static long GetSize(this IFileSystem fs, string path) { + var fs2 = fs as IFileSystem2; + if (fs2 != null) return fs2.GetSize(path); + + // this is implementing GetSize for IFileSystem, the old way using (var file = fs.OpenFile(path)) { return file.Length; diff --git a/src/Umbraco.Core/IO/FileSystemProviderManager.cs b/src/Umbraco.Core/IO/FileSystemProviderManager.cs index b2bcaee61f..df922fb2b0 100644 --- a/src/Umbraco.Core/IO/FileSystemProviderManager.cs +++ b/src/Umbraco.Core/IO/FileSystemProviderManager.cs @@ -4,17 +4,28 @@ using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Reflection; -using System.Text; -using Umbraco.Core.CodeAnnotations; using Umbraco.Core.Configuration; +using Umbraco.Core.Scoping; namespace Umbraco.Core.IO -{ +{ public class FileSystemProviderManager { private readonly FileSystemProvidersSection _config; + private readonly ConcurrentSet _wrappers = new ConcurrentSet(); - #region Singleton + private readonly ConcurrentDictionary _providerLookup = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _filesystems = new ConcurrentDictionary(); + + private ShadowWrapper _macroPartialFileSystem; + private ShadowWrapper _partialViewsFileSystem; + private ShadowWrapper _stylesheetsFileSystem; + private ShadowWrapper _scriptsFileSystem; + private ShadowWrapper _xsltFileSystem; + private ShadowWrapper _masterPagesFileSystem; + private ShadowWrapper _mvcViewsFileSystem; + + #region Singleton & Constructor private static readonly FileSystemProviderManager Instance = new FileSystemProviderManager(); @@ -23,109 +34,268 @@ namespace Umbraco.Core.IO get { return Instance; } } - #endregion + // for tests only, totally unsafe + internal void Reset() + { + _wrappers.Clear(); + _providerLookup.Clear(); + _filesystems.Clear(); + CreateWellKnownFileSystems(); + } - #region Constructors + private IScopeProviderInternal ScopeProvider + { + // this is bad, but enough for now, and we'll refactor + // in v8 when we'll get rid of this class' singleton + // beware: means that we capture the "current" scope provider - take care in tests! + get { return ApplicationContext.Current == null ? null : ApplicationContext.Current.ScopeProvider as IScopeProviderInternal; } + } internal FileSystemProviderManager() { - _config = (FileSystemProvidersSection)ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders"); + _config = (FileSystemProvidersSection) ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders"); + CreateWellKnownFileSystems(); + } + + private void CreateWellKnownFileSystems() + { + var macroPartialFileSystem = new PhysicalFileSystem(SystemDirectories.MacroPartials); + var partialViewsFileSystem = new PhysicalFileSystem(SystemDirectories.PartialViews); + var stylesheetsFileSystem = new PhysicalFileSystem(SystemDirectories.Css); + var scriptsFileSystem = new PhysicalFileSystem(SystemDirectories.Scripts); + var xsltFileSystem = new PhysicalFileSystem(SystemDirectories.Xslt); + var masterPagesFileSystem = new PhysicalFileSystem(SystemDirectories.Masterpages); + var mvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews); + + _macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "Views/MacroPartials", ScopeProvider); + _partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "Views/Partials", ScopeProvider); + _stylesheetsFileSystem = new ShadowWrapper(stylesheetsFileSystem, "css", ScopeProvider); + _scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", ScopeProvider); + _xsltFileSystem = new ShadowWrapper(xsltFileSystem, "xslt", ScopeProvider); + _masterPagesFileSystem = new ShadowWrapper(masterPagesFileSystem, "masterpages", ScopeProvider); + _mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "Views", ScopeProvider); + + // filesystems obtained from GetFileSystemProvider are already wrapped and do not need to be wrapped again + MediaFileSystem = GetFileSystemProvider(); } #endregion - /// - /// used to cache the lookup of how to construct this object so we don't have to reflect each time. - /// - private class ProviderConstructionInfo + #region Well-Known FileSystems + + public IFileSystem2 MacroPartialsFileSystem { get { return _macroPartialFileSystem; } } + public IFileSystem2 PartialViewsFileSystem { get { return _partialViewsFileSystem; } } + public IFileSystem2 StylesheetsFileSystem { get { return _stylesheetsFileSystem; } } + public IFileSystem2 ScriptsFileSystem { get { return _scriptsFileSystem; } } + public IFileSystem2 XsltFileSystem { get { return _xsltFileSystem; } } + public IFileSystem2 MasterPagesFileSystem { get { return _mvcViewsFileSystem; } } + public IFileSystem2 MvcViewsFileSystem { get { return _mvcViewsFileSystem; } } + public MediaFileSystem MediaFileSystem { get; private set; } + + #endregion + + #region Providers + + /// + /// used to cache the lookup of how to construct this object so we don't have to reflect each time. + /// + private class ProviderConstructionInfo { public object[] Parameters { get; set; } public ConstructorInfo Constructor { get; set; } - public string ProviderAlias { get; set; } + //public string ProviderAlias { get; set; } } - private readonly ConcurrentDictionary _providerLookup = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _wrappedProviderLookup = new ConcurrentDictionary(); - /// - /// Returns the underlying (non-typed) file system provider for the alias specified + /// Gets an underlying (non-typed) filesystem supporting a strongly-typed filesystem. /// - /// - /// - /// - /// It is recommended to use the typed GetFileSystemProvider method instead to get a strongly typed provider instance. - /// + /// The alias of the strongly-typed filesystem. + /// The non-typed filesystem supporting the strongly-typed filesystem with the specified alias. + /// This method should not be used directly, used instead. public IFileSystem GetUnderlyingFileSystemProvider(string alias) { - //either get the constructor info from cache or create it and add to cache - var ctorInfo = _providerLookup.GetOrAdd(alias, s => - { - var providerConfig = _config.Providers[s]; - if (providerConfig == null) - throw new ArgumentException(string.Format("No provider found with the alias '{0}'", s)); - - var providerType = Type.GetType(providerConfig.Type); - if (providerType == null) - throw new InvalidOperationException(string.Format("Could not find type '{0}'", providerConfig.Type)); - - if (providerType.IsAssignableFrom(typeof (IFileSystem))) - throw new InvalidOperationException(string.Format("The type '{0}' does not implement IFileSystem", providerConfig.Type)); - - var paramCount = providerConfig.Parameters != null ? providerConfig.Parameters.Count : 0; - var constructor = providerType.GetConstructors() - .SingleOrDefault(x => x.GetParameters().Count() == paramCount - && x.GetParameters().All(y => providerConfig.Parameters.AllKeys.Contains(y.Name))); - if (constructor == null) - throw new InvalidOperationException(string.Format("Could not find constructor for type '{0}' which accepts {1} parameters", providerConfig.Type, paramCount)); - - var parameters = new object[paramCount]; - for (var i = 0; i < paramCount; i++) - parameters[i] = providerConfig.Parameters[providerConfig.Parameters.AllKeys[i]].Value; - - //return the new constructor info class to cache so we don't have to do this again. - return new ProviderConstructionInfo() - { - Constructor = constructor, - Parameters = parameters, - ProviderAlias = s - }; - }); - - var fs = (IFileSystem)ctorInfo.Constructor.Invoke(ctorInfo.Parameters); - return fs; + return GetUnderlyingFileSystemProvider(alias, null); } /// - /// Returns the strongly typed file system provider + /// Gets an underlying (non-typed) filesystem supporting a strongly-typed filesystem. /// - /// - /// - public TProviderTypeFilter GetFileSystemProvider() - where TProviderTypeFilter : FileSystemWrapper + /// The alias of the strongly-typed filesystem. + /// /// A fallback creator for the filesystem. + /// The non-typed filesystem supporting the strongly-typed filesystem with the specified alias. + /// This method should not be used directly, used instead. + private IFileSystem GetUnderlyingFileSystemProvider(string alias, Func fallback) { - //get the alias for the type from cache or look it up and add it to the cache, then we don't have to reflect each time - var alias = _wrappedProviderLookup.GetOrAdd(typeof (TProviderTypeFilter), fsType => - { - //validate the ctor - var constructor = fsType.GetConstructors() - .SingleOrDefault(x => - x.GetParameters().Count() == 1 && TypeHelper.IsTypeAssignableFrom(x.GetParameters().Single().ParameterType)); - if (constructor == null) - throw new InvalidOperationException("The type of " + fsType + " must inherit from FileSystemWrapper and must have a constructor that accepts one parameter of type " + typeof(IFileSystem)); + // either get the constructor info from cache or create it and add to cache + var ctorInfo = _providerLookup.GetOrAdd(alias, _ => GetUnderlyingFileSystemCtor(alias, fallback)); + return ctorInfo == null ? fallback() : (IFileSystem) ctorInfo.Constructor.Invoke(ctorInfo.Parameters); + } - var attr = - (FileSystemProviderAttribute)fsType.GetCustomAttributes(typeof(FileSystemProviderAttribute), false). - SingleOrDefault(); + private IFileSystem GetUnderlyingFileSystemNoCache(string alias, Func fallback) + { + var ctorInfo = GetUnderlyingFileSystemCtor(alias, fallback); + return ctorInfo == null ? fallback() : (IFileSystem) ctorInfo.Constructor.Invoke(ctorInfo.Parameters); + } - if (attr == null) - throw new InvalidOperationException(string.Format("The provider type filter '{0}' is missing the required FileSystemProviderAttribute", typeof(FileSystemProviderAttribute).FullName)); + private ProviderConstructionInfo GetUnderlyingFileSystemCtor(string alias, Func fallback) + { + // get config + var providerConfig = _config.Providers[alias]; + if (providerConfig == null) + { + if (fallback != null) return null; + throw new ArgumentException(string.Format("No provider found with alias {0}.", alias)); + } - return attr.Alias; - }); - - var innerFs = GetUnderlyingFileSystemProvider(alias); - var outputFs = Activator.CreateInstance(typeof (TProviderTypeFilter), innerFs); - return (TProviderTypeFilter)outputFs; + // get the filesystem type + var providerType = Type.GetType(providerConfig.Type); + if (providerType == null) + throw new InvalidOperationException(string.Format("Could not find type {0}.", providerConfig.Type)); + + // ensure it implements IFileSystem + if (providerType.IsAssignableFrom(typeof(IFileSystem))) + throw new InvalidOperationException(string.Format("Type {0} does not implement IFileSystem.", providerType.FullName)); + + // find a ctor matching the config parameters + var paramCount = providerConfig.Parameters != null ? providerConfig.Parameters.Count : 0; + var constructor = providerType.GetConstructors().SingleOrDefault(x + => x.GetParameters().Length == paramCount && x.GetParameters().All(y => providerConfig.Parameters.AllKeys.Contains(y.Name))); + if (constructor == null) + throw new InvalidOperationException(string.Format("Type {0} has no ctor matching the {1} configuration parameter(s).", providerType.FullName, paramCount)); + + var parameters = new object[paramCount]; + if (providerConfig.Parameters != null) // keeps ReSharper happy + for (var i = 0; i < paramCount; i++) + parameters[i] = providerConfig.Parameters[providerConfig.Parameters.AllKeys[i]].Value; + + return new ProviderConstructionInfo + { + Constructor = constructor, + Parameters = parameters, + //ProviderAlias = s + }; + } + + /// + /// Gets a strongly-typed filesystem. + /// + /// The type of the filesystem. + /// A strongly-typed filesystem of the specified type. + /// + /// Ideally, this should cache the instances, but that would break backward compatibility, so we + /// only do it for our own MediaFileSystem - for everything else, it's the responsibility of the caller + /// to ensure that they maintain singletons. This is important for singletons, as each filesystem maintains + /// its own shadow and having multiple instances would lead to inconsistencies. + /// Note that any filesystem created by this method *after* shadowing begins, will *not* be + /// shadowing (and an exception will be thrown by the ShadowWrapper). + /// + public TFileSystem GetFileSystemProvider() + where TFileSystem : FileSystemWrapper + { + return GetFileSystemProvider(null); + } + + /// + /// Gets a strongly-typed filesystem. + /// + /// The type of the filesystem. + /// A fallback creator for the inner filesystem. + /// A strongly-typed filesystem of the specified type. + /// + /// The fallback creator is used only if nothing is configured. + /// Ideally, this should cache the instances, but that would break backward compatibility, so we + /// only do it for our own MediaFileSystem - for everything else, it's the responsibility of the caller + /// to ensure that they maintain singletons. This is important for singletons, as each filesystem maintains + /// its own shadow and having multiple instances would lead to inconsistencies. + /// Note that any filesystem created by this method *after* shadowing begins, will *not* be + /// shadowing (and an exception will be thrown by the ShadowWrapper). + /// + public TFileSystem GetFileSystemProvider(Func fallback) + where TFileSystem : FileSystemWrapper + { + var alias = GetFileSystemAlias(); + return (TFileSystem) _filesystems.GetOrAdd(alias, _ => + { + // gets the inner fs, create the strongly-typed fs wrapping the inner fs, register & return + // so we are double-wrapping here + // could be optimized by having FileSystemWrapper inherit from ShadowWrapper, maybe + var innerFs = GetUnderlyingFileSystemNoCache(alias, fallback); + var shadowWrapper = new ShadowWrapper(innerFs, "typed/" + alias, ScopeProvider); + var fs = (IFileSystem2) Activator.CreateInstance(typeof (TFileSystem), shadowWrapper); + _wrappers.Add(shadowWrapper); // keeping a reference to the wrapper + return fs; + }); + } + + private string GetFileSystemAlias() + { + var fsType = typeof(TFileSystem); + + // validate the ctor + var constructor = fsType.GetConstructors().SingleOrDefault(x + => x.GetParameters().Length == 1 && TypeHelper.IsTypeAssignableFrom(x.GetParameters().Single().ParameterType)); + if (constructor == null) + throw new InvalidOperationException("Type " + fsType.FullName + " must inherit from FileSystemWrapper and have a constructor that accepts one parameter of type " + typeof(IFileSystem).FullName + "."); + + // find the attribute and get the alias + var attr = (FileSystemProviderAttribute)fsType.GetCustomAttributes(typeof(FileSystemProviderAttribute), false).SingleOrDefault(); + if (attr == null) + throw new InvalidOperationException("Type " + fsType.FullName + "is missing the required FileSystemProviderAttribute."); + + return attr.Alias; + } + + #endregion + + #region Shadow + + internal ICompletable Shadow(Guid id) + { + var typed = _wrappers.ToArray(); + var wrappers = new ShadowWrapper[typed.Length + 7]; + var i = 0; + while (i < typed.Length) wrappers[i] = typed[i++]; + wrappers[i++] = _macroPartialFileSystem; + wrappers[i++] = _partialViewsFileSystem; + wrappers[i++] = _stylesheetsFileSystem; + wrappers[i++] = _scriptsFileSystem; + wrappers[i++] = _xsltFileSystem; + wrappers[i++] = _masterPagesFileSystem; + wrappers[i] = _mvcViewsFileSystem; + + return new ShadowFileSystems(id, wrappers); + } + + #endregion + + private class ConcurrentSet + where T : class + { + private readonly HashSet _set = new HashSet(); + + public void Add(T item) + { + lock (_set) + { + _set.Add(item); + } + } + + public void Clear() + { + lock (_set) + { + _set.Clear(); + } + } + + public T[] ToArray() + { + lock (_set) + { + return _set.ToArray(); + } + } } } } diff --git a/src/Umbraco.Core/IO/FileSystemWrapper.cs b/src/Umbraco.Core/IO/FileSystemWrapper.cs index ba2ad8f48b..97b8a2f8f6 100644 --- a/src/Umbraco.Core/IO/FileSystemWrapper.cs +++ b/src/Umbraco.Core/IO/FileSystemWrapper.cs @@ -14,93 +14,119 @@ namespace Umbraco.Core.IO /// /// This abstract class just wraps the 'real' IFileSystem object passed in to its constructor. /// - public abstract class FileSystemWrapper : IFileSystem + public abstract class FileSystemWrapper : IFileSystem2 { - private readonly IFileSystem _wrapped; - - protected FileSystemWrapper(IFileSystem wrapped) + protected FileSystemWrapper(IFileSystem wrapped) { - _wrapped = wrapped; + Wrapped = wrapped; } - public IEnumerable GetDirectories(string path) + internal IFileSystem Wrapped { get; set; } + + public IEnumerable GetDirectories(string path) { - return _wrapped.GetDirectories(path); + return Wrapped.GetDirectories(path); } public void DeleteDirectory(string path) { - _wrapped.DeleteDirectory(path); + Wrapped.DeleteDirectory(path); } public void DeleteDirectory(string path, bool recursive) { - _wrapped.DeleteDirectory(path, recursive); + Wrapped.DeleteDirectory(path, recursive); } public bool DirectoryExists(string path) { - return _wrapped.DirectoryExists(path); + return Wrapped.DirectoryExists(path); } public void AddFile(string path, Stream stream) { - _wrapped.AddFile(path, stream); + Wrapped.AddFile(path, stream); } - public void AddFile(string path, Stream stream, bool overrideIfExists) + public void AddFile(string path, Stream stream, bool overrideExisting) { - _wrapped.AddFile(path, stream, overrideIfExists); + Wrapped.AddFile(path, stream, overrideExisting); } public IEnumerable GetFiles(string path) { - return _wrapped.GetFiles(path); + return Wrapped.GetFiles(path); } public IEnumerable GetFiles(string path, string filter) { - return _wrapped.GetFiles(path, filter); + return Wrapped.GetFiles(path, filter); } public Stream OpenFile(string path) { - return _wrapped.OpenFile(path); + return Wrapped.OpenFile(path); } public void DeleteFile(string path) { - _wrapped.DeleteFile(path); + Wrapped.DeleteFile(path); } public bool FileExists(string path) { - return _wrapped.FileExists(path); + return Wrapped.FileExists(path); } public string GetRelativePath(string fullPathOrUrl) { - return _wrapped.GetRelativePath(fullPathOrUrl); + return Wrapped.GetRelativePath(fullPathOrUrl); } public string GetFullPath(string path) { - return _wrapped.GetFullPath(path); + return Wrapped.GetFullPath(path); } public string GetUrl(string path) { - return _wrapped.GetUrl(path); + return Wrapped.GetUrl(path); } public DateTimeOffset GetLastModified(string path) { - return _wrapped.GetLastModified(path); + return Wrapped.GetLastModified(path); } public DateTimeOffset GetCreated(string path) { - return _wrapped.GetCreated(path); + return Wrapped.GetCreated(path); } - } + + // explicitely implementing - not breaking + long IFileSystem2.GetSize(string path) + { + var wrapped2 = Wrapped as IFileSystem2; + return wrapped2 == null ? Wrapped.GetSize(path) : wrapped2.GetSize(path); + } + + // explicitely implementing - not breaking + bool IFileSystem2.CanAddPhysical + { + get + { + var wrapped2 = Wrapped as IFileSystem2; + return wrapped2 != null && wrapped2.CanAddPhysical; + } + } + + // explicitely implementing - not breaking + void IFileSystem2.AddFile(string path, string physicalPath, bool overrideIfExists, bool copy) + { + var wrapped2 = Wrapped as IFileSystem2; + if (wrapped2 == null) + throw new NotSupportedException(); + wrapped2.AddFile(path, physicalPath, overrideIfExists, copy); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 3b38432c5c..0d96dd0af1 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -4,42 +4,161 @@ using System.IO; namespace Umbraco.Core.IO { - //TODO: There is no way to create a directory here without creating a file in a directory and then deleting it - //TODO: Should probably implement a rename? - - public interface IFileSystem + /// + /// Provides methods allowing the manipulation of files within an Umbraco application. + /// + public interface IFileSystem { + /// + /// Gets all directories matching the given path. + /// + /// The path to the directories. + /// + /// The representing the matched directories. + /// IEnumerable GetDirectories(string path); + /// + /// Deletes the specified directory. + /// + /// The name of the directory to remove. void DeleteDirectory(string path); + /// + /// Deletes the specified directory and, if indicated, any subdirectories and files in the directory. + /// + /// Azure blob storage has no real concept of directories so deletion is always recursive. + /// The name of the directory to remove. + /// Whether to remove directories, subdirectories, and files in path. void DeleteDirectory(string path, bool recursive); + /// + /// Determines whether the specified directory exists. + /// + /// The directory to check. + /// + /// True if the directory exists and the user has permission to view it; otherwise false. + /// bool DirectoryExists(string path); - + + /// + /// Adds a file to the file system. + /// + /// The path to the given file. + /// The containing the file contents. void AddFile(string path, Stream stream); + /// + /// Adds a file to the file system. + /// + /// The path to the given file. + /// The containing the file contents. + /// Whether to override the file if it already exists. void AddFile(string path, Stream stream, bool overrideIfExists); + /// + /// Gets all files matching the given path. + /// + /// The path to the files. + /// + /// The representing the matched files. + /// IEnumerable GetFiles(string path); + /// + /// Gets all files matching the given path and filter. + /// + /// The path to the files. + /// A filter that allows the querying of file extension. *.jpg + /// + /// The representing the matched files. + /// IEnumerable GetFiles(string path, string filter); + /// + /// Gets a representing the file at the gieven path. + /// + /// The path to the file. + /// + /// . + /// Stream OpenFile(string path); + /// + /// Deletes the specified file. + /// + /// The name of the file to remove. void DeleteFile(string path); + /// + /// Determines whether the specified file exists. + /// + /// The file to check. + /// + /// True if the file exists and the user has permission to view it; otherwise false. + /// bool FileExists(string path); - + /// + /// Returns the application relative path to the file. + /// + /// The full path or url. + /// + /// The representing the relative path. + /// string GetRelativePath(string fullPathOrUrl); + /// + /// Gets the full qualified path to the file. + /// + /// The file to return the full path for. + /// + /// The representing the full path. + /// string GetFullPath(string path); + /// + /// Returns the application relative url to the file. + /// + /// The path to return the url for. + /// + /// representing the relative url. + /// string GetUrl(string path); + /// + /// Gets the last modified date/time of the file, expressed as a UTC value. + /// + /// The path to the file. + /// + /// . + /// DateTimeOffset GetLastModified(string path); + /// + /// Gets the created date/time of the file, expressed as a UTC value. + /// + /// The path to the file. + /// + /// . + /// DateTimeOffset GetCreated(string path); } + + // this should be part of IFileSystem but we don't want to change the interface + public interface IFileSystem2 : IFileSystem + { + long GetSize(string path); + + bool CanAddPhysical { get; } + + void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false); + + // TODO: implement these + // + //void CreateDirectory(string path); + // + //// move or rename, directory or file + //void Move(string source, string target); + } } diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 286acf0285..2ee0463435 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.Reflection; using System.IO; using System.Configuration; +using System.Linq; using System.Web; using System.Text.RegularExpressions; using System.Web.Hosting; using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; namespace Umbraco.Core.IO { @@ -351,7 +351,54 @@ namespace Umbraco.Core.IO writer.Write(contents); } } - - } + + } + + /// + /// Checks if a given path is a full path including drive letter + /// + /// + /// + // From: http://stackoverflow.com/a/35046453/5018 + internal static bool IsFullPath(this string path) + { + return string.IsNullOrWhiteSpace(path) == false + && path.IndexOfAny(Path.GetInvalidPathChars().ToArray()) == -1 + && Path.IsPathRooted(path) + && Path.GetPathRoot(path).Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) == false; + } + + /// + /// Get properly formatted relative path from an existing absolute or relative path + /// + /// + /// + internal static string GetRelativePath(this string path) + { + if (path.IsFullPath()) + { + var rootDirectory = GetRootDirectorySafe(); + var relativePath = path.ToLowerInvariant().Replace(rootDirectory.ToLowerInvariant(), string.Empty); + path = relativePath; + } + + return path.EnsurePathIsApplicationRootPrefixed(); + } + + /// + /// Ensures that a path has `~/` as prefix + /// + /// + /// + internal static string EnsurePathIsApplicationRootPrefixed(this string path) + { + if (path.StartsWith("~/")) + return path; + if (path.StartsWith("/") == false && path.StartsWith("\\") == false) + path = string.Format("/{0}", path); + if (path.StartsWith("~") == false) + path = string.Format("~{0}", path); + return path; + } } } diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index b35264d752..c57cf94cef 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -1,9 +1,19 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; +using Umbraco.Core.Media; +using Umbraco.Core.Media.Exif; +using Umbraco.Core.Models; namespace Umbraco.Core.IO { @@ -14,59 +24,701 @@ namespace Umbraco.Core.IO public class MediaFileSystem : FileSystemWrapper { private readonly IContentSection _contentConfig; + private readonly UploadAutoFillProperties _uploadAutoFillProperties; + private readonly ILogger _logger; - public MediaFileSystem(IFileSystem wrapped) - : this(wrapped, UmbracoConfig.For.UmbracoSettings().Content) - { - } + private readonly object _folderCounterLock = new object(); + private long _folderCounter; + private bool _folderCounterInitialized; - public MediaFileSystem(IFileSystem wrapped, IContentSection contentConfig) : base(wrapped) + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + private static readonly Dictionary DefaultSizes = new Dictionary { + { 100, "thumb" }, + { 500, "big-thumb" } + }; + + public MediaFileSystem(IFileSystem wrapped) + : this(wrapped, UmbracoConfig.For.UmbracoSettings().Content, ApplicationContext.Current.ProfilingLogger.Logger) + { } + + public MediaFileSystem(IFileSystem wrapped, IContentSection contentConfig, ILogger logger) + : base(wrapped) + { + _logger = logger; _contentConfig = contentConfig; + _uploadAutoFillProperties = new UploadAutoFillProperties(this, logger, contentConfig); } - public string GetRelativePath(int propertyId, string fileName) + internal UploadAutoFillProperties UploadAutoFillProperties { get { return _uploadAutoFillProperties; } } + + // note - this is currently experimental / being developed + //public static bool UseTheNewMediaPathScheme { get; set; } + public const bool UseTheNewMediaPathScheme = false; + + // none of the methods below are used in Core anymore + + [Obsolete("This low-level method should NOT exist.")] + public string GetRelativePath(int propertyId, string fileName) { - var seperator = _contentConfig.UploadAllowDirectories + var sep = _contentConfig.UploadAllowDirectories ? Path.DirectorySeparatorChar : '-'; - return propertyId.ToString(CultureInfo.InvariantCulture) + seperator + fileName; + return propertyId.ToString(CultureInfo.InvariantCulture) + sep + fileName; } + [Obsolete("This low-level method should NOT exist.", false)] public string GetRelativePath(string subfolder, string fileName) { - var seperator = _contentConfig.UploadAllowDirectories + var sep = _contentConfig.UploadAllowDirectories ? Path.DirectorySeparatorChar : '-'; - return subfolder + seperator + fileName; + return subfolder + sep + fileName; } - public IEnumerable GetThumbnails(string path) - { - var parentDirectory = Path.GetDirectoryName(path); - var extension = Path.GetExtension(path); + #region Media Path - return GetFiles(parentDirectory) - .Where(x => x.StartsWith(path.TrimEnd(extension) + "_thumb") || x.StartsWith(path.TrimEnd(extension) + "_big-thumb")) - .ToList(); - } + /// + /// Gets the file path of a media file. + /// + /// The file name. + /// The unique identifier of the content/media owning the file. + /// The unique identifier of the property type owning the file. + /// The filesystem-relative path to the media file. + /// With the old media path scheme, this CREATES a new media path each time it is invoked. + public string GetMediaPath(string filename, Guid cuid, Guid puid) + { + filename = Path.GetFileName(filename); + if (filename == null) throw new ArgumentException("Cannot become a safe filename.", "filename"); + filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); - public void DeleteFile(string path, bool deleteThumbnails) - { - DeleteFile(path); + string folder; + if (UseTheNewMediaPathScheme == false) + { + // old scheme: filepath is "/" OR "-" + // default media filesystem maps to "~/media/" + folder = GetNextFolder(); + } + else + { + // new scheme: path is "/" where xuid is a combination of cuid and puid + // default media filesystem maps to "~/media/" + // assumes that cuid and puid keys can be trusted - and that a single property type + // for a single content cannot store two different files with the same name + folder = Combine(cuid, puid).ToHexString(/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... + } - if (deleteThumbnails == false) - return; + var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories + ? Path.Combine(folder, filename) + : folder + "-" + filename; - DeleteThumbnails(path); - } + return filepath; + } - public void DeleteThumbnails(string path) - { - GetThumbnails(path) - .ForEach(DeleteFile); - } - } + private static byte[] Combine(Guid guid1, Guid guid2) + { + var bytes1 = guid1.ToByteArray(); + var bytes2 = guid2.ToByteArray(); + var bytes = new byte[bytes1.Length]; + for (var i = 0; i < bytes1.Length; i++) + bytes[i] = (byte)(bytes1[i] ^ bytes2[i]); + return bytes; + } + + /// + /// Gets the file path of a media file. + /// + /// The file name. + /// A previous file path. + /// The unique identifier of the content/media owning the file. + /// The unique identifier of the property type owning the file. + /// The filesystem-relative path to the media file. + /// In the old, legacy, number-based scheme, we try to re-use the media folder + /// specified by . Else, we CREATE a new one. Each time we are invoked. + public string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid) + { + if (UseTheNewMediaPathScheme || string.IsNullOrWhiteSpace(prevpath)) + return GetMediaPath(filename, cuid, puid); + + filename = Path.GetFileName(filename); + if (filename == null) throw new ArgumentException("Cannot become a safe filename.", "filename"); + filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); + + // old scheme, with a previous path + // prevpath should be "/" OR "-" + // and we want to reuse the "" part, so try to find it + + var sep = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? "/" : "-"; + var pos = prevpath.IndexOf(sep, StringComparison.Ordinal); + var s = pos > 0 ? prevpath.Substring(0, pos) : null; + int ignored; + + var folder = (pos > 0 && int.TryParse(s, out ignored)) ? s : GetNextFolder(); + + // ReSharper disable once AssignNullToNotNullAttribute + var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories + ? Path.Combine(folder, filename) + : folder + "-" + filename; + + return filepath; + } + + /// + /// Gets the next media folder in the original number-based scheme. + /// + /// + /// Should be private, is internal for legacy FileHandlerData which is obsolete. + internal string GetNextFolder() + { + EnsureFolderCounterIsInitialized(); + return Interlocked.Increment(ref _folderCounter).ToString(CultureInfo.InvariantCulture); + } + + private void EnsureFolderCounterIsInitialized() + { + lock (_folderCounterLock) + { + if (_folderCounterInitialized) return; + + _folderCounter = 1000; // seed + var directories = GetDirectories(""); + foreach (var directory in directories) + { + long folderNumber; + if (long.TryParse(directory, out folderNumber) && folderNumber > _folderCounter) + _folderCounter = folderNumber; + } + + // note: not multi-domains ie LB safe as another domain could create directories + // while we read and parse them - don't fix, move to new scheme eventually + + _folderCounterInitialized = true; + } + } + + #endregion + + #region Associated Media Files + + /// + /// Stores a media file. + /// + /// The content item owning the media file. + /// The property type owning the media file. + /// The media file name. + /// A stream containing the media bytes. + /// An optional filesystem-relative filepath to the previous media file. + /// The filesystem-relative filepath to the media file. + /// + /// The file is considered "owned" by the content/propertyType. + /// If an is provided then that file (and thumbnails) is deleted + /// before the new file is saved, and depending on the media path scheme, the folder + /// may be reused for the new file. + /// + public string StoreFile(IContentBase content, PropertyType propertyType, string filename, Stream filestream, string oldpath) + { + if (content == null) throw new ArgumentNullException("content"); + if (propertyType == null) throw new ArgumentNullException("propertyType"); + if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentException("Null or empty.", "filename"); + if (filestream == null) throw new ArgumentNullException("filestream"); + + // clear the old file, if any + if (string.IsNullOrWhiteSpace(oldpath) == false) + DeleteFile(oldpath, true); + + // get the filepath, store the data + // use oldpath as "prevpath" to try and reuse the folder, in original number-based scheme + var filepath = GetMediaPath(filename, oldpath, content.Key, propertyType.Key); + AddFile(filepath, filestream); + return filepath; + } + + /// + /// Clears a media file. + /// + /// The filesystem-relative path to the media file. + public new void DeleteFile(string filepath) + { + DeleteFile(filepath, true); + } + + /// + /// Copies a media file. + /// + /// The content item owning the copy of the media file. + /// The property type owning the copy of the media file. + /// The filesystem-relative path to the source media file. + /// The filesystem-relative path to the copy of the media file. + public string CopyFile(IContentBase content, PropertyType propertyType, string sourcepath) + { + if (content == null) throw new ArgumentNullException("content"); + if (propertyType == null) throw new ArgumentNullException("propertyType"); + if (string.IsNullOrWhiteSpace(sourcepath)) throw new ArgumentException("Null or empty.", "sourcepath"); + + // ensure we have a file to copy + if (FileExists(sourcepath) == false) return null; + + // get the filepath + var filename = Path.GetFileName(sourcepath); + var filepath = GetMediaPath(filename, content.Key, propertyType.Key); + this.CopyFile(sourcepath, filepath); + + return filepath; + } + + /// + /// Gets or creates a property for a content item. + /// + /// The content item. + /// The property type alias. + /// The property. + private static Property GetProperty(IContentBase content, string propertyTypeAlias) + { + var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + if (property != null) return property; + + var propertyType = content.GetContentType().CompositionPropertyTypes + .FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + if (propertyType == null) + throw new Exception("No property type exists with alias " + propertyTypeAlias + "."); + + property = new Property(propertyType); + content.Properties.Add(property); + return property; + } + + public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filename, Stream filestream) + { + var property = GetProperty(content, propertyTypeAlias); + var svalue = property.Value as string; + var oldpath = svalue == null ? null : GetRelativePath(svalue); + var filepath = StoreFile(content, property.PropertyType, filename, filestream, oldpath); + property.Value = GetUrl(filepath); + SetUploadFile(content, property, filepath, filestream); + } + + public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filepath) + { + var property = GetProperty(content, propertyTypeAlias); + var svalue = property.Value as string; + var oldpath = svalue == null ? null : GetRelativePath(svalue); + if (string.IsNullOrWhiteSpace(oldpath) == false && oldpath != filepath) + DeleteFile(oldpath, true); + property.Value = GetUrl(filepath); + using (var filestream = OpenFile(filepath)) + { + SetUploadFile(content, property, filepath, filestream); + } + } + + /// + /// Sets a file for the FileUpload property editor and populates autofill properties + /// + /// + /// + /// + /// + private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream) + { + // will use filepath for extension, and filestream for length + _uploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream); + } + + #endregion + + #region Image + + /// + /// Gets a value indicating whether the file extension corresponds to an image. + /// + /// The file extension. + /// A value indicating whether the file extension corresponds to an image. + public bool IsImageFile(string extension) + { + if (extension == null) return false; + extension = extension.TrimStart('.'); + return UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(extension); + } + + /// + /// Gets the dimensions of an image. + /// + /// A stream containing the image bytes. + /// The dimension of the image. + /// First try with EXIF as it is faster and does not load the entire image + /// in memory. Fallback to GDI which means loading the image in memory and thus + /// use potentially large amounts of memory. + public Size GetDimensions(Stream stream) + { + //Try to load with exif + try + { + var jpgInfo = ImageFile.FromStream(stream); + + if (jpgInfo.Format != ImageFileFormat.Unknown + && jpgInfo.Properties.ContainsKey(ExifTag.PixelYDimension) + && jpgInfo.Properties.ContainsKey(ExifTag.PixelXDimension)) + { + var height = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelYDimension].Value); + var width = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelXDimension].Value); + if (height > 0 && width > 0) + { + return new Size(width, height); + } + } + } + catch (Exception) + { + //We will just swallow, just means we can't read exif data, we don't want to log an error either + } + + //we have no choice but to try to read in via GDI + using (var image = Image.FromStream(stream)) + { + + var fileWidth = image.Width; + var fileHeight = image.Height; + return new Size(fileWidth, fileHeight); + } + } + + #endregion + + #region Manage thumbnails + + // note: this does not find 'custom' thumbnails? + // will find _thumb and _big-thumb but NOT _custom? + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public IEnumerable GetThumbnails(string path) + { + var parentDirectory = Path.GetDirectoryName(path); + var extension = Path.GetExtension(path); + + return GetFiles(parentDirectory) + .Where(x => x.StartsWith(path.TrimEnd(extension) + "_thumb") || x.StartsWith(path.TrimEnd(extension) + "_big-thumb")) + .ToList(); + } + + public void DeleteFile(string path, bool deleteThumbnails) + { + base.DeleteFile(path); + + if (deleteThumbnails == false) + return; + + DeleteThumbnails(path); + } + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public void DeleteThumbnails(string path) + { + GetThumbnails(path) + .ForEach(x => base.DeleteFile(x)); + } + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public void CopyThumbnails(string sourcePath, string targetPath) + { + var targetPathBase = Path.GetDirectoryName(targetPath) ?? ""; + foreach (var sourceThumbPath in GetThumbnails(sourcePath)) + { + var sourceThumbFilename = Path.GetFileName(sourceThumbPath) ?? ""; + var targetThumbPath = Path.Combine(targetPathBase, sourceThumbFilename); + this.CopyFile(sourceThumbPath, targetThumbPath); + } + } + + public void DeleteMediaFiles(IEnumerable files) + { + files = files.Distinct(); + + Parallel.ForEach(files, file => + { + try + { + if (file.IsNullOrWhiteSpace()) return; + + if (FileExists(file) == false) return; + DeleteFile(file, true); + + if (UseTheNewMediaPathScheme == false) + { + // old scheme: filepath is "/" OR "-" + // remove the directory if any + var dir = Path.GetDirectoryName(file); + if (string.IsNullOrWhiteSpace(dir) == false) + DeleteDirectory(dir, true); + } + else + { + // new scheme: path is "/" where xuid is a combination of cuid and puid + // remove the directory + var dir = Path.GetDirectoryName(file); + DeleteDirectory(dir, true); + } + } + catch (Exception e) + { + _logger.Error("Failed to delete attached file \"" + file + "\".", e); + } + }); + } + + + #endregion + + #region GenerateThumbnails + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public IEnumerable GenerateThumbnails( + Image image, + string filepath, + string preValue) + { + if (string.IsNullOrWhiteSpace(preValue)) + return GenerateThumbnails(image, filepath); + + var additionalSizes = new List(); + var sep = preValue.Contains(",") ? "," : ";"; + var values = preValue.Split(new[] { sep }, StringSplitOptions.RemoveEmptyEntries); + foreach (var value in values) + { + int size; + if (int.TryParse(value, out size)) + additionalSizes.Add(size); + } + + return GenerateThumbnails(image, filepath, additionalSizes); + } + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public IEnumerable GenerateThumbnails( + Image image, + string filepath, + IEnumerable additionalSizes = null) + { + var w = image.Width; + var h = image.Height; + + var sizes = additionalSizes == null ? DefaultSizes.Keys : DefaultSizes.Keys.Concat(additionalSizes); + + // start with default sizes, + // add additional sizes, + // filter out duplicates, + // filter out those that would be larger that the original image + // and create the thumbnail + return sizes + .Distinct() + .Where(x => w >= x && h >= x) + .Select(x => GenerateResized(image, filepath, DefaultSizes.ContainsKey(x) ? DefaultSizes[x] : "", x)) + .ToList(); // now + } + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public IEnumerable GenerateThumbnails( + Stream filestream, + string filepath, + PropertyType propertyType) + { + // get the original image from the original stream + if (filestream.CanSeek) filestream.Seek(0, 0); + using (var image = Image.FromStream(filestream)) + { + return GenerateThumbnails(image, filepath, propertyType); + } + } + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public IEnumerable GenerateThumbnails( + Image image, + string filepath, + PropertyType propertyType) + { + // if the editor is an upload field, check for additional thumbnail sizes + // that can be defined in the prevalue for the property data type. otherwise, + // just use the default sizes. + var sizes = propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias + ? ApplicationContext.Current.Services.DataTypeService + .GetPreValuesByDataTypeId(propertyType.DataTypeDefinitionId) + .FirstOrDefault() + : string.Empty; + + return GenerateThumbnails(image, filepath, sizes); + } + + #endregion + + #region GenerateResized - Generate at resized filepath derived from origin filepath + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight) + { + return GenerateResized(originImage, originFilepath, sizeName, maxWidthHeight, -1, -1); + } + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int fixedWidth, int fixedHeight) + { + return GenerateResized(originImage, originFilepath, sizeName, -1, fixedWidth, fixedHeight); + } + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight, int fixedWidth, int fixedHeight) + { + if (string.IsNullOrWhiteSpace(sizeName)) + sizeName = "UMBRACOSYSTHUMBNAIL"; + var extension = Path.GetExtension(originFilepath) ?? string.Empty; + var filebase = originFilepath.TrimEnd(extension); + var resizedFilepath = filebase + "_" + sizeName + extension; + + return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, fixedWidth, fixedHeight); + } + + #endregion + + #region GenerateResizedAt - Generate at specified resized filepath + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight) + { + return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, -1, -1); + } + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public ResizedImage GenerateResizedAt(Image originImage, int fixedWidth, int fixedHeight, string resizedFilepath) + { + return GenerateResizedAt(originImage, resizedFilepath, -1, fixedWidth, fixedHeight); + } + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight, int fixedWidth, int fixedHeight) + { + // target dimensions + int width, height; + + // if maxWidthHeight then get ratio + if (maxWidthHeight > 0) + { + var fx = (float)originImage.Size.Width / maxWidthHeight; + var fy = (float)originImage.Size.Height / maxWidthHeight; + var f = Math.Max(fx, fy); // fit in thumbnail size + width = (int)Math.Round(originImage.Size.Width / f); + height = (int)Math.Round(originImage.Size.Height / f); + if (width == 0) width = 1; + if (height == 0) height = 1; + } + else if (fixedWidth > 0 && fixedHeight > 0) + { + width = fixedWidth; + height = fixedHeight; + } + else + { + width = height = 1; + } + + // create new image with best quality settings + using (var bitmap = new Bitmap(width, height)) + using (var graphics = Graphics.FromImage(bitmap)) + { + // if the image size is rather large we cannot use the best quality interpolation mode + // because we'll get out of mem exceptions. So we detect how big the image is and use + // the mid quality interpolation mode when the image size exceeds our max limit. + graphics.InterpolationMode = originImage.Width > 5000 || originImage.Height > 5000 + ? InterpolationMode.Bilinear // mid quality + : InterpolationMode.HighQualityBicubic; // best quality + + // everything else is best-quality + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + graphics.CompositingQuality = CompositingQuality.HighQuality; + + // copy the old image to the new and resize + var rect = new Rectangle(0, 0, width, height); + graphics.DrawImage(originImage, rect, 0, 0, originImage.Width, originImage.Height, GraphicsUnit.Pixel); + + // get an encoder - based upon the file type + var extension = (Path.GetExtension(resizedFilepath) ?? "").TrimStart('.').ToLowerInvariant(); + var encoders = ImageCodecInfo.GetImageEncoders(); + ImageCodecInfo encoder; + switch (extension) + { + case "png": + encoder = encoders.Single(t => t.MimeType.Equals("image/png")); + break; + case "gif": + encoder = encoders.Single(t => t.MimeType.Equals("image/gif")); + break; + case "tif": + case "tiff": + encoder = encoders.Single(t => t.MimeType.Equals("image/tiff")); + break; + case "bmp": + encoder = encoders.Single(t => t.MimeType.Equals("image/bmp")); + break; + // TODO: this is dirty, defaulting to jpg but the return value of this thing is used all over the + // place so left it here, but it needs to not set a codec if it doesn't know which one to pick + // Note: when fixing this: both .jpg and .jpeg should be handled as extensions + default: + encoder = encoders.Single(t => t.MimeType.Equals("image/jpeg")); + break; + } + + // set compresion ratio to 90% + var encoderParams = new EncoderParameters(); + encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 90L); + + // save the new image + using (var stream = new MemoryStream()) + { + bitmap.Save(stream, encoder, encoderParams); + stream.Seek(0, 0); + if (resizedFilepath.Contains("UMBRACOSYSTHUMBNAIL")) + { + var filepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToInvariantString()); + AddFile(filepath, stream); + if (extension != "jpg") + { + filepath = filepath.TrimEnd(extension) + "jpg"; + stream.Seek(0, 0); + AddFile(filepath, stream); + } + // TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385 + stream.Seek(0, 0); + resizedFilepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", width + "x" + height); + } + + AddFile(resizedFilepath, stream); + } + + return new ResizedImage(resizedFilepath, width, height); + } + } + + #endregion + + #region Inner classes + + [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public class ResizedImage + { + public ResizedImage() + { } + + public ResizedImage(string filepath, int width, int height) + { + Filepath = filepath; + Width = width; + Height = height; + } + + public string Filepath { get; set; } + public int Width { get; set; } + public int Height { get; set; } + } + + #endregion + } } diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 47daff932d..e0fc406ff2 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -6,29 +6,32 @@ using Umbraco.Core.Logging; namespace Umbraco.Core.IO { - public class PhysicalFileSystem : IFileSystem + public class PhysicalFileSystem : IFileSystem2 { // the rooted, filesystem path, using directory separator chars, NOT ending with a separator // eg "c:" or "c:\path\to\site" or "\\server\path" private readonly string _rootPath; - // the ??? url, using url separator chars, NOT ending with a separator - // eg "" (?) or "/Scripts" or ??? + // _rootPath, but with separators replaced by forward-slashes + // eg "c:" or "c:/path/to/site" or "//server/path" + // (is used in GetRelativePath) + private readonly string _rootPathFwd; + + // the relative url, using url separator chars, NOT ending with a separator + // eg "" or "/Views" or "/Media" or "//Media" in case of a virtual path private readonly string _rootUrl; + // virtualRoot should be "~/path/to/root" eg "~/Views" + // the "~/" is mandatory. public PhysicalFileSystem(string virtualRoot) { if (virtualRoot == null) throw new ArgumentNullException("virtualRoot"); if (virtualRoot.StartsWith("~/") == false) throw new ArgumentException("The virtualRoot argument must be a virtual path and start with '~/'"); - _rootPath = IOHelper.MapPath(virtualRoot); - _rootPath = EnsureDirectorySeparatorChar(_rootPath); - _rootPath = _rootPath.TrimEnd(Path.DirectorySeparatorChar); - - _rootUrl = IOHelper.ResolveUrl(virtualRoot); - _rootUrl = EnsureUrlSeparatorChar(_rootUrl); - _rootUrl = _rootUrl.TrimEnd('/'); + _rootPath = EnsureDirectorySeparatorChar(IOHelper.MapPath(virtualRoot)).TrimEnd(Path.DirectorySeparatorChar); + _rootPathFwd = EnsureUrlSeparatorChar(_rootPath); + _rootUrl = EnsureUrlSeparatorChar(IOHelper.ResolveUrl(virtualRoot)).TrimEnd('/'); } public PhysicalFileSystem(string rootPath, string rootUrl) @@ -43,22 +46,24 @@ namespace Umbraco.Core.IO throw new ArgumentException("The rootPath argument cannot be a virtual path and cannot start with '~/'"); // rootPath should be... rooted, as in, it's a root path! - // but the test suite App.config cannot really "root" anything so we'll have to do it here - - //var localRoot = AppDomain.CurrentDomain.BaseDirectory; - var localRoot = IOHelper.GetRootDirectorySafe(); if (Path.IsPathRooted(rootPath) == false) { + // but the test suite App.config cannot really "root" anything so we have to do it here + var localRoot = IOHelper.GetRootDirectorySafe(); rootPath = Path.Combine(localRoot, rootPath); } - rootPath = EnsureDirectorySeparatorChar(rootPath); - rootUrl = EnsureUrlSeparatorChar(rootUrl); - - _rootPath = rootPath.TrimEnd(Path.DirectorySeparatorChar); - _rootUrl = rootUrl.TrimEnd('/'); + _rootPath = EnsureDirectorySeparatorChar(rootPath).TrimEnd(Path.DirectorySeparatorChar); + _rootPathFwd = EnsureUrlSeparatorChar(_rootPath); + _rootUrl = EnsureUrlSeparatorChar(rootUrl).TrimEnd('/'); } + /// + /// Gets directories in a directory. + /// + /// The filesystem-relative path to the directory. + /// The filesystem-relative path to the directories in the directory. + /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetDirectories(string path) { var fullPath = GetFullPath(path); @@ -80,11 +85,20 @@ namespace Umbraco.Core.IO return Enumerable.Empty(); } + /// + /// Deletes a directory. + /// + /// The filesystem-relative path of the directory. public void DeleteDirectory(string path) { DeleteDirectory(path, false); } + /// + /// Deletes a directory. + /// + /// The filesystem-relative path of the directory. + /// A value indicating whether to recursively delete sub-directories. public void DeleteDirectory(string path, bool recursive) { var fullPath = GetFullPath(path); @@ -101,38 +115,73 @@ namespace Umbraco.Core.IO } } + /// + /// Gets a value indicating whether a directory exists. + /// + /// The filesystem-relative path of the directory. + /// A value indicating whether a directory exists. public bool DirectoryExists(string path) { var fullPath = GetFullPath(path); return Directory.Exists(fullPath); } + /// + /// Saves a file. + /// + /// The filesystem-relative path of the file. + /// A stream containing the file data. + /// Overrides the existing file, if any. public void AddFile(string path, Stream stream) { AddFile(path, stream, true); } - public void AddFile(string path, Stream stream, bool overrideIfExists) + /// + /// Saves a file. + /// + /// The filesystem-relative path of the file. + /// A stream containing the file data. + /// A value indicating whether to override the existing file, if any. + /// If a file exists and is false, an exception is thrown. + public void AddFile(string path, Stream stream, bool overrideExisting) { var fullPath = GetFullPath(path); var exists = File.Exists(fullPath); - if (exists && overrideIfExists == false) + if (exists && overrideExisting == false) throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // ensure it exists + var directory = Path.GetDirectoryName(fullPath); + if (directory == null) throw new InvalidOperationException("Could not get directory."); + Directory.CreateDirectory(directory); // ensure it exists + // if can seek, be safe and go back to start, else... + // hope that the stream hasn't been read already if (stream.CanSeek) - stream.Seek(0, 0); + stream.Seek(0, SeekOrigin.Begin); - using (var destination = (Stream)File.Create(fullPath)) + using (var destination = (Stream) File.Create(fullPath)) stream.CopyTo(destination); } + /// + /// Gets files in a directory. + /// + /// The filesystem-relative path of the directory. + /// The filesystem-relative path to the files in the directory. + /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetFiles(string path) { return GetFiles(path, "*.*"); } + /// + /// Gets files in a directory. + /// + /// The filesystem-relative path of the directory. + /// A filter. + /// The filesystem-relative path to the matching files in the directory. + /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetFiles(string path, string filter) { var fullPath = GetFullPath(path); @@ -154,12 +203,21 @@ namespace Umbraco.Core.IO return Enumerable.Empty(); } + /// + /// Opens a file. + /// + /// The filesystem-relative path to the file. + /// public Stream OpenFile(string path) { var fullPath = GetFullPath(path); return File.OpenRead(fullPath); } + /// + /// Deletes a file. + /// + /// The filesystem-relative path to the file. public void DeleteFile(string path) { var fullPath = GetFullPath(path); @@ -176,52 +234,53 @@ namespace Umbraco.Core.IO } } + /// + /// Gets a value indicating whether a file exists. + /// + /// The filesystem-relative path to the file. + /// A value indicating whether the file exists. public bool FileExists(string path) { var fullpath = GetFullPath(path); return File.Exists(fullpath); } - // beware, many things depend on how the GetRelative/AbsolutePath methods work! - /// - /// Gets the relative path. + /// Gets the filesystem-relative path of a full path or of an url. /// /// The full path or url. /// The path, relative to this filesystem's root. /// /// The relative path is relative to this filesystem's root, not starting with any - /// directory separator. If input was recognized as a url (path), then output uses url (path) separator - /// chars. + /// directory separator. All separators are forward-slashes. /// public string GetRelativePath(string fullPathOrUrl) { // test url var path = fullPathOrUrl.Replace('\\', '/'); // ensure url separator char - if (IOHelper.PathStartsWith(path, _rootUrl, '/')) // if it starts with the root url... - return path.Substring(_rootUrl.Length) // strip it - .TrimStart('/'); // it's relative + // if it starts with the root url, strip it and trim the starting slash to make it relative + // eg "/Media/1234/img.jpg" => "1234/img.jpg" + if (IOHelper.PathStartsWith(path, _rootUrl, '/')) + return path.Substring(_rootUrl.Length).TrimStart('/'); - // test path - path = EnsureDirectorySeparatorChar(fullPathOrUrl); + // if it starts with the root path, strip it and trim the starting slash to make it relative + // eg "c:/websites/test/root/Media/1234/img.jpg" => "1234/img.jpg" + if (IOHelper.PathStartsWith(path, _rootPathFwd, '/')) + return path.Substring(_rootPathFwd.Length).TrimStart('/'); - if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) // if it starts with the root path - return path.Substring(_rootPath.Length) // strip it - .TrimStart(Path.DirectorySeparatorChar); // it's relative - - // unchanged - including separators - return fullPathOrUrl; + // unchanged - what else? + return path; } /// /// Gets the full path. /// - /// The full or relative path. + /// The full or filesystem-relative path. /// The full path. /// /// On the physical filesystem, the full path is the rooted (ie non-relative), safe (ie within this - /// filesystem's root) path. All separators are converted to Path.DirectorySeparatorChar. + /// filesystem's root) path. All separators are Path.DirectorySeparatorChar. /// public string GetFullPath(string path) { @@ -229,49 +288,107 @@ namespace Umbraco.Core.IO var opath = path; path = EnsureDirectorySeparatorChar(path); + // TODO in v8 this should be cleaned up + // the first part should probably removed + // not sure what we are doing here - so if input starts with a (back) slash, // we assume it's not a FS relative path and we try to convert it... but it // really makes little sense? if (path.StartsWith(Path.DirectorySeparatorChar.ToString())) path = GetRelativePath(path); - // if already a full path, return - if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) - return path; + // if not already rooted, combine with the root path + if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar) == false) + path = Path.Combine(_rootPath, path); - // else combine and sanitize, ie GetFullPath will take care of any relative + // sanitize - GetFullPath will take care of any relative // segments in path, eg '../../foo.tmp' - it may throw a SecurityException // if the combined path reaches illegal parts of the filesystem - var fpath = Path.Combine(_rootPath, path); - fpath = Path.GetFullPath(fpath); + path = Path.GetFullPath(path); // at that point, path is within legal parts of the filesystem, ie we have // permissions to reach that path, but it may nevertheless be outside of // our root path, due to relative segments, so better check - if (IOHelper.PathStartsWith(fpath, _rootPath, Path.DirectorySeparatorChar)) - return fpath; + if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) + return path; + // nothing prevents us to reach the file, security-wise, yet it is outside + // this filesystem's root - throw throw new FileSecurityException("File '" + opath + "' is outside this filesystem's root."); } + /// + /// Gets the url. + /// + /// The filesystem-relative path. + /// The url. + /// All separators are forward-slashes. public string GetUrl(string path) { path = EnsureUrlSeparatorChar(path).Trim('/'); return _rootUrl + "/" + path; } + /// + /// Gets the last-modified date of a directory or file. + /// + /// The filesystem-relative path to the directory or the file. + /// The last modified date of the directory or the file. public DateTimeOffset GetLastModified(string path) { - return DirectoryExists(path) - ? new DirectoryInfo(GetFullPath(path)).LastWriteTimeUtc - : new FileInfo(GetFullPath(path)).LastWriteTimeUtc; + var fullpath = GetFullPath(path); + return DirectoryExists(fullpath) + ? new DirectoryInfo(fullpath).LastWriteTimeUtc + : new FileInfo(fullpath).LastWriteTimeUtc; } + /// + /// Gets the created date of a directory or file. + /// + /// The filesystem-relative path to the directory or the file. + /// The created date of the directory or the file. public DateTimeOffset GetCreated(string path) { - return DirectoryExists(path) - ? Directory.GetCreationTimeUtc(GetFullPath(path)) - : File.GetCreationTimeUtc(GetFullPath(path)); + var fullpath = GetFullPath(path); + return DirectoryExists(fullpath) + ? Directory.GetCreationTimeUtc(fullpath) + : File.GetCreationTimeUtc(fullpath); + } + + /// + /// Gets the size of a file. + /// + /// The filesystem-relative path to the file. + /// The file of the size, in bytes. + /// If the file does not exist, returns -1. + public long GetSize(string path) + { + var fullPath = GetFullPath(path); + var file = new FileInfo(fullPath); + return file.Exists ? file.Length : -1; + } + + public bool CanAddPhysical { get { return true; } } + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + var fullPath = GetFullPath(path); + + if (File.Exists(fullPath)) + { + if (overrideIfExists == false) + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + File.Delete(fullPath); + } + + var directory = Path.GetDirectoryName(fullPath); + if (directory == null) throw new InvalidOperationException("Could not get directory."); + Directory.CreateDirectory(directory); // ensure it exists + + if (copy) + File.Copy(physicalPath, fullPath); + else + File.Move(physicalPath, fullPath); } #region Helper Methods diff --git a/src/Umbraco.Core/IO/ResizedImage.cs b/src/Umbraco.Core/IO/ResizedImage.cs deleted file mode 100644 index 6586699ecf..0000000000 --- a/src/Umbraco.Core/IO/ResizedImage.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Umbraco.Core.IO -{ - internal class ResizedImage - { - public ResizedImage() - { - } - - public ResizedImage(int width, int height, string fileName) - { - Width = width; - Height = height; - FileName = fileName; - } - - public int Width { get; set; } - public int Height { get; set; } - public string FileName { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs new file mode 100644 index 0000000000..8645399a0b --- /dev/null +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -0,0 +1,390 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Umbraco.Core.IO +{ + internal class ShadowFileSystem : IFileSystem2 + { + private readonly IFileSystem _fs; + private readonly IFileSystem2 _sfs; + + public ShadowFileSystem(IFileSystem fs, IFileSystem2 sfs) + { + _fs = fs; + _sfs = sfs; + } + + public IFileSystem Inner + { + get { return _fs; } + } + + public void Complete() + { + if (_nodes == null) return; + var exceptions = new List(); + foreach (var kvp in _nodes) + { + if (kvp.Value.IsExist) + { + if (kvp.Value.IsFile) + { + try + { + var fs2 = _fs as IFileSystem2; + if (fs2 != null && fs2.CanAddPhysical) + { + fs2.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move + } + else + { + using (var stream = _sfs.OpenFile(kvp.Key)) + _fs.AddFile(kvp.Key, stream, true); + } + } + catch (Exception e) + { + exceptions.Add(new Exception("Could not save file \"" + kvp.Key + "\".", e)); + } + } + } + else + { + try + { + if (kvp.Value.IsDir) + _fs.DeleteDirectory(kvp.Key, true); + else + _fs.DeleteFile(kvp.Key); + } + catch (Exception e) + { + exceptions.Add(new Exception("Could not delete " + (kvp.Value.IsDir ? "directory": "file") + " \"" + kvp.Key + "\".", e)); + } + } + } + _nodes.Clear(); + + if (exceptions.Count == 0) return; + throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); + } + + private Dictionary _nodes; + + private Dictionary Nodes { get { return _nodes ?? (_nodes = new Dictionary()); } } + + private class ShadowNode + { + public ShadowNode(bool isDelete, bool isdir) + { + IsDelete = isDelete; + IsDir = isdir; + } + + public bool IsDelete { get; private set; } + public bool IsDir { get; private set; } + + public bool IsExist { get { return IsDelete == false; } } + public bool IsFile { get { return IsDir == false; } } + } + + private static string NormPath(string path) + { + return path.ToLowerInvariant().Replace("\\", "/"); + } + + // values can be "" (root), "foo", "foo/bar"... + private static bool IsChild(string path, string input) + { + if (input.StartsWith(path) == false || input.Length < path.Length + 2) + return false; + if (path.Length > 0 && input[path.Length] != '/') return false; + var pos = input.IndexOf("/", path.Length + 1, StringComparison.OrdinalIgnoreCase); + return pos < 0; + } + + private static bool IsDescendant(string path, string input) + { + if (input.StartsWith(path) == false || input.Length < path.Length + 2) + return false; + return path.Length == 0 || input[path.Length] == '/'; + } + + public IEnumerable GetDirectories(string path) + { + var normPath = NormPath(path); + var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); + var directories = _fs.GetDirectories(path); + return directories + .Except(shadows.Where(kvp => (kvp.Value.IsDir && kvp.Value.IsDelete) || (kvp.Value.IsFile && kvp.Value.IsExist)) + .Select(kvp => kvp.Key)) + .Union(shadows.Where(kvp => kvp.Value.IsDir && kvp.Value.IsExist).Select(kvp => kvp.Key)) + .Distinct(); + } + + public void DeleteDirectory(string path) + { + DeleteDirectory(path, false); + } + + public void DeleteDirectory(string path, bool recursive) + { + if (DirectoryExists(path) == false) return; + var normPath = NormPath(path); + if (recursive) + { + Nodes[normPath] = new ShadowNode(true, true); + var remove = Nodes.Where(x => IsDescendant(normPath, x.Key)).ToList(); + foreach (var kvp in remove) Nodes.Remove(kvp.Key); + Delete(path, true); + } + else + { + if (Nodes.Any(x => IsChild(normPath, x.Key) && x.Value.IsExist) // shadow content + || _fs.GetDirectories(path).Any() || _fs.GetFiles(path).Any()) // actual content + throw new InvalidOperationException("Directory is not empty."); + Nodes[path] = new ShadowNode(true, true); + var remove = Nodes.Where(x => IsChild(normPath, x.Key)).ToList(); + foreach (var kvp in remove) Nodes.Remove(kvp.Key); + Delete(path, false); + } + } + + private void Delete(string path, bool recurse) + { + foreach (var file in _fs.GetFiles(path)) + { + Nodes[NormPath(file)] = new ShadowNode(true, false); + } + foreach (var dir in _fs.GetDirectories(path)) + { + Nodes[NormPath(dir)] = new ShadowNode(true, true); + if (recurse) Delete(dir, true); + } + } + + public bool DirectoryExists(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf)) + return sf.IsDir && sf.IsExist; + return _fs.DirectoryExists(path); + } + + public void AddFile(string path, Stream stream) + { + AddFile(path, stream, true); + } + + public void AddFile(string path, Stream stream, bool overrideIfExists) + { + ShadowNode sf; + var normPath = NormPath(path); + if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + + var parts = normPath.Split('/'); + for (var i = 0; i < parts.Length - 1; i++) + { + var dirPath = string.Join("/", parts.Take(i + 1)); + ShadowNode sd; + if (Nodes.TryGetValue(dirPath, out sd)) + { + if (sd.IsFile) throw new InvalidOperationException("Invalid path."); + if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); + } + else + { + if (_fs.DirectoryExists(dirPath)) continue; + if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); + Nodes[dirPath] = new ShadowNode(false, true); + } + } + + _sfs.AddFile(path, stream, overrideIfExists); + Nodes[normPath] = new ShadowNode(false, false); + } + + public IEnumerable GetFiles(string path) + { + return GetFiles(path, null); + } + + public IEnumerable GetFiles(string path, string filter) + { + var normPath = NormPath(path); + var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); + var files = filter != null ? _fs.GetFiles(path, filter) : _fs.GetFiles(path); + var wildcard = filter == null ? null : new WildcardExpression(filter); + return files + .Except(shadows.Where(kvp => (kvp.Value.IsFile && kvp.Value.IsDelete) || kvp.Value.IsDir) + .Select(kvp => kvp.Key)) + .Union(shadows.Where(kvp => kvp.Value.IsFile && kvp.Value.IsExist && (wildcard == null || wildcard.IsMatch(kvp.Key))).Select(kvp => kvp.Key)) + .Distinct(); + } + + public Stream OpenFile(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf)) + return sf.IsDir || sf.IsDelete ? null : _sfs.OpenFile(path); + return _fs.OpenFile(path); + } + + public void DeleteFile(string path) + { + if (FileExists(path) == false) return; + Nodes[NormPath(path)] = new ShadowNode(true, false); + } + + public bool FileExists(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf)) + return sf.IsFile && sf.IsExist; + return _fs.FileExists(path); + } + + public string GetRelativePath(string fullPathOrUrl) + { + return _fs.GetRelativePath(fullPathOrUrl); + } + + public string GetFullPath(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf)) + return sf.IsDir || sf.IsDelete ? null : _sfs.GetFullPath(path); + return _fs.GetFullPath(path); + } + + public string GetUrl(string path) + { + return _fs.GetUrl(path); + } + + public DateTimeOffset GetLastModified(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf) == false) return _fs.GetLastModified(path); + if (sf.IsDelete) throw new InvalidOperationException("Invalid path."); + return _sfs.GetLastModified(path); + } + + public DateTimeOffset GetCreated(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf) == false) return _fs.GetCreated(path); + if (sf.IsDelete) throw new InvalidOperationException("Invalid path."); + return _sfs.GetCreated(path); + } + + public long GetSize(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf) == false) + { + // the inner filesystem (_fs) can be IFileSystem2... or just IFileSystem + // figure it out and use the most effective GetSize method + var fs2 = _fs as IFileSystem2; + return fs2 == null ? _fs.GetSize(path) : fs2.GetSize(path); + } + if (sf.IsDelete || sf.IsDir) throw new InvalidOperationException("Invalid path."); + return _sfs.GetSize(path); + } + + public bool CanAddPhysical { get { return true; } } + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + ShadowNode sf; + var normPath = NormPath(path); + if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + + var parts = normPath.Split('/'); + for (var i = 0; i < parts.Length - 1; i++) + { + var dirPath = string.Join("/", parts.Take(i + 1)); + ShadowNode sd; + if (Nodes.TryGetValue(dirPath, out sd)) + { + if (sd.IsFile) throw new InvalidOperationException("Invalid path."); + if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); + } + else + { + if (_fs.DirectoryExists(dirPath)) continue; + if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); + Nodes[dirPath] = new ShadowNode(false, true); + } + } + + _sfs.AddFile(path, physicalPath, overrideIfExists, copy); + Nodes[normPath] = new ShadowNode(false, false); + } + + // copied from System.Web.Util.Wildcard internal + internal class WildcardExpression + { + private readonly string _pattern; + private readonly bool _caseInsensitive; + private Regex _regex; + + private static Regex metaRegex = new Regex("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]"); + private static Regex questRegex = new Regex("\\?"); + private static Regex starRegex = new Regex("\\*"); + private static Regex commaRegex = new Regex(","); + private static Regex slashRegex = new Regex("(?=/)"); + private static Regex backslashRegex = new Regex("(?=[\\\\:])"); + + public WildcardExpression(string pattern, bool caseInsensitive = true) + { + _pattern = pattern; + _caseInsensitive = caseInsensitive; + } + + private void EnsureRegex(string pattern) + { + if (_regex != null) return; + + var options = RegexOptions.None; + + // match right-to-left (for speed) if the pattern starts with a * + + if (pattern.Length > 0 && pattern[0] == '*') + options = RegexOptions.RightToLeft | RegexOptions.Singleline; + else + options = RegexOptions.Singleline; + + // case insensitivity + + if (_caseInsensitive) + options |= RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + + // Remove regex metacharacters + + pattern = metaRegex.Replace(pattern, "\\$0"); + + // Replace wildcard metacharacters with regex codes + + pattern = questRegex.Replace(pattern, "."); + pattern = starRegex.Replace(pattern, ".*"); + pattern = commaRegex.Replace(pattern, "\\z|\\A"); + + // anchor the pattern at beginning and end, and return the regex + + _regex = new Regex("\\A" + pattern + "\\z", options); + } + + public bool IsMatch(string input) + { + EnsureRegex(_pattern); + return _regex.IsMatch(input); + } + } + } +} diff --git a/src/Umbraco.Core/IO/ShadowFileSystems.cs b/src/Umbraco.Core/IO/ShadowFileSystems.cs new file mode 100644 index 0000000000..2fe703c3df --- /dev/null +++ b/src/Umbraco.Core/IO/ShadowFileSystems.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.IO +{ + internal class ShadowFileSystems : ICompletable + { + // note: taking a reference to the _manager instead of using manager.Current + // to avoid using Current everywhere but really, we support only 1 scope at + // a time, not multiple scopes in case of multiple managers (not supported) + + private static readonly object Locker = new object(); + private static Guid _currentId = Guid.Empty; + private readonly Guid _id; + private readonly ShadowWrapper[] _wrappers; + private bool _completed; + + public ShadowFileSystems(Guid id, ShadowWrapper[] wrappers) + { + lock (Locker) + { + if (_currentId != Guid.Empty) + throw new InvalidOperationException("Already shadowing."); + _currentId = id; + + LogHelper.Debug("Shadow " + id + "."); + _id = id; + _wrappers = wrappers; + foreach (var wrapper in _wrappers) + wrapper.Shadow(id); + } + } + + public void Complete() + { + _completed = true; + } + + public void Dispose() + { + lock (Locker) + { + LogHelper.Debug("UnShadow " + _id + " (" + (_completed ? "complete" : "abort") + ")."); + + var exceptions = new List(); + foreach (var wrapper in _wrappers) + { + try + { + // this may throw an AggregateException if some of the changes could not be applied + wrapper.UnShadow(_completed); + } + catch (AggregateException ae) + { + exceptions.Add(ae); + } + } + + _currentId = Guid.Empty; + + if (exceptions.Count > 0) + throw new AggregateException(_completed ? "Failed to apply all changes (see exceptions)." : "Failed to abort (see exceptions).", exceptions); + } + } + + // for tests + internal static void ResetId() + { + _currentId = Guid.Empty; + } + } +} diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs new file mode 100644 index 0000000000..ef113a02c2 --- /dev/null +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.IO +{ + internal class ShadowWrapper : IFileSystem2 + { + private readonly IScopeProviderInternal _scopeProvider; + private readonly IFileSystem _innerFileSystem; + private readonly string _shadowPath; + private ShadowFileSystem _shadowFileSystem; + private string _shadowDir; + + public ShadowWrapper(IFileSystem innerFileSystem, string shadowPath, IScopeProviderInternal scopeProvider) + { + _innerFileSystem = innerFileSystem; + _shadowPath = shadowPath; + _scopeProvider = scopeProvider; + } + + internal void Shadow(Guid id) + { + // note: no thread-safety here, because ShadowFs is thread-safe due to the check + // on ShadowFileSystemsScope.None - and if None is false then we should be running + // in a single thread anyways + + var virt = "~/App_Data/TEMP/ShadowFs/" + id + "/" + _shadowPath; + _shadowDir = IOHelper.MapPath(virt); + Directory.CreateDirectory(_shadowDir); + var tempfs = new PhysicalFileSystem(virt); + _shadowFileSystem = new ShadowFileSystem(_innerFileSystem, tempfs); + } + + internal void UnShadow(bool complete) + { + var shadowFileSystem = _shadowFileSystem; + var dir = _shadowDir; + _shadowFileSystem = null; + _shadowDir = null; + + try + { + // this may throw an AggregateException if some of the changes could not be applied + if (complete) shadowFileSystem.Complete(); + } + finally + { + // in any case, cleanup + try + { + Directory.Delete(dir, true); + + // shadowPath make be path/to/dir, remove each + dir = dir.Replace("/", "\\"); + var min = IOHelper.MapPath("~/App_Data/TEMP/ShadowFs").Length; + var pos = dir.LastIndexOf("\\", StringComparison.OrdinalIgnoreCase); + while (pos > min) + { + dir = dir.Substring(0, pos); + if (Directory.EnumerateFileSystemEntries(dir).Any() == false) + Directory.Delete(dir, true); + else + break; + pos = dir.LastIndexOf("\\", StringComparison.OrdinalIgnoreCase); + } + } + catch + { + // ugly, isn't it? but if we cannot cleanup, bah, just leave it there + } + } + } + + private IFileSystem FileSystem + { + get + { + var isScoped = _scopeProvider != null && _scopeProvider.AmbientScope != null && _scopeProvider.AmbientScope.ScopedFileSystems; + + // if the filesystem is created *after* shadowing starts, it won't be shadowing + // better not ignore that situation and raised a meaningful (?) exception + if (isScoped && _shadowFileSystem == null) + throw new Exception("The filesystems are shadowing, but this filesystem is not."); + + return isScoped + ? _shadowFileSystem + : _innerFileSystem; + } + } + + public IEnumerable GetDirectories(string path) + { + return FileSystem.GetDirectories(path); + } + + public void DeleteDirectory(string path) + { + FileSystem.DeleteDirectory(path); + } + + public void DeleteDirectory(string path, bool recursive) + { + FileSystem.DeleteDirectory(path, recursive); + } + + public bool DirectoryExists(string path) + { + return FileSystem.DirectoryExists(path); + } + + public void AddFile(string path, Stream stream) + { + FileSystem.AddFile(path, stream); + } + + public void AddFile(string path, Stream stream, bool overrideExisting) + { + FileSystem.AddFile(path, stream, overrideExisting); + } + + public IEnumerable GetFiles(string path) + { + return FileSystem.GetFiles(path); + } + + public IEnumerable GetFiles(string path, string filter) + { + return FileSystem.GetFiles(path, filter); + } + + public Stream OpenFile(string path) + { + return FileSystem.OpenFile(path); + } + + public void DeleteFile(string path) + { + FileSystem.DeleteFile(path); + } + + public bool FileExists(string path) + { + return FileSystem.FileExists(path); + } + + public string GetRelativePath(string fullPathOrUrl) + { + return FileSystem.GetRelativePath(fullPathOrUrl); + } + + public string GetFullPath(string path) + { + return FileSystem.GetFullPath(path); + } + + public string GetUrl(string path) + { + return FileSystem.GetUrl(path); + } + + public DateTimeOffset GetLastModified(string path) + { + return FileSystem.GetLastModified(path); + } + + public DateTimeOffset GetCreated(string path) + { + return FileSystem.GetCreated(path); + } + + public long GetSize(string path) + { + var filesystem = FileSystem; // will be either a ShadowFileSystem OR the actual underlying IFileSystem + + // and the underlying filesystem can be IFileSystem2... or just IFileSystem + // figure it out and use the most effective GetSize method + var filesystem2 = filesystem as IFileSystem2; + return filesystem2 == null ? filesystem.GetSize(path) : filesystem2.GetSize(path); + } + + public bool CanAddPhysical + { + get + { + var fileSystem2 = FileSystem as IFileSystem2; + return fileSystem2 != null && fileSystem2.CanAddPhysical; + } + } + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + var fileSystem2 = FileSystem as IFileSystem2; + if (fileSystem2 == null) + throw new NotSupportedException(); + fileSystem2.AddFile(path, physicalPath, overrideIfExists, copy); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index 2dfad2d103..9c815504c7 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -96,7 +96,23 @@ namespace Umbraco.Core.IO } } - + public static string PartialViews + { + get + { + return MvcViews + "/Partials/"; + } + } + + public static string MacroPartials + { + get + { + return MvcViews + "/MacroPartials/"; + + } + } + public static string Media { get diff --git a/src/Umbraco.Core/IO/SystemFiles.cs b/src/Umbraco.Core/IO/SystemFiles.cs index 48bdea2884..437ddd3ef7 100644 --- a/src/Umbraco.Core/IO/SystemFiles.cs +++ b/src/Umbraco.Core/IO/SystemFiles.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Configuration; using System.IO; using System.Linq; @@ -72,15 +73,28 @@ namespace Umbraco.Core.IO { get { - if (GlobalSettings.ContentCacheXmlStoredInCodeGen && SystemUtilities.GetCurrentTrustLevel() == AspNetHostingPermissionLevel.Unrestricted) - { - return Path.Combine(HttpRuntime.CodegenDir, @"UmbracoData\umbraco.config"); + switch (GlobalSettings.ContentCacheXmlStorageLocation) + { + case ContentXmlStorage.AspNetTemp: + return Path.Combine(HttpRuntime.CodegenDir, @"UmbracoData\umbraco.config"); + case ContentXmlStorage.EnvironmentTemp: + var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); + var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoXml", + //include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back + // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not + // utilizing an old path + appDomainHash); + return Path.Combine(cachePath, "umbraco.config"); + case ContentXmlStorage.Default: + return IOHelper.ReturnPath("umbracoContentXML", "~/App_Data/umbraco.config"); + default: + throw new ArgumentOutOfRangeException(); } - return IOHelper.ReturnPath("umbracoContentXML", "~/App_Data/umbraco.config"); } } [Obsolete("Use GlobalSettings.ContentCacheXmlStoredInCodeGen instead")] + [EditorBrowsable(EditorBrowsableState.Never)] internal static bool ContentCacheXmlStoredInCodeGen { get { return GlobalSettings.ContentCacheXmlStoredInCodeGen; } diff --git a/src/Umbraco.Core/IO/UmbracoMediaFile.cs b/src/Umbraco.Core/IO/UmbracoMediaFile.cs index c466db29af..18438831e2 100644 --- a/src/Umbraco.Core/IO/UmbracoMediaFile.cs +++ b/src/Umbraco.Core/IO/UmbracoMediaFile.cs @@ -1,13 +1,7 @@ using System; -using System.Collections.Generic; using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; using System.IO; -using System.Linq; -using System.Threading.Tasks; using System.Web; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Media; @@ -86,15 +80,15 @@ namespace Umbraco.Core.IO private void Initialize() { Filename = _fs.GetFileName(Path); - Extension = string.IsNullOrEmpty(_fs.GetExtension(Path)) == false - ? _fs.GetExtension(Path).Substring(1).ToLowerInvariant() + var ext = _fs.GetExtension(Path); + Extension = string.IsNullOrEmpty(ext) == false + ? ext.TrimStart('.').ToLowerInvariant() : ""; Url = _fs.GetUrl(Path); + Exists = _fs.FileExists(Path); if (Exists == false) - { LogHelper.Warn("The media file doesn't exist: " + Path); - } } public bool Exists { get; private set; } @@ -117,27 +111,15 @@ namespace Umbraco.Core.IO { get { - if (_length == null) - { - if (Exists) - { - _length = _fs.GetSize(Path); - } - else - { - _length = -1; - } - } + if (_length.HasValue) return _length.Value; + _length = Exists ? _fs.GetSize(Path) : -1; return _length.Value; } } public bool SupportsResizing { - get - { - return UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(Extension); - } + get { return _fs.IsImageFile(Extension); } } public string GetFriendlyName() @@ -147,67 +129,49 @@ namespace Umbraco.Core.IO public Size GetDimensions() { - if (_size == null) - { - if (_fs.FileExists(Path)) - { - EnsureFileSupportsResizing(); + if (_size.HasValue) return _size.Value; - using (var fs = _fs.OpenFile(Path)) - { - _size = ImageHelper.GetDimensions(fs); - } - } - else + if (_fs.FileExists(Path)) + { + EnsureFileSupportsResizing(); + + using (var fs = _fs.OpenFile(Path)) { - _size = new Size(-1, -1); + _size = _fs.GetDimensions(fs); } } + else + { + _size = new Size(-1, -1); + } + return _size.Value; } public string Resize(int width, int height) { - if (Exists) - { - EnsureFileSupportsResizing(); + if (Exists == false) return string.Empty; - var fileNameThumb = DoResize(width, height, -1, string.Empty); - - return _fs.GetUrl(fileNameThumb); - } - return string.Empty; + EnsureFileSupportsResizing(); + var filepath = Resize(width, height, -1, string.Empty); + return _fs.GetUrl(filepath); } public string Resize(int maxWidthHeight, string fileNameAddition) { - if (Exists) - { - EnsureFileSupportsResizing(); + if (Exists == false) return string.Empty; - var fileNameThumb = DoResize(-1, -1, maxWidthHeight, fileNameAddition); - - return _fs.GetUrl(fileNameThumb); - } - return string.Empty; + EnsureFileSupportsResizing(); + var filepath = Resize(-1, -1, maxWidthHeight, fileNameAddition); + return _fs.GetUrl(filepath); } - private string DoResize(int width, int height, int maxWidthHeight, string fileNameAddition) + private string Resize(int width, int height, int maxWidthHeight, string sizeName) { - using (var fs = _fs.OpenFile(Path)) + using (var filestream = _fs.OpenFile(Path)) + using (var image = Image.FromStream(filestream)) { - using (var image = Image.FromStream(fs)) - { - var fileNameThumb = string.IsNullOrWhiteSpace(fileNameAddition) - ? string.Format("{0}_UMBRACOSYSTHUMBNAIL." + Extension, Path.Substring(0, Path.LastIndexOf(".", StringComparison.Ordinal))) - : string.Format("{0}_{1}." + Extension, Path.Substring(0, Path.LastIndexOf(".", StringComparison.Ordinal)), fileNameAddition); - - var thumbnail = maxWidthHeight == -1 - ? ImageHelper.GenerateThumbnail(image, width, height, fileNameThumb, Extension, _fs) - : ImageHelper.GenerateThumbnail(image, maxWidthHeight, fileNameThumb, Extension, _fs); - - return thumbnail.FileName; - } + return _fs.GenerateResized(image, Path, sizeName, maxWidthHeight, width, height).Filepath; } } @@ -216,7 +180,5 @@ namespace Umbraco.Core.IO if (SupportsResizing == false) throw new InvalidOperationException(string.Format("The file {0} is not an image, so can't get dimensions", Filename)); } - - } } diff --git a/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs b/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs index c226bd03c8..cd62008cd4 100644 --- a/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs +++ b/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Logging /// This is an old/deprecated logger and has been superceded by ParallelForwardingAppender which is included in Umbraco and /// also by AsyncForwardingAppender in the Log4Net.Async library. ///
- [Obsolete("This is superceded by the ParallelForwardingAppender, this will be removed in v8")] + [Obsolete("This is superceded by the ParallelForwardingAppender, this will be removed in v8, do not use this")] [EditorBrowsable(EditorBrowsableState.Never)] public class AsynchronousRollingFileAppender : RollingFileAppender { diff --git a/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs b/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs index c1b3306129..f5ae689da9 100644 --- a/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs +++ b/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs @@ -3,7 +3,11 @@ using System.Linq; namespace Umbraco.Core.Logging { - internal class DebugDiagnosticsLogger : ILogger + /// + /// Implements on top of System.Diagnostics.Debug. + /// + /// Useful for tests. + public class DebugDiagnosticsLogger : ILogger { public void Error(Type callingType, string message, Exception exception) { diff --git a/src/Umbraco.Core/Logging/Logger.cs b/src/Umbraco.Core/Logging/Logger.cs index ae8bb60fcd..d0bfcbfca0 100644 --- a/src/Umbraco.Core/Logging/Logger.cs +++ b/src/Umbraco.Core/Logging/Logger.cs @@ -2,10 +2,13 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Web; using log4net; using log4net.Config; +using Umbraco.Core.Configuration; +using Umbraco.Core.Diagnostics; namespace Umbraco.Core.Logging { @@ -56,18 +59,72 @@ namespace Umbraco.Core.Logging internal ILog LoggerFor(object getTypeFromInstance) { if (getTypeFromInstance == null) throw new ArgumentNullException("getTypeFromInstance"); - + return LogManager.GetLogger(getTypeFromInstance.GetType()); } - + public void Error(Type callingType, string message, Exception exception) { - var logger = LogManager.GetLogger(callingType); - if (logger != null) - logger.Error((message), exception); + var logger = LogManager.GetLogger(callingType); + if (logger == null) return; + + var dump = false; + + if (IsTimeoutThreadAbortException(exception)) + { + message += "\r\nThe thread has been aborted, because the request has timed out."; + + // dump if configured, or if stacktrace contains Monitor.ReliableEnter + dump = UmbracoConfig.For.CoreDebug().DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(exception); + + // dump if it is ok to dump (might have a cap on number of dump...) + dump &= MiniDump.OkToDump(); + } + + if (dump) + { + try + { + var dumped = MiniDump.Dump(withException: true); + message += dumped + ? "\r\nA minidump was created in App_Data/MiniDump" + : "\r\nFailed to create a minidump"; + } + catch (Exception e) + { + message += string.Format("\r\nFailed to create a minidump ({0}: {1})", e.GetType().FullName, e.Message); + } + } + + logger.Error(message, exception); } - public void Warn(Type callingType, string message, params Func[] formatItems) + private static bool IsMonitorEnterThreadAbortException(Exception exception) + { + var abort = exception as ThreadAbortException; + if (abort == null) return false; + + var stacktrace = abort.StackTrace; + return stacktrace.Contains("System.Threading.Monitor.ReliableEnter"); + } + + private static bool IsTimeoutThreadAbortException(Exception exception) + { + var abort = exception as ThreadAbortException; + if (abort == null) return false; + + if (abort.ExceptionState == null) return false; + + var stateType = abort.ExceptionState.GetType(); + if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false; + + var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic); + if (timeoutField == null) return false; + + return (bool) timeoutField.GetValue(abort.ExceptionState); + } + + public void Warn(Type callingType, string message, params Func[] formatItems) { var logger = LogManager.GetLogger(callingType); if (logger == null || logger.IsWarnEnabled == false) return; @@ -82,7 +139,7 @@ namespace Umbraco.Core.Logging if (showHttpTrace && HttpContext.Current != null) { HttpContext.Current.Trace.Warn(callingType.Name, string.Format(message, formatItems.Select(x => x.Invoke()).ToArray())); - } + } var logger = LogManager.GetLogger(callingType); if (logger == null || logger.IsWarnEnabled == false) return; @@ -99,7 +156,7 @@ namespace Umbraco.Core.Logging var logger = LogManager.GetLogger(callingType); if (logger == null || logger.IsWarnEnabled == false) return; var executedParams = formatItems.Select(x => x.Invoke()).ToArray(); - logger.WarnFormat((message) + ". Exception: " + e, executedParams); + logger.WarnFormat((message) + ". Exception: " + e, executedParams); } /// diff --git a/src/Umbraco.Core/Logging/ParallelForwardingAppender.cs b/src/Umbraco.Core/Logging/ParallelForwardingAppender.cs index 48bb3ec710..c930347791 100644 --- a/src/Umbraco.Core/Logging/ParallelForwardingAppender.cs +++ b/src/Umbraco.Core/Logging/ParallelForwardingAppender.cs @@ -13,301 +13,8 @@ namespace Umbraco.Core.Logging /// /// Borrowed from https://github.com/cjbhaines/Log4Net.Async - will reference Nuget packages directly in v8 /// - public class ParallelForwardingAppender : AsyncForwardingAppenderBase, IDisposable + [Obsolete("Use the Log4Net.Async.ParallelForwardingAppender instead this will be removed in future versions")] + public class ParallelForwardingAppender : Log4Net.Async.ParallelForwardingAppender { - #region Private Members - - private const int DefaultBufferSize = 1000; - private BlockingCollection _loggingEvents; - private CancellationTokenSource _loggingCancelationTokenSource; - private CancellationToken _loggingCancelationToken; - private Task _loggingTask; - private Double _shutdownFlushTimeout = 2; - private TimeSpan _shutdownFlushTimespan = TimeSpan.FromSeconds(2); - private static readonly Type ThisType = typeof(ParallelForwardingAppender); - private volatile bool shutDownRequested; - private int? bufferSize = DefaultBufferSize; - - #endregion Private Members - - #region Properties - - /// - /// Gets or sets the number of LoggingEvents that will be buffered. Set to null for unlimited. - /// - public override int? BufferSize - { - get { return bufferSize; } - set { bufferSize = value; } - } - - public int BufferEntryCount - { - get - { - if (_loggingEvents == null) return 0; - return _loggingEvents.Count; - } - } - - /// - /// Gets or sets the time period in which the system will wait for appenders to flush before canceling the background task. - /// - public Double ShutdownFlushTimeout - { - get - { - return _shutdownFlushTimeout; - } - set - { - _shutdownFlushTimeout = value; - } - } - - protected override string InternalLoggerName - { - get - { - { - return "ParallelForwardingAppender"; - } - } - } - - #endregion Properties - - #region Startup - - public override void ActivateOptions() - { - base.ActivateOptions(); - _shutdownFlushTimespan = TimeSpan.FromSeconds(_shutdownFlushTimeout); - StartForwarding(); - } - - private void StartForwarding() - { - if (shutDownRequested) - { - return; - } - //Create a collection which will block the thread and wait for new entries - //if the collection is empty - if (BufferSize.HasValue && BufferSize > 0) - { - _loggingEvents = new BlockingCollection(BufferSize.Value); - } - else - { - //No limit on the number of events. - _loggingEvents = new BlockingCollection(); - } - //The cancellation token is used to cancel a running task gracefully. - _loggingCancelationTokenSource = new CancellationTokenSource(); - _loggingCancelationToken = _loggingCancelationTokenSource.Token; - _loggingTask = new Task(SubscriberLoop, _loggingCancelationToken); - _loggingTask.Start(); - } - - #endregion Startup - - #region Shutdown - - private void CompleteSubscriberTask() - { - shutDownRequested = true; - if (_loggingEvents == null || _loggingEvents.IsAddingCompleted) - { - return; - } - //Don't allow more entries to be added. - _loggingEvents.CompleteAdding(); - //Allow some time to flush - Thread.Sleep(_shutdownFlushTimespan); - if (!_loggingTask.IsCompleted && !_loggingCancelationToken.IsCancellationRequested) - { - _loggingCancelationTokenSource.Cancel(); - //Wait here so that the error logging messages do not get into a random order. - //Don't pass the cancellation token because we are not interested - //in catching the OperationCanceledException that results. - _loggingTask.Wait(); - } - if (!_loggingEvents.IsCompleted) - { - ForwardInternalError("The buffer was not able to be flushed before timeout occurred.", null, ThisType); - } - } - - protected override void OnClose() - { - CompleteSubscriberTask(); - base.OnClose(); - } - - #endregion Shutdown - - #region Appending - - protected override void Append(LoggingEvent loggingEvent) - { - if (_loggingEvents == null || _loggingEvents.IsAddingCompleted || loggingEvent == null) - { - return; - } - - loggingEvent.Fix = Fix; - //In the case where blocking on a full collection, and the task is subsequently completed, the cancellation token - //will prevent the entry from attempting to add to the completed collection which would result in an exception. - _loggingEvents.Add(new LoggingEventContext(loggingEvent, HttpContext), _loggingCancelationToken); - } - - protected override void Append(LoggingEvent[] loggingEvents) - { - if (_loggingEvents == null || _loggingEvents.IsAddingCompleted || loggingEvents == null) - { - return; - } - - foreach (var loggingEvent in loggingEvents) - { - Append(loggingEvent); - } - } - - #endregion Appending - - #region Forwarding - - /// - /// Iterates over a BlockingCollection containing LoggingEvents. - /// - private void SubscriberLoop() - { - Thread.CurrentThread.Name = String.Format("{0} ParallelForwardingAppender Subscriber Task", Name); - //The task will continue in a blocking loop until - //the queue is marked as adding completed, or the task is canceled. - try - { - //This call blocks until an item is available or until adding is completed - foreach (var entry in _loggingEvents.GetConsumingEnumerable(_loggingCancelationToken)) - { - HttpContext = entry.HttpContext; - ForwardLoggingEvent(entry.LoggingEvent, ThisType); - } - } - catch (OperationCanceledException ex) - { - //The thread was canceled before all entries could be forwarded and the collection completed. - ForwardInternalError("Subscriber task was canceled before completion.", ex, ThisType); - //Cancellation is called in the CompleteSubscriberTask so don't call that again. - } - catch (ThreadAbortException ex) - { - //Thread abort may occur on domain unload. - ForwardInternalError("Subscriber task was aborted.", ex, ThisType); - //Cannot recover from a thread abort so complete the task. - CompleteSubscriberTask(); - //The exception is swallowed because we don't want the client application - //to halt due to a logging issue. - } - catch (Exception ex) - { - //On exception, try to log the exception - ForwardInternalError("Subscriber task error in forwarding loop.", ex, ThisType); - //Any error in the loop is going to be some sort of extenuating circumstance from which we - //probably cannot recover anyway. Complete subscribing. - CompleteSubscriberTask(); - } - } - - #endregion Forwarding - - #region IDisposable Implementation - - private bool _disposed = false; - - //Implement IDisposable. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - if (_loggingTask != null) - { - if (!(_loggingTask.IsCanceled || _loggingTask.IsCompleted || _loggingTask.IsFaulted)) - { - try - { - CompleteSubscriberTask(); - } - catch (Exception ex) - { - LogLog.Error(ThisType, "Exception Completing Subscriber Task in Dispose Method", ex); - } - } - try - { - _loggingTask.Dispose(); - } - catch (Exception ex) - { - LogLog.Error(ThisType, "Exception Disposing Logging Task", ex); - } - finally - { - _loggingTask = null; - } - } - if (_loggingEvents != null) - { - try - { - _loggingEvents.Dispose(); - } - catch (Exception ex) - { - LogLog.Error(ThisType, "Exception Disposing BlockingCollection", ex); - } - finally - { - _loggingEvents = null; - } - } - if (_loggingCancelationTokenSource != null) - { - try - { - _loggingCancelationTokenSource.Dispose(); - } - catch (Exception ex) - { - LogLog.Error(ThisType, "Exception Disposing CancellationTokenSource", ex); - } - finally - { - _loggingCancelationTokenSource = null; - } - } - } - _disposed = true; - } - } - - // Use C# destructor syntax for finalization code. - ~ParallelForwardingAppender() - { - // Simply call Dispose(false). - Dispose(false); - } - - #endregion IDisposable Implementation } } \ No newline at end of file diff --git a/src/Umbraco.Core/Media/IImageUrlProvider.cs b/src/Umbraco.Core/Media/IImageUrlProvider.cs index 3854e1f1ec..29c0ae34ed 100644 --- a/src/Umbraco.Core/Media/IImageUrlProvider.cs +++ b/src/Umbraco.Core/Media/IImageUrlProvider.cs @@ -1,8 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Media { - public interface IImageUrlProvider + // note: because this interface is obsolete is is *not* IDiscoverable, and in case the + // PluginManager is asked to find types implementing this interface it will fall back + // to a complete scan. + + [Obsolete("IImageUrlProvider is no longer used and will be removed in future versions")] + public interface IImageUrlProvider // IDiscoverable { string Name { get; } string GetImageUrlFromMedia(int mediaId, IDictionary parameters); diff --git a/src/Umbraco.Core/Media/IThumbnailProvider.cs b/src/Umbraco.Core/Media/IThumbnailProvider.cs index a5be69b72e..18b8453324 100644 --- a/src/Umbraco.Core/Media/IThumbnailProvider.cs +++ b/src/Umbraco.Core/Media/IThumbnailProvider.cs @@ -1,6 +1,13 @@ -namespace Umbraco.Core.Media +using System; + +namespace Umbraco.Core.Media { - public interface IThumbnailProvider + // note: because this interface is obsolete is is *not* IDiscoverable, and in case the + // PluginManager is asked to find types implementing this interface it will fall back + // to a complete scan. + + [Obsolete("Thumbnails are generated by ImageProcessor, use that instead")] + public interface IThumbnailProvider // : IDiscoverable { bool CanProvideThumbnail(string fileUrl); string GetThumbnailUrl(string fileUrl); diff --git a/src/Umbraco.Core/Media/ImageExtensions.cs b/src/Umbraco.Core/Media/ImageExtensions.cs new file mode 100644 index 0000000000..c20be13d31 --- /dev/null +++ b/src/Umbraco.Core/Media/ImageExtensions.cs @@ -0,0 +1,21 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; + +namespace Umbraco.Core.Media +{ + public static class ImageExtensions + { + /// + /// Gets the MIME type of an image. + /// + /// The image. + /// The MIME type of the image. + public static string GetMimeType(this Image image) + { + var format = image.RawFormat; + var codec = ImageCodecInfo.GetImageDecoders().First(c => c.FormatID == format.Guid); + return codec.MimeType; + } + } +} diff --git a/src/Umbraco.Core/Media/ImageHelper.cs b/src/Umbraco.Core/Media/ImageHelper.cs deleted file mode 100644 index 99fc278e61..0000000000 --- a/src/Umbraco.Core/Media/ImageHelper.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.Globalization; -using System.IO; -using System.Linq; -using Umbraco.Core.IO; -using Umbraco.Core.Media.Exif; - -namespace Umbraco.Core.Media -{ - /// - /// A helper class used for imaging - /// - internal static class ImageHelper - { - /// - /// Gets the dimensions of an image based on a stream - /// - /// - /// - /// - /// First try with EXIF, this is because it is insanely faster and doesn't use any memory to read exif data than to load in the entire - /// image via GDI. Otherwise loading an image into GDI consumes a crazy amount of memory on large images. - /// - /// Of course EXIF data might not exist in every file and can only exist in JPGs - /// - public static Size GetDimensions(Stream imageStream) - { - //Try to load with exif - try - { - var jpgInfo = ImageFile.FromStream(imageStream); - - if (jpgInfo.Format != ImageFileFormat.Unknown - && jpgInfo.Properties.ContainsKey(ExifTag.PixelYDimension) - && jpgInfo.Properties.ContainsKey(ExifTag.PixelXDimension)) - { - var height = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelYDimension].Value); - var width = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelXDimension].Value); - if (height > 0 && width > 0) - { - return new Size(width, height); - } - } - } - catch (Exception) - { - //We will just swallow, just means we can't read exif data, we don't want to log an error either - } - - //we have no choice but to try to read in via GDI - using (var image = Image.FromStream(imageStream)) - { - - var fileWidth = image.Width; - var fileHeight = image.Height; - return new Size(fileWidth, fileHeight); - } - - } - - public static string GetMimeType(this Image image) - { - var format = image.RawFormat; - var codec = ImageCodecInfo.GetImageDecoders().First(c => c.FormatID == format.Guid); - return codec.MimeType; - } - - /// - /// Creates the thumbnails if the image is larger than all of the specified ones. - /// - /// - /// - /// - /// - /// - /// - internal static IEnumerable GenerateMediaThumbnails( - IFileSystem fs, - string fileName, - string extension, - Image originalImage, - IEnumerable additionalThumbSizes) - { - - var result = new List(); - - var allSizesDictionary = new Dictionary { { 100, "thumb" }, { 500, "big-thumb" } }; - - //combine the static dictionary with the additional sizes with only unique values - var allSizes = allSizesDictionary.Select(kv => kv.Key) - .Union(additionalThumbSizes.Where(x => x > 0).Distinct()); - - var sizesDictionary = allSizes.ToDictionary(s => s, s => allSizesDictionary.ContainsKey(s) ? allSizesDictionary[s] : ""); - - foreach (var s in sizesDictionary) - { - var size = s.Key; - var name = s.Value; - if (originalImage.Width >= size && originalImage.Height >= size) - { - result.Add(Resize(fs, fileName, extension, size, name, originalImage)); - } - } - - return result; - } - - /// - /// Performs an image resize - /// - /// - /// - /// - /// - /// - /// - /// - private static ResizedImage Resize(IFileSystem fileSystem, string path, string extension, int maxWidthHeight, string fileNameAddition, Image originalImage) - { - var fileNameThumb = string.IsNullOrWhiteSpace(fileNameAddition) - ? string.Format("{0}_UMBRACOSYSTHUMBNAIL." + extension, path.Substring(0, path.LastIndexOf(".", StringComparison.Ordinal))) - : string.Format("{0}_{1}." + extension, path.Substring(0, path.LastIndexOf(".", StringComparison.Ordinal)), fileNameAddition); - - var thumb = GenerateThumbnail( - originalImage, - maxWidthHeight, - fileNameThumb, - extension, - fileSystem); - - return thumb; - } - - internal static ResizedImage GenerateThumbnail(Image image, int maxWidthHeight, string thumbnailFileName, string extension, IFileSystem fs) - { - return GenerateThumbnail(image, maxWidthHeight, -1, -1, thumbnailFileName, extension, fs); - } - - internal static ResizedImage GenerateThumbnail(Image image, int fixedWidth, int fixedHeight, string thumbnailFileName, string extension, IFileSystem fs) - { - return GenerateThumbnail(image, -1, fixedWidth, fixedHeight, thumbnailFileName, extension, fs); - } - - private static ResizedImage GenerateThumbnail(Image image, int maxWidthHeight, int fixedWidth, int fixedHeight, string thumbnailFileName, string extension, IFileSystem fs) - { - // Generate thumbnail - float f = 1; - if (maxWidthHeight >= 0) - { - var fx = (float)image.Size.Width / maxWidthHeight; - var fy = (float)image.Size.Height / maxWidthHeight; - - // must fit in thumbnail size - f = Math.Max(fx, fy); - } - - //depending on if we are doing fixed width resizing or not. - fixedWidth = (maxWidthHeight > 0) ? image.Width : fixedWidth; - fixedHeight = (maxWidthHeight > 0) ? image.Height : fixedHeight; - - var widthTh = (int)Math.Round(fixedWidth / f); - var heightTh = (int)Math.Round(fixedHeight / f); - - // fixes for empty width or height - if (widthTh == 0) - widthTh = 1; - if (heightTh == 0) - heightTh = 1; - - // Create new image with best quality settings - using (var bp = new Bitmap(widthTh, heightTh)) - { - using (var g = Graphics.FromImage(bp)) - { - //if the image size is rather large we cannot use the best quality interpolation mode - // because we'll get out of mem exceptions. So we'll detect how big the image is and use - // the mid quality interpolation mode when the image size exceeds our max limit. - - if (image.Width > 5000 || image.Height > 5000) - { - //use mid quality - g.InterpolationMode = InterpolationMode.Bilinear; - } - else - { - //use best quality - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - } - - - g.SmoothingMode = SmoothingMode.HighQuality; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; - g.CompositingQuality = CompositingQuality.HighQuality; - - // Copy the old image to the new and resized - var rect = new Rectangle(0, 0, widthTh, heightTh); - g.DrawImage(image, rect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel); - - // Copy metadata - var imageEncoders = ImageCodecInfo.GetImageEncoders(); - ImageCodecInfo codec; - switch (extension.ToLower()) - { - case "png": - codec = imageEncoders.Single(t => t.MimeType.Equals("image/png")); - break; - case "gif": - codec = imageEncoders.Single(t => t.MimeType.Equals("image/gif")); - break; - case "tif": - case "tiff": - codec = imageEncoders.Single(t => t.MimeType.Equals("image/tiff")); - break; - case "bmp": - codec = imageEncoders.Single(t => t.MimeType.Equals("image/bmp")); - break; - // TODO: this is dirty, defaulting to jpg but the return value of this thing is used all over the - // place so left it here, but it needs to not set a codec if it doesn't know which one to pick - // Note: when fixing this: both .jpg and .jpeg should be handled as extensions - default: - codec = imageEncoders.Single(t => t.MimeType.Equals("image/jpeg")); - break; - } - - // Set compresion ratio to 90% - var ep = new EncoderParameters(); - ep.Param[0] = new EncoderParameter(Encoder.Quality, 90L); - - // Save the new image using the dimensions of the image - var predictableThumbnailName = thumbnailFileName.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToString(CultureInfo.InvariantCulture)); - var predictableThumbnailNameJpg = predictableThumbnailName.Substring(0, predictableThumbnailName.LastIndexOf(".", StringComparison.Ordinal)) + ".jpg"; - using (var ms = new MemoryStream()) - { - bp.Save(ms, codec, ep); - ms.Seek(0, 0); - - fs.AddFile(predictableThumbnailName, ms); - fs.AddFile(predictableThumbnailNameJpg, ms); - } - - // TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385 - var newFileName = thumbnailFileName.Replace("UMBRACOSYSTHUMBNAIL", string.Format("{0}x{1}", widthTh, heightTh)); - using (var ms = new MemoryStream()) - { - bp.Save(ms, codec, ep); - ms.Seek(0, 0); - - fs.AddFile(newFileName, ms); - } - - return new ResizedImage(widthTh, heightTh, newFileName); - } - } - } - - } -} diff --git a/src/Umbraco.Core/Media/MediaSubfolderCounter.cs b/src/Umbraco.Core/Media/MediaSubfolderCounter.cs deleted file mode 100644 index 6f0142cacf..0000000000 --- a/src/Umbraco.Core/Media/MediaSubfolderCounter.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Umbraco.Core.IO; - -namespace Umbraco.Core.Media -{ - /// - /// Internal singleton to handle the numbering of subfolders within the Media-folder. - /// When this class is initiated it will look for numbered subfolders and select the highest number, - /// which will be the start point for the naming of the next subfolders. If no subfolders exists - /// then the starting point will be 1000, ie. /media/1000/koala.jpg - /// - internal class MediaSubfolderCounter - { - #region Singleton - - private long _numberedFolder = 1000;//Default starting point - private static readonly ReaderWriterLockSlim ClearLock = new ReaderWriterLockSlim(); - private static readonly Lazy Lazy = new Lazy(() => new MediaSubfolderCounter()); - - public static MediaSubfolderCounter Current { get { return Lazy.Value; } } - - private MediaSubfolderCounter() - { - var folders = new List(); - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - var directories = fs.GetDirectories(""); - foreach (var directory in directories) - { - long dirNum; - if (long.TryParse(directory, out dirNum)) - { - folders.Add(dirNum); - } - } - var last = folders.OrderBy(x => x).LastOrDefault(); - if(last != default(long)) - _numberedFolder = last; - } - - #endregion - - /// - /// Returns an increment of the numbered media subfolders. - /// - /// A value - public long Increment() - { - using (new ReadLock(ClearLock)) - { - _numberedFolder = _numberedFolder + 1; - return _numberedFolder; - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs new file mode 100644 index 0000000000..bf6f7c59ad --- /dev/null +++ b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs @@ -0,0 +1,214 @@ +using System; +using System.Drawing; +using System.IO; +using System.Linq; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Media +{ + /// + /// Provides methods to manage auto-fill properties for upload fields. + /// + internal class UploadAutoFillProperties + { + private readonly ILogger _logger; + private readonly MediaFileSystem _mediaFileSystem; + private readonly IContentSection _contentSettings; + + public UploadAutoFillProperties(MediaFileSystem mediaFileSystem, ILogger logger, IContentSection contentSettings) + { + _mediaFileSystem = mediaFileSystem; + _logger = logger; + _contentSettings = contentSettings; + } + + /// + /// Gets the auto-fill configuration for a specified property alias. + /// + /// The property type alias. + /// The auto-fill configuration for the specified property alias, or null. + public IImagingAutoFillUploadField GetConfig(string propertyTypeAlias) + { + var autoFillConfigs = _contentSettings.ImageAutoFillProperties; + return autoFillConfigs == null ? null : autoFillConfigs.FirstOrDefault(x => x.Alias == propertyTypeAlias); + } + + /// + /// Resets the auto-fill properties of a content item, for a specified property alias. + /// + /// The content item. + /// The property type alias. + public void Reset(IContentBase content, string propertyTypeAlias) + { + if (content == null) throw new ArgumentNullException("content"); + if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); + + // get the config, no config = nothing to do + var autoFillConfig = GetConfig(propertyTypeAlias); + if (autoFillConfig == null) return; // nothing + + // reset + Reset(content, autoFillConfig); + } + + /// + /// Resets the auto-fill properties of a content item, for a specified auto-fill configuration. + /// + /// The content item. + /// The auto-fill configuration. + public void Reset(IContentBase content, IImagingAutoFillUploadField autoFillConfig) + { + if (content == null) throw new ArgumentNullException("content"); + if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); + + ResetProperties(content, autoFillConfig); + } + + /// + /// Populates the auto-fill properties of a content item. + /// + /// The content item. + /// The property type alias. + /// The filesystem-relative filepath, or null to clear properties. + public void Populate(IContentBase content, string propertyTypeAlias, string filepath) + { + if (content == null) throw new ArgumentNullException("content"); + if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); + + // no property = nothing to do + if (content.Properties.Contains(propertyTypeAlias) == false) return; + + // get the config, no config = nothing to do + var autoFillConfig = GetConfig(propertyTypeAlias); + if (autoFillConfig == null) return; // nothing + + // populate + Populate(content, autoFillConfig, filepath); + } + + /// + /// Populates the auto-fill properties of a content item. + /// + /// The content item. + /// The property type alias. + /// The filesystem-relative filepath, or null to clear properties. + /// The stream containing the file data. + public void Populate(IContentBase content, string propertyTypeAlias, string filepath, Stream filestream) + { + if (content == null) throw new ArgumentNullException("content"); + if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); + + // no property = nothing to do + if (content.Properties.Contains(propertyTypeAlias) == false) return; + + // get the config, no config = nothing to do + var autoFillConfig = GetConfig(propertyTypeAlias); + if (autoFillConfig == null) return; // nothing + + // populate + Populate(content, autoFillConfig, filepath, filestream); + } + + /// + /// Populates the auto-fill properties of a content item, for a specified auto-fill configuration. + /// + /// The content item. + /// The auto-fill configuration. + /// The filesystem path to the uploaded file. + /// The parameter is the path relative to the filesystem. + public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath) + { + if (content == null) throw new ArgumentNullException("content"); + if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); + + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace()) + { + ResetProperties(content, autoFillConfig); + } + else + { + // if anything goes wrong, just reset the properties + try + { + using (var filestream = _mediaFileSystem.OpenFile(filepath)) + { + var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); + var size = _mediaFileSystem.IsImageFile(extension) ? (Size?) _mediaFileSystem.GetDimensions(filestream) : null; + SetProperties(content, autoFillConfig, size, filestream.Length, extension); + } + } + catch (Exception ex) + { + _logger.Error(typeof(UploadAutoFillProperties), "Could not populate upload auto-fill properties for file \"" + + filepath + "\".", ex); + ResetProperties(content, autoFillConfig); + } + } + } + + /// + /// Populates the auto-fill properties of a content item. + /// + /// The content item. + /// + /// The filesystem-relative filepath, or null to clear properties. + /// The stream containing the file data. + public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream) + { + if (content == null) throw new ArgumentNullException("content"); + if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); + + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace() || filestream == null) + { + ResetProperties(content, autoFillConfig); + } + else + { + var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); + var size = _mediaFileSystem.IsImageFile(extension) ? (Size?)_mediaFileSystem.GetDimensions(filestream) : null; + SetProperties(content, autoFillConfig, size, filestream.Length, extension); + } + } + + private static void SetProperties(IContentBase content, IImagingAutoFillUploadField autoFillConfig, Size? size, long length, string extension) + { + if (content == null) throw new ArgumentNullException("content"); + if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); + + if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) + content.Properties[autoFillConfig.WidthFieldAlias].Value = size.HasValue ? size.Value.Width.ToInvariantString() : string.Empty; + + if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) + content.Properties[autoFillConfig.HeightFieldAlias].Value = size.HasValue ? size.Value.Height.ToInvariantString() : string.Empty; + + if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) + content.Properties[autoFillConfig.LengthFieldAlias].Value = length; + + if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) + content.Properties[autoFillConfig.ExtensionFieldAlias].Value = extension; +} + + private static void ResetProperties(IContentBase content, IImagingAutoFillUploadField autoFillConfig) + { + if (content == null) throw new ArgumentNullException("content"); + if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); + + if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) + content.Properties[autoFillConfig.WidthFieldAlias].Value = string.Empty; + + if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) + content.Properties[autoFillConfig.HeightFieldAlias].Value = string.Empty; + + if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) + content.Properties[autoFillConfig.LengthFieldAlias].Value = string.Empty; + + if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) + content.Properties[autoFillConfig.ExtensionFieldAlias].Value = string.Empty; + } + } +} diff --git a/src/Umbraco.Core/Models/ApplicationTree.cs b/src/Umbraco.Core/Models/ApplicationTree.cs index cc754ec694..0397b4238d 100644 --- a/src/Umbraco.Core/Models/ApplicationTree.cs +++ b/src/Umbraco.Core/Models/ApplicationTree.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Diagnostics; +using Umbraco.Core.Logging; namespace Umbraco.Core.Models { @@ -84,16 +86,64 @@ namespace Umbraco.Core.Models public string Type { get; set; } private Type _runtimeType; - + /// /// Returns the CLR type based on it's assembly name stored in the config /// /// public Type GetRuntimeType() { - return _runtimeType ?? (_runtimeType = System.Type.GetType(Type)); - } - + if (_runtimeType != null) + return _runtimeType; + _runtimeType = TryGetType(Type); + return _runtimeType; + } + + /// + /// Used to try to get and cache the tree type + /// + /// + /// + internal static Type TryGetType(string type) + { + try + { + return ResolvedTypes.GetOrAdd(type, s => + { + var result = System.Type.GetType(type); + if (result != null) + { + return result; + } + + //we need to implement a bit of a hack here due to some trees being renamed and backwards compat + var parts = type.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 2) + { + if (parts[1].Trim() == "umbraco" && parts[0].StartsWith("Umbraco.Web.Trees") && parts[0].EndsWith("Controller") == false) + { + //if it's one of our controllers but it's not suffixed with "Controller" then add it and try again + var tempType = parts[0] + "Controller, umbraco"; + + result = System.Type.GetType(tempType); + if (result != null) + { + return result; + } + } + } + + throw new InvalidOperationException("Could not resolve type"); + }); + } + catch (InvalidOperationException) + { + //swallow, this is our own exception, couldn't find the type + return null; + } + } + + private static readonly ConcurrentDictionary ResolvedTypes = new ConcurrentDictionary(); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index ec73e1ff5e..cef06ec4f7 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -274,6 +274,9 @@ namespace Umbraco.Core.Models /// public bool HasPublishedVersion { get { return PublishedVersionGuid != default(Guid); } } + [IgnoreDataMember] + internal DateTime PublishedDate { get; set; } + /// /// Changes the Trashed state of the content object /// diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 9a90e5ac47..c7e6829983 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -1,25 +1,14 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.Globalization; using System.IO; using System.Linq; using System.Web; -using System.Xml; using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; -using Umbraco.Core.Media; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Strings; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Services; namespace Umbraco.Core.Models @@ -52,7 +41,7 @@ namespace Umbraco.Core.Models /// * The item exists and is published /// * A call to ContentService.Save is made /// * The item has not been modified whatsoever apart from changing it's published status from published to saved - /// + /// /// In this case, there is no reason to make any database changes at all /// internal static bool RequiresSaving(this IContent entity) @@ -72,7 +61,7 @@ namespace Umbraco.Core.Models /// * The item exists and is published /// * A call to ContentService.Save is made /// * The item has not been modified whatsoever apart from changing it's published status from published to saved - /// + /// /// In this case, there is no reason to make any database changes at all /// internal static bool RequiresSaving(this IContent entity, PublishedState publishedState) @@ -80,7 +69,7 @@ namespace Umbraco.Core.Models var publishedChanged = entity.IsPropertyDirty("Published") && publishedState != PublishedState.Unpublished; //check if any user prop has changed var propertyValueChanged = entity.IsAnyUserPropertyDirty(); - + //We need to know if any other property apart from Published was changed here //don't create a new version if the published state has changed to 'Save' but no data has actually been changed if (publishedChanged && entity.Published == false && propertyValueChanged == false) @@ -312,9 +301,9 @@ namespace Umbraco.Core.Models return content.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Contains(recycleBinId.ToInvariantString()); } - + /// - /// Removes characters that are not valide XML characters from all entity properties + /// Removes characters that are not valide XML characters from all entity properties /// of type string. See: http://stackoverflow.com/a/961504/5018 /// /// @@ -329,7 +318,7 @@ namespace Umbraco.Core.Models { if (property.Value is string) { - var value = (string)property.Value; + var value = (string) property.Value; property.Value = value.ToValidXmlString(); } } @@ -410,7 +399,7 @@ namespace Umbraco.Core.Models /// /// Set property values by alias with an annonymous object /// - public static void PropertyValues(this IContent content, object value) + public static void PropertyValues(this IContentBase content, object value) { if (value == null) throw new Exception("No properties has been passed in"); @@ -443,160 +432,122 @@ namespace Umbraco.Core.Models } } + public static IContentTypeComposition GetContentType(this IContentBase contentBase) + { + if (contentBase == null) throw new ArgumentNullException("contentBase"); + + var content = contentBase as IContent; + if (content != null) return content.ContentType; + var media = contentBase as IMedia; + if (media != null) return media.ContentType; + var member = contentBase as IMember; + if (member != null) return member.ContentType; + throw new NotSupportedException("Unsupported IContentBase implementation: " + contentBase.GetType().FullName + "."); + } + #region SetValue for setting file contents /// - /// Sets and uploads the file from a HttpPostedFileBase object as the property value + /// Stores and sets an uploaded HttpPostedFileBase as a property value. /// - /// to add property value to - /// Alias of the property to save the value on - /// The containing the file that will be uploaded + /// A content item. + /// The property alias. + /// The uploaded . public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileBase value) { - // Ensure we get the filename without the path in IE in intranet mode + // ensure we get the filename without the path in IE in intranet mode // http://stackoverflow.com/questions/382464/httppostedfile-filename-different-from-ie - var fileName = value.FileName; - if (fileName.LastIndexOf(@"\") > 0) - fileName = fileName.Substring(fileName.LastIndexOf(@"\") + 1); + var filename = value.FileName; + var pos = filename.LastIndexOf(@"\", StringComparison.InvariantCulture); + if (pos > 0) + filename = filename.Substring(pos + 1); - var name = - IOHelper.SafeFileName( - fileName.Substring(fileName.LastIndexOf(IOHelper.DirSepChar) + 1, - fileName.Length - fileName.LastIndexOf(IOHelper.DirSepChar) - 1) - .ToLower()); + // strip any directory info + pos = filename.LastIndexOf(IOHelper.DirSepChar); + if (pos > 0) + filename = filename.Substring(pos + 1); - if (string.IsNullOrEmpty(name) == false) - SetFileOnContent(content, propertyTypeAlias, name, value.InputStream); + // get a safe & clean filename + filename = IOHelper.SafeFileName(filename); + if (string.IsNullOrWhiteSpace(filename)) return; + filename = filename.ToLower(); + + FileSystemProviderManager.Current.MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, value.InputStream); } /// - /// Sets and uploads the file from a HttpPostedFile object as the property value + /// Stores and sets an uploaded HttpPostedFile as a property value. /// - /// to add property value to - /// Alias of the property to save the value on - /// The containing the file that will be uploaded + /// A content item. + /// The property alias. + /// The uploaded . public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFile value) { - SetValue(content, propertyTypeAlias, (HttpPostedFileBase)new HttpPostedFileWrapper(value)); + SetValue(content, propertyTypeAlias, (HttpPostedFileBase) new HttpPostedFileWrapper(value)); } /// - /// Sets and uploads the file from a HttpPostedFileWrapper object as the property value + /// Stores and sets an uploaded HttpPostedFileWrapper as a property value. /// - /// to add property value to - /// Alias of the property to save the value on - /// The containing the file that will be uploaded + /// A content item. + /// The property alias. + /// The uploaded . [Obsolete("There is no reason for this overload since HttpPostedFileWrapper inherits from HttpPostedFileBase")] public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileWrapper value) { - SetValue(content, propertyTypeAlias, (HttpPostedFileBase)value); + SetValue(content, propertyTypeAlias, (HttpPostedFileBase) value); } /// - /// Sets and uploads the file from a as the property value + /// Stores and sets a file as a property value. /// - /// to add property value to - /// Alias of the property to save the value on - /// Name of the file - /// to save to disk - public static void SetValue(this IContentBase content, string propertyTypeAlias, string fileName, Stream fileStream) + /// A content item. + /// The property alias. + /// The name of the file. + /// A stream containing the file data. + /// This really is for FileUpload fields only, and should be obsoleted. For anything else, + /// you need to store the file by yourself using Store and then figure out + /// how to deal with auto-fill properties (if any) and thumbnails (if any) by yourself. + public static void SetValue(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream) { - var name = IOHelper.SafeFileName(fileName); + if (filename == null || filestream == null) return; - if (string.IsNullOrEmpty(name) == false && fileStream != null) - SetFileOnContent(content, propertyTypeAlias, name, fileStream); + // get a safe & clean filename + filename = IOHelper.SafeFileName(filename); + if (string.IsNullOrWhiteSpace(filename)) return; + filename = filename.ToLower(); + + FileSystemProviderManager.Current.MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, filestream); } - private static void SetFileOnContent(IContentBase content, string propertyTypeAlias, string filename, Stream fileStream) + /// + /// Stores a file. + /// + /// A content item. + /// The property alias. + /// The name of the file. + /// A stream containing the file data. + /// The original file path, if any. + /// The path to the file, relative to the media filesystem. + /// + /// Does NOT set the property value, so one should probably store the file and then do + /// something alike: property.Value = MediaHelper.FileSystem.GetUrl(filepath). + /// The original file path is used, in the old media file path scheme, to try and reuse + /// the "folder number" that was assigned to the previous file referenced by the property, + /// if any. + /// + public static string StoreFile(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream, string filepath) { - var property = content.Properties.FirstOrDefault(x => x.Alias == propertyTypeAlias); - if (property == null) - return; - - //TODO: ALl of this naming logic needs to be put into the ImageHelper and then we need to change FileUploadPropertyValueEditor to do the same! - - var numberedFolder = MediaSubfolderCounter.Current.Increment(); - var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? Path.Combine(numberedFolder.ToString(CultureInfo.InvariantCulture), filename) - : numberedFolder + "-" + filename; - - var extension = Path.GetExtension(filename).Substring(1).ToLowerInvariant(); - - //the file size is the length of the stream in bytes - var fileSize = fileStream.Length; - - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - fs.AddFile(fileName, fileStream); - - //Check if file supports resizing and create thumbnails - var supportsResizing = UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(extension); - - //the config section used to auto-fill properties - IImagingAutoFillUploadField uploadFieldConfigNode = null; - - //Check for auto fill of additional properties - if (UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties != null) - { - uploadFieldConfigNode = UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties - .FirstOrDefault(x => x.Alias == propertyTypeAlias); - - } - - if (supportsResizing) - { - //get the original image from the original stream - if (fileStream.CanSeek) fileStream.Seek(0, 0); - using (var originalImage = Image.FromStream(fileStream)) - { - var additionalSizes = new List(); - - //Look up Prevalues for this upload datatype - if it is an upload datatype - get additional configured sizes - if (property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias) - { - //Get Prevalues by the DataType's Id: property.PropertyType.DataTypeId - var values = ApplicationContext.Current.Services.DataTypeService.GetPreValuesByDataTypeId(property.PropertyType.DataTypeDefinitionId); - var thumbnailSizes = values.FirstOrDefault(); - //Additional thumbnails configured as prevalues on the DataType - if (thumbnailSizes != null) - { - foreach (var thumb in thumbnailSizes.Split(new[] { ";", "," }, StringSplitOptions.RemoveEmptyEntries)) - { - int thumbSize; - if (thumb != "" && int.TryParse(thumb, out thumbSize)) - { - additionalSizes.Add(thumbSize); - } - } - } - } - - ImageHelper.GenerateMediaThumbnails(fs, fileName, extension, originalImage, additionalSizes); - - //while the image is still open, we'll check if we need to auto-populate the image properties - if (uploadFieldConfigNode != null) - { - content.SetValue(uploadFieldConfigNode.WidthFieldAlias, originalImage.Width.ToString(CultureInfo.InvariantCulture)); - content.SetValue(uploadFieldConfigNode.HeightFieldAlias, originalImage.Height.ToString(CultureInfo.InvariantCulture)); - } - - } - } - - //if auto-fill is true, then fill the remaining, non-image properties - if (uploadFieldConfigNode != null) - { - content.SetValue(uploadFieldConfigNode.LengthFieldAlias, fileSize.ToString(CultureInfo.InvariantCulture)); - content.SetValue(uploadFieldConfigNode.ExtensionFieldAlias, extension); - } - - //Set the value of the property to that of the uploaded file's url - property.Value = fs.GetUrl(fileName); + var propertyType = content.GetContentType() + .CompositionPropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + if (propertyType == null) throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + "."); + return FileSystemProviderManager.Current.MediaFileSystem.StoreFile(content, propertyType, filename, filestream, filepath); } #endregion #region User/Profile methods - + /// /// Gets the for the Creator of this media item. /// @@ -671,7 +622,7 @@ namespace Umbraco.Core.Models ///// ///// ///// - ///// The tags returned are only relavent for published content & saved media or members + ///// The tags returned are only relavent for published content & saved media or members ///// //public static IEnumerable GetTags(this IContentBase content, string propertyTypeAlias, string tagGroup = "default") //{ diff --git a/src/Umbraco.Core/Models/ContentTypeExtensions.cs b/src/Umbraco.Core/Models/ContentTypeExtensions.cs deleted file mode 100644 index a47b430979..0000000000 --- a/src/Umbraco.Core/Models/ContentTypeExtensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Services; - -namespace Umbraco.Core.Models -{ - internal static class ContentTypeExtensions - { - /// - /// Get all descendant content types - /// - /// - /// - public static IEnumerable Descendants(this IContentTypeBase contentType) - { - var contentTypeService = ApplicationContext.Current.Services.ContentTypeService; - var descendants = contentTypeService.GetContentTypeChildren(contentType.Id) - .SelectRecursive(type => contentTypeService.GetContentTypeChildren(type.Id)); - return descendants; - } - - /// - /// Get all descendant and self content types - /// - /// - /// - public static IEnumerable DescendantsAndSelf(this IContentTypeBase contentType) - { - var descendantsAndSelf = new[] { contentType }.Concat(contentType.Descendants()); - return descendantsAndSelf; - } - - ///// - ///// Returns the descendant content type Ids for the given content type - ///// - ///// - ///// - //public static IEnumerable DescendantIds(this IContentTypeBase contentType) - //{ - // return ((ContentTypeService) ApplicationContext.Current.Services.ContentTypeService) - // .GetDescendantContentTypeIds(contentType.Id); - //} - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentXmlEntity.cs b/src/Umbraco.Core/Models/ContentXmlEntity.cs index 0450fdc72e..93185834a3 100644 --- a/src/Umbraco.Core/Models/ContentXmlEntity.cs +++ b/src/Umbraco.Core/Models/ContentXmlEntity.cs @@ -42,6 +42,7 @@ namespace Umbraco.Core.Models public Guid Key { get; set; } public DateTime CreateDate { get; set; } public DateTime UpdateDate { get; set; } + public DateTime? DeletedDate { get; set; } /// /// Special case, always return false, this will cause the repositories managing @@ -60,5 +61,7 @@ namespace Umbraco.Core.Models DeepCloneHelper.DeepCloneRefProperties(this, clone); return clone; } + + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs index 14c8640fc9..3798cb08e0 100644 --- a/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs +++ b/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs @@ -16,6 +16,11 @@ namespace Umbraco.Core.Models.Editors /// public class ContentPropertyData { + public ContentPropertyData(object value, PreValueCollection preValues) + : this(value, preValues, new Dictionary()) + { + } + public ContentPropertyData(object value, PreValueCollection preValues, IDictionary additionalData) { Value = value; @@ -28,6 +33,9 @@ namespace Umbraco.Core.Models.Editors /// public object Value { get; private set; } + /// + /// The pre-value collection for the content property + /// public PreValueCollection PreValues { get; private set; } /// diff --git a/src/Umbraco.Core/Models/EntityBase/Entity.cs b/src/Umbraco.Core/Models/EntityBase/Entity.cs index 637255a1c8..d4da2676c1 100644 --- a/src/Umbraco.Core/Models/EntityBase/Entity.cs +++ b/src/Umbraco.Core/Models/EntityBase/Entity.cs @@ -101,6 +101,9 @@ namespace Umbraco.Core.Models.EntityBase set { SetPropertyValueAndDetectChanges(value, ref _updateDate, Ps.Value.UpdateDateSelector); } } + [IgnoreDataMember] + public DateTime? DeletedDate { get; set; } + internal virtual void ResetIdentity() { _hasIdentity = false; @@ -113,8 +116,10 @@ namespace Umbraco.Core.Models.EntityBase /// internal virtual void AddingEntity() { - CreateDate = DateTime.Now; - UpdateDate = DateTime.Now; + if (IsPropertyDirty("CreateDate") == false || _createDate == default(DateTime)) + CreateDate = DateTime.Now; + if (IsPropertyDirty("UpdateDate") == false || _updateDate == default(DateTime)) + UpdateDate = CreateDate; } /// @@ -122,7 +127,8 @@ namespace Umbraco.Core.Models.EntityBase /// internal virtual void UpdatingEntity() { - UpdateDate = DateTime.Now; + if (IsPropertyDirty("UpdateDate") == false || _updateDate == default(DateTime)) + UpdateDate = DateTime.Now; } /// diff --git a/src/Umbraco.Core/Models/EntityBase/IAggregateRoot.cs b/src/Umbraco.Core/Models/EntityBase/IAggregateRoot.cs index 4298dd9cf4..aacb5185e0 100644 --- a/src/Umbraco.Core/Models/EntityBase/IAggregateRoot.cs +++ b/src/Umbraco.Core/Models/EntityBase/IAggregateRoot.cs @@ -3,7 +3,7 @@ /// /// Marker interface for aggregate roots /// - public interface IAggregateRoot : IEntity + public interface IAggregateRoot : IDeletableEntity { } diff --git a/src/Umbraco.Core/Models/EntityBase/IDeletableEntity.cs b/src/Umbraco.Core/Models/EntityBase/IDeletableEntity.cs new file mode 100644 index 0000000000..42f91b6a6c --- /dev/null +++ b/src/Umbraco.Core/Models/EntityBase/IDeletableEntity.cs @@ -0,0 +1,11 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Models.EntityBase +{ + public interface IDeletableEntity : IEntity + { + [DataMember] + DateTime? DeletedDate { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/EntityBase/IEntity.cs b/src/Umbraco.Core/Models/EntityBase/IEntity.cs index 81f5f632ef..059983bb38 100644 --- a/src/Umbraco.Core/Models/EntityBase/IEntity.cs +++ b/src/Umbraco.Core/Models/EntityBase/IEntity.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Models.EntityBase /// /// Guid based Id /// - /// The key is currectly used to store the Unique Id from the + /// The key is currectly used to store the Unique Id from the /// umbracoNode table, which many of the entities are based on. [DataMember] Guid Key { get; set; } diff --git a/src/Umbraco.Core/Models/File.cs b/src/Umbraco.Core/Models/File.cs index e271774b8d..41838a3841 100644 --- a/src/Umbraco.Core/Models/File.cs +++ b/src/Umbraco.Core/Models/File.cs @@ -49,7 +49,10 @@ namespace Umbraco.Core.Models return path .Replace('\\', System.IO.Path.DirectorySeparatorChar) .Replace('/', System.IO.Path.DirectorySeparatorChar); - //.TrimStart(System.IO.Path.DirectorySeparatorChar); + + //Don't strip the start - this was a bug fixed in 7.3, see ScriptRepositoryTests.PathTests + //.TrimStart(System.IO.Path.DirectorySeparatorChar) + //.TrimStart('/'); } /// diff --git a/src/Umbraco.Core/Models/GridValue.cs b/src/Umbraco.Core/Models/GridValue.cs new file mode 100644 index 0000000000..87f6146af6 --- /dev/null +++ b/src/Umbraco.Core/Models/GridValue.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Models +{ + /// + /// A model representing the value saved for the grid + /// + public class GridValue + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("sections")] + public IEnumerable Sections { get; set; } + + public class GridSection + { + [JsonProperty("grid")] + public string Grid { get; set; } + + [JsonProperty("rows")] + public IEnumerable Rows { get; set; } + } + + public class GridRow + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("id")] + public Guid Id { get; set; } + + [JsonProperty("areas")] + public IEnumerable Areas { get; set; } + + [JsonProperty("styles")] + public JToken Styles { get; set; } + + [JsonProperty("config")] + public JToken Config { get; set; } + } + + public class GridArea + { + [JsonProperty("grid")] + public string Grid { get; set; } + + [JsonProperty("controls")] + public IEnumerable Controls { get; set; } + + [JsonProperty("styles")] + public JToken Styles { get; set; } + + [JsonProperty("config")] + public JToken Config { get; set; } + } + + public class GridControl + { + [JsonProperty("value")] + public JToken Value { get; set; } + + [JsonProperty("editor")] + public GridEditor Editor { get; set; } + + [JsonProperty("styles")] + public JToken Styles { get; set; } + + [JsonProperty("config")] + public JToken Config { get; set; } + } + + public class GridEditor + { + [JsonProperty("alias")] + public string Alias { get; set; } + + [JsonProperty("view")] + public string View { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/IMacroProperty.cs b/src/Umbraco.Core/Models/IMacroProperty.cs index a6c1d9ca5f..8726f51317 100644 --- a/src/Umbraco.Core/Models/IMacroProperty.cs +++ b/src/Umbraco.Core/Models/IMacroProperty.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.Serialization; using Umbraco.Core.Models.EntityBase; @@ -11,6 +12,9 @@ namespace Umbraco.Core.Models [DataMember] int Id { get; set; } + [DataMember] + Guid Key { get; set; } + /// /// Gets or sets the Alias of the Property /// diff --git a/src/Umbraco.Core/Models/IMediaType.cs b/src/Umbraco.Core/Models/IMediaType.cs index 29e4b665ba..e8f7c16190 100644 --- a/src/Umbraco.Core/Models/IMediaType.cs +++ b/src/Umbraco.Core/Models/IMediaType.cs @@ -4,7 +4,7 @@ namespace Umbraco.Core.Models { /// /// Defines a ContentType, which Media is based on - /// public interface IMediaType : IContentTypeComposition { diff --git a/src/Umbraco.Core/Models/IXsltFile.cs b/src/Umbraco.Core/Models/IXsltFile.cs new file mode 100644 index 0000000000..f6d4418f88 --- /dev/null +++ b/src/Umbraco.Core/Models/IXsltFile.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Core.Models +{ + public interface IXsltFile : IFile + { + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index 0dc95a8987..fab34e5f17 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -13,6 +13,7 @@ namespace Umbraco.Core.Models.Identity public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { config.CreateMap() + .ForMember(user => user.LastLoginDateUtc, expression => expression.MapFrom(user => user.LastLoginDate.ToUniversalTime())) .ForMember(user => user.Email, expression => expression.MapFrom(user => user.Email)) .ForMember(user => user.Id, expression => expression.MapFrom(user => user.Id)) .ForMember(user => user.LockoutEndDateUtc, expression => expression.MapFrom(user => user.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null)) diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs index cba4fc514a..c867dcf622 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -24,12 +24,17 @@ namespace Umbraco.Core.Models.Identity /// /// public IdentityUser() - { + { this.Claims = new List(); this.Roles = new List(); this.Logins = new List(); } + /// + /// Last login date + /// + public virtual DateTime? LastLoginDateUtc { get; set; } + /// /// Email /// diff --git a/src/Umbraco.Core/Models/Macro.cs b/src/Umbraco.Core/Models/Macro.cs index ef5479c316..58ee94bdc9 100644 --- a/src/Umbraco.Core/Models/Macro.cs +++ b/src/Umbraco.Core/Models/Macro.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Reflection; using System.Runtime.Serialization; -using System.Text.RegularExpressions; -using Umbraco.Core.IO; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Strings; @@ -27,11 +24,12 @@ namespace Umbraco.Core.Models _addedProperties = new List(); _removedProperties = new List(); } - + /// /// Creates an item with pre-filled properties /// /// + /// /// /// /// @@ -43,10 +41,11 @@ namespace Umbraco.Core.Models /// /// /// - public Macro(int id, bool useInEditor, int cacheDuration, string @alias, string name, string controlType, string controlAssembly, string xsltPath, bool cacheByPage, bool cacheByMember, bool dontRender, string scriptPath) + public Macro(int id, Guid key, bool useInEditor, int cacheDuration, string @alias, string name, string controlType, string controlAssembly, string xsltPath, bool cacheByPage, bool cacheByMember, bool dontRender, string scriptPath) : this() { Id = id; + Key = key; UseInEditor = useInEditor; CacheDuration = cacheDuration; Alias = alias.ToCleanString(CleanStringType.Alias); diff --git a/src/Umbraco.Core/Models/MacroProperty.cs b/src/Umbraco.Core/Models/MacroProperty.cs index 0f29c18881..d3d82d10ad 100644 --- a/src/Umbraco.Core/Models/MacroProperty.cs +++ b/src/Umbraco.Core/Models/MacroProperty.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Models { public MacroProperty() { - + _key = Guid.NewGuid(); } /// @@ -30,6 +30,7 @@ namespace Umbraco.Core.Models _alias = alias; _name = name; _sortOrder = sortOrder; + _key = Guid.NewGuid(); //try to get the new mapped parameter editor var mapped = LegacyParameterEditorAliasConverter.GetNewAliasFromLegacyAlias(editorAlias, false); @@ -45,16 +46,18 @@ namespace Umbraco.Core.Models /// Ctor for creating an existing property /// /// + /// /// /// /// /// - internal MacroProperty(int id, string @alias, string name, int sortOrder, string editorAlias) + internal MacroProperty(int id, Guid key, string @alias, string name, int sortOrder, string editorAlias) { _id = id; _alias = alias; _name = name; _sortOrder = sortOrder; + _key = key; //try to get the new mapped parameter editor var mapped = LegacyParameterEditorAliasConverter.GetNewAliasFromLegacyAlias(editorAlias, false); @@ -66,6 +69,7 @@ namespace Umbraco.Core.Models _editorAlias = editorAlias; } + private Guid _key; private string _alias; private string _name; private int _sortOrder; @@ -76,6 +80,7 @@ namespace Umbraco.Core.Models private class PropertySelectors { + public readonly PropertyInfo KeySelector = ExpressionHelper.GetPropertyInfo(x => x.Key); public readonly PropertyInfo AliasSelector = ExpressionHelper.GetPropertyInfo(x => x.Alias); public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); public readonly PropertyInfo SortOrderSelector = ExpressionHelper.GetPropertyInfo(x => x.SortOrder); @@ -83,6 +88,16 @@ namespace Umbraco.Core.Models public readonly PropertyInfo PropertyTypeSelector = ExpressionHelper.GetPropertyInfo(x => x.EditorAlias); } + /// + /// Gets or sets the Key of the Property + /// + [DataMember] + public Guid Key + { + get { return _key; } + set { SetPropertyValueAndDetectChanges(value, ref _key, Ps.Value.KeySelector); } + } + /// /// Gets or sets the Alias of the Property /// diff --git a/src/Umbraco.Core/Models/MediaExtensions.cs b/src/Umbraco.Core/Models/MediaExtensions.cs index 1f2e1b62b2..a006548773 100644 --- a/src/Umbraco.Core/Models/MediaExtensions.cs +++ b/src/Umbraco.Core/Models/MediaExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -9,73 +8,56 @@ using Umbraco.Core.PropertyEditors.ValueConverters; namespace Umbraco.Core.Models { - internal static class MediaExtensions + public 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 + /// Gets the url of a media item. /// - /// public static string GetUrl(this IMedia media, string propertyAlias, ILogger logger) { var propertyType = media.PropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyAlias)); - if (propertyType != null) + if (propertyType == null) return string.Empty; + + var val = media.Properties[propertyType]; + if (val == null) return string.Empty; + + var jsonString = val.Value as string; + if (jsonString == null) return string.Empty; + + if (propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias) + return jsonString; + + if (propertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias) { - var val = media.Properties[propertyType]; - if (val != null) + if (jsonString.DetectIsJson() == false) + return jsonString; + + try { - 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 - } + 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; } } + + // 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 + /// Gets the urls of a media item. /// - /// 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(); + return contentSection.ImageAutoFillProperties + .Select(field => media.GetUrl(field.Alias, logger)) + .Where(link => string.IsNullOrWhiteSpace(link) == false) + .ToArray(); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PropertyTypeCollection.cs b/src/Umbraco.Core/Models/PropertyTypeCollection.cs index 38abd0c57d..a06e6d737d 100644 --- a/src/Umbraco.Core/Models/PropertyTypeCollection.cs +++ b/src/Umbraco.Core/Models/PropertyTypeCollection.cs @@ -69,6 +69,7 @@ namespace Umbraco.Core.Models OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + //TODO: Instead of 'new' this should explicitly implement one of the collection interfaces members internal new void Add(PropertyType item) { using (new WriteLock(_addLocker)) diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index 1ed15a8fb4..5989f885cb 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -110,6 +110,12 @@ namespace Umbraco.Core.Models _ruleCollection.Clear(); } + + internal void ClearRemovedRules() + { + _removedRules.Clear(); + } + [DataMember] public int LoginNodeId { diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentEnumerable.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentEnumerable.cs new file mode 100644 index 0000000000..7891253188 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentEnumerable.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// The published content enumerable, this model is to allow ToString to be overriden for value converters to support legacy requests for string values + /// + public class PublishedContentEnumerable : IEnumerable + { + /// + /// The items in the collection + /// + private readonly IEnumerable _items; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The published content items + /// + public PublishedContentEnumerable(IEnumerable publishedContent) + { + if (publishedContent == null) throw new ArgumentNullException("publishedContent"); + _items = publishedContent; + } + + /// + /// The ToString method to convert the objects back to CSV + /// + /// + /// The . + /// + public override string ToString() + { + return string.Join(",", _items.Select(x => x.Id)); + } + + /// + /// The get enumerator. + /// + /// + /// The . + /// + public IEnumerator GetEnumerator() + { + return _items.GetEnumerator(); + } + + /// + /// The get enumerator. + /// + /// + /// The . + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs index fef066e0b1..56745ece66 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Linq; namespace Umbraco.Core.Models.PublishedContent { - + [TypeConverter(typeof(PublishedContentTypeConverter))] public class PublishedContentExtended : PublishedContentWrapped, IPublishedContentExtended { #region Constructor @@ -186,6 +187,11 @@ namespace Umbraco.Core.Models.PublishedContent } #endregion + + public override string ToString() + { + return Id.ToString(); + } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index d05960b08f..63164cf271 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -179,7 +179,7 @@ namespace Umbraco.Core.Models.PublishedContent } if (contentType == null) - throw new Exception(string.Format("ContentTypeService failed to find a {0} type with alias \"{1}\".", + throw new InvalidOperationException(string.Format("ContentTypeService failed to find a {0} type with alias \"{1}\".", itemType.ToString().ToLower(), alias)); return new PublishedContentType(contentType); diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs new file mode 100644 index 0000000000..4141a19b35 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Linq; + +namespace Umbraco.Core.Models.PublishedContent +{ + internal class PublishedContentTypeConverter : TypeConverter + { + private static readonly Type[] ConvertableTypes = new[] + { + typeof(int) + }; + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return ConvertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType)) + || base.CanConvertFrom(context, destinationType); + } + + public override object ConvertTo( + ITypeDescriptorContext context, + CultureInfo culture, + object value, + Type destinationType) + { + var publishedContent = value as IPublishedContent; + if (publishedContent == null) + return null; + + if (TypeHelper.IsTypeAssignableFrom(destinationType)) + { + return publishedContent.Id; + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index 1875ae03b3..d586bc155a 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -106,12 +106,13 @@ namespace Umbraco.Core.Models.PublishedContent /// The property type alias. /// The datatype definition identifier. /// The property editor alias. + /// Generally used only for testing, in production this will always be true /// /// The new published property type does not belong to a published content type. /// The values of and are /// assumed to be valid and consistent. /// - internal PublishedPropertyType(string propertyTypeAlias, int dataTypeDefinitionId, string propertyEditorAlias) + internal PublishedPropertyType(string propertyTypeAlias, int dataTypeDefinitionId, string propertyEditorAlias, bool initConverters = true) { // ContentType // - in unit tests, to be set by PublishedContentType when creating it @@ -122,7 +123,8 @@ namespace Umbraco.Core.Models.PublishedContent DataTypeId = dataTypeDefinitionId; PropertyEditorAlias = propertyEditorAlias; - InitializeConverters(); + if (initConverters) + InitializeConverters(); } #endregion diff --git a/src/Umbraco.Core/Models/Range.cs b/src/Umbraco.Core/Models/Range.cs new file mode 100644 index 0000000000..3095d6342d --- /dev/null +++ b/src/Umbraco.Core/Models/Range.cs @@ -0,0 +1,52 @@ +using System; +namespace Umbraco.Core.Models +{ + // The Range class. Adapted from http://stackoverflow.com/questions/5343006/is-there-a-c-sharp-type-for-representing-an-integer-range + /// Generic parameter. + public class Range where T : IComparable + { + /// Minimum value of the range. + public T Minimum { get; set; } + + /// Maximum value of the range. + public T Maximum { get; set; } + + /// Presents the Range in readable format. + /// String representation of the Range + public override string ToString() + { + return string.Format("{0},{1}", this.Minimum, this.Maximum); + } + + /// Determines if the range is valid. + /// True if range is valid, else false + public bool IsValid() + { + return this.Minimum.CompareTo(this.Maximum) <= 0; + } + + /// Determines if the provided value is inside the range. + /// The value to test + /// True if the value is inside Range, else false + public bool ContainsValue(T value) + { + return (this.Minimum.CompareTo(value) <= 0) && (value.CompareTo(this.Maximum) <= 0); + } + + /// Determines if this Range is inside the bounds of another range. + /// The parent range to test on + /// True if range is inclusive, else false + public bool IsInsideRange(Range range) + { + return this.IsValid() && range.IsValid() && range.ContainsValue(this.Minimum) && range.ContainsValue(this.Maximum); + } + + /// Determines if another range is inside the bounds of this range. + /// The child range to test + /// True if range is inside, else false + public bool ContainsRange(Range range) + { + return this.IsValid() && range.IsValid() && this.ContainsValue(range.Minimum) && this.ContainsValue(range.Maximum); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/DataTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/DataTypeDto.cs index b0e63b0726..54146114a8 100644 --- a/src/Umbraco.Core/Models/Rdbms/DataTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/DataTypeDto.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Models.Rdbms internal class DataTypeDto { [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 30)] + [PrimaryKeyColumn(IdentitySeed = 40)] public int PrimaryKey { get; set; } [Column("nodeId")] diff --git a/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs b/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs index 652e63df0b..6e8fc29d95 100644 --- a/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Models.Rdbms internal class DataTypePreValueDto { [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 6)] + [PrimaryKeyColumn(IdentitySeed = 10)] public int Id { get; set; } [Column("datatypeNodeId")] diff --git a/src/Umbraco.Core/Models/Rdbms/DocumentPublishedReadOnlyDto.cs b/src/Umbraco.Core/Models/Rdbms/DocumentPublishedReadOnlyDto.cs index 4a7e359d91..7c6507f499 100644 --- a/src/Umbraco.Core/Models/Rdbms/DocumentPublishedReadOnlyDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/DocumentPublishedReadOnlyDto.cs @@ -19,5 +19,8 @@ namespace Umbraco.Core.Models.Rdbms [Column("newest")] public bool Newest { get; set; } + + [Column("updateDate")] + public DateTime VersionDate { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/LockDto.cs b/src/Umbraco.Core/Models/Rdbms/LockDto.cs new file mode 100644 index 0000000000..b543ce4241 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/LockDto.cs @@ -0,0 +1,29 @@ +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoLock")] + [PrimaryKey("id")] + [ExplicitColumns] + internal class LockDto + { + public LockDto() + { + Value = 1; + } + + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoLock")] + public int Id { get; set; } + + [Column("value")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int Value { get; set; } + + [Column("name")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(64)] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/MacroDto.cs b/src/Umbraco.Core/Models/Rdbms/MacroDto.cs index 855b4d1f93..ba2cee9356 100644 --- a/src/Umbraco.Core/Models/Rdbms/MacroDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/MacroDto.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; @@ -13,6 +14,10 @@ namespace Umbraco.Core.Models.Rdbms [PrimaryKeyColumn] public int Id { get; set; } + [Column("uniqueId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacro_UniqueId")] + public Guid UniqueId { get; set; } + [Column("macroUseInEditor")] [Constraint(Default = "0")] public bool UseInEditor { get; set; } diff --git a/src/Umbraco.Core/Models/Rdbms/MacroPropertyDto.cs b/src/Umbraco.Core/Models/Rdbms/MacroPropertyDto.cs index e2efddc829..7cfe6c3da7 100644 --- a/src/Umbraco.Core/Models/Rdbms/MacroPropertyDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/MacroPropertyDto.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Persistence; +using System; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms @@ -11,7 +12,12 @@ namespace Umbraco.Core.Models.Rdbms [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } - + + // important to use column name != cmsMacro.uniqueId (fix in v8) + [Column("uniquePropertyId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_UniquePropertyId")] + public Guid UniqueId { get; set; } + [Column("editorAlias")] public string EditorAlias { get; set; } diff --git a/src/Umbraco.Core/Models/Rdbms/MemberDto.cs b/src/Umbraco.Core/Models/Rdbms/MemberDto.cs index e5f7b3f17c..cbe9f909f8 100644 --- a/src/Umbraco.Core/Models/Rdbms/MemberDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/MemberDto.cs @@ -22,6 +22,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("LoginName")] [Length(1000)] [Constraint(Default = "''")] + [Index(IndexTypes.NonClustered, Name = "IX_cmsMember_LoginName")] public string LoginName { get; set; } [Column("Password")] diff --git a/src/Umbraco.Core/Models/Rdbms/NodeDto.cs b/src/Umbraco.Core/Models/Rdbms/NodeDto.cs index c5fac092df..9b575e8c95 100644 --- a/src/Umbraco.Core/Models/Rdbms/NodeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/NodeDto.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Models.Rdbms [ExplicitColumns] internal class NodeDto { - public const int NodeIdSeed = 1050; + public const int NodeIdSeed = 1060; [Column("id")] [PrimaryKeyColumn(Name = "PK_structure", IdentitySeed = NodeIdSeed)] @@ -35,6 +35,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("path")] [Length(150)] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoNodePath")] public string Path { get; set; } [Column("sortOrder")] diff --git a/src/Umbraco.Core/Models/Rdbms/RelationDto.cs b/src/Umbraco.Core/Models/Rdbms/RelationDto.cs index 368904a5cb..d3d741a191 100644 --- a/src/Umbraco.Core/Models/Rdbms/RelationDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/RelationDto.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("parentId")] [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelation_parentChildType", ForColumns = "parentId,childId,relType")] public int ParentId { get; set; } [Column("childId")] diff --git a/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs index 8a6eb5c02a..d13ce33520 100644 --- a/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs @@ -15,6 +15,10 @@ namespace Umbraco.Core.Models.Rdbms [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] public int Id { get; set; } + [Column("typeUniqueId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_UniqueId")] + public Guid UniqueId { get; set; } + [Column("dual")] public bool Dual { get; set; } @@ -25,11 +29,13 @@ namespace Umbraco.Core.Models.Rdbms public Guid ChildObjectType { get; set; } [Column("name")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] public string Name { get; set; } [Column("alias")] [NullSetting(NullSetting = NullSettings.Null)] [Length(100)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] public string Alias { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UmbracoDeployChecksumDto.cs b/src/Umbraco.Core/Models/Rdbms/UmbracoDeployChecksumDto.cs deleted file mode 100644 index 06d904ee88..0000000000 --- a/src/Umbraco.Core/Models/Rdbms/UmbracoDeployChecksumDto.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseAnnotations; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; - -namespace Umbraco.Core.Models.Rdbms -{ - [TableName("umbracoDeployChecksum")] - [PrimaryKey("id")] - [ExplicitColumns] - internal class UmbracoDeployChecksumDto - { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoDeployChecksum")] - public int Id { get; set; } - - [Column("entityType")] - [Length(32)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoDeployChecksum", ForColumns = "entityType,entityGuid,entityPath")] - public string EntityType { get; set; } - - [Column("entityGuid")] - [NullSetting(NullSetting = NullSettings.Null)] - public Guid EntityGuid { get; set; } - - [Column("entityPath")] - [Length(256)] - [NullSetting(NullSetting = NullSettings.Null)] - public string EntityPath { get; set; } - - [Column("localChecksum")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Length(32)] - public string LocalChecksum { get; set; } - - [Column("compositeChecksum")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(32)] - public string CompositeChecksum { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UmbracoDeployDependencyDto.cs b/src/Umbraco.Core/Models/Rdbms/UmbracoDeployDependencyDto.cs deleted file mode 100644 index 765a32c929..0000000000 --- a/src/Umbraco.Core/Models/Rdbms/UmbracoDeployDependencyDto.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseAnnotations; - -namespace Umbraco.Core.Models.Rdbms -{ - [TableName("umbracoDeployDependency")] - [ExplicitColumns] - internal class UmbracoDeployDependencyDto - { - [Column("sourceId")] - [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_umbracoDeployDependency", OnColumns = "sourceId, targetId")] - [ForeignKey(typeof(UmbracoDeployChecksumDto), Name = "FK_umbracoDeployDependency_umbracoDeployChecksum_id1")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int SourceId { get; set; } - - [Column("targetId")] - [ForeignKey(typeof(UmbracoDeployChecksumDto), Name = "FK_umbracoDeployDependency_umbracoDeployChecksum_id2")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int TargetId { get; set; } - - [Column("mode")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int Mode { get; set; } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs b/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs index 1e6662735f..f0d769a21e 100644 --- a/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs @@ -15,6 +15,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("nodeId")] [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUser2NodePermission_nodeId")] public int NodeId { get; set; } [Column("permission")] diff --git a/src/Umbraco.Core/Models/Template.cs b/src/Umbraco.Core/Models/Template.cs index 5913b0f63b..bef4384943 100644 --- a/src/Umbraco.Core/Models/Template.cs +++ b/src/Umbraco.Core/Models/Template.cs @@ -15,7 +15,7 @@ using Umbraco.Core.Strings; namespace Umbraco.Core.Models { /// - /// Represents a Template file + /// Represents a Template file. /// [Serializable] [DataContract(IsReference = true)] diff --git a/src/Umbraco.Core/Models/TemplateOnDisk.cs b/src/Umbraco.Core/Models/TemplateOnDisk.cs new file mode 100644 index 0000000000..a8420adcb6 --- /dev/null +++ b/src/Umbraco.Core/Models/TemplateOnDisk.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a Template file that can have its content on disk. + /// + [Serializable] + [DataContract(IsReference = true)] + public class TemplateOnDisk : Template + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the template. + /// The alias of the template. + public TemplateOnDisk(string name, string alias) + : base(name, alias) + { + IsOnDisk = true; + } + + /// + /// Gets or sets a value indicating whether the content is on disk already. + /// + public bool IsOnDisk { get; set; } + + /// + /// Gets or sets the content. + /// + /// + /// Getting the content while the template is "on disk" throws, + /// the template must be saved before its content can be retrieved. + /// Setting the content means it is not "on disk" anymore, and the + /// template becomes (and behaves like) a normal template. + /// + public override string Content + { + get + { + return IsOnDisk ? string.Empty : base.Content; + } + set + { + base.Content = value; + IsOnDisk = false; + } + } + } +} diff --git a/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs b/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs index 42e1a0d415..a27657cbe0 100644 --- a/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs +++ b/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs @@ -113,6 +113,11 @@ namespace Umbraco.Core.Models } } + /// + /// When resolved from EntityService this checks if the entity has the HasChildren flag + /// + /// + /// public static bool HasChildren(this IUmbracoEntity entity) { if (entity.AdditionalData.ContainsKey("HasChildren")) @@ -133,6 +138,11 @@ namespace Umbraco.Core.Models return entity.AdditionalData.GetValueIgnoreCase(key, defaultVal); } + /// + /// When resolved from EntityService this checks if the entity has the IsContainer flag + /// + /// + /// public static bool IsContainer(this IUmbracoEntity entity) { if (entity.AdditionalData.ContainsKeyIgnoreCase("IsContainer") == false) return false; diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index 2be2010301..0df7a21e57 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -1,4 +1,6 @@ -using Umbraco.Core.CodeAnnotations; +using System; +using System.ComponentModel; +using Umbraco.Core.CodeAnnotations; namespace Umbraco.Core.Models { @@ -17,12 +19,14 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.ContentItemType)] [FriendlyName("Content Item Type")] + [Obsolete("This is not used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] ContentItemType, /// /// Root /// - [UmbracoObjectType(Constants.ObjectTypes.SystemRoot)] + [UmbracoObjectType(Constants.ObjectTypes.SystemRoot)] [FriendlyName("Root")] ROOT, @@ -31,6 +35,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Document, typeof(IContent))] [FriendlyName("Document")] + [UmbracoUdiType(Constants.UdiEntityType.Document)] Document, /// @@ -38,6 +43,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Media, typeof(IMedia))] [FriendlyName("Media")] + [UmbracoUdiType(Constants.UdiEntityType.Media)] Media, /// @@ -45,6 +51,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MemberType, typeof(IMemberType))] [FriendlyName("Member Type")] + [UmbracoUdiType(Constants.UdiEntityType.MemberType)] MemberType, /// @@ -52,6 +59,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Template, typeof(ITemplate))] [FriendlyName("Template")] + [UmbracoUdiType(Constants.UdiEntityType.Template)] Template, /// @@ -59,6 +67,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MemberGroup)] [FriendlyName("Member Group")] + [UmbracoUdiType(Constants.UdiEntityType.MemberGroup)] MemberGroup, //TODO: What is a 'Content Item' supposed to be??? @@ -67,6 +76,8 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.ContentItem)] [FriendlyName("Content Item")] + [Obsolete("This is not used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] ContentItem, /// @@ -74,6 +85,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MediaType, typeof(IMediaType))] [FriendlyName("Media Type")] + [UmbracoUdiType(Constants.UdiEntityType.MediaType)] MediaType, /// @@ -81,13 +93,14 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DocumentType, typeof(IContentType))] [FriendlyName("Document Type")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentType)] DocumentType, /// /// Recycle Bin /// [UmbracoObjectType(Constants.ObjectTypes.ContentRecycleBin)] - [FriendlyName("Recycle Bin")] + [FriendlyName("Recycle Bin")] RecycleBin, /// @@ -95,6 +108,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Stylesheet)] [FriendlyName("Stylesheet")] + [UmbracoUdiType(Constants.UdiEntityType.Stylesheet)] Stylesheet, /// @@ -102,6 +116,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Member, typeof(IMember))] [FriendlyName("Member")] + [UmbracoUdiType(Constants.UdiEntityType.Member)] Member, /// @@ -109,6 +124,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DataType, typeof(IDataTypeDefinition))] [FriendlyName("Data Type")] + [UmbracoUdiType(Constants.UdiEntityType.DataType)] DataType, /// @@ -116,6 +132,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DocumentTypeContainer)] [FriendlyName("Document Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentTypeContainer)] DocumentTypeContainer, /// @@ -123,6 +140,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MediaTypeContainer)] [FriendlyName("Media Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.MediaTypeContainer)] MediaTypeContainer, /// @@ -130,8 +148,36 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DataTypeContainer)] [FriendlyName("Data Type Container")] - DataTypeContainer + [UmbracoUdiType(Constants.UdiEntityType.DataTypeContainer)] + DataTypeContainer, + /// + /// Relation type + /// + [UmbracoObjectType(Constants.ObjectTypes.RelationType)] + [FriendlyName("Relation Type")] + [UmbracoUdiType(Constants.UdiEntityType.RelationType)] + RelationType, + /// + /// Forms Form + /// + [UmbracoObjectType(Constants.ObjectTypes.FormsForm)] + [FriendlyName("Form")] + FormsForm, + + /// + /// Forms PreValue + /// + [UmbracoObjectType(Constants.ObjectTypes.FormsPreValue)] + [FriendlyName("PreValue")] + FormsPreValue, + + /// + /// Forms DataSource + /// + [UmbracoObjectType(Constants.ObjectTypes.FormsDataSource)] + [FriendlyName("DataSource")] + FormsDataSource } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs b/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs index 34ee29b96f..5f92e6425e 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs @@ -12,6 +12,7 @@ namespace Umbraco.Core.Models { //MUST be concurrent to avoid thread collisions! private static readonly ConcurrentDictionary UmbracoObjectTypeCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary UmbracoObjectTypeUdiCache = new ConcurrentDictionary(); /// /// Get an UmbracoObjectTypes value from it's name @@ -43,6 +44,21 @@ namespace Umbraco.Core.Models return umbracoObjectType; } + public static string GetUdiType(Guid guid) + { + var umbracoObjectType = Constants.UdiEntityType.Unknown; + + foreach (var name in Enum.GetNames(typeof(UmbracoObjectTypes))) + { + var objType = GetUmbracoObjectType(name); + if (objType.GetGuid() == guid) + { + umbracoObjectType = GetUdiType(objType); + } + } + return umbracoObjectType; + } + /// /// Extension method for the UmbracoObjectTypes enum to return the enum GUID /// @@ -68,6 +84,26 @@ namespace Umbraco.Core.Models }); } + public static string GetUdiType(this UmbracoObjectTypes umbracoObjectType) + { + return UmbracoObjectTypeUdiCache.GetOrAdd(umbracoObjectType, types => + { + var type = typeof(UmbracoObjectTypes); + var memInfo = type.GetMember(umbracoObjectType.ToString()); + var attributes = memInfo[0].GetCustomAttributes(typeof(UmbracoUdiTypeAttribute), + false); + + if (attributes.Length == 0) + return Constants.UdiEntityType.Unknown; + + var attribute = ((UmbracoUdiTypeAttribute)attributes[0]); + if (attribute == null) + return Constants.UdiEntityType.Unknown; + + return attribute.UdiType; + }); + } + /// /// Extension method for the UmbracoObjectTypes enum to return the enum name /// diff --git a/src/Umbraco.Core/Models/XsltFile.cs b/src/Umbraco.Core/Models/XsltFile.cs new file mode 100644 index 0000000000..07f4d00084 --- /dev/null +++ b/src/Umbraco.Core/Models/XsltFile.cs @@ -0,0 +1,32 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a XSLT file + /// + [Serializable] + [DataContract(IsReference = true)] + public class XsltFile : File, IXsltFile + { + public XsltFile(string path) + : this(path, (Func) null) + { } + + internal XsltFile(string path, Func getFileContent) + : base(path, getFileContent) + { } + + /// + /// Indicates whether the current entity has an identity, which in this case is a path/name. + /// + /// + /// Overrides the default Entity identity check. + /// + public override bool HasIdentity + { + get { return string.IsNullOrEmpty(Path) == false; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/NamedUdiRange.cs b/src/Umbraco.Core/NamedUdiRange.cs new file mode 100644 index 0000000000..c87e5db82e --- /dev/null +++ b/src/Umbraco.Core/NamedUdiRange.cs @@ -0,0 +1,34 @@ +namespace Umbraco.Core +{ + /// + /// Represents a complemented with a name. + /// + public class NamedUdiRange : UdiRange + { + /// + /// Initializes a new instance of the class with a and an optional selector. + /// + /// A . + /// An optional selector. + public NamedUdiRange(Udi udi, string selector = Constants.DeploySelector.This) + : base(udi, selector) + { } + + /// + /// Initializes a new instance of the class with a , a name, and an optional selector. + /// + /// A . + /// A name. + /// An optional selector. + public NamedUdiRange(Udi udi, string name, string selector = Constants.DeploySelector.This) + : base(udi, selector) + { + Name = name; + } + + /// + /// Gets or sets the name of the range. + /// + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 6ed424c232..6a1befaad8 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -468,19 +468,19 @@ namespace Umbraco.Core /// /// /// - internal static IDictionary ToDictionary(this T o, + public static IDictionary ToDictionary(this T o, params Expression>[] ignoreProperties) { return o.ToDictionary(ignoreProperties.Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray()); } - /// - /// Turns object into dictionary - /// - /// - /// Properties to ignore - /// - internal static IDictionary ToDictionary(this object o, params string[] ignoreProperties) + /// + /// Turns object into dictionary + /// + /// + /// Properties to ignore + /// + public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) { if (o != null) { @@ -647,5 +647,10 @@ namespace Umbraco.Core return "[GetPropertyValueException]"; } } + + internal static Guid AsGuid(this object value) + { + return value is Guid ? (Guid) value : Guid.Empty; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs b/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs index 5e170f47f4..ad35b81ffb 100644 --- a/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs +++ b/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs @@ -22,6 +22,7 @@ namespace Umbraco.Core.ObjectResolution private readonly string _httpContextKey; private readonly List _instanceTypes = new List(); private IEnumerable _sortedValues; + private readonly Func _httpContextGetter; private int _defaultPluginWeight = 100; @@ -42,12 +43,7 @@ namespace Umbraco.Core.ObjectResolution if (logger == null) throw new ArgumentNullException("logger"); CanResolveBeforeFrozen = false; if (scope == ObjectLifetimeScope.HttpRequest) - { - if (HttpContext.Current == null) - throw new InvalidOperationException("Use alternative constructor accepting a HttpContextBase object in order to set the lifetime scope to HttpRequest when HttpContext.Current is null"); - - CurrentHttpContext = new HttpContextWrapper(HttpContext.Current); - } + _httpContextGetter = () => new HttpContextWrapper(HttpContext.Current); ServiceProvider = serviceProvider; Logger = logger; @@ -84,7 +80,7 @@ namespace Umbraco.Core.ObjectResolution LifetimeScope = ObjectLifetimeScope.HttpRequest; _httpContextKey = GetType().FullName; ServiceProvider = serviceProvider; - CurrentHttpContext = httpContext; + _httpContextGetter = () => httpContext; _instanceTypes = new List(); InitializeAppInstances(); @@ -160,7 +156,16 @@ namespace Umbraco.Core.ObjectResolution /// Gets or sets the used to initialize this object, if any. /// /// If not null, then LifetimeScope will be ObjectLifetimeScope.HttpRequest. - protected HttpContextBase CurrentHttpContext { get; private set; } + protected HttpContextBase CurrentHttpContext + { + get + { + var context = _httpContextGetter == null ? null : _httpContextGetter(); + if (context == null) + throw new InvalidOperationException("Cannot use this resolver with lifetime 'HttpRequest' when there is no current HttpContext. Either use the ctor accepting an HttpContextBase, or use the resolver from within a request exclusively."); + return context; + } + } /// /// Returns the service provider used to instantiate objects @@ -196,7 +201,7 @@ namespace Umbraco.Core.ObjectResolution /// /// Gets or sets the default type weight. /// - /// Determines the weight of types that do not have a WeightAttribute set on + /// Determines the weight of types that do not have a WeightAttribute set on /// them, when calling GetSortedValues. protected virtual int DefaultPluginWeight { @@ -276,7 +281,7 @@ namespace Umbraco.Core.ObjectResolution /// Removes a type. /// /// The type to remove. - /// the resolver does not support removing types, or + /// the resolver does not support removing types, or /// the type is not a valid type for the resolver. public virtual void RemoveType(Type value) { @@ -296,7 +301,7 @@ namespace Umbraco.Core.ObjectResolution /// Removes a type. /// /// The type to remove. - /// the resolver does not support removing types, or + /// the resolver does not support removing types, or /// the type is not a valid type for the resolver. public void RemoveType() where T : TResolved @@ -309,7 +314,7 @@ namespace Umbraco.Core.ObjectResolution /// /// The types to add. /// The types are appended at the end of the list. - /// the resolver does not support adding types, or + /// the resolver does not support adding types, or /// a type is not a valid type for the resolver, or a type is already in the collection of types. protected void AddTypes(IEnumerable types) { @@ -336,7 +341,7 @@ namespace Umbraco.Core.ObjectResolution /// /// The type to add. /// The type is appended at the end of the list. - /// the resolver does not support adding types, or + /// the resolver does not support adding types, or /// the type is not a valid type for the resolver, or the type is already in the collection of types. public virtual void AddType(Type value) { @@ -362,7 +367,7 @@ namespace Umbraco.Core.ObjectResolution /// /// The type to add. /// The type is appended at the end of the list. - /// the resolver does not support adding types, or + /// the resolver does not support adding types, or /// the type is not a valid type for the resolver, or the type is already in the collection of types. public void AddType() where T : TResolved @@ -404,7 +409,7 @@ namespace Umbraco.Core.ObjectResolution /// /// The zero-based index at which the type should be inserted. /// The type to insert. - /// the resolver does not support inserting types, or + /// the resolver does not support inserting types, or /// the type is not a valid type for the resolver, or the type is already in the collection of types. /// is out of range. public virtual void InsertType(int index, Type value) @@ -430,7 +435,7 @@ namespace Umbraco.Core.ObjectResolution /// Inserts a type at the beginning of the list. /// /// The type to insert. - /// the resolver does not support inserting types, or + /// the resolver does not support inserting types, or /// the type is not a valid type for the resolver, or the type is already in the collection of types. public virtual void InsertType(Type value) { @@ -464,7 +469,7 @@ namespace Umbraco.Core.ObjectResolution /// /// The existing type before which to insert. /// The type to insert. - /// the resolver does not support inserting types, or + /// the resolver does not support inserting types, or /// one of the types is not a valid type for the resolver, or the existing type is not in the collection, /// or the new type is already in the collection of types. public virtual void InsertTypeBefore(Type existingType, Type value) @@ -498,7 +503,7 @@ namespace Umbraco.Core.ObjectResolution /// /// The existing type before which to insert. /// The type to insert. - /// the resolver does not support inserting types, or + /// the resolver does not support inserting types, or /// one of the types is not a valid type for the resolver, or the existing type is not in the collection, /// or the new type is already in the collection of types. public void InsertTypeBefore() diff --git a/src/Umbraco.Core/ObjectResolution/Resolution.cs b/src/Umbraco.Core/ObjectResolution/Resolution.cs index 0b478d54cf..3cdaab365b 100644 --- a/src/Umbraco.Core/ObjectResolution/Resolution.cs +++ b/src/Umbraco.Core/ObjectResolution/Resolution.cs @@ -63,49 +63,6 @@ namespace Umbraco.Core.ObjectResolution } } - // NOTE - the ugly code below exists only because of umbraco.BusinessLogic.Actions.Action.ReRegisterActionsAndHandlers - // which wants to re-register actions and handlers instead of properly restarting the application. Don't even think - // about using it for anything else. Also, while the backdoor is open, the resolution system is locked so nothing - // can work properly => deadlocks. Therefore, open the backdoor, do resolution changes EXCLUSIVELY, and close the door! - - /// - /// Returns a disposable object that reprents dirty access to temporarily unfrozen resolution configuration. - /// - /// - /// Should not be used. - /// Should be used in a using(Resolution.DirtyBackdoorToConfiguration) { ... } mode. - /// Because we just lift the frozen state, and we don't actually re-freeze, the Frozen event does not trigger. - /// - internal static IDisposable DirtyBackdoorToConfiguration - { - get { return new DirtyBackdoor(); } - } - - // keep the class here because it needs write-access to Resolution.IsFrozen - private class DirtyBackdoor : IDisposable - { - - private readonly IDisposable _lock; - private readonly bool _frozen; - - public DirtyBackdoor() - { - LogHelper.Debug(typeof(DirtyBackdoor), "Creating back door for resolution"); - - _lock = new WriteLock(ConfigurationLock); - _frozen = _isFrozen; - _isFrozen = false; - } - - public void Dispose() - { - LogHelper.Debug(typeof(DirtyBackdoor), "Disposing back door for resolution"); - - _isFrozen = _frozen; - _lock.Dispose(); - } - } - /// /// Freezes resolution. /// diff --git a/src/Umbraco.Core/OrderedHashSet.cs b/src/Umbraco.Core/OrderedHashSet.cs new file mode 100644 index 0000000000..801f1a9a41 --- /dev/null +++ b/src/Umbraco.Core/OrderedHashSet.cs @@ -0,0 +1,50 @@ +using System.Collections.ObjectModel; + +namespace Umbraco.Core +{ + /// + /// A custom collection similar to HashSet{T} which only contains unique items, however this collection keeps items in order + /// and is customizable to keep the newest or oldest equatable item + /// + /// + internal class OrderedHashSet : KeyedCollection + { + private readonly bool _keepOldest; + + public OrderedHashSet(bool keepOldest = true) + { + _keepOldest = keepOldest; + } + + protected override void InsertItem(int index, T item) + { + if (Dictionary == null) + { + base.InsertItem(index, item); + } + else + { + var exists = Dictionary.ContainsKey(item); + + //if we want to keep the newest, then we need to remove the old item and add the new one + if (exists == false) + { + base.InsertItem(index, item); + } + else if(_keepOldest == false) + { + if (Remove(item)) + { + index--; + } + base.InsertItem(index, item); + } + } + } + + protected override T GetKeyForItem(T item) + { + return item; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs b/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs new file mode 100644 index 0000000000..13bb4bfc77 --- /dev/null +++ b/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Packaging.Models +{ + [Serializable] + [DataContract(IsReference = true)] + internal class UninstallationSummary + { + public MetaData MetaData { get; set; } + public IEnumerable DataTypesUninstalled { get; set; } + public IEnumerable LanguagesUninstalled { get; set; } + public IEnumerable DictionaryItemsUninstalled { get; set; } + public IEnumerable MacrosUninstalled { get; set; } + public IEnumerable FilesUninstalled { get; set; } + public IEnumerable TemplatesUninstalled { get; set; } + public IEnumerable ContentTypesUninstalled { get; set; } + public IEnumerable StylesheetsUninstalled { get; set; } + public IEnumerable ContentUninstalled { get; set; } + public bool PackageUninstalled { get; set; } + } + + internal static class UninstallationSummaryExtentions + { + public static UninstallationSummary InitEmpty(this UninstallationSummary summary) + { + summary.ContentUninstalled = new List(); + summary.ContentTypesUninstalled = new List(); + summary.DataTypesUninstalled = new List(); + summary.DictionaryItemsUninstalled = new List(); + summary.FilesUninstalled = new List(); + summary.LanguagesUninstalled = new List(); + summary.MacrosUninstalled = new List(); + summary.MetaData = new MetaData(); + summary.TemplatesUninstalled = new List(); + summary.PackageUninstalled = false; + return summary; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs b/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs index 8852475543..57664b8a83 100644 --- a/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs +++ b/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs @@ -29,20 +29,24 @@ namespace Umbraco.Core.Packaging /// public static IEnumerable ScanAssembliesForTypeReference(IEnumerable assemblys, out string[] errorReport) { - var appDomain = GetTempAppDomain(); - var type = typeof(PackageBinaryInspector); - try + // beware! when toying with domains, use a safe call context! + using (new SafeCallContext()) { - var value = (PackageBinaryInspector)appDomain.CreateInstanceAndUnwrap( - type.Assembly.FullName, - type.FullName); - // do NOT turn PerformScan into static (even if ReSharper says so)! - var result = value.PerformScan(assemblys.ToArray(), out errorReport); - return result; - } - finally - { - AppDomain.Unload(appDomain); + var appDomain = GetTempAppDomain(); + var type = typeof(PackageBinaryInspector); + try + { + var value = (PackageBinaryInspector) appDomain.CreateInstanceAndUnwrap( + type.Assembly.FullName, + type.FullName); + // do NOT turn PerformScan into static (even if ReSharper says so)! + var result = value.PerformScan(assemblys.ToArray(), out errorReport); + return result; + } + finally + { + AppDomain.Unload(appDomain); + } } } @@ -78,7 +82,7 @@ namespace Umbraco.Core.Packaging /// /// Performs the assembly scanning /// - /// + /// /// /// /// @@ -107,7 +111,7 @@ namespace Umbraco.Core.Packaging /// /// Performs the assembly scanning /// - /// + /// /// /// /// @@ -154,7 +158,7 @@ namespace Umbraco.Core.Packaging //get the list of assembly names to compare below var loadedNames = loaded.Select(x => x.GetName().Name).ToArray(); - + //Then load each referenced assembly into the context foreach (var a in loaded) { @@ -170,7 +174,7 @@ namespace Umbraco.Core.Packaging } catch (FileNotFoundException) { - //if an exception occurs it means that a referenced assembly could not be found + //if an exception occurs it means that a referenced assembly could not be found errors.Add( string.Concat("This package references the assembly '", assemblyName.Name, @@ -179,7 +183,7 @@ namespace Umbraco.Core.Packaging } catch (Exception ex) { - //if an exception occurs it means that a referenced assembly could not be found + //if an exception occurs it means that a referenced assembly could not be found errors.Add( string.Concat("This package could not be verified for compatibility. An error occurred while loading a referenced assembly '", assemblyName.Name, @@ -197,7 +201,7 @@ namespace Umbraco.Core.Packaging { //now we need to see if they contain any type 'T' var reflectedAssembly = a; - + try { var found = reflectedAssembly.GetExportedTypes() @@ -210,8 +214,8 @@ namespace Umbraco.Core.Packaging } catch (Exception ex) { - //This is a hack that nobody can seem to get around, I've read everything and it seems that - // this is quite a common thing when loading types into reflection only load context, so + //This is a hack that nobody can seem to get around, I've read everything and it seems that + // this is quite a common thing when loading types into reflection only load context, so // we're just going to ignore this specific one for now var typeLoadEx = ex as TypeLoadException; if (typeLoadEx != null) @@ -232,7 +236,7 @@ namespace Umbraco.Core.Packaging LogHelper.Error("An error occurred scanning package assemblies", ex); } } - + } errorReport = errors.ToArray(); @@ -252,7 +256,7 @@ namespace Umbraco.Core.Packaging var contractType = contractAssemblyLoadFrom.GetExportedTypes() .FirstOrDefault(x => x.FullName == typeof(T).FullName && x.Assembly.FullName == typeof(T).Assembly.FullName); - + if (contractType == null) { throw new InvalidOperationException("Could not find type " + typeof(T) + " in the LoadFrom assemblies"); @@ -281,12 +285,8 @@ namespace Umbraco.Core.Packaging PrivateBinPathProbe = AppDomain.CurrentDomain.SetupInformation.PrivateBinPathProbe }; - //create new domain with full trust - return AppDomain.CreateDomain( - appName, - AppDomain.CurrentDomain.Evidence, - domainSetup, - new PermissionSet(PermissionState.Unrestricted)); + // create new domain with full trust + return AppDomain.CreateDomain(appName, AppDomain.CurrentDomain.Evidence, domainSetup, new PermissionSet(PermissionState.Unrestricted)); } } } diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs new file mode 100644 index 0000000000..49c6f933fb --- /dev/null +++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs @@ -0,0 +1,11 @@ +// ReSharper disable once CheckNamespace +namespace Umbraco.Core +{ + static partial class Constants + { + public static class Locks + { + public const int Servers = -331; + } + } +} diff --git a/src/Umbraco.Core/Persistence/DatabaseDebugHelper.cs b/src/Umbraco.Core/Persistence/DatabaseDebugHelper.cs new file mode 100644 index 0000000000..4cb8327f5b --- /dev/null +++ b/src/Umbraco.Core/Persistence/DatabaseDebugHelper.cs @@ -0,0 +1,171 @@ +#if DEBUG_DATABASES +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Umbraco.Core.Persistence +{ + internal static class DatabaseDebugHelper + { + private const int CommandsSize = 100; + private static readonly Queue>> Commands = new Queue>>(); + + public static void SetCommand(IDbCommand command, string context) + { + var prof = command as StackExchange.Profiling.Data.ProfiledDbCommand; + if (prof != null) command = prof.InternalCommand; + lock (Commands) + { + Commands.Enqueue(Tuple.Create(context, new WeakReference(command))); + while (Commands.Count > CommandsSize) Commands.Dequeue(); + } + } + + public static string GetCommandContext(IDbCommand command) + { + lock (Commands) + { + var tuple = Commands.FirstOrDefault(x => + { + IDbCommand c; + return x.Item2.TryGetTarget(out c) && c == command; + }); + return tuple == null ? "?" : tuple.Item1; + } + } + + public static string GetReferencedObjects(IDbConnection con) + { + var prof = con as StackExchange.Profiling.Data.ProfiledDbConnection; + if (prof != null) con = prof.InnerConnection; + var ceCon = con as System.Data.SqlServerCe.SqlCeConnection; + if (ceCon != null) return null; // "NotSupported: SqlCE"; + var dbCon = con as DbConnection; + return dbCon == null + ? "NotSupported: " + con.GetType() + : GetReferencedObjects(dbCon); + } + + public static string GetReferencedObjects(DbConnection con) + { + var t = con.GetType(); + + var field = t.GetField("_innerConnection", BindingFlags.Instance | BindingFlags.NonPublic); + if (field == null) throw new Exception("panic: _innerConnection (" + t + ")."); + var innerConnection = field.GetValue(con); + + var tin = innerConnection.GetType(); + + var fi = con is System.Data.SqlClient.SqlConnection + ? tin.BaseType.BaseType.GetField("_referenceCollection", BindingFlags.Instance | BindingFlags.NonPublic) + : tin.BaseType.GetField("_referenceCollection", BindingFlags.Instance | BindingFlags.NonPublic); + if (fi == null) + //return ""; + throw new Exception("panic: referenceCollection."); + + var rc = fi.GetValue(innerConnection); + if (rc == null) + //return ""; + throw new Exception("panic: innerCollection."); + + field = rc.GetType().BaseType.GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic); + if (field == null) throw new Exception("panic: items."); + var items = field.GetValue(rc); + var prop = items.GetType().GetProperty("Length", BindingFlags.Instance | BindingFlags.Public); + if (prop == null) throw new Exception("panic: Length."); + var count = Convert.ToInt32(prop.GetValue(items, null)); + var miGetValue = items.GetType().GetMethod("GetValue", new[] { typeof(int) }); + if (miGetValue == null) throw new Exception("panic: GetValue."); + + if (count == 0) return null; + + StringBuilder result = null; + var hasb = false; + + for (var i = 0; i < count; i++) + { + var referencedObj = miGetValue.Invoke(items, new object[] { i }); + + var hasTargetProp = referencedObj.GetType().GetProperty("HasTarget"); + if (hasTargetProp == null) throw new Exception("panic: HasTarget"); + var hasTarget = Convert.ToBoolean(hasTargetProp.GetValue(referencedObj, null)); + if (hasTarget == false) continue; + + if (hasb == false) + { + result = new StringBuilder(); + result.AppendLine("ReferencedItems"); + hasb = true; + } + + //var inUseProp = referencedObj.GetType().GetProperty("InUse"); + //if (inUseProp == null) throw new Exception("panic: InUse."); + //var inUse = Convert.ToBoolean(inUseProp.GetValue(referencedObj, null)); + var inUse = "?"; + + var targetProp = referencedObj.GetType().GetProperty("Target"); + if (targetProp == null) throw new Exception("panic: Target."); + var objTarget = targetProp.GetValue(referencedObj, null); + + result.AppendFormat("\tDiff.Item id=\"{0}\" inUse=\"{1}\" type=\"{2}\" hashCode=\"{3}\"" + Environment.NewLine, + i, inUse, objTarget.GetType(), objTarget.GetHashCode()); + + DbCommand cmd = null; + if (objTarget is DbDataReader) + { + //var rdr = objTarget as DbDataReader; + try + { + cmd = objTarget.GetType().GetProperty("Command", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(objTarget, null) as DbCommand; + } + catch (Exception e) + { + result.AppendFormat("\t\tObjTarget: DbDataReader, Exception: {0}" + Environment.NewLine, e); + } + } + else if (objTarget is DbCommand) + { + cmd = objTarget as DbCommand; + } + if (cmd == null) + { + result.AppendFormat("\t\tObjTarget: {0}" + Environment.NewLine, objTarget.GetType()); + continue; + } + + result.AppendFormat("\t\tCommand type=\"{0}\" hashCode=\"{1}\"" + Environment.NewLine, + cmd.GetType(), cmd.GetHashCode()); + + var context = GetCommandContext(cmd); + result.AppendFormat("\t\t\tContext: {0}" + Environment.NewLine, context); + + var properties = cmd.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); + foreach (var pi in properties) + { + if (pi.PropertyType.IsPrimitive || pi.PropertyType == typeof(string)) + result.AppendFormat("\t\t\t{0}: {1}" + Environment.NewLine, pi.Name, pi.GetValue(cmd, null)); + + if (pi.PropertyType != typeof (DbConnection) || pi.Name != "Connection") continue; + + var con1 = pi.GetValue(cmd, null) as DbConnection; + result.AppendFormat("\t\t\tConnection type=\"{0}\" state=\"{1}\" hashCode=\"{2}\"" + Environment.NewLine, + con1.GetType(), con1.State, con1.GetHashCode()); + + var propertiesCon = con1.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); + foreach (var picon in propertiesCon) + { + if (picon.PropertyType.IsPrimitive || picon.PropertyType == typeof(string)) + result.AppendFormat("\t\t\t\t{0}: {1}" + Environment.NewLine, picon.Name, picon.GetValue(con1, null)); + } + } + } + + return result == null ? null : result.ToString(); + } + } +} +#endif diff --git a/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs b/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs index 3e6d245416..5ef29aa951 100644 --- a/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs +++ b/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Persistence { ValidateDatabase(database); - database.Execute("UPDATE umbracoNode SET sortOrder = (CASE WHEN (sortOrder=1) THEN -1 ELSE 1 END) WHERE id=@id", + database.Execute("UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { @id = nodeId }); } @@ -36,7 +36,7 @@ namespace Umbraco.Core.Persistence { ValidateDatabase(database); - database.ExecuteScalar("SELECT sortOrder FROM umbracoNode WHERE id=@id", + database.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new { @id = nodeId }); } } diff --git a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs index 658b8ebabe..c27147895c 100644 --- a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs @@ -1,7 +1,6 @@ using System; -using System.Web; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence { @@ -13,19 +12,17 @@ namespace Umbraco.Core.Persistence /// it will create one per context, otherwise it will be a global singleton object which is NOT thread safe /// since we need (at least) a new instance of the database object per thread. /// - internal class DefaultDatabaseFactory : DisposableObject, IDatabaseFactory + internal class DefaultDatabaseFactory : DisposableObject, IDatabaseFactory2 { private readonly string _connectionStringName; private readonly ILogger _logger; public string ConnectionString { get; private set; } public string ProviderName { get; private set; } - - //very important to have ThreadStatic: - // see: http://issues.umbraco.org/issue/U4-2172 - [ThreadStatic] - private static volatile UmbracoDatabase _nonHttpInstance; - private static readonly object Locker = new object(); + //private static readonly object Locker = new object(); + + // bwc imposes a weird x-dependency between database factory and scope provider... + public IScopeProviderInternal ScopeProvider { get; set; } /// /// Constructor accepting custom connection string @@ -34,9 +31,12 @@ namespace Umbraco.Core.Persistence /// public DefaultDatabaseFactory(string connectionStringName, ILogger logger) { - if (logger == null) throw new ArgumentNullException("logger"); + if (logger == null) throw new ArgumentNullException("logger"); Mandate.ParameterNotNullOrEmpty(connectionStringName, "connectionStringName"); - _connectionStringName = connectionStringName; + + //if (NonContextValue != null) throw new Exception("NonContextValue is not null."); + + _connectionStringName = connectionStringName; _logger = logger; } @@ -48,68 +48,39 @@ namespace Umbraco.Core.Persistence /// public DefaultDatabaseFactory(string connectionString, string providerName, ILogger logger) { - if (logger == null) throw new ArgumentNullException("logger"); + if (logger == null) throw new ArgumentNullException("logger"); Mandate.ParameterNotNullOrEmpty(connectionString, "connectionString"); Mandate.ParameterNotNullOrEmpty(providerName, "providerName"); - ConnectionString = connectionString; + + //if (NonContextValue != null) throw new Exception("NonContextValue is not null."); + + ConnectionString = connectionString; ProviderName = providerName; _logger = logger; } public UmbracoDatabase CreateDatabase() { - //no http context, create the singleton global object - if (HttpContext.Current == null) - { - if (_nonHttpInstance == null) - { - lock (Locker) - { - //double check - if (_nonHttpInstance == null) - { - _nonHttpInstance = string.IsNullOrEmpty(ConnectionString) == false && string.IsNullOrEmpty(ProviderName) == false - ? new UmbracoDatabase(ConnectionString, ProviderName, _logger) - : new UmbracoDatabase(_connectionStringName, _logger); - } - } - } - return _nonHttpInstance; - } - - //we have an http context, so only create one per request - if (HttpContext.Current.Items.Contains(typeof(DefaultDatabaseFactory)) == false) - { - HttpContext.Current.Items.Add(typeof (DefaultDatabaseFactory), - string.IsNullOrEmpty(ConnectionString) == false && string.IsNullOrEmpty(ProviderName) == false - ? new UmbracoDatabase(ConnectionString, ProviderName, _logger) - : new UmbracoDatabase(_connectionStringName, _logger)); - } - return (UmbracoDatabase)HttpContext.Current.Items[typeof(DefaultDatabaseFactory)]; + return ScopeProvider.GetAmbientOrNoScope().Database; } - protected override void DisposeResources() - { - if (HttpContext.Current == null) - { - _nonHttpInstance.Dispose(); - } - else - { - if (HttpContext.Current.Items.Contains(typeof(DefaultDatabaseFactory))) - { - ((UmbracoDatabase)HttpContext.Current.Items[typeof(DefaultDatabaseFactory)]).Dispose(); - } - } - } - - // during tests, the thread static var can leak between tests - // this method provides a way to force-reset the variable - internal void ResetForTests() + public UmbracoDatabase CreateNewDatabase() { - if (_nonHttpInstance == null) return; - _nonHttpInstance.Dispose(); - _nonHttpInstance = null; + return CreateDatabaseInstance(); + } - } + + internal UmbracoDatabase CreateDatabaseInstance() + { + var database = string.IsNullOrEmpty(ConnectionString) == false && string.IsNullOrEmpty(ProviderName) == false + ? new UmbracoDatabase(ConnectionString, ProviderName, _logger) + : new UmbracoDatabase(_connectionStringName, _logger); + database.DatabaseFactory = this; + return database; + } + + protected override void DisposeResources() + { + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs index 5dcec8fed0..512f02e8b5 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs @@ -27,15 +27,28 @@ namespace Umbraco.Core.Persistence.Factories #region Implementation of IEntityFactory - public IContent BuildEntity(DocumentDto dto) + /// + /// Builds a IContent item from the dto(s) and content type + /// + /// + /// This DTO can contain all of the information to build an IContent item, however in cases where multiple entities are being built, + /// a separate publishedDto entity will be supplied in place of the 's own + /// ResultColumn DocumentPublishedReadOnlyDto + /// + /// + /// + /// When querying for multiple content items the main DTO will not contain the ResultColumn DocumentPublishedReadOnlyDto and a separate publishedDto instance will be supplied + /// + /// + public static IContent BuildEntity(DocumentDto dto, IContentType contentType, DocumentPublishedReadOnlyDto publishedDto = null) { - var content = new Content(dto.Text, dto.ContentVersionDto.ContentDto.NodeDto.ParentId, _contentType); + var content = new Content(dto.Text, dto.ContentVersionDto.ContentDto.NodeDto.ParentId, contentType); try { content.DisableChangeTracking(); - content.Id = _id; + content.Id = dto.NodeId; content.Key = dto.ContentVersionDto.ContentDto.NodeDto.UniqueId; content.Name = dto.Text; content.NodeName = dto.ContentVersionDto.ContentDto.NodeDto.Text; @@ -49,11 +62,19 @@ namespace Umbraco.Core.Persistence.Factories content.Published = dto.Published; content.CreateDate = dto.ContentVersionDto.ContentDto.NodeDto.CreateDate; content.UpdateDate = dto.ContentVersionDto.VersionDate; - content.ExpireDate = dto.ExpiresDate.HasValue ? dto.ExpiresDate.Value : (DateTime?) null; - content.ReleaseDate = dto.ReleaseDate.HasValue ? dto.ReleaseDate.Value : (DateTime?) null; + content.ExpireDate = dto.ExpiresDate.HasValue ? dto.ExpiresDate.Value : (DateTime?)null; + content.ReleaseDate = dto.ReleaseDate.HasValue ? dto.ReleaseDate.Value : (DateTime?)null; content.Version = dto.ContentVersionDto.VersionId; + content.PublishedState = dto.Published ? PublishedState.Published : PublishedState.Unpublished; - content.PublishedVersionGuid = dto.DocumentPublishedReadOnlyDto == null ? default(Guid) : dto.DocumentPublishedReadOnlyDto.VersionId; + + //Check if the publishedDto has been supplied, if not the use the dto's own DocumentPublishedReadOnlyDto value + content.PublishedVersionGuid = publishedDto == null + ? (dto.DocumentPublishedReadOnlyDto == null ? default(Guid) : dto.DocumentPublishedReadOnlyDto.VersionId) + : publishedDto.VersionId; + content.PublishedDate = publishedDto == null + ? (dto.DocumentPublishedReadOnlyDto == null ? default(DateTime) : dto.DocumentPublishedReadOnlyDto.VersionDate) + : publishedDto.VersionDate; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 @@ -64,6 +85,13 @@ namespace Umbraco.Core.Persistence.Factories { content.EnableChangeTracking(); } + + } + + [Obsolete("Use the static BuildEntity instead so we don't have to allocate one of these objects everytime we want to map values")] + public IContent BuildEntity(DocumentDto dto) + { + return BuildEntity(dto, _contentType); } public DocumentDto BuildDto(IContent entity) diff --git a/src/Umbraco.Core/Persistence/Factories/MacroFactory.cs b/src/Umbraco.Core/Persistence/Factories/MacroFactory.cs index 2ec20b08eb..02d95b3776 100644 --- a/src/Umbraco.Core/Persistence/Factories/MacroFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MacroFactory.cs @@ -7,12 +7,9 @@ namespace Umbraco.Core.Persistence.Factories { internal class MacroFactory { - #region Implementation of IEntityFactory - public IMacro BuildEntity(MacroDto dto) { - var model = new Macro(dto.Id, dto.UseInEditor, dto.RefreshRate, dto.Alias, dto.Name, dto.ScriptType, dto.ScriptAssembly, dto.Xslt, dto.CacheByPage, dto.CachePersonalized, dto.DontRender, dto.Python); - + var model = new Macro(dto.Id, dto.UniqueId, dto.UseInEditor, dto.RefreshRate, dto.Alias, dto.Name, dto.ScriptType, dto.ScriptAssembly, dto.Xslt, dto.CacheByPage, dto.CachePersonalized, dto.DontRender, dto.Python); try { @@ -20,7 +17,7 @@ namespace Umbraco.Core.Persistence.Factories foreach (var p in dto.MacroPropertyDtos) { - model.Properties.Add(new MacroProperty(p.Id, p.Alias, p.Name, p.SortOrder, p.EditorAlias)); + model.Properties.Add(new MacroProperty(p.Id, p.UniqueId, p.Alias, p.Name, p.SortOrder, p.EditorAlias)); } //on initial construction we don't want to have dirty properties tracked @@ -36,8 +33,9 @@ namespace Umbraco.Core.Persistence.Factories public MacroDto BuildDto(IMacro entity) { - var dto = new MacroDto() - { + var dto = new MacroDto + { + UniqueId = entity.Key, Alias = entity.Alias, CacheByPage = entity.CacheByPage, CachePersonalized = entity.CacheByMember, @@ -58,8 +56,6 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - #endregion - private List BuildPropertyDtos(IMacro entity) { var list = new List(); @@ -67,6 +63,7 @@ namespace Umbraco.Core.Persistence.Factories { var text = new MacroPropertyDto { + UniqueId = p.Key, Alias = p.Alias, Name = p.Name, Macro = entity.Id, diff --git a/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs b/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs index 0fcb654cb7..5729bb125e 100644 --- a/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs @@ -27,15 +27,15 @@ namespace Umbraco.Core.Persistence.Factories #region Implementation of IEntityFactory - public IMedia BuildEntity(ContentVersionDto dto) + public static IMedia BuildEntity(ContentVersionDto dto, IMediaType contentType) { - var media = new Models.Media(dto.ContentDto.NodeDto.Text, dto.ContentDto.NodeDto.ParentId, _contentType); + var media = new Models.Media(dto.ContentDto.NodeDto.Text, dto.ContentDto.NodeDto.ParentId, contentType); try { media.DisableChangeTracking(); - media.Id = _id; + media.Id = dto.NodeId; media.Key = dto.ContentDto.NodeDto.UniqueId; media.Path = dto.ContentDto.NodeDto.Path; media.CreatorId = dto.ContentDto.NodeDto.UserId.Value; @@ -55,6 +55,13 @@ namespace Umbraco.Core.Persistence.Factories { media.EnableChangeTracking(); } + + } + + [Obsolete("Use the static BuildEntity instead so we don't have to allocate one of these objects everytime we want to map values")] + public IMedia BuildEntity(ContentVersionDto dto) + { + return BuildEntity(dto, _contentType); } public ContentVersionDto BuildDto(IMedia entity) diff --git a/src/Umbraco.Core/Persistence/Factories/MemberFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberFactory.cs index 2901f48539..7b28808429 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberFactory.cs @@ -28,17 +28,17 @@ namespace Umbraco.Core.Persistence.Factories #region Implementation of IEntityFactory - public IMember BuildEntity(MemberDto dto) + public static IMember BuildEntity(MemberDto dto, IMemberType contentType) { var member = new Member( dto.ContentVersionDto.ContentDto.NodeDto.Text, - dto.Email, dto.LoginName, dto.Password, _contentType); + dto.Email, dto.LoginName, dto.Password, contentType); try { member.DisableChangeTracking(); - member.Id = _id; + member.Id = dto.NodeId; member.Key = dto.ContentVersionDto.ContentDto.NodeDto.UniqueId; member.Path = dto.ContentVersionDto.ContentDto.NodeDto.Path; member.CreatorId = dto.ContentVersionDto.ContentDto.NodeDto.UserId.Value; @@ -62,6 +62,12 @@ namespace Umbraco.Core.Persistence.Factories } } + [Obsolete("Use the static BuildEntity instead so we don't have to allocate one of these objects everytime we want to map values")] + public IMember BuildEntity(MemberDto dto) + { + return BuildEntity(dto, _contentType); + } + public MemberDto BuildDto(IMember entity) { var dto = new MemberDto diff --git a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs index b4d5d2e566..ba7a47165b 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs @@ -9,9 +9,10 @@ namespace Umbraco.Core.Persistence.Factories { internal class MemberTypeReadOnlyFactory { - public IMemberType BuildEntity(MemberTypeReadOnlyDto dto) + public IMemberType BuildEntity(MemberTypeReadOnlyDto dto, out bool needsSaving) { var standardPropertyTypes = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); + needsSaving = false; var memberType = new MemberType(dto.ParentId); @@ -47,6 +48,12 @@ namespace Umbraco.Core.Persistence.Factories { if (dto.PropertyTypes.Any(x => x.Alias.Equals(standardPropertyType.Key))) continue; + // beware! + // means that we can return a memberType "from database" that has some property types + // that do *not* come from the database and therefore are incomplete eg have no key, + // no id, no dataTypeDefinitionId - ouch! - better notify caller of the situation + needsSaving = true; + //Add the standard PropertyType to the current list propertyTypes.Add(standardPropertyType.Value); diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index 446bd426ad..f202d8c321 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -30,11 +30,11 @@ namespace Umbraco.Core.Persistence.Factories _updateDate = updateDate; } - public IEnumerable BuildEntity(PropertyDataDto[] dtos) + public static IEnumerable BuildEntity(IReadOnlyCollection dtos, PropertyType[] compositionTypeProperties, DateTime createDate, DateTime updateDate) { var properties = new List(); - foreach (var propertyType in _compositionTypeProperties) + foreach (var propertyType in compositionTypeProperties) { var propertyDataDto = dtos.LastOrDefault(x => x.PropertyTypeId == propertyType.Id); var property = propertyDataDto == null @@ -47,8 +47,8 @@ namespace Umbraco.Core.Persistence.Factories //on initial construction we don't want to have dirty properties tracked property.DisableChangeTracking(); - property.CreateDate = _createDate; - property.UpdateDate = _updateDate; + property.CreateDate = createDate; + property.UpdateDate = updateDate; // http://issues.umbraco.org/issue/U4-1946 property.ResetDirtyProperties(false); properties.Add(property); @@ -57,12 +57,18 @@ namespace Umbraco.Core.Persistence.Factories { property.EnableChangeTracking(); } - + } return properties; } + [Obsolete("Use the static method instead, there's no reason to allocate one of these classes everytime we want to map values")] + public IEnumerable BuildEntity(PropertyDataDto[] dtos) + { + return BuildEntity(dtos, _compositionTypeProperties, _createDate, _updateDate); + } + public IEnumerable BuildDto(IEnumerable properties) { var propertyDataDtos = new List(); diff --git a/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs index 98d4f30042..b112535817 100644 --- a/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Persistence.Factories entity.DisableChangeTracking(); entity.Id = dto.Id; + entity.Key = dto.UniqueId; entity.IsBidirectional = dto.Dual; entity.Name = dto.Name; @@ -38,10 +39,13 @@ namespace Umbraco.Core.Persistence.Factories ChildObjectType = entity.ChildObjectType, Dual = entity.IsBidirectional, Name = entity.Name, - ParentObjectType = entity.ParentObjectType + ParentObjectType = entity.ParentObjectType, + UniqueId = entity.Key }; if (entity.HasIdentity) + { dto.Id = entity.Id; + } return dto; } diff --git a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs index 18073c088e..ded7c60676 100644 --- a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs @@ -6,6 +6,7 @@ using System.Reflection; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Strings; namespace Umbraco.Core.Persistence.Factories { @@ -17,7 +18,7 @@ namespace Umbraco.Core.Persistence.Factories //figure out what extra properties we have that are not on the IUmbracoEntity and add them to additional data foreach (var k in originalEntityProperties.Keys - .Select(x => new { orig = x, title = x.ConvertCase(StringAliasCaseType.PascalCase) }) + .Select(x => new { orig = x, title = x.ToCleanString(CleanStringType.PascalCase | CleanStringType.Ascii | CleanStringType.ConvertCase) }) .Where(x => entityProps.InvariantContains(x.title) == false)) { entity.AdditionalData[k.title] = originalEntityProperties[k.orig]; @@ -50,7 +51,7 @@ namespace Umbraco.Core.Persistence.Factories entity.ContentTypeThumbnail = asDictionary.ContainsKey("thumbnail") ? (d.thumbnail ?? string.Empty) : string.Empty; var publishedVersion = default(Guid); - //some content items don't have a published version + //some content items don't have a published/newest version if (asDictionary.ContainsKey("publishedVersion") && asDictionary["publishedVersion"] != null) { Guid.TryParse(d.publishedVersion.ToString(), out publishedVersion); @@ -75,65 +76,6 @@ namespace Umbraco.Core.Persistence.Factories entity.EnableChangeTracking(); } } - - public UmbracoEntity BuildEntity(EntityRepository.UmbracoEntityDto dto) - { - var entity = new UmbracoEntity(dto.Trashed) - { - CreateDate = dto.CreateDate, - CreatorId = dto.UserId.Value, - Id = dto.NodeId, - Key = dto.UniqueId, - Level = dto.Level, - Name = dto.Text, - NodeObjectTypeId = dto.NodeObjectType.Value, - ParentId = dto.ParentId, - Path = dto.Path, - SortOrder = dto.SortOrder, - HasChildren = dto.Children > 0, - ContentTypeAlias = dto.Alias ?? string.Empty, - ContentTypeIcon = dto.Icon ?? string.Empty, - ContentTypeThumbnail = dto.Thumbnail ?? string.Empty, - }; - - entity.IsPublished = dto.PublishedVersion != default(Guid) || (dto.NewestVersion != default(Guid) && dto.PublishedVersion == dto.NewestVersion); - entity.IsDraft = dto.NewestVersion != default(Guid) && (dto.PublishedVersion == default(Guid) || dto.PublishedVersion != dto.NewestVersion); - entity.HasPendingChanges = (dto.PublishedVersion != default(Guid) && dto.NewestVersion != default(Guid)) && dto.PublishedVersion != dto.NewestVersion; - - if (dto.UmbracoPropertyDtos != null) - { - foreach (var propertyDto in dto.UmbracoPropertyDtos) - { - entity.AdditionalData[propertyDto.PropertyAlias] = new UmbracoEntity.EntityProperty - { - PropertyEditorAlias = propertyDto.PropertyEditorAlias, - Value = propertyDto.NTextValue.IsNullOrWhiteSpace() - ? propertyDto.NVarcharValue - : propertyDto.NTextValue.ConvertToJsonIfPossible() - }; - } - } - - return entity; - } - - public EntityRepository.UmbracoEntityDto BuildDto(UmbracoEntity entity) - { - var node = new EntityRepository.UmbracoEntityDto - { - CreateDate = entity.CreateDate, - Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), - NodeId = entity.Id, - NodeObjectType = entity.NodeObjectTypeId, - ParentId = entity.ParentId, - Path = entity.Path, - SortOrder = entity.SortOrder, - Text = entity.Name, - Trashed = entity.Trashed, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; - return node; - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/IDatabaseFactory.cs b/src/Umbraco.Core/Persistence/IDatabaseFactory.cs index b0efb7f94a..8def46b0fb 100644 --- a/src/Umbraco.Core/Persistence/IDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/IDatabaseFactory.cs @@ -1,12 +1,30 @@ using System; +using System.ComponentModel; namespace Umbraco.Core.Persistence { - /// - /// Used to create the UmbracoDatabase for use in the DatabaseContext - /// + [Obsolete("Use IDatabaseFactory2 instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public interface IDatabaseFactory : IDisposable { + /// + /// gets or creates the ambient database + /// + /// UmbracoDatabase CreateDatabase(); } + + /// + /// Used to create the UmbracoDatabase for use in the DatabaseContext + /// +#pragma warning disable 618 + public interface IDatabaseFactory2 : IDatabaseFactory +#pragma warning restore 618 + { + /// + /// creates a new database + /// + /// + UmbracoDatabase CreateNewDatabase(); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/LockedRepository.cs b/src/Umbraco.Core/Persistence/LockedRepository.cs index b5d2d672f2..c092a3329f 100644 --- a/src/Umbraco.Core/Persistence/LockedRepository.cs +++ b/src/Umbraco.Core/Persistence/LockedRepository.cs @@ -7,21 +7,27 @@ namespace Umbraco.Core.Persistence internal class LockedRepository where TRepository : IDisposable, IRepository { - public LockedRepository(Transaction transaction, IDatabaseUnitOfWork unitOfWork, TRepository repository) + public LockedRepository(IDatabaseUnitOfWork unitOfWork, TRepository repository) { - Transaction = transaction; UnitOfWork = unitOfWork; Repository = repository; } - public Transaction Transaction { get; private set; } + public LockedRepository(Transaction transaction, IDatabaseUnitOfWork unitOfWork, TRepository repository) + { + //Transaction = transaction; + UnitOfWork = unitOfWork; + Repository = repository; + } + + //public Transaction Transaction { get; private set; } public IDatabaseUnitOfWork UnitOfWork { get; private set; } public TRepository Repository { get; private set; } - public void Commit() - { - UnitOfWork.Commit(); - Transaction.Complete(); - } + //public void Commit() + //{ + // UnitOfWork.Commit(); + // Transaction.Complete(); + //} } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/LockingRepository.cs b/src/Umbraco.Core/Persistence/LockingRepository.cs index f513073e71..6d3d5d54c1 100644 --- a/src/Umbraco.Core/Persistence/LockingRepository.cs +++ b/src/Umbraco.Core/Persistence/LockingRepository.cs @@ -10,11 +10,11 @@ namespace Umbraco.Core.Persistence internal class LockingRepository where TRepository : IDisposable, IRepository { - private readonly IDatabaseUnitOfWorkProvider _uowProvider; - private readonly Func _repositoryFactory; + private readonly IScopeUnitOfWorkProvider _uowProvider; + private readonly Func _repositoryFactory; private readonly int[] _readLockIds, _writeLockIds; - public LockingRepository(IDatabaseUnitOfWorkProvider uowProvider, Func repositoryFactory, + public LockingRepository(IScopeUnitOfWorkProvider uowProvider, Func repositoryFactory, IEnumerable readLockIds, IEnumerable writeLockIds) { Mandate.ParameterNotNull(uowProvider, "uowProvider"); @@ -28,76 +28,88 @@ namespace Umbraco.Core.Persistence public void WithReadLocked(Action> action, bool autoCommit = true) { - var uow = _uowProvider.GetUnitOfWork(); - using (var transaction = uow.Database.GetTransaction(IsolationLevel.RepeatableRead)) + using (var uow = _uowProvider.GetUnitOfWork(IsolationLevel.RepeatableRead)) { + // getting the database creates a scope and a transaction + // the scope is IsolationLevel.RepeatableRead (because UnitOfWork is) + // and will throw if outer scope (if any) has a lower isolation level + foreach (var lockId in _readLockIds) uow.Database.AcquireLockNodeReadLock(lockId); using (var repository = _repositoryFactory(uow)) { - action(new LockedRepository(transaction, uow, repository)); + action(new LockedRepository(uow, repository)); if (autoCommit == false) return; uow.Commit(); - transaction.Complete(); - } - } + + } // dispose repository => dispose uow => complete (or not) scope + } // dispose uow again => nothing } public TResult WithReadLocked(Func, TResult> func, bool autoCommit = true) { - var uow = _uowProvider.GetUnitOfWork(); - using (var transaction = uow.Database.GetTransaction(IsolationLevel.RepeatableRead)) + using (var uow = _uowProvider.GetUnitOfWork(IsolationLevel.RepeatableRead)) { + // getting the database creates a scope and a transaction + // the scope is IsolationLevel.RepeatableRead (because UnitOfWork is) + // and will throw if outer scope (if any) has a lower isolation level + foreach (var lockId in _readLockIds) uow.Database.AcquireLockNodeReadLock(lockId); using (var repository = _repositoryFactory(uow)) { - var ret = func(new LockedRepository(transaction, uow, repository)); + var ret = func(new LockedRepository(uow, repository)); if (autoCommit == false) return ret; uow.Commit(); - transaction.Complete(); return ret; - } - } + + } // dispose repository => dispose uow => complete (or not) scope + } // dispose uow again => nothing } public void WithWriteLocked(Action> action, bool autoCommit = true) { - var uow = _uowProvider.GetUnitOfWork(); - using (var transaction = uow.Database.GetTransaction(IsolationLevel.RepeatableRead)) + using (var uow = _uowProvider.GetUnitOfWork(IsolationLevel.RepeatableRead)) { + // getting the database creates a scope and a transaction + // the scope is IsolationLevel.RepeatableRead (because UnitOfWork is) + // and will throw if outer scope (if any) has a lower isolation level + foreach (var lockId in _writeLockIds) uow.Database.AcquireLockNodeWriteLock(lockId); using (var repository = _repositoryFactory(uow)) { - action(new LockedRepository(transaction, uow, repository)); + action(new LockedRepository(uow, repository)); if (autoCommit == false) return; uow.Commit(); - transaction.Complete(); - } - } + + } // dispose repository => dispose uow => complete (or not) scope + } // dispose uow again => nothing } public TResult WithWriteLocked(Func, TResult> func, bool autoCommit = true) { - var uow = _uowProvider.GetUnitOfWork(); - using (var transaction = uow.Database.GetTransaction(IsolationLevel.RepeatableRead)) + using (var uow = _uowProvider.GetUnitOfWork(IsolationLevel.RepeatableRead)) { + // getting the database creates a scope and a transaction + // the scope is IsolationLevel.RepeatableRead (because UnitOfWork is) + // and will throw if outer scope (if any) has a lower isolation level + foreach (var lockId in _writeLockIds) uow.Database.AcquireLockNodeReadLock(lockId); using (var repository = _repositoryFactory(uow)) { - var ret = func(new LockedRepository(transaction, uow, repository)); + var ret = func(new LockedRepository(uow, repository)); if (autoCommit == false) return ret; uow.Commit(); - transaction.Complete(); return ret; - } - } + + } // dispose repository => dispose uow => complete (or not) scope + } // dispose uow again => nothing } } } diff --git a/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs b/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs index 40ec415b30..99f9cebf6b 100644 --- a/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs @@ -2,11 +2,12 @@ using System.Collections.Concurrent; using System.Linq.Expressions; using System.Reflection; +using umbraco.interfaces; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Mappers { - public abstract class BaseMapper + public abstract class BaseMapper : IDiscoverable { private readonly ISqlSyntaxProvider _sqlSyntax; diff --git a/src/Umbraco.Core/Persistence/Mappers/MappingResolver.cs b/src/Umbraco.Core/Persistence/Mappers/MappingResolver.cs index 6909c77744..a4dee60ee9 100644 --- a/src/Umbraco.Core/Persistence/Mappers/MappingResolver.cs +++ b/src/Umbraco.Core/Persistence/Mappers/MappingResolver.cs @@ -31,7 +31,7 @@ namespace Umbraco.Core.Persistence.Mappers /// /// /// - internal BaseMapper ResolveMapperByType(Type type) + public virtual BaseMapper ResolveMapperByType(Type type) { return _mapperCache.GetOrAdd(type, type1 => { @@ -67,7 +67,7 @@ namespace Umbraco.Core.Persistence.Mappers return Attempt.Succeed(mapper); } - internal string GetMapping(Type type, string propertyName) + public virtual string GetMapping(Type type, string propertyName) { var mapper = ResolveMapperByType(type); var result = mapper.Map(propertyName); diff --git a/src/Umbraco.Core/Persistence/Migrations/IMigration.cs b/src/Umbraco.Core/Persistence/Migrations/IMigration.cs index 2769400e44..8d0adcf4bc 100644 --- a/src/Umbraco.Core/Persistence/Migrations/IMigration.cs +++ b/src/Umbraco.Core/Persistence/Migrations/IMigration.cs @@ -1,9 +1,11 @@ -namespace Umbraco.Core.Persistence.Migrations +using umbraco.interfaces; + +namespace Umbraco.Core.Persistence.Migrations { /// /// Marker interface for database migrations /// - public interface IMigration + public interface IMigration : IDiscoverable { void Up(); void Down(); diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 8b33599436..002af11611 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -33,7 +33,12 @@ namespace Umbraco.Core.Persistence.Migrations.Initial CreateUmbracNodeData(); } - if(tableName.Equals("cmsContentType")) + if (tableName.Equals("umbracoLock")) + { + CreateUmbracoLockData(); + } + + if (tableName.Equals("cmsContentType")) { CreateCmsContentTypeData(); } @@ -125,25 +130,28 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.System.DefaultMembersListViewDataTypeId, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-97", SortOrder = 2, UniqueId = new Guid("AA2C52A0-CE87-4E65-A47C-7DF09358585D"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"), Text = Constants.Conventions.MediaTypes.Folder, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"), Text = Constants.Conventions.MediaTypes.Image, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = new Guid("a6857c73-d6e9-480c-b6e6-f15f6ad11125"), Text = "Content Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = new Guid("93929b9a-93a2-4e2a-b239-d99334440a59"), Text = "Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = new Guid("2b24165f-9782-4aa3-b459-1de4a4d21f60"), Text = "Member Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1040, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1040", SortOrder = 2, UniqueId = new Guid("21e798da-e06e-4eda-a511-ed257f78d4fa"), Text = "Related Links", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1041, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1041", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1043, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1043", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = new Guid(Constants.ObjectTypes.MemberType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1045, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1045", SortOrder = 2, UniqueId = new Guid("7E3962CC-CE20-4FFC-B661-5897A894BA7E"), Text = "Multiple Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + //New UDI pickers with newer Ids + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Related Links", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); //TODO: We're not creating these for 7.0 //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1039, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1039", SortOrder = 2, UniqueId = new Guid("06f349a9-c949-4b6a-8660-59c10451af42"), Text = "Ultimate Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1038, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1038", SortOrder = 2, UniqueId = new Guid("1251c96c-185c-4e9b-93f4-b48205573cbd"), Text = "Simple Editor", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1042, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1042", SortOrder = 2, UniqueId = new Guid("0a452bd5-83f9-4bc3-8403-1286e13fb77e"), Text = "Macro Container", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + } + private void CreateUmbracoLockData() + { // all lock objects - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.System.ServersLock, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1," + Constants.System.ServersLock, SortOrder = 1, UniqueId = new Guid("0AF5E610-A310-4B6F-925F-E928D5416AF7"), Text = "LOCK: Servers", NodeObjectType = Constants.ObjectTypes.LockObjectGuid, CreateDate = DateTime.Now }); + _database.Insert("umbracoLock", "id", false, new LockDto { Id = Constants.Locks.Servers, Name = "Servers" }); } private void CreateCmsContentTypeData() @@ -238,17 +246,19 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 12, DataTypeId = -40, PropertyEditorAlias = Constants.PropertyEditors.RadioButtonListAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 13, DataTypeId = -41, PropertyEditorAlias = Constants.PropertyEditors.DateAlias, DbType = "Date" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 14, DataTypeId = -42, PropertyEditorAlias = Constants.PropertyEditors.DropDownListAlias, DbType = "Integer" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 15, DataTypeId = -43, PropertyEditorAlias = Constants.PropertyEditors.CheckBoxListAlias, DbType = "Nvarchar" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 16, DataTypeId = 1034, PropertyEditorAlias = Constants.PropertyEditors.ContentPickerAlias, DbType = "Integer" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 18, DataTypeId = 1036, PropertyEditorAlias = Constants.PropertyEditors.MemberPickerAlias, DbType = "Integer" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 17, DataTypeId = 1035, PropertyEditorAlias = Constants.PropertyEditors.MultipleMediaPickerAlias, DbType = "Nvarchar" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 21, DataTypeId = 1040, PropertyEditorAlias = Constants.PropertyEditors.RelatedLinksAlias, DbType = "Ntext" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 15, DataTypeId = -43, PropertyEditorAlias = Constants.PropertyEditors.CheckBoxListAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 22, DataTypeId = 1041, PropertyEditorAlias = Constants.PropertyEditors.TagsAlias, DbType = "Ntext" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 24, DataTypeId = 1043, PropertyEditorAlias = Constants.PropertyEditors.ImageCropperAlias, DbType = "Ntext" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 25, DataTypeId = 1045, PropertyEditorAlias = Constants.PropertyEditors.MultipleMediaPickerAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = -26, DataTypeId = Constants.System.DefaultContentListViewDataTypeId, PropertyEditorAlias = Constants.PropertyEditors.ListViewAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = -27, DataTypeId = Constants.System.DefaultMediaListViewDataTypeId, PropertyEditorAlias = Constants.PropertyEditors.ListViewAlias, DbType = "Nvarchar" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = -28, DataTypeId = Constants.System.DefaultMembersListViewDataTypeId, PropertyEditorAlias = Constants.PropertyEditors.ListViewAlias, DbType = "Nvarchar" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = -28, DataTypeId = Constants.System.DefaultMembersListViewDataTypeId, PropertyEditorAlias = Constants.PropertyEditors.ListViewAlias, DbType = "Nvarchar" }); + + //New UDI pickers with newer Ids + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 26, DataTypeId = 1046, PropertyEditorAlias = Constants.PropertyEditors.ContentPicker2Alias, DbType = "Nvarchar" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 27, DataTypeId = 1047, PropertyEditorAlias = Constants.PropertyEditors.MemberPicker2Alias, DbType = "Nvarchar" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 28, DataTypeId = 1048, PropertyEditorAlias = Constants.PropertyEditors.MediaPicker2Alias, DbType = "Ntext" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 29, DataTypeId = 1049, PropertyEditorAlias = Constants.PropertyEditors.MediaPicker2Alias, DbType = "Ntext" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 30, DataTypeId = 1050, PropertyEditorAlias = Constants.PropertyEditors.RelatedLinks2Alias, DbType = "Ntext" }); //TODO: We're not creating these for 7.0 //_database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 19, DataTypeId = 1038, PropertyEditorAlias = Constants.PropertyEditors.MarkdownEditorAlias, DbType = "Ntext" }); @@ -260,10 +270,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial { _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 3, Alias = "", SortOrder = 0, DataTypeNodeId = -87, Value = ",code,undo,redo,cut,copy,mcepasteword,stylepicker,bold,italic,bullist,numlist,outdent,indent,mcelink,unlink,mceinsertanchor,mceimage,umbracomacro,mceinserttable,umbracoembed,mcecharmap,|1|1,2,3,|0|500,400|1049,|true|" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 4, Alias = "group", SortOrder = 0, DataTypeNodeId = 1041, Value = "default" }); - - //default's for MultipleMediaPickerAlias picker - _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 5, Alias = "multiPicker", SortOrder = 0, DataTypeNodeId = 1045, Value = "1" }); - + //defaults for the member list _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -1, Alias = "pageSize", SortOrder = 1, DataTypeNodeId = Constants.System.DefaultMembersListViewDataTypeId, Value = "10" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -2, Alias = "orderBy", SortOrder = 2, DataTypeNodeId = Constants.System.DefaultMembersListViewDataTypeId, Value = "username" }); @@ -280,12 +287,19 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -7, Alias = "orderDirection", SortOrder = 3, DataTypeNodeId = Constants.System.DefaultMediaListViewDataTypeId, Value = "desc" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -8, Alias = "layouts", SortOrder = 4, DataTypeNodeId = Constants.System.DefaultMediaListViewDataTypeId, Value = "[" + cardLayout + "," + listLayout + "]" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -9, Alias = "includeProperties", SortOrder = 5, DataTypeNodeId = Constants.System.DefaultMediaListViewDataTypeId, Value = "[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]" }); + + //default's for MultipleMediaPickerAlias picker + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 6, Alias = "multiPicker", SortOrder = 0, DataTypeNodeId = 1049, Value = "1" }); } private void CreateUmbracoRelationTypeData() { - _database.Insert("umbracoRelationType", "id", false, new RelationTypeDto { Id = 1, Alias = Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = new Guid(Constants.ObjectTypes.Document), ParentObjectType = new Guid(Constants.ObjectTypes.Document), Dual = true, Name = Constants.Conventions.RelationTypes.RelateDocumentOnCopyName }); - _database.Insert("umbracoRelationType", "id", false, new RelationTypeDto { Id = 2, Alias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = new Guid(Constants.ObjectTypes.Document), ParentObjectType = new Guid(Constants.ObjectTypes.Document), Dual = false, Name = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName }); + var relationType = new RelationTypeDto { Id = 1, Alias = Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = new Guid(Constants.ObjectTypes.Document), ParentObjectType = new Guid(Constants.ObjectTypes.Document), Dual = true, Name = Constants.Conventions.RelationTypes.RelateDocumentOnCopyName }; + relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + _database.Insert("umbracoRelationType", "id", false, relationType); + relationType = new RelationTypeDto { Id = 2, Alias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = new Guid(Constants.ObjectTypes.Document), ParentObjectType = new Guid(Constants.ObjectTypes.Document), Dual = false, Name = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName }; + relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + _database.Insert("umbracoRelationType", "id", false, relationType); } private void CreateCmsTaskTypeData() @@ -298,7 +312,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial var dto = new MigrationDto { Id = 1, - Name = GlobalSettings.UmbracoMigrationName, + Name = Constants.System.UmbracoMigrationName, Version = UmbracoVersion.GetSemanticVersion().ToString(), CreateDate = DateTime.Now }; diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index ab477953b4..83ae1de7e1 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial /// /// Represents the initial database schema creation by running CreateTable for all DTOs against the db. /// - internal class DatabaseSchemaCreation + public class DatabaseSchemaCreation { /// /// Constructor @@ -82,9 +82,10 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {43, typeof (CacheInstructionDto)}, {44, typeof (ExternalLoginDto)}, {45, typeof (MigrationDto)}, - {46, typeof (UmbracoDeployChecksumDto)}, - {47, typeof (UmbracoDeployDependencyDto)}, - {48, typeof (RedirectUrlDto) } + //46, removed: UmbracoDeployChecksumDto + //47, removed: UmbracoDeployDependencyDto + {48, typeof (RedirectUrlDto) }, + {49, typeof (LockDto) } }; #endregion @@ -344,7 +345,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial /// Raises the event. /// /// The instance containing the event data. - protected internal virtual void FireBeforeCreation(DatabaseCreationEventArgs e) + internal virtual void FireBeforeCreation(DatabaseCreationEventArgs e) { if (BeforeCreation != null) { @@ -360,7 +361,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial /// Raises the event. /// /// The instance containing the event data. - protected virtual void FireAfterCreation(DatabaseCreationEventArgs e) + internal virtual void FireAfterCreation(DatabaseCreationEventArgs e) { if (AfterCreation != null) { diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs index 47772c5c15..a34db10839 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs @@ -47,7 +47,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial if (ValidTables.Any(x => x.InvariantEquals("umbracoMigration"))) { - var allMigrations = migrationEntryService.GetAll(GlobalSettings.UmbracoMigrationName); + var allMigrations = migrationEntryService.GetAll(Constants.System.UmbracoMigrationName); mostrecent = allMigrations.OrderByDescending(x => x.Version).Select(x => x.Version).FirstOrDefault(); } @@ -136,6 +136,12 @@ namespace Umbraco.Core.Persistence.Migrations.Initial return new Version(7, 4, 0); } + //if the error indicates a problem with the column cmsMacroProperty.uniquePropertyId then it is not version 7.6 since that is when it is added + if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMacroProperty,uniquePropertyId")))) + { + return new Version(7, 5, 0); + } + return UmbracoVersion.Current; } diff --git a/src/Umbraco.Core/Persistence/Migrations/MigrationBase.cs b/src/Umbraco.Core/Persistence/Migrations/MigrationBase.cs index a19aaa24ad..cd08825e2d 100644 --- a/src/Umbraco.Core/Persistence/Migrations/MigrationBase.cs +++ b/src/Umbraco.Core/Persistence/Migrations/MigrationBase.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core.Persistence.Migrations Logger = logger; } - internal IMigrationContext Context; + public IMigrationContext Context { get; internal set; } public abstract void Up(); public abstract void Down(); diff --git a/src/Umbraco.Core/Persistence/Migrations/Syntax/Alter/AlterSyntaxBuilder.cs b/src/Umbraco.Core/Persistence/Migrations/Syntax/Alter/AlterSyntaxBuilder.cs index e253f8e607..fc1971858f 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Syntax/Alter/AlterSyntaxBuilder.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Syntax/Alter/AlterSyntaxBuilder.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Persistence.Migrations.Syntax.Alter.Column; +using System; +using Umbraco.Core.Persistence.Migrations.Syntax.Alter.Column; using Umbraco.Core.Persistence.Migrations.Syntax.Alter.Expressions; using Umbraco.Core.Persistence.Migrations.Syntax.Alter.Table; using Umbraco.Core.Persistence.SqlSyntax; @@ -25,6 +26,15 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Alter return new AlterTableBuilder(_context, _databaseProviders, expression); } + /// + /// The problem with this is that only under particular circumstances is the expression added to the context + /// so you wouldn't actually know if you are using it correctly or not and chances are you are not and therefore + /// the statement won't even execute whereas using the IAlterTableSyntax to modify a column is guaranteed to add + /// the expression to the context. + /// + /// + /// + [Obsolete("Use the IAlterTableSyntax to modify a column instead, this will be removed in future versions")] public IAlterColumnSyntax Column(string columnName) { var expression = new AlterColumnExpression(_context.CurrentDatabaseProvider, _databaseProviders, _sqlSyntax) {Column = {Name = columnName}}; diff --git a/src/Umbraco.Core/Persistence/Migrations/Syntax/Alter/IAlterSyntaxBuilder.cs b/src/Umbraco.Core/Persistence/Migrations/Syntax/Alter/IAlterSyntaxBuilder.cs index 479c2eadce..77db6b8cde 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Syntax/Alter/IAlterSyntaxBuilder.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Syntax/Alter/IAlterSyntaxBuilder.cs @@ -1,11 +1,22 @@ -using Umbraco.Core.Persistence.Migrations.Syntax.Alter.Column; +using System; +using Umbraco.Core.Persistence.Migrations.Syntax.Alter.Column; using Umbraco.Core.Persistence.Migrations.Syntax.Alter.Table; namespace Umbraco.Core.Persistence.Migrations.Syntax.Alter { public interface IAlterSyntaxBuilder : IFluentSyntax { - IAlterTableSyntax Table(string tableName); + IAlterTableSyntax Table(string tableName); + + /// + /// The problem with this is that only under particular circumstances is the expression added to the context + /// so you wouldn't actually know if you are using it correctly or not and chances are you are not and therefore + /// the statement won't even execute whereas using the IAlterTableSyntax to modify a column is guaranteed to add + /// the expression to the context. + /// + /// + /// + [Obsolete("Use the IAlterTableSyntax to modify a column instead, this will be removed in future versions")] IAlterColumnSyntax Column(string columnName); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/CreateBuilder.cs b/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/CreateBuilder.cs index e3ad6f972d..6afb0a4ca7 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/CreateBuilder.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/CreateBuilder.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Persistence.Migrations.Syntax.Create.Expressions; using Umbraco.Core.Persistence.Migrations.Syntax.Create.ForeignKey; using Umbraco.Core.Persistence.Migrations.Syntax.Create.Index; using Umbraco.Core.Persistence.Migrations.Syntax.Create.Table; +using Umbraco.Core.Persistence.Migrations.Syntax.Execute.Expressions; using Umbraco.Core.Persistence.Migrations.Syntax.Expressions; using Umbraco.Core.Persistence.SqlSyntax; @@ -27,6 +28,24 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Create _databaseProviders = databaseProviders; } + public void Table() + { + var tableDefinition = DefinitionFactory.GetTableDefinition(_sqlSyntax, typeof(T)); + + AddSql(_sqlSyntax.Format(tableDefinition)); + AddSql(_sqlSyntax.FormatPrimaryKey(tableDefinition)); + foreach (var sql in _sqlSyntax.Format(tableDefinition.ForeignKeys)) + AddSql(sql); + foreach (var sql in _sqlSyntax.Format(tableDefinition.Indexes)) + AddSql(sql); + } + + private void AddSql(string sql) + { + var expression = new ExecuteSqlStatementExpression(_context.CurrentDatabaseProvider, _databaseProviders, _sqlSyntax) { SqlStatement = sql }; + _context.Expressions.Add(expression); + } + public ICreateTableWithColumnSyntax Table(string tableName) { var expression = new CreateTableExpression(_context.CurrentDatabaseProvider, _databaseProviders, _sqlSyntax) { TableName = tableName }; diff --git a/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/ICreateBuilder.cs b/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/ICreateBuilder.cs index e079127705..3cb2fe7e9f 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/ICreateBuilder.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/ICreateBuilder.cs @@ -8,6 +8,8 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Create { public interface ICreateBuilder : IFluentSyntax { + void Table(); + ICreateTableWithColumnSyntax Table(string tableName); ICreateColumnOnTableSyntax Column(string columnName); diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourNineZero/RemoveUmbracoAppConstraints.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourNineZero/RemoveUmbracoAppConstraints.cs index 0f648d34eb..062ec4f22b 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourNineZero/RemoveUmbracoAppConstraints.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourNineZero/RemoveUmbracoAppConstraints.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionFourNineZero { - [MigrationAttribute("4.9.0", 0, GlobalSettings.UmbracoMigrationName)] + [MigrationAttribute("4.9.0", 0, Constants.System.UmbracoMigrationName)] public class RemoveUmbracoAppConstraints : MigrationBase { public RemoveUmbracoAppConstraints(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourOneZero/AddPreviewXmlTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourOneZero/AddPreviewXmlTable.cs index 4e8d3165fb..4ac06d1531 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourOneZero/AddPreviewXmlTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourOneZero/AddPreviewXmlTable.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionFourOneZero { - [Migration("4.1.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("4.1.0", 0, Constants.System.UmbracoMigrationName)] public class AddPreviewXmlTable : MigrationBase { public AddPreviewXmlTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroPropertyTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroPropertyTable.cs index b61ac55448..8dfe51f8dc 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroPropertyTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroPropertyTable.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven /// /// Creats a unique index across two columns so we cannot have duplicate property aliases for one macro /// - [Migration("7.0.0", 5, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 5, Constants.System.UmbracoMigrationName)] public class AddIndexToCmsMacroPropertyTable : MigrationBase { private readonly bool _skipIndexCheck; diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroTable.cs index d18cb430c0..e6c237d4d6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddIndexToCmsMacroTable.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven /// /// Creates a unique index on the macro alias so we cannot have duplicates by alias /// - [Migration("7.0.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 4, Constants.System.UmbracoMigrationName)] public class AddIndexToCmsMacroTable : MigrationBase { private readonly bool _forTesting; diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddPropertyEditorAliasColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddPropertyEditorAliasColumn.cs index e20c2cfb0b..474da442be 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddPropertyEditorAliasColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AddPropertyEditorAliasColumn.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 0, Constants.System.UmbracoMigrationName)] public class AddPropertyEditorAliasColumn : MigrationBase { public AddPropertyEditorAliasColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterCmsMacroPropertyTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterCmsMacroPropertyTable.cs index 9154c6c985..474b8a3c13 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterCmsMacroPropertyTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterCmsMacroPropertyTable.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven /// needs to be changed to editorAlias, we'll do this by removing the constraint,changing the macroPropertyType to the new /// editorAlias column (and maintaing data so we can reference it) /// - [Migration("7.0.0", 6, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 6, Constants.System.UmbracoMigrationName)] public class AlterCmsMacroPropertyTable : MigrationBase { public AlterCmsMacroPropertyTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs index d822b7593a..704fcffc84 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 8, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 8, Constants.System.UmbracoMigrationName)] public class AlterTagRelationsTable : MigrationBase { public AlterTagRelationsTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagsTable.cs index d069d8222d..32bd9d1403 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagsTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagsTable.cs @@ -8,7 +8,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 9, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 9, Constants.System.UmbracoMigrationName)] public class AlterTagsTable : MigrationBase { public AlterTagsTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterUserTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterUserTable.cs index 28c3eb15f2..d82d73f7c3 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterUserTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterUserTable.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 3, Constants.System.UmbracoMigrationName)] public class AlterUserTable : MigrationBase { public AlterUserTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AssignMissingKeysAndIndexes.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AssignMissingKeysAndIndexes.cs index 9de1ea0871..c76031e4e4 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AssignMissingKeysAndIndexes.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AssignMissingKeysAndIndexes.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven /// and it wasn't a MySQL install. /// see: http://issues.umbraco.org/issue/U4-5707 /// - [Migration("7.0.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 0, Constants.System.UmbracoMigrationName)] public class AssignMissingKeysAndIndexes : MigrationBase { public AssignMissingKeysAndIndexes(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/DropControlIdColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/DropControlIdColumn.cs index 0917411f8b..7928255968 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/DropControlIdColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/DropControlIdColumn.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 2, Constants.System.UmbracoMigrationName)] public class DropControlIdColumn : MigrationBase { public DropControlIdColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/RemoveCmsMacroPropertyTypeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/RemoveCmsMacroPropertyTypeTable.cs index f4f4b9065c..1c83ef5972 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/RemoveCmsMacroPropertyTypeTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/RemoveCmsMacroPropertyTypeTable.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 7, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 7, Constants.System.UmbracoMigrationName)] public class RemoveCmsMacroPropertyTypeTable : MigrationBase { public RemoveCmsMacroPropertyTypeTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateControlIdToPropertyEditorAlias.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateControlIdToPropertyEditorAlias.cs index ef464667dd..f94fb7ce2c 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateControlIdToPropertyEditorAlias.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateControlIdToPropertyEditorAlias.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven /// /// Updates the data in the changed propertyEditorAlias column after it has been changed by ChangeControlIdColumn /// - [Migration("7.0.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 1, Constants.System.UmbracoMigrationName)] public class UpdateControlIdToPropertyEditorAlias : MigrationBase { public UpdateControlIdToPropertyEditorAlias(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs index 749996ea36..d29935acd2 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs @@ -16,7 +16,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { - [Migration("7.0.0", 10, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", 10, Constants.System.UmbracoMigrationName)] public class UpdateRelatedLinksData : MigrationBase { public UpdateRelatedLinksData(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/UpdateAllowedMediaTypesAtRoot.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/UpdateAllowedMediaTypesAtRoot.cs index c9a0d509e6..1acb979ff6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/UpdateAllowedMediaTypesAtRoot.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/UpdateAllowedMediaTypesAtRoot.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveFiv /// /// See: http://issues.umbraco.org/issue/U4-4196 /// - [Migration("7.5.5", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.5.5", 1, Constants.System.UmbracoMigrationName)] public class UpdateAllowedMediaTypesAtRoot : MigrationBase { public UpdateAllowedMediaTypesAtRoot(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/AddRedirectUrlTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/AddRedirectUrlTable.cs index 508c0f284b..829631c7f4 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/AddRedirectUrlTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/AddRedirectUrlTable.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveZero { - [Migration("7.5.0", 100, GlobalSettings.UmbracoMigrationName)] + [Migration("7.5.0", 100, Constants.System.UmbracoMigrationName)] public class AddRedirectUrlTable : MigrationBase { public AddRedirectUrlTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/EnsureServersLockObject.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/EnsureServersLockObject.cs index 6f9d74e5db..4c83a70fe2 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/EnsureServersLockObject.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/EnsureServersLockObject.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveZer // This migration exists for 7.3.0 but it seems like it was not always running properly // if you're upgrading from 7.3.0 or higher than we add this migration, if you're upgrading // from 7.3.0 or lower then you will already get this migration in the migration to get to 7.3.0 - [Migration("7.3.0", "7.5.0", 10, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", "7.5.0", 10, Constants.System.UmbracoMigrationName)] public class EnsureServersLockObject : MigrationBase { public EnsureServersLockObject(ISqlSyntaxProvider sqlSyntax, ILogger logger) @@ -22,7 +22,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveZer // for some reason it was not, so it was created during migrations but not during // new installs, so for ppl that upgrade, make sure they have it - EnsureLockObject(Constants.System.ServersLock, "0AF5E610-A310-4B6F-925F-E928D5416AF7", "LOCK: Servers"); + EnsureLockObject(Constants.Locks.Servers, "0AF5E610-A310-4B6F-925F-E928D5416AF7", "LOCK: Servers"); } public override void Down() diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs index 96523e25e8..f8c4c14bbe 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveZer /// /// This is here to re-remove these tables, we dropped them in 7.3 but new installs created them again so we're going to re-drop them /// - [Migration("7.5.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.5.0", 1, Constants.System.UmbracoMigrationName)] public class RemoveStylesheetDataAndTablesAgain : MigrationBase { public RemoveStylesheetDataAndTablesAgain(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/UpdateUniqueIndexOnCmsPropertyData.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/UpdateUniqueIndexOnCmsPropertyData.cs index f8e6abe42e..8ac4d86290 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/UpdateUniqueIndexOnCmsPropertyData.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/UpdateUniqueIndexOnCmsPropertyData.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveZer /// /// See: http://issues.umbraco.org/issue/U4-8522 /// - [Migration("7.5.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.5.0", 2, Constants.System.UmbracoMigrationName)] public class UpdateUniqueIndexOnCmsPropertyData : MigrationBase { public UpdateUniqueIndexOnCmsPropertyData(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddDataDecimalColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddDataDecimalColumn.cs index 442b92d2b5..26fb006e82 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddDataDecimalColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddDataDecimalColumn.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero { - [Migration("7.4.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 1, Constants.System.UmbracoMigrationName)] public class AddDataDecimalColumn : MigrationBase { public AddDataDecimalColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUmbracoDeployTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUmbracoDeployTables.cs index a39cea2ee0..0a1e6058a9 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUmbracoDeployTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUmbracoDeployTables.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero { - [Migration("7.4.0", 5, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 5, Constants.System.UmbracoMigrationName)] public class AddUmbracoDeployTables : MigrationBase { public AddUmbracoDeployTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs index fe600f6b69..5f987fc966 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero { - [Migration("7.4.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 2, Constants.System.UmbracoMigrationName)] public class AddUniqueIdPropertyTypeGroupColumn : MigrationBase { public AddUniqueIdPropertyTypeGroupColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/EnsureContentTypeUniqueIdsAreConsistent.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/EnsureContentTypeUniqueIdsAreConsistent.cs index 20200a3230..c24048a6a7 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/EnsureContentTypeUniqueIdsAreConsistent.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/EnsureContentTypeUniqueIdsAreConsistent.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZer /// alias, so we need to ensure that these are initially consistent on /// all environments (based on the alias). /// - [Migration("7.4.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 3, Constants.System.UmbracoMigrationName)] public class EnsureContentTypeUniqueIdsAreConsistent : MigrationBase { public EnsureContentTypeUniqueIdsAreConsistent(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/FixListViewMediaSortOrder.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/FixListViewMediaSortOrder.cs index e1fa9e9257..7d5714218a 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/FixListViewMediaSortOrder.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/FixListViewMediaSortOrder.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero { - [Migration("7.4.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 4, Constants.System.UmbracoMigrationName)] public class FixListViewMediaSortOrder : MigrationBase { public FixListViewMediaSortOrder(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs index 3d285c2715..5f4120f243 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero { - [Migration("7.4.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("7.4.0", 4, Constants.System.UmbracoMigrationName)] public class RemoveParentIdPropertyTypeGroupColumn : MigrationBase { public RemoveParentIdPropertyTypeGroupColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenOneZero/AssignMissingPrimaryForMySqlKeys.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenOneZero/AssignMissingPrimaryForMySqlKeys.cs index 6fa0eaa5dc..904a4a9349 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenOneZero/AssignMissingPrimaryForMySqlKeys.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenOneZero/AssignMissingPrimaryForMySqlKeys.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenOneZero // this is because when the 7.0.0 migrations are executed, this primary key get's created so if this migration is also executed // we will get exceptions because it is trying to create the PK two times. - [Migration("7.0.0", "7.1.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.0.0", "7.1.0", 0, Constants.System.UmbracoMigrationName)] public class AssignMissingPrimaryForMySqlKeys : MigrationBase { public AssignMissingPrimaryForMySqlKeys(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs new file mode 100644 index 0000000000..c7fae7b2aa --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 3, Constants.System.UmbracoMigrationName)] + public class AddIndexToCmsMemberLoginName : MigrationBase + { + public AddIndexToCmsMemberLoginName(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + Execute.Code(database => + { + //Now we need to check if we can actually d6 this because we won't be able to if there's data in there that is too long + //http://issues.umbraco.org/issue/U4-9758 + + var colLen = (SqlSyntax is MySqlSyntaxProvider) + ? database.ExecuteScalar("select max(LENGTH(LoginName)) from cmsMember") + : database.ExecuteScalar("select max(datalength(LoginName)) from cmsMember"); + + if (colLen < 900 == false && colLen != null) + { + return null; + } + + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_cmsMember_LoginName")) == false) + { + var localContext = new LocalMigrationContext(Context.CurrentDatabaseProvider, database, SqlSyntax, Logger); + + //we can apply the index + localContext.Create.Index("IX_cmsMember_LoginName").OnTable("cmsMember") + .OnColumn("LoginName") + .Ascending() + .WithOptions() + .NonClustered(); + + return localContext.GetSql(); + } + + return null; + + }); + + + } + + public override void Down() + { + Delete.Index("IX_cmsMember_LoginName").OnTable("cmsMember"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs new file mode 100644 index 0000000000..c7fb573761 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexToUmbracoNodePath : MigrationBase + { + public AddIndexToUmbracoNodePath(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoNodePath")) == false) + { + Create.Index("IX_umbracoNodePath").OnTable("umbracoNode") + .OnColumn("path") + .Ascending() + .WithOptions() + .NonClustered(); + } + } + + public override void Down() + { + Delete.Index("IX_umbracoNodePath").OnTable("umbracoNode"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUser2NodePermission.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUser2NodePermission.cs new file mode 100644 index 0000000000..b510ef428c --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUser2NodePermission.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexToUser2NodePermission : MigrationBase + { + public AddIndexToUser2NodePermission(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoUser2NodePermission_nodeId")) == false) + { + Create.Index("IX_umbracoUser2NodePermission_nodeId").OnTable("umbracoUser2NodePermission") + .OnColumn("nodeId") + .Ascending() + .WithOptions() + .NonClustered(); + } + } + + public override void Down() + { + Delete.Index("IX_umbracoUser2NodePermission_nodeId").OnTable("cmsMember"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs new file mode 100644 index 0000000000..b8c0d78ef1 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexesToUmbracoRelationTables : MigrationBase + { + public AddIndexesToUmbracoRelationTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database).ToArray(); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelation_parentChildType")) == false) + { + //This will remove any corrupt/duplicate data in the relation table before the index is applied + //Ensure this executes in a defered block which will be done inside of the migration transaction + this.Execute.Code(database => + { + //We need to check if this index has corrupted data and then clear that data + var duplicates = database.Fetch("SELECT parentId,childId,relType FROM umbracoRelation GROUP BY parentId,childId,relType HAVING COUNT(*) > 1"); + if (duplicates.Count > 0) + { + //need to fix this there cannot be duplicates so we'll take the latest entries, it's really not going to matter though + foreach (var duplicate in duplicates) + { + var ids = database.Fetch("SELECT id FROM umbracoRelation WHERE parentId=@parentId AND childId=@childId AND relType=@relType ORDER BY datetime DESC", + new { parentId = duplicate.parentId, childId = duplicate.childId, relType = duplicate.relType }); + + if (ids.Count == 1) + { + //this is just a safety check, this should absolutely never happen + throw new InvalidOperationException("Duplicates were detected but could not be discovered"); + } + + //delete the others + ids = ids.Skip(0).ToList(); + + //iterate in groups of 2000 to avoid the max sql parameter limit + foreach (var idGroup in ids.InGroupsOf(2000)) + { + database.Execute("DELETE FROM umbracoRelation WHERE id IN (@ids)", new { ids = idGroup }); + } + } + } + return ""; + }); + + //unique index to prevent duplicates - and for better perf + Create.Index("IX_umbracoRelation_parentChildType").OnTable("umbracoRelation") + .OnColumn("parentId").Ascending() + .OnColumn("childId").Ascending() + .OnColumn("relType").Ascending() + .WithOptions() + .Unique(); + } + + //need indexes on alias and name for relation type since these are queried against + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelationType_alias")) == false) + { + Create.Index("IX_umbracoRelationType_alias").OnTable("umbracoRelationType") + .OnColumn("alias") + .Ascending() + .WithOptions() + .NonClustered(); + } + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelationType_name")) == false) + { + Create.Index("IX_umbracoRelationType_name").OnTable("umbracoRelationType") + .OnColumn("name") + .Ascending() + .WithOptions() + .NonClustered(); + } + + } + + public override void Down() + { + Delete.Index("IX_umbracoNodePath").OnTable("umbracoNode"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockObjects.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockObjects.cs new file mode 100644 index 0000000000..ed6fcaae23 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockObjects.cs @@ -0,0 +1,38 @@ +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 101, Constants.System.UmbracoMigrationName)] + public class AddLockObjects : MigrationBase + { + public AddLockObjects(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + EnsureLockObject(Constants.Locks.Servers, "Servers"); + } + + public override void Down() + { + // not implemented + } + + private void EnsureLockObject(int id, string name) + { + Execute.Code(db => + { + var exists = db.Exists(id); + if (exists) return string.Empty; + // be safe: delete old umbracoNode lock objects if any + db.Execute("DELETE FROM umbracoNode WHERE id=@id;", new { id }); + // then create umbracoLock object + db.Execute("INSERT umbracoLock (id, name, value) VALUES (@id, @name, 1);", new { id, name }); + return string.Empty; + }); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockTable.cs new file mode 100644 index 0000000000..809f18e77e --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddLockTable.cs @@ -0,0 +1,31 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 100, Constants.System.UmbracoMigrationName)] + public class AddLockTable : MigrationBase + { + public AddLockTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + if (tables.InvariantContains("umbracoLock") == false) + { + Create.Table("umbracoLock") + .WithColumn("id").AsInt32().PrimaryKey("PK_umbracoLock") + .WithColumn("value").AsInt32().NotNullable() + .WithColumn("name").AsString(64).NotNullable(); + } + } + + public override void Down() + { + // not implemented + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddMacroUniqueIdColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddMacroUniqueIdColumn.cs new file mode 100644 index 0000000000..a4a99391db --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddMacroUniqueIdColumn.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddMacroUniqueIdColumn : MigrationBase + { + public AddMacroUniqueIdColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals("cmsMacro") && x.ColumnName.InvariantEquals("uniqueId")) == false) + { + Create.Column("uniqueId").OnTable("cmsMacro").AsGuid().Nullable(); + Execute.Code(UpdateMacroGuids); + Alter.Table("cmsMacro").AlterColumn("uniqueId").AsGuid().NotNullable(); + Create.Index("IX_cmsMacro_UniqueId").OnTable("cmsMacro").OnColumn("uniqueId") + .Ascending() + .WithOptions().NonClustered() + .WithOptions().Unique(); + + } + + if (columns.Any(x => x.TableName.InvariantEquals("cmsMacroProperty") && x.ColumnName.InvariantEquals("uniquePropertyId")) == false) + { + Create.Column("uniquePropertyId").OnTable("cmsMacroProperty").AsGuid().Nullable(); + Execute.Code(UpdateMacroPropertyGuids); + Alter.Table("cmsMacroProperty").AlterColumn("uniquePropertyId").AsGuid().NotNullable(); + Create.Index("IX_cmsMacroProperty_UniquePropertyId").OnTable("cmsMacroProperty").OnColumn("uniquePropertyId") + .Ascending() + .WithOptions().NonClustered() + .WithOptions().Unique(); + } + } + + private static string UpdateMacroGuids(Database database) + { + var updates = database.Query("SELECT id, macroAlias FROM cmsMacro") + .Select(macro => Tuple.Create((int) macro.id, ("macro____" + (string) macro.macroAlias).ToGuid())) + .ToList(); + + foreach (var update in updates) + database.Execute("UPDATE cmsMacro set uniqueId=@guid WHERE id=@id", new { guid = update.Item2, id = update.Item1 }); + + return string.Empty; + } + + private static string UpdateMacroPropertyGuids(Database database) + { + var updates = database.Query(@"SELECT cmsMacroProperty.id id, macroPropertyAlias propertyAlias, cmsMacro.macroAlias macroAlias +FROM cmsMacroProperty +JOIN cmsMacro ON cmsMacroProperty.macro=cmsMacro.id") + .Select(prop => Tuple.Create((int) prop.id, ("macro____" + (string) prop.macroAlias + "____" + (string) prop.propertyAlias).ToGuid())) + .ToList(); + + foreach (var update in updates) + database.Execute("UPDATE cmsMacroProperty set uniquePropertyId=@guid WHERE id=@id", new { guid = update.Item2, id = update.Item1 }); + + return string.Empty; + } + + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddRelationTypeUniqueIdColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddRelationTypeUniqueIdColumn.cs new file mode 100644 index 0000000000..c18f795c3a --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddRelationTypeUniqueIdColumn.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddRelationTypeUniqueIdColumn : MigrationBase + { + public AddRelationTypeUniqueIdColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals("umbracoRelationType") && x.ColumnName.InvariantEquals("typeUniqueId")) == false) + { + Create.Column("typeUniqueId").OnTable("umbracoRelationType").AsGuid().Nullable(); + Execute.Code(UpdateRelationTypeGuids); + Alter.Table("umbracoRelationType").AlterColumn("typeUniqueId").AsGuid().NotNullable(); + Create.Index("IX_umbracoRelationType_UniqueId").OnTable("umbracoRelationType").OnColumn("typeUniqueId") + .Ascending() + .WithOptions().NonClustered() + .WithOptions().Unique(); + } + } + + private static string UpdateRelationTypeGuids(Database database) + { + var updates = database.Query("SELECT id, alias, name FROM umbracoRelationType") + .Select(relationType => Tuple.Create((int) relationType.id, ("relationType____" + (string) relationType.alias + "____" + (string) relationType.name).ToGuid())) + .ToList(); + + foreach (var update in updates) + database.Execute("UPDATE umbracoRelationType set typeUniqueId=@guid WHERE id=@id", new { guid = update.Item2, id = update.Item1 }); + + return string.Empty; + } + + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/NormalizeTemplateGuids.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/NormalizeTemplateGuids.cs new file mode 100644 index 0000000000..f9f0d0efbb --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/NormalizeTemplateGuids.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class NormalizeTemplateGuids : MigrationBase + { + public NormalizeTemplateGuids(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + Execute.Code(UpdateTemplateGuids); + } + + private static string UpdateTemplateGuids(Database database) + { + // we need this migration because ppl running pre-7.6 on Cloud and Courier have templates in different + // environments having different GUIDs (Courier does not sync template GUIDs) and we need to normalize + // these GUIDs so templates with the same alias on different environments have the same GUID. + // however, if already running a prerelease version of 7.6, we do NOT want to normalize the GUIDs as quite + // probably, we are already running Deploy and the GUIDs are OK. assuming noone is running a prerelease + // of 7.6 on Courier. + // so... testing if we already have a 7.6.0 version installed. not pretty but...? + // + var version = database.FirstOrDefault("SELECT version FROM umbracoMigration WHERE name=@name ORDER BY version DESC", new { name = Constants.System.UmbracoMigrationName }); + if (version != null && version.StartsWith("7.6.0")) return string.Empty; + + var updates = database.Query(@"SELECT umbracoNode.id, cmsTemplate.alias FROM umbracoNode +JOIN cmsTemplate ON umbracoNode.id=cmsTemplate.nodeId +WHERE nodeObjectType = @guid", new { guid = Constants.ObjectTypes.TemplateTypeGuid}) + .Select(template => Tuple.Create((int) template.id, ("template____" + (string) template.alias).ToGuid())) + .ToList(); + + foreach (var update in updates) + database.Execute("UPDATE umbracoNode set uniqueId=@guid WHERE id=@id", new { guid = update.Item2, id = update.Item1 }); + + return string.Empty; + } + + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/ReduceLoginNameColumnsSize.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/ReduceLoginNameColumnsSize.cs new file mode 100644 index 0000000000..140bfa3553 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/ReduceLoginNameColumnsSize.cs @@ -0,0 +1,52 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 2, Constants.System.UmbracoMigrationName)] + public class ReduceLoginNameColumnsSize : MigrationBase + { + public ReduceLoginNameColumnsSize(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + //Now we need to check if we can actually d6 this because we won't be able to if there's data in there that is too long + //http://issues.umbraco.org/issue/U4-9758 + + Execute.Code(database => + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(database); + + var colLen = (SqlSyntax is MySqlSyntaxProvider) + ? database.ExecuteScalar("select max(LENGTH(LoginName)) from cmsMember") + : database.ExecuteScalar("select max(datalength(LoginName)) from cmsMember"); + + if (colLen < 900 == false) return null; + + var localContext = new LocalMigrationContext(Context.CurrentDatabaseProvider, database, SqlSyntax, Logger); + + //if it exists we need to drop it first + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_cmsMember_LoginName"))) + { + localContext.Delete.Index("IX_cmsMember_LoginName").OnTable("cmsMember"); + } + + //we can apply the col length change + localContext.Alter.Table("cmsMember") + .AlterColumn("LoginName") + .AsString(225) + .NotNullable(); + + return localContext.GetSql(); + }); + } + + public override void Down() + { + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemovePropertyDataIdIndex.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemovePropertyDataIdIndex.cs index b50c8e5f94..e041ad1425 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemovePropertyDataIdIndex.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemovePropertyDataIdIndex.cs @@ -1,5 +1,4 @@ using System.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.SqlSyntax; @@ -8,7 +7,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero /// /// See: http://issues.umbraco.org/issue/U4-9188 /// - [Migration("7.6.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] public class UpdateUniqueIndexOnCmsPropertyData : MigrationBase { public UpdateUniqueIndexOnCmsPropertyData(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemoveUmbracoDeployTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemoveUmbracoDeployTables.cs new file mode 100644 index 0000000000..5cf129700f --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemoveUmbracoDeployTables.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class RemoveUmbracoDeployTables : MigrationBase + { + public RemoveUmbracoDeployTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + + // there are two versions of umbracoDeployDependency, + // 1. one created by 7.4 and never used, we need to remove it (has a sourceId column) + // 2. one created by Deploy itself, we need to keep it (has a sourceUdi column) + if (tables.InvariantContains("umbracoDeployDependency")) + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + if (columns.Any(x => x.TableName.InvariantEquals("umbracoDeployDependency") && x.ColumnName.InvariantEquals("sourceId"))) + Delete.Table("umbracoDeployDependency"); + } + + // always remove umbracoDeployChecksum + if (tables.InvariantContains("umbracoDeployChecksum")) + Delete.Table("umbracoDeployChecksum"); + } + + public override void Down() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs index 50f78ca66d..c3d7857d75 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeOn /// /// This fixes the storage of user languages from the old format like en_us to en-US /// - [Migration("7.3.1", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.1", 0, Constants.System.UmbracoMigrationName)] public class UpdateUserLanguagesToIsoCode : MigrationBase { public UpdateUserLanguagesToIsoCode(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeTwo/EnsureMigrationsTableIdentityIsCorrect.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeTwo/EnsureMigrationsTableIdentityIsCorrect.cs index 91b4bd6438..a52abf1331 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeTwo/EnsureMigrationsTableIdentityIsCorrect.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeTwo/EnsureMigrationsTableIdentityIsCorrect.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeTw /// This reinserts all migrations in the migrations table to account for initial rows inserted /// on creation without identity enabled. /// - [Migration("7.3.2", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.2", 0, Constants.System.UmbracoMigrationName)] public class EnsureMigrationsTableIdentityIsCorrect : MigrationBase { public EnsureMigrationsTableIdentityIsCorrect(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs index 0fbec244b2..b5bb24e40e 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 9, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 9, Constants.System.UmbracoMigrationName)] public class AddExternalLoginsTable : MigrationBase { public AddExternalLoginsTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs index eea56031d4..0baf5bff1a 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 14, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 14, Constants.System.UmbracoMigrationName)] public class AddForeignKeysForLanguageAndDictionaryTables : MigrationBase { public AddForeignKeysForLanguageAndDictionaryTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddMigrationTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddMigrationTable.cs index 079dbe1465..856a1d8ff6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddMigrationTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddMigrationTable.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 11, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 11, Constants.System.UmbracoMigrationName)] public class AddMigrationTable : MigrationBase { public AddMigrationTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddPublicAccessTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddPublicAccessTables.cs index 16c0a923ae..b4ec1c20a6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddPublicAccessTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddPublicAccessTables.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 6, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 6, Constants.System.UmbracoMigrationName)] public class AddPublicAccessTables : MigrationBase { public AddPublicAccessTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs index c9db282d85..4865a77ab8 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 0, Constants.System.UmbracoMigrationName)] public class AddRelationTypeForDocumentOnDelete : MigrationBase { public AddRelationTypeForDocumentOnDelete(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs index 118dc1fc06..00ab602343 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 17, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 17, Constants.System.UmbracoMigrationName)] public class AddServerRegistrationColumnsAndLock : MigrationBase { public AddServerRegistrationColumnsAndLock(ISqlSyntaxProvider sqlSyntax, ILogger logger) @@ -23,7 +23,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe Create.Column("isMaster").OnTable("umbracoServer").AsBoolean().NotNullable().WithDefaultValue(0); } - EnsureLockObject(Constants.System.ServersLock, "0AF5E610-A310-4B6F-925F-E928D5416AF7", "LOCK: Servers"); + EnsureLockObject(Constants.Locks.Servers, "0AF5E610-A310-4B6F-925F-E928D5416AF7", "LOCK: Servers"); } public override void Down() diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs index 7589c7000d..070ef5d512 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 13, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 13, Constants.System.UmbracoMigrationName)] public class AddUniqueIdPropertyTypeColumn : MigrationBase { public AddUniqueIdPropertyTypeColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserColumns.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserColumns.cs index 46fde95005..01a3c61791 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserColumns.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserColumns.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 10, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 10, Constants.System.UmbracoMigrationName)] public class AddUserColumns : MigrationBase { public AddUserColumns(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CleanUpCorruptedPublishedFlags.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CleanUpCorruptedPublishedFlags.cs index d62ad7645d..f8dfc0e8bb 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CleanUpCorruptedPublishedFlags.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CleanUpCorruptedPublishedFlags.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe /// Older corrupted dbs might have multiple published flags for a content item, this shouldn't be possible /// so we need to clear the content flag on the older version /// - [Migration("7.3.0", 18, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 18, Constants.System.UmbracoMigrationName)] public class CleanUpCorruptedPublishedFlags : MigrationBase { public CleanUpCorruptedPublishedFlags(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs index afa5a1e675..d51aceb679 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 1, Constants.System.UmbracoMigrationName)] public class CreateCacheInstructionTable : MigrationBase { public CreateCacheInstructionTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs index 01db36abd9..59236106ab 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe /// /// Remove the master column after we've migrated all of the values into the 'ParentId' and Path column of Umbraco node /// - [Migration("7.3.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 1, Constants.System.UmbracoMigrationName)] public class MigrateAndRemoveTemplateMasterColumn : MigrationBase { diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateStylesheetDataToFile.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateStylesheetDataToFile.cs index 35e4befc55..40d2a6f183 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateStylesheetDataToFile.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateStylesheetDataToFile.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe /// These files will then be copied over once the entire migration is complete so that if any migration fails and the db changes are /// rolled back, the original files won't be affected. /// - [Migration("7.3.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 2, Constants.System.UmbracoMigrationName)] public class MigrateStylesheetDataToFile : MigrationBase { public MigrateStylesheetDataToFile(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs index d60385926b..16d1c6fbf5 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 7, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 7, Constants.System.UmbracoMigrationName)] public class MovePublicAccessXmlDataToDb : MigrationBase { public MovePublicAccessXmlDataToDb(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveHelpTextColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveHelpTextColumn.cs index d4911bd65a..5370ec0de3 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveHelpTextColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveHelpTextColumn.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 8, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 8, Constants.System.UmbracoMigrationName)] public class RemoveHelpTextColumn : MigrationBase { public RemoveHelpTextColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveLanguageLocaleColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveLanguageLocaleColumn.cs index a793cc6bbc..bf57364615 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveLanguageLocaleColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveLanguageLocaleColumn.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 4, Constants.System.UmbracoMigrationName)] public class RemoveLanguageLocaleColumn : MigrationBase { public RemoveLanguageLocaleColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveStylesheetDataAndTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveStylesheetDataAndTables.cs index da6d1b957d..e6b195cbf4 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveStylesheetDataAndTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveStylesheetDataAndTables.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 3, Constants.System.UmbracoMigrationName)] public class RemoveStylesheetDataAndTables : MigrationBase { public RemoveStylesheetDataAndTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveUmbracoLoginsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveUmbracoLoginsTable.cs index b842ec041a..5fef478cc6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveUmbracoLoginsTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/RemoveUmbracoLoginsTable.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 15, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 15, Constants.System.UmbracoMigrationName)] public class RemoveUmbracoLoginsTable : MigrationBase { public RemoveUmbracoLoginsTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/UpdateUniqueIdToHaveCorrectIndexType.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/UpdateUniqueIdToHaveCorrectIndexType.cs index ee6849c1e4..79d7b3111c 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/UpdateUniqueIdToHaveCorrectIndexType.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/UpdateUniqueIdToHaveCorrectIndexType.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero { - [Migration("7.3.0", 5, GlobalSettings.UmbracoMigrationName)] + [Migration("7.3.0", 5, Constants.System.UmbracoMigrationName)] public class UpdateUniqueIdToHaveCorrectIndexType : MigrationBase { public UpdateUniqueIdToHaveCorrectIndexType(ISqlSyntaxProvider sqlSyntax, ILogger logger) @@ -29,7 +29,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe }).ToArray(); //must be non-nullable - Alter.Column("uniqueID").OnTable("umbracoNode").AsGuid().NotNullable(); + Alter.Table("umbracoNode").AlterColumn("uniqueID").AsGuid().NotNullable(); //make sure it already exists if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoNodeUniqueID"))) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddIndexToUmbracoNodeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddIndexToUmbracoNodeTable.cs index 87c39b5f7e..7d5109a1db 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddIndexToUmbracoNodeTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddIndexToUmbracoNodeTable.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwoZero { - [Migration("7.2.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("7.2.0", 3, Constants.System.UmbracoMigrationName)] public class AddIndexToUmbracoNodeTable : MigrationBase { private readonly bool _skipIndexCheck; diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddMissingForeignKeyForContentType.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddMissingForeignKeyForContentType.cs index 31cb6132cc..47b1f334d3 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddMissingForeignKeyForContentType.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddMissingForeignKeyForContentType.cs @@ -8,7 +8,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwoZero { - [Migration("7.2.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.2.0", 1, Constants.System.UmbracoMigrationName)] public class AddMissingForeignKeyForContentType : MigrationBase { public AddMissingForeignKeyForContentType(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AlterDataTypePreValueTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AlterDataTypePreValueTable.cs index e0d8e1b5dd..ba3938dd18 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AlterDataTypePreValueTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AlterDataTypePreValueTable.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwoZero { - [Migration("7.2.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.2.0", 0, Constants.System.UmbracoMigrationName)] public class AlterDataTypePreValueTable : MigrationBase { public AlterDataTypePreValueTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/RemoveCmsDocumentAliasColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/RemoveCmsDocumentAliasColumn.cs index 2a8440af64..e67a8e7756 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/RemoveCmsDocumentAliasColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/RemoveCmsDocumentAliasColumn.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwoZero { - [Migration("7.2.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.2.0", 2, Constants.System.UmbracoMigrationName)] public class RemoveCmsDocumentAliasColumn : MigrationBase { public RemoveCmsDocumentAliasColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/DeleteAppTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/DeleteAppTables.cs index a5b9bc1415..b4d394bec2 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/DeleteAppTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/DeleteAppTables.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 10, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 10, Constants.System.UmbracoMigrationName)] public class DeleteAppTables : MigrationBase { public DeleteAppTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/EnsureAppsTreesUpdated.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/EnsureAppsTreesUpdated.cs index 219936e0e2..49cedc6dc7 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/EnsureAppsTreesUpdated.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/EnsureAppsTreesUpdated.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 9, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 9, Constants.System.UmbracoMigrationName)] public class EnsureAppsTreesUpdated : MigrationBase { public EnsureAppsTreesUpdated(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/MoveMasterContentTypeData.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/MoveMasterContentTypeData.cs index 3548d19c33..6e77050fd4 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/MoveMasterContentTypeData.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/MoveMasterContentTypeData.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 5, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 5, Constants.System.UmbracoMigrationName)] public class MoveMasterContentTypeData : MigrationBase { public MoveMasterContentTypeData(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/NewCmsContentType2ContentTypeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/NewCmsContentType2ContentTypeTable.cs index 622cefc44c..ee427bb517 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/NewCmsContentType2ContentTypeTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/NewCmsContentType2ContentTypeTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 4, Constants.System.UmbracoMigrationName)] public class NewCmsContentType2ContentTypeTable : MigrationBase { public NewCmsContentType2ContentTypeTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RemoveMasterContentTypeColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RemoveMasterContentTypeColumn.cs index 7042e3c21f..e8964ced28 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RemoveMasterContentTypeColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RemoveMasterContentTypeColumn.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 6, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 6, Constants.System.UmbracoMigrationName)] public class RemoveMasterContentTypeColumn : MigrationBase { public RemoveMasterContentTypeColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameCmsTabTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameCmsTabTable.cs index 9c97cd3444..c0738383d2 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameCmsTabTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameCmsTabTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 0, Constants.System.UmbracoMigrationName)] public class RenameCmsTabTable : MigrationBase { public RenameCmsTabTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameTabIdColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameTabIdColumn.cs index 14d3049d7e..fff439c03d 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameTabIdColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/RenameTabIdColumn.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 7, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 7, Constants.System.UmbracoMigrationName)] public class RenameTabIdColumn : MigrationBase { public RenameTabIdColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeAllowedContentTypeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeAllowedContentTypeTable.cs index ede56d08e9..11168a69cb 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeAllowedContentTypeTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeAllowedContentTypeTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 3, Constants.System.UmbracoMigrationName)] public class UpdateCmsContentTypeAllowedContentTypeTable : MigrationBase { public UpdateCmsContentTypeAllowedContentTypeTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeTable.cs index 0443c7f0bf..eb57f87c95 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentTypeTable.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 2, Constants.System.UmbracoMigrationName)] public class UpdateCmsContentTypeTable : MigrationBase { public UpdateCmsContentTypeTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentVersionTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentVersionTable.cs index f78048fcb1..cee55fee41 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentVersionTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsContentVersionTable.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 8, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 8, Constants.System.UmbracoMigrationName)] public class UpdateCmsContentVersionTable : MigrationBase { public UpdateCmsContentVersionTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsPropertyTypeGroupTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsPropertyTypeGroupTable.cs index 4b13c8b51d..7e0244bc57 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsPropertyTypeGroupTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSix/UpdateCmsPropertyTypeGroupTable.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSix { - [Migration("6.0.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", 1, Constants.System.UmbracoMigrationName)] public class UpdateCmsPropertyTypeGroupTable : MigrationBase { public UpdateCmsPropertyTypeGroupTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixOneZero/CreateServerRegistryTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixOneZero/CreateServerRegistryTable.cs index 9037422960..bcd7ac5d6f 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixOneZero/CreateServerRegistryTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixOneZero/CreateServerRegistryTable.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixOneZero { - [Migration("6.1.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("6.1.0", 0, Constants.System.UmbracoMigrationName)] public class CreateServerRegistryTable : MigrationBase { public CreateServerRegistryTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AddChangeDocumentTypePermission.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AddChangeDocumentTypePermission.cs index bad80e7c3a..42a652b166 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AddChangeDocumentTypePermission.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AddChangeDocumentTypePermission.cs @@ -6,8 +6,8 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero { - [Migration("7.1.0", 3, GlobalSettings.UmbracoMigrationName)] - [Migration("6.2.0", 3, GlobalSettings.UmbracoMigrationName)] + [Migration("7.1.0", 3, Constants.System.UmbracoMigrationName)] + [Migration("6.2.0", 3, Constants.System.UmbracoMigrationName)] public class AddChangeDocumentTypePermission : MigrationBase { public AddChangeDocumentTypePermission(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs index 0938d9be91..64f2970bdf 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs @@ -8,8 +8,8 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero { - [Migration("7.1.0", 1, GlobalSettings.UmbracoMigrationName)] - [Migration("6.2.0", 1, GlobalSettings.UmbracoMigrationName)] + [Migration("7.1.0", 1, Constants.System.UmbracoMigrationName)] + [Migration("6.2.0", 1, Constants.System.UmbracoMigrationName)] public class AdditionalIndexesAndKeys : MigrationBase { public AdditionalIndexesAndKeys(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys.cs index d8ecf855eb..5f2d6292c1 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys.cs @@ -8,8 +8,8 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero { //see: http://issues.umbraco.org/issue/U4-4430 - [Migration("7.1.0", 0, GlobalSettings.UmbracoMigrationName)] - [Migration("6.2.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("7.1.0", 0, Constants.System.UmbracoMigrationName)] + [Migration("6.2.0", 0, Constants.System.UmbracoMigrationName)] public class AssignMissingPrimaryForMySqlKeys : MigrationBase { public AssignMissingPrimaryForMySqlKeys(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys2.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys2.cs index b41bdea124..da7a5ce609 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys2.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AssignMissingPrimaryForMySqlKeys2.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero //We have to target this specifically to ensure this DOES NOT execute if upgrading from a version previous to 6.0, // this is because when the 6.0.0 migrations are executed, this primary key get's created so if this migration is also executed // we will get exceptions because it is trying to create the PK two times. - [Migration("6.0.0", "6.2.0", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.0", "6.2.0", 0, Constants.System.UmbracoMigrationName)] public class AssignMissingPrimaryForMySqlKeys2 : MigrationBase { public AssignMissingPrimaryForMySqlKeys2(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/ChangePasswordColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/ChangePasswordColumn.cs index cad1c0e127..327291f34b 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/ChangePasswordColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/ChangePasswordColumn.cs @@ -5,8 +5,8 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero { - [Migration("7.1.0", 2, GlobalSettings.UmbracoMigrationName)] - [Migration("6.2.0", 2, GlobalSettings.UmbracoMigrationName)] + [Migration("7.1.0", 2, Constants.System.UmbracoMigrationName)] + [Migration("6.2.0", 2, Constants.System.UmbracoMigrationName)] public class ChangePasswordColumn : MigrationBase { public ChangePasswordColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/UpdateToNewMemberPropertyAliases.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/UpdateToNewMemberPropertyAliases.cs index 454d0e18ed..4038bdd1e9 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/UpdateToNewMemberPropertyAliases.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/UpdateToNewMemberPropertyAliases.cs @@ -7,8 +7,8 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero { - [Migration("7.1.0", 4, GlobalSettings.UmbracoMigrationName)] - [Migration("6.2.0", 4, GlobalSettings.UmbracoMigrationName)] + [Migration("7.1.0", 4, Constants.System.UmbracoMigrationName)] + [Migration("6.2.0", 4, Constants.System.UmbracoMigrationName)] public class UpdateToNewMemberPropertyAliases : MigrationBase { public UpdateToNewMemberPropertyAliases(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs index 32c81700a8..15817d313d 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixZeroOne { - [Migration("6.0.2", 0, GlobalSettings.UmbracoMigrationName)] + [Migration("6.0.2", 0, Constants.System.UmbracoMigrationName)] public class UpdatePropertyTypesAndGroups : MigrationBase { public UpdatePropertyTypesAndGroups(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) diff --git a/src/Umbraco.Core/Persistence/PetaPoco.cs b/src/Umbraco.Core/Persistence/PetaPoco.cs index 88ff456ff6..79b3ce3871 100644 --- a/src/Umbraco.Core/Persistence/PetaPoco.cs +++ b/src/Umbraco.Core/Persistence/PetaPoco.cs @@ -167,12 +167,12 @@ namespace Umbraco.Core.Persistence var providerName = Constants.DatabaseProviders.SqlServer; if (ConfigurationManager.ConnectionStrings[connectionStringName] != null) { - if (!string.IsNullOrEmpty(ConfigurationManager.ConnectionStrings[connectionStringName].ProviderName)) + if (string.IsNullOrEmpty(ConfigurationManager.ConnectionStrings[connectionStringName].ProviderName) == false) providerName = ConfigurationManager.ConnectionStrings[connectionStringName].ProviderName; } else { - throw new InvalidOperationException("Can't find a connection string with the name '" + connectionStringName + "'"); + throw new NullReferenceException("Can't find a connection string with the name '" + connectionStringName + "'"); } // Store factory and connection string @@ -181,7 +181,7 @@ namespace Umbraco.Core.Persistence CommonConstruct(); } - internal enum DBType + public enum DBType { SqlServer, SqlServerCE, @@ -190,7 +190,9 @@ namespace Umbraco.Core.Persistence Oracle, SQLite } - DBType _dbType = DBType.SqlServer; + private DBType _dbType = DBType.SqlServer; + + public DBType DatabaseType { get { return _dbType; } } // Common initialization private void CommonConstruct() @@ -224,13 +226,18 @@ namespace Umbraco.Core.Persistence // Automatically close one open shared connection public void Dispose() { - // Automatically close one open connection reference - // (Works with KeepConnectionAlive and manually opening a shared connection) - CloseSharedConnection(); + Dispose(true); } - // Set to true to keep the first opened connection alive until this object is disposed - public bool KeepConnectionAlive { get; set; } + protected virtual void Dispose(bool disposing) + { + // Automatically close one open connection reference + // (Works with KeepConnectionAlive and manually opening a shared connection) + CloseSharedConnection(); + } + + // Set to true to keep the first opened connection alive until this object is disposed + public bool KeepConnectionAlive { get; set; } // Open a connection (can be nested) public void OpenSharedConnection() @@ -325,8 +332,16 @@ namespace Umbraco.Core.Persistence if (_transactionDepth == 1) { OpenSharedConnection(); - _transaction = _sharedConnection.BeginTransaction(isolationLevel); - _transactionCancelled = false; + try + { + _transaction = _sharedConnection.BeginTransaction(isolationLevel); + } + + catch (Exception e) + { + throw; + } + _transactionCancelled = false; OnBeginTransaction(); } else if (isolationLevel > _transaction.IsolationLevel) @@ -688,7 +703,7 @@ namespace Umbraco.Core.Persistence static Regex rxColumns = new Regex(@"\A\s*SELECT\s+((?:\((?>\((?)|\)(?<-depth>)|.?)*(?(depth)(?!))\)|.)*?)(?\((?)|\)(?<-depth>)|.?)*(?(depth)(?!))\)|[\w\(\)\.])+(?:\s+(?:ASC|DESC))?(?:\s*,\s*(?:\((?>\((?)|\)(?<-depth>)|.?)*(?(depth)(?!))\)|[\w\(\)\.])+(?:\s+(?:ASC|DESC))?)*", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled); - static Regex rxDistinct = new Regex(@"\ADISTINCT\s", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled); + static Regex rxDistinct = new Regex(@"\ADISTINCT\s", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled); public static bool SplitSqlForPaging(string sql, out string sqlCount, out string sqlSelectRemoved, out string sqlOrderBy) { sqlSelectRemoved = null; @@ -708,10 +723,9 @@ namespace Umbraco.Core.Persistence sqlCount = sql.Substring(0, g.Index) + "COUNT(" + m.Groups[1].ToString().Trim() + ") " + sql.Substring(g.Index + g.Length); else sqlCount = sql.Substring(0, g.Index) + "COUNT(*) " + sql.Substring(g.Index + g.Length); - - - // Look for an "ORDER BY " clause - m = rxOrderBy.Match(sqlCount); + + // Look for an "ORDER BY " clause + m = rxOrderBy.Match(sqlCount); if (!m.Success) { sqlOrderBy = null; @@ -722,8 +736,7 @@ namespace Umbraco.Core.Persistence sqlOrderBy = g.ToString(); sqlCount = sqlCount.Substring(0, g.Index) + sqlCount.Substring(g.Index + g.Length); } - - return true; + return true; } /// @@ -805,7 +818,7 @@ namespace Umbraco.Core.Persistence result.CurrentPage = page; result.ItemsPerPage = itemsPerPage; result.TotalItems = ExecuteScalar(sqlCount, args); - result.TotalPages = result.TotalItems / itemsPerPage; + result.TotalPages = result.TotalItems / itemsPerPage; if ((result.TotalItems % itemsPerPage) != 0) result.TotalPages++; diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index 22e66935bf..8626714a43 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -105,8 +105,8 @@ namespace Umbraco.Core.Persistence // try to update var rowCount = updateCommand.IsNullOrWhiteSpace() - ? db.Update(poco) - : db.Update(updateCommand, updateArgs); + ? db.Update(poco) + : db.Update(updateCommand, updateArgs); if (rowCount > 0) return RecordPersistenceType.Update; @@ -162,7 +162,7 @@ namespace Umbraco.Core.Persistence [Obsolete("Use the DatabaseSchemaHelper instead")] public static void CreateTable(this Database db) - where T : new() + where T : new() { var creator = new DatabaseSchemaHelper(db, LoggerResolver.Current.Logger, SqlSyntaxContext.SqlSyntaxProvider); creator.CreateTable(); @@ -170,7 +170,7 @@ namespace Umbraco.Core.Persistence [Obsolete("Use the DatabaseSchemaHelper instead")] public static void CreateTable(this Database db, bool overwrite) - where T : new() + where T : new() { var creator = new DatabaseSchemaHelper(db, LoggerResolver.Current.Logger, SqlSyntaxContext.SqlSyntaxProvider); creator.CreateTable(overwrite); @@ -183,16 +183,31 @@ namespace Umbraco.Core.Persistence /// /// /// + [Obsolete("Use the method that specifies an SqlSyntaxContext instance instead")] public static void BulkInsertRecords(this Database db, IEnumerable collection) { - //don't do anything if there are no records. - if (collection.Any() == false) - return; + db.BulkInsertRecords(collection, null, SqlSyntaxContext.SqlSyntaxProvider, true, false); + } - using (var tr = db.GetTransaction()) - { - db.BulkInsertRecords(collection, tr, SqlSyntaxContext.SqlSyntaxProvider, true, true); // use native, commit - } + + /// + /// Performs the bulk insertion + /// + /// + /// + /// + /// + /// + /// If this is false this will try to just generate bulk insert statements instead of using the current SQL platform's bulk + /// insert logic. For SQLCE, bulk insert statements do not work so if this is false it will insert one at a time. + /// + /// The number of items inserted + public static int BulkInsertRecords(this Database db, + IEnumerable collection, + ISqlSyntaxProvider syntaxProvider, + bool useNativeSqlPlatformBulkInsert = true) + { + return BulkInsertRecords(db, collection, null, syntaxProvider, useNativeSqlPlatformBulkInsert, false); } /// @@ -217,6 +232,26 @@ namespace Umbraco.Core.Persistence bool useNativeSqlPlatformBulkInsert = true, bool commitTrans = false) { + db.OpenSharedConnection(); + try + { + return BulkInsertRecordsTry(db, collection, tr, syntaxProvider, useNativeSqlPlatformBulkInsert, commitTrans); + } + finally + { + db.CloseSharedConnection(); + } + } + + public static int BulkInsertRecordsTry(this Database db, + IEnumerable collection, + Transaction tr, + ISqlSyntaxProvider syntaxProvider, + bool useNativeSqlPlatformBulkInsert = true, + bool commitTrans = false) + { + if (commitTrans && tr == null) + throw new ArgumentNullException("tr", "The transaction cannot be null if commitTrans is true."); //don't do anything if there are no records. if (collection.Any() == false) @@ -224,15 +259,15 @@ namespace Umbraco.Core.Persistence return 0; } - var pd = Database.PocoData.ForType(typeof(T)); - if (pd == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + var pd = Database.PocoData.ForType(typeof (T)); + if (pd == null) throw new InvalidOperationException("Could not find PocoData for " + typeof (T)); try { int processed = 0; var usedNativeSqlPlatformInserts = useNativeSqlPlatformBulkInsert - && NativeSqlPlatformBulkInsertRecords(db, syntaxProvider, pd, collection, out processed); + && NativeSqlPlatformBulkInsertRecords(db, syntaxProvider, pd, collection, out processed); if (usedNativeSqlPlatformInserts == false) { @@ -266,17 +301,13 @@ namespace Umbraco.Core.Persistence } if (commitTrans) - { tr.Complete(); - } return processed; } catch { if (commitTrans) - { tr.Dispose(); - } throw; } @@ -318,13 +349,16 @@ namespace Umbraco.Core.Persistence IEnumerable collection, out string[] sql) { + if (db == null) throw new ArgumentNullException("db"); + if (db.Connection == null) throw new ArgumentException("db.Connection is null."); + var tableName = db.EscapeTableName(pd.TableInfo.TableName); //get all columns to include and format for sql var cols = string.Join(", ", pd.Columns - .Where(c => IncludeColumn(pd, c)) - .Select(c => tableName + "." + db.EscapeSqlIdentifier(c.Key)).ToArray()); + .Where(c => IncludeColumn(pd, c)) + .Select(c => tableName + "." + db.EscapeSqlIdentifier(c.Key)).ToArray()); var itemArray = collection.ToArray(); @@ -338,9 +372,9 @@ namespace Umbraco.Core.Persistence // 4168 / 262 = 15.908... = there will be 16 trans in total //all items will be included if we have disabled db parameters - var itemsPerTrans = Math.Floor(2000.00 / paramsPerItem); + var itemsPerTrans = Math.Floor(2000.00/paramsPerItem); //there will only be one transaction if we have disabled db parameters - var numTrans = Math.Ceiling(itemArray.Length / itemsPerTrans); + var numTrans = Math.Ceiling(itemArray.Length/itemsPerTrans); var sqlQueries = new List(); var commands = new List(); @@ -348,8 +382,8 @@ namespace Umbraco.Core.Persistence for (var tIndex = 0; tIndex < numTrans; tIndex++) { var itemsForTrans = itemArray - .Skip(tIndex * (int)itemsPerTrans) - .Take((int)itemsPerTrans); + .Skip(tIndex*(int) itemsPerTrans) + .Take((int) itemsPerTrans); var cmd = db.CreateCommand(db.Connection, string.Empty); var pocoValues = new List(); @@ -399,7 +433,6 @@ namespace Umbraco.Core.Persistence /// The number of records inserted private static bool NativeSqlPlatformBulkInsertRecords(Database db, ISqlSyntaxProvider syntaxProvider, Database.PocoData pd, IEnumerable collection, out int processed) { - var dbConnection = db.Connection; //unwrap the profiled connection if there is one @@ -428,7 +461,6 @@ namespace Umbraco.Core.Persistence //could not use the SQL server's specific bulk insert operations processed = 0; return false; - } /// diff --git a/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs index d2fa98ef12..090410782a 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs @@ -1,8 +1,10 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Text; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; @@ -27,6 +29,7 @@ namespace Umbraco.Core.Persistence return sql.From(sqlSyntax.GetQuotedTableName(tableName)); } + [Obsolete("Use the overload specifying ISqlSyntaxProvider instead")] public static Sql Where(this Sql sql, Expression> predicate) { var expresionist = new PocoToSqlExpressionVisitor(); @@ -34,6 +37,13 @@ namespace Umbraco.Core.Persistence return sql.Where(whereExpression, expresionist.GetSqlParameters()); } + public static Sql Where(this Sql sql, Expression> predicate, ISqlSyntaxProvider sqlSyntax) + { + var expresionist = new PocoToSqlExpressionVisitor(sqlSyntax); + var whereExpression = expresionist.Visit(predicate); + return sql.Where(whereExpression, expresionist.GetSqlParameters()); + } + private static string GetFieldName(Expression> fieldSelector, ISqlSyntaxProvider sqlSyntax) { var field = ExpressionHelper.FindProperty(fieldSelector) as PropertyInfo; @@ -57,6 +67,26 @@ namespace Umbraco.Core.Persistence return sql.Where(fieldName + " IN (@values)", new { values }); } + [Obsolete("Use the overload specifying ISqlSyntaxProvider instead")] + public static Sql WhereAnyIn(this Sql sql, Expression>[] fieldSelectors, IEnumerable values) + { + return sql.WhereAnyIn(fieldSelectors, values, SqlSyntaxContext.SqlSyntaxProvider); + } + + public static Sql WhereAnyIn(this Sql sql, Expression>[] fieldSelectors, IEnumerable values, ISqlSyntaxProvider sqlSyntax) + { + var fieldNames = fieldSelectors.Select(x => GetFieldName(x, sqlSyntax)).ToArray(); + var sb = new StringBuilder(); + sb.Append("("); + for (var i = 0; i < fieldNames.Length; i++) + { + if (i > 0) sb.Append(" OR "); + sb.Append(fieldNames[i] + " IN (@values)"); + } + sb.Append(")"); + return sql.Where(sb.ToString(), new { values }); + } + [Obsolete("Use the overload specifying ISqlSyntaxProvider instead")] public static Sql OrderBy(this Sql sql, Expression> columnMember) { diff --git a/src/Umbraco.Core/Persistence/Querying/CachedExpression.cs b/src/Umbraco.Core/Persistence/Querying/CachedExpression.cs new file mode 100644 index 0000000000..5c502ec170 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Querying/CachedExpression.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq.Expressions; + +namespace Umbraco.Core.Persistence.Querying +{ + /// + /// Represents an expression which caches the visitor's result. + /// + internal class CachedExpression : Expression + { + private string _visitResult; + + /// + /// Gets or sets the inner Expression. + /// + public Expression InnerExpression { get; private set; } + + /// + /// Gets or sets the compiled SQL statement output. + /// + public string VisitResult + { + get { return _visitResult; } + set + { + if (Visited) + throw new InvalidOperationException("Cached expression has already been visited."); + _visitResult = value; + Visited = true; + } + } + + /// + /// Gets or sets a value indicating whether the cache Expression has been compiled already. + /// + public bool Visited { get; private set; } + + /// + /// Replaces the inner expression. + /// + /// expression. + /// The new expression is assumed to have different parameter but produce the same SQL statement. + public void Wrap(Expression expression) + { + InnerExpression = expression; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index 678ceb1d8e..fe73638b4b 100644 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -9,49 +9,6 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Querying { - /// - /// Represents an expression which caches the visitor's result. - /// - internal class CachedExpression : Expression - { - private string _visitResult; - - /// - /// Gets or sets the inner Expression. - /// - public Expression InnerExpression { get; private set; } - - /// - /// Gets or sets the compiled SQL statement output. - /// - public string VisitResult - { - get { return _visitResult; } - set - { - if (Visited) - throw new InvalidOperationException("Cached expression has already been visited."); - _visitResult = value; - Visited = true; - } - } - - /// - /// Gets or sets a value indicating whether the cache Expression has been compiled already. - /// - public bool Visited { get; private set; } - - /// - /// Replaces the inner expression. - /// - /// expression. - /// The new expression is assumed to have different parameter but produce the same SQL statement. - public void Wrap(Expression expression) - { - InnerExpression = expression; - } - } - /// /// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression. /// @@ -581,6 +538,18 @@ namespace Umbraco.Core.Persistence.Querying case "InvariantContains": case "InvariantEquals": + //special case, if it is 'Contains' and the argumet that Contains is being called on is + //Enumerable and the methodArgs is the actual member access, then it's an SQL IN claus + if (m.Object == null + && m.Arguments[0].Type != typeof(string) + && m.Arguments.Count == 2 + && methodArgs.Length == 1 + && methodArgs[0].NodeType == ExpressionType.MemberAccess + && TypeHelper.IsTypeAssignableFrom(m.Arguments[0].Type)) + { + goto case "SqlIn"; + } + string compareValue; if (methodArgs[0].NodeType != ExpressionType.Constant) @@ -597,13 +566,6 @@ namespace Umbraco.Core.Persistence.Querying compareValue = methodArgs[0].ToString(); } - //special case, if it is 'Contains' and the member that Contains is being called on is not a string, then - // we should be doing an 'In' clause - but we currently do not support this - if (methodArgs[0].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) - { - throw new NotSupportedException("An array Contains method is not supported"); - } - //default column type var colType = TextColumnType.NVarchar; @@ -705,29 +667,33 @@ namespace Umbraco.Core.Persistence.Querying // } // return string.Format("{0}{1}", r, s); - //case "In": + case "SqlIn": - // var member = Expression.Convert(m.Arguments[0], typeof(object)); - // var lambda = Expression.Lambda>(member); - // var getter = lambda.Compile(); + if (m.Object == null && methodArgs.Length == 1 && methodArgs[0].NodeType == ExpressionType.MemberAccess) + { + var memberAccess = VisitMemberAccess((MemberExpression) methodArgs[0]); + + var member = Expression.Convert(m.Arguments[0], typeof(object)); + var lambda = Expression.Lambda>(member); + var getter = lambda.Compile(); - // var inArgs = (object[])getter(); + var inArgs = (IEnumerable)getter(); - // var sIn = new StringBuilder(); - // foreach (var e in inArgs) - // { - // SqlParameters.Add(e); + var sIn = new StringBuilder(); + foreach (var e in inArgs) + { + SqlParameters.Add(e); - // sIn.AppendFormat("{0}{1}", - // sIn.Length > 0 ? "," : "", - // string.Format("@{0}", SqlParameters.Count - 1)); + sIn.AppendFormat("{0}{1}", + sIn.Length > 0 ? "," : "", + string.Format("@{0}", SqlParameters.Count - 1)); + } - // //sIn.AppendFormat("{0}{1}", - // // sIn.Length > 0 ? "," : "", - // // GetQuotedValue(e, e.GetType())); - // } + return string.Format("{0} IN ({1})", memberAccess, sIn); + } + + throw new NotSupportedException("SqlIn must contain the member being accessed"); - // return string.Format("{0} {1} ({2})", r, m.Method.Name, sIn.ToString()); //case "Desc": // return string.Format("{0} DESC", r); //case "Alias": diff --git a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs index b265a5b587..da1f93a032 100644 --- a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs +++ b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs @@ -1,5 +1,6 @@ using System; using System.Linq.Expressions; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.SqlSyntax; @@ -12,16 +13,19 @@ namespace Umbraco.Core.Persistence.Querying /// This object is stateful and cannot be re-used to parse an expression. internal class ModelToSqlExpressionVisitor : ExpressionVisitorBase { + private readonly MappingResolver _mappingResolver; private readonly BaseMapper _mapper; - public ModelToSqlExpressionVisitor(ISqlSyntaxProvider sqlSyntax, BaseMapper mapper) + public ModelToSqlExpressionVisitor(ISqlSyntaxProvider sqlSyntax, MappingResolver mappingResolver) : base(sqlSyntax) { - _mapper = mapper; + _mapper = mappingResolver.ResolveMapperByType(typeof(T)); + _mappingResolver = mappingResolver; } + [Obsolete("Use the overload the specifies a SqlSyntaxProvider")] public ModelToSqlExpressionVisitor() - : this(SqlSyntaxContext.SqlSyntaxProvider, MappingResolver.Current.ResolveMapperByType(typeof(T))) + : this(SqlSyntaxContext.SqlSyntaxProvider, MappingResolver.Current) { } protected override string VisitMemberAccess(MemberExpression m) @@ -35,7 +39,7 @@ namespace Umbraco.Core.Persistence.Querying { var field = _mapper.Map(m.Member.Name, true); if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException("The mapper returned an empty field for the member name: " + m.Member.Name); + throw new InvalidOperationException(string.Format("The mapper returned an empty field for the member name: {0} for type: {1}", m.Member.Name, m.Expression.Type)); return field; } //already compiled, return @@ -49,13 +53,42 @@ namespace Umbraco.Core.Persistence.Querying { var field = _mapper.Map(m.Member.Name, true); if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException("The mapper returned an empty field for the member name: " + m.Member.Name); + throw new InvalidOperationException(string.Format("The mapper returned an empty field for the member name: {0} for type: {1}", m.Member.Name, m.Expression.Type)); return field; } //already compiled, return return string.Empty; } + if (m.Expression != null + && m.Expression.Type != typeof(T) + && TypeHelper.IsTypeAssignableFrom(m.Expression.Type) + && EndsWithConstant(m) == false) + { + //if this is the case, it means we have a sub expression / nested property access, such as: x.ContentType.Alias == "Test"; + //and since the sub type (x.ContentType) is not the same as x, we need to resolve a mapper for x.ContentType to get it's mapped SQL column + + //don't execute if compiled + if (Visited == false) + { + var subMapper = _mappingResolver.ResolveMapperByType(m.Expression.Type); + if (subMapper == null) + throw new NullReferenceException("No mapper found for type " + m.Expression.Type); + var field = subMapper.Map(m.Member.Name, true); + if (field.IsNullOrWhiteSpace()) + throw new InvalidOperationException(string.Format("The mapper returned an empty field for the member name: {0} for type: {1}", m.Member.Name, m.Expression.Type)); + return field; + } + //already compiled, return + return string.Empty; + } + + //TODO: When m.Expression.NodeType == ExpressionType.Constant and it's an expression like: content => aliases.Contains(content.ContentType.Alias); + // then an SQL parameter will be added for aliases as an array, however in SqlIn on the subclass it will manually add these SqlParameters anyways, + // however the query will still execute because the SQL that is written will only contain the correct indexes of SQL parameters, this would be ignored, + // I'm just unsure right now due to time constraints how to make it correct. It won't matter right now and has been working already with this bug but I've + // only just discovered what it is actually doing. + var member = Expression.Convert(m, typeof(object)); var lambda = Expression.Lambda>(member); var getter = lambda.Compile(); @@ -70,5 +103,24 @@ namespace Umbraco.Core.Persistence.Querying return string.Empty; } + + /// + /// Determines if the MemberExpression ends in a Constant value + /// + /// + /// + private bool EndsWithConstant(MemberExpression m) + { + Expression expr = m; + + while (expr is MemberExpression) + { + var memberExpr = expr as MemberExpression; + expr = memberExpr.Expression; + } + + var constExpr = expr as ConstantExpression; + return constExpr != null; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs index 6b527296df..4569b95853 100644 --- a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs +++ b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs @@ -14,12 +14,19 @@ namespace Umbraco.Core.Persistence.Querying { private readonly Database.PocoData _pd; - public PocoToSqlExpressionVisitor() - : base(SqlSyntaxContext.SqlSyntaxProvider) + + public PocoToSqlExpressionVisitor(ISqlSyntaxProvider syntaxProvider) + : base(syntaxProvider) { _pd = new Database.PocoData(typeof(T)); } + [Obsolete("Use the overload the specifies a SqlSyntaxProvider")] + public PocoToSqlExpressionVisitor() + : this(SqlSyntaxContext.SqlSyntaxProvider) + { + } + protected override string VisitMemberAccess(MemberExpression m) { if (m.Expression != null && diff --git a/src/Umbraco.Core/Persistence/Querying/SqlStringExtensions.cs b/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs similarity index 83% rename from src/Umbraco.Core/Persistence/Querying/SqlStringExtensions.cs rename to src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs index cbecc0a591..593955734e 100644 --- a/src/Umbraco.Core/Persistence/Querying/SqlStringExtensions.cs +++ b/src/Umbraco.Core/Persistence/Querying/SqlExpressionExtensions.cs @@ -1,4 +1,5 @@ -using System; +using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; namespace Umbraco.Core.Persistence.Querying @@ -6,8 +7,13 @@ namespace Umbraco.Core.Persistence.Querying /// /// String extension methods used specifically to translate into SQL /// - internal static class SqlStringExtensions + internal static class SqlExpressionExtensions { + public static bool SqlIn(this IEnumerable collection, T item) + { + return collection.Contains(item); + } + public static bool SqlWildcard(this string str, string txt, TextColumnType columnType) { var wildcardmatch = new Regex("^" + Regex.Escape(txt). diff --git a/src/Umbraco.Core/Persistence/Relators/MacroPropertyRelator.cs b/src/Umbraco.Core/Persistence/Relators/MacroPropertyRelator.cs index 2f457edfd8..fb563fa25c 100644 --- a/src/Umbraco.Core/Persistence/Relators/MacroPropertyRelator.cs +++ b/src/Umbraco.Core/Persistence/Relators/MacroPropertyRelator.cs @@ -3,48 +3,6 @@ using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Relators { - //internal class TaskUserRelator - //{ - // internal TaskDto Current; - - // internal TaskDto Map(TaskDto a, UserDto p) - // { - // // Terminating call. Since we can return null from this function - // // we need to be ready for PetaPoco to callback later with null - // // parameters - // if (a == null) - // return Current; - - // // Is this the same TaskDto as the current one we're processing - // if (Current != null && Current.Id == a.Id) - // { - // // Yes, set the user - // Current.MacroPropertyDtos.Add(p); - - // // Return null to indicate we're not done with this Macro yet - // return null; - // } - - // // This is a different Macro to the current one, or this is the - // // first time through and we don't have one yet - - // // Save the current Macro - // var prev = Current; - - // // Setup the new current Macro - // Current = a; - // Current.MacroPropertyDtos = new List(); - // //this can be null since we are doing a left join - // if (p.Alias != null) - // { - // Current.MacroPropertyDtos.Add(p); - // } - - // // Return the now populated previous Macro (or null if first time through) - // return prev; - // } - //} - internal class MacroPropertyRelator { internal MacroDto Current; diff --git a/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs index 53ffda9364..d425755579 100644 --- a/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class AuditRepository : PetaPocoRepositoryBase, IAuditRepository { - public AuditRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public AuditRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs b/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs new file mode 100644 index 0000000000..05061c47af --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs @@ -0,0 +1,31 @@ +namespace Umbraco.Core.Persistence.Repositories +{ + internal enum BaseQueryType + { + /// + /// A query to return all information for a single item + /// + /// + /// In some cases this will be the same as + /// + FullSingle, + + /// + /// A query to return all information for multiple items + /// + /// + /// In some cases this will be the same as + /// + FullMultiple, + + /// + /// A query to return the ids for items + /// + Ids, + + /// + /// A query to return the count for items + /// + Count + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentPreviewRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentPreviewRepository.cs index b368488833..ee19161ae0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentPreviewRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentPreviewRepository.cs @@ -17,10 +17,9 @@ namespace Umbraco.Core.Persistence.Repositories internal class ContentPreviewRepository : PetaPocoRepositoryBase> where TContent : IContentBase { - public ContentPreviewRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public ContentPreviewRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) - { - } + { } #region Not implemented (don't need to for the purposes of this repo) protected override ContentPreviewEntity PerformGet(int id) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 14e6c0e9a6..a34a77d3b4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Xml; @@ -31,7 +32,7 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ContentPreviewRepository _contentPreviewRepository; private readonly ContentXmlRepository _contentXmlRepository; - public ContentRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) + public ContentRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) : base(work, cacheHelper, logger, syntaxProvider, contentSection) { if (contentTypeRepository == null) throw new ArgumentNullException("contentTypeRepository"); @@ -41,8 +42,8 @@ namespace Umbraco.Core.Persistence.Repositories _templateRepository = templateRepository; _tagRepository = tagRepository; _cacheHelper = cacheHelper; - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, syntaxProvider); - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, syntaxProvider); + _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.NoCache, logger, syntaxProvider); + _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.NoCache, logger, syntaxProvider); EnsureUniqueNaming = true; } @@ -53,9 +54,9 @@ namespace Umbraco.Core.Persistence.Repositories protected override IContent PerformGet(int id) { - var sql = GetBaseQuery(false) + var sql = GetBaseQuery(BaseQueryType.FullSingle) .Where(GetBaseWhereClause(), new { Id = id }) - .Where(x => x.Newest) + .Where(x => x.Newest, SqlSyntax) .OrderByDescending(x => x.VersionDate, SqlSyntax); var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); @@ -63,70 +64,112 @@ namespace Umbraco.Core.Persistence.Repositories if (dto == null) return null; - var content = CreateContentFromDto(dto, dto.ContentVersionDto.VersionId, sql); + var content = CreateContentFromDto(dto, sql); return content; } protected override IEnumerable PerformGetAll(params int[] ids) { - var sql = GetBaseQuery(false); - if (ids.Any()) + Func translate = s => { - sql.Where("umbracoNode.id in (@ids)", new { ids }); - } + if (ids.Any()) + { + s.Where("umbracoNode.id in (@ids)", new { ids }); + } + //we only want the newest ones with this method + s.Where(x => x.Newest, SqlSyntax); + return s; + }; + + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); + var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); - //we only want the newest ones with this method - sql.Where(x => x.Newest); - - return ProcessQuery(sql); + return ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); } protected override IEnumerable PerformGetByQuery(IQuery query) { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .Where(x => x.Newest) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); + var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); - return ProcessQuery(sql); + Func, Sql> translate = (translator) => + { + return translator.Translate() + .Where(x => x.Newest, SqlSyntax) + .OrderByDescending(x => x.VersionDate, SqlSyntax) + .OrderBy(x => x.SortOrder, SqlSyntax); + }; + + var translatorFull = new SqlTranslator(sqlBaseFull, query); + var translatorIds = new SqlTranslator(sqlBaseIds, query); + + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds))); } #endregion #region Overrides of PetaPocoRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) + /// + /// Returns the base query to return Content + /// + /// + /// + /// + /// Content queries will differ depending on what needs to be returned: + /// * FullSingle: When querying for a single document, this will include the Outer join to fetch the content item's published version info + /// * FullMultiple: When querying for multiple documents, this will exclude the Outer join to fetch the content item's published version info - this info would need to be fetched separately + /// * Ids: This would essentially be the same as FullMultiple however the columns specified will only return the Ids for the documents + /// * Count: A query to return the count for documents + /// + protected override Sql GetBaseQuery(BaseQueryType queryType) { - var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", + var sql = new Sql(); + sql.Select(queryType == BaseQueryType.Count ? "COUNT(*)" : (queryType == BaseQueryType.Ids ? "cmsDocument.nodeId" : "*")) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.VersionId, right => right.VersionId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId); + //TODO: IF we want to enable querying on content type information this will need to be joined + //.InnerJoin(SqlSyntax) + //.On(SqlSyntax, left => left.ContentTypeId, right => right.NodeId, SqlSyntax); + + if (queryType == BaseQueryType.FullSingle) + { + //The only reason we apply this left outer join is to be able to pull back the DocumentPublishedReadOnlyDto + //information with the entire data set, so basically this will get both the latest document and also it's published + //version if it has one. When performing a count or when retrieving Ids like in paging, this is unecessary + //and causes huge performance overhead for the SQL server, especially when sorting the result. + //We also don't include this outer join when querying for multiple entities since it is much faster to fetch this information + //in a separate query. For a single entity this is ok. + + var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", SqlSyntax.GetQuotedTableName("cmsDocument"), SqlSyntax.GetQuotedTableName("cmsDocument2"), SqlSyntax.GetQuotedColumnName("nodeId"), SqlSyntax.GetQuotedColumnName("published")); - var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.VersionId, right => right.VersionId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - // cannot do this because PetaPoco does not know how to alias the table //.LeftOuterJoin() //.On(left => left.NodeId, right => right.NodeId) // so have to rely on writing our own SQL - .Append(sqlx/*, new { @published = true }*/) + sql.Append(sqlx /*, new { @published = true }*/); + } + + sql.Where(x => x.NodeObjectType == NodeObjectTypeId, SqlSyntax); - .Where(x => x.NodeObjectType == NodeObjectTypeId); return sql; } + protected override Sql GetBaseQuery(bool isCount) + { + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); + } + protected override string GetBaseWhereClause() { return "umbracoNode.id = @Id"; @@ -173,20 +216,32 @@ namespace Umbraco.Core.Persistence.Repositories // not bring much safety - so this reverts to updating each record individually, // and it may be slower in the end, but should be more resilient. - var baseId = 0; var contentTypeIdsA = contentTypeIds == null ? new int[0] : contentTypeIds.ToArray(); + + Func translate = (bId, sql) => + { + if (contentTypeIdsA.Length > 0) + { + sql.WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + } + + sql + .Where(x => x.NodeId > bId && x.Trashed == false, SqlSyntax) + .Where(x => x.Published, SqlSyntax) + .OrderBy(x => x.NodeId, SqlSyntax); + + return sql; + }; + + var baseId = 0; + while (true) { // get the next group of nodes - var query = GetBaseQuery(false); - if (contentTypeIdsA.Length > 0) - query = query - .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); - query = query - .Where(x => x.NodeId > baseId && x.Trashed == false) - .Where(x => x.Published) - .OrderBy(x => x.NodeId, SqlSyntax); - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) + var sqlFull = translate(baseId, GetBaseQuery(BaseQueryType.FullMultiple)); + var sqlIds = translate(baseId, GetBaseQuery(BaseQueryType.Ids)); + + var xmlItems = ProcessQuery(SqlSyntax.SelectTop(sqlFull, groupSize), new PagingSqlQuery(SqlSyntax.SelectTop(sqlIds, groupSize))) .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) .ToList(); @@ -206,30 +261,70 @@ namespace Umbraco.Core.Persistence.Repositories Logger.Error("Could not rebuild XML for nodeId=" + xmlItem.NodeId, e); } } - baseId = xmlItems.Last().NodeId; + baseId = xmlItems[xmlItems.Count - 1].NodeId; } + + //now delete the items that shouldn't be there + var sqlAllIds = translate(0, GetBaseQuery(BaseQueryType.Ids)); + var allContentIds = Database.Fetch(sqlAllIds); + var docObjectType = Guid.Parse(Constants.ObjectTypes.Document); + var xmlIdsQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId); + + if (contentTypeIdsA.Length > 0) + { + xmlIdsQuery.InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.ContentTypeId) + .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + } + + xmlIdsQuery.Where(dto => dto.NodeObjectType == docObjectType, SqlSyntax); + + var allXmlIds = Database.Fetch(xmlIdsQuery); + + var toRemove = allXmlIds.Except(allContentIds).ToArray(); + if (toRemove.Length > 0) + { + foreach (var idGroup in toRemove.InGroupsOf(2000)) + { + Database.Execute("DELETE FROM cmsContentXml WHERE nodeId IN (@ids)", new { ids = idGroup }); + } + } + } public override IEnumerable GetAllVersions(int id) { - var sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new { Id = id }) - .OrderByDescending(x => x.VersionDate, SqlSyntax); - return ProcessQuery(sql, true); + Func translate = s => + { + return s.Where(GetBaseWhereClause(), new {Id = id}) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + }; + + var sqlFull = translate(GetBaseQuery(BaseQueryType.FullMultiple)); + var sqlIds = translate(GetBaseQuery(BaseQueryType.Ids)); + + return ProcessQuery(sqlFull, new PagingSqlQuery(sqlIds), true, includeAllVersions:true); } public override IContent GetByVersion(Guid versionId) { - var sql = GetBaseQuery(false); + var sql = GetBaseQuery(BaseQueryType.FullSingle); + //TODO: cmsContentVersion.VersionId has a Unique Index constraint applied, seems silly then to also add OrderByDescending since it would be impossible to return more than one. sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate); + sql.OrderByDescending(x => x.VersionDate, SqlSyntax); var dto = Database.Fetch(sql).FirstOrDefault(); if (dto == null) return null; - var content = CreateContentFromDto(dto, versionId, sql); + var content = CreateContentFromDto(dto, sql); return content; } @@ -238,10 +333,11 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = new Sql() .Select("*") - .From() - .InnerJoin().On(left => left.VersionId, right => right.VersionId) - .Where(x => x.VersionId == versionId) - .Where(x => x.Newest != true); + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.VersionId, right => right.VersionId) + .Where(x => x.VersionId == versionId, SqlSyntax) + .Where(x => x.Newest != true, SqlSyntax); var dto = Database.Fetch(sql).FirstOrDefault(); if (dto == null) return; @@ -259,7 +355,8 @@ namespace Umbraco.Core.Persistence.Repositories var sql = new Sql() .Select("*") .From() - .InnerJoin().On(left => left.VersionId, right => right.VersionId) + .InnerJoin() + .On(left => left.VersionId, right => right.VersionId) .Where(x => x.NodeId == id) .Where(x => x.VersionDate < versionDate) .Where(x => x.Newest != true); @@ -416,11 +513,13 @@ namespace Umbraco.Core.Persistence.Repositories dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto { VersionId = dto.VersionId, + VersionDate = dto.UpdateDate, Newest = true, NodeId = dto.NodeId, - Published = true + Published = true }; - ((Content)entity).PublishedVersionGuid = dto.VersionId; + ((Content) entity).PublishedVersionGuid = dto.VersionId; + ((Content) entity).PublishedDate = dto.UpdateDate; } entity.ResetDirtyProperties(); @@ -446,7 +545,8 @@ namespace Umbraco.Core.Persistence.Repositories } else { - entity.UpdateDate = DateTime.Now; + if (entity.IsPropertyDirty("UpdateDate") == false || entity.UpdateDate == default(DateTime)) + entity.UpdateDate = DateTime.Now; } //Ensure unique name on the same level @@ -499,6 +599,7 @@ namespace Umbraco.Core.Persistence.Repositories //if (((ICanBeDirty)entity).IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) if (entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, shouldCreateNewVersion)) { + //TODO: This perf can be improved, it could easily be UPDATE WHERE.... (one SQL call instead of many) var publishedDocs = Database.Fetch("WHERE nodeId = @Id AND published = @IsPublished", new { Id = entity.Id, IsPublished = true }); foreach (var doc in publishedDocs) { @@ -512,6 +613,7 @@ namespace Umbraco.Core.Persistence.Repositories } //Look up (newest) entries by id in cmsDocument table to set newest = false + //TODO: This perf can be improved, it could easily be UPDATE WHERE.... (one SQL call instead of many) var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); foreach (var documentDto in documentDtos) { @@ -588,22 +690,26 @@ namespace Umbraco.Core.Persistence.Repositories dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto { VersionId = dto.VersionId, + VersionDate = dto.UpdateDate, Newest = true, NodeId = dto.NodeId, Published = true }; - ((Content)entity).PublishedVersionGuid = dto.VersionId; + ((Content) entity).PublishedVersionGuid = dto.VersionId; + ((Content) entity).PublishedDate = dto.UpdateDate; } else if (publishedStateChanged) { dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto { - VersionId = default(Guid), + VersionId = default (Guid), + VersionDate = default (DateTime), Newest = false, NodeId = dto.NodeId, Published = false }; - ((Content)entity).PublishedVersionGuid = default(Guid); + ((Content) entity).PublishedVersionGuid = default(Guid); + ((Content) entity).PublishedDate = default (DateTime); } entity.ResetDirtyProperties(); @@ -616,21 +722,27 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetByPublishedVersion(IQuery query) { + Func, Sql> translate = t => + { + return t.Translate() + .Where(x => x.Published, SqlSyntax) + .OrderBy(x => x.Level, SqlSyntax) + .OrderBy(x => x.SortOrder, SqlSyntax); + }; + // we WANT to return contents in top-down order, ie parents should come before children // ideal would be pure xml "document order" which can be achieved with: // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder // but that's probably an overkill - sorting by level,sortOrder should be enough - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .Where(x => x.Published) - .OrderBy(x => x.Level, SqlSyntax) - .OrderBy(x => x.SortOrder, SqlSyntax); + var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); + var translatorFull = new SqlTranslator(sqlFull, query); + var sqlIds = GetBaseQuery(BaseQueryType.Ids); + var translatorIds = new SqlTranslator(sqlIds, query); - return ProcessQuery(sql, true); + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); } - + /// /// This builds the Xml document used for the XML cache /// @@ -662,10 +774,14 @@ namespace Umbraco.Core.Persistence.Repositories parent.Attributes.Append(pIdAtt); xmlDoc.AppendChild(parent); - const string sql = @"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsContentXml.xml, umbracoNode.level from umbracoNode + //Ensure that only nodes that have published versions are selected + var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsContentXml.{0}, umbracoNode.{1} from umbracoNode inner join cmsContentXml on cmsContentXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type where umbracoNode.id in (select cmsDocument.nodeId from cmsDocument where cmsDocument.published = 1) -order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; +order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", + SqlSyntax.GetQuotedColumnName("xml"), + SqlSyntax.GetQuotedColumnName("level"), + SqlSyntax.GetQuotedColumnName("level")); XmlElement last = null; @@ -716,13 +832,8 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; public void ClearPublished(IContent content) { - // race cond! - var documentDtos = Database.Fetch("WHERE nodeId=@id AND published=@published", new { id = content.Id, published = true }); - foreach (var documentDto in documentDtos) - { - documentDto.Published = false; - Database.Update(documentDto); - } + var sql = "UPDATE cmsDocument SET published=0 WHERE nodeId=@id AND published=1"; + Database.Execute(sql, new {id = content.Id}); } /// @@ -802,9 +913,9 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsDocument", "nodeId"), - sql => ProcessQuery(sql), orderBy, orderDirection, orderBySystemField, + (sqlFull, pagingSqlQuery) => ProcessQuery(sqlFull, pagingSqlQuery), orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -835,50 +946,110 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; return base.GetDatabaseFieldNameForOrderBy(orderBy); } - private IEnumerable ProcessQuery(Sql sql, bool withCache = false) + /// + /// This is the underlying method that processes most queries for this repository + /// + /// + /// The FullMultiple SQL without the outer join to return all data required to create an IContent excluding it's published state data which this will query separately + /// + /// + /// The Id SQL without the outer join to just return all document ids - used to process the properties for the content item + /// + /// + /// + /// Generally when querying for content we only want to return the most recent version of the content item, however in some cases like when + /// we want to return all versions of a content item, we can't simply return the latest + /// + /// + private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false, bool includeAllVersions = false) { // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sql); + var dtos = Database.Fetch(sqlFull); if (dtos.Count == 0) return Enumerable.Empty(); + + //Go and get all of the published version data separately for this data, this is because when we are querying + //for multiple content items we don't include the outer join to fetch this data in the same query because + //it is insanely slow. Instead we just fetch the published version data separately in one query. - var content = new IContent[dtos.Count]; - var defs = new List(); - var templateIds = new List(); - - for (var i = 0; i < dtos.Count; i++) + //we need to parse the original SQL statement and reduce the columns to just cmsDocument.nodeId so that we can use + // the statement to go get the published data for all of the items by using an inner join + var parsedOriginalSql = "SELECT cmsDocument.nodeId " + sqlFull.SQL.Substring(sqlFull.SQL.IndexOf("FROM", StringComparison.Ordinal)); + //now remove everything from an Orderby clause and beyond + if (parsedOriginalSql.InvariantContains("ORDER BY ")) { - var dto = dtos[i]; + parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); + } + + //order by update date DESC, if there is corrupted published flags we only want the latest! + var publishedSql = new Sql(@"SELECT cmsDocument.nodeId, cmsDocument.published, cmsDocument.versionId, cmsDocument.updateDate, cmsDocument.newest +FROM cmsDocument INNER JOIN cmsContentVersion ON cmsContentVersion.VersionId = cmsDocument.versionId +WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN +(" + parsedOriginalSql + @") +ORDER BY cmsContentVersion.id DESC +", sqlFull.Arguments); + + //go and get the published version data, we do a Query here and not a Fetch so we are + //not allocating a whole list to memory just to allocate another list in memory since + //we are assigning this data to a keyed collection for fast lookup below + var publishedData = Database.Query(publishedSql); + var publishedDataCollection = new DocumentPublishedReadOnlyDtoCollection(); + foreach (var publishedDto in publishedData) + { + //double check that there's no corrupt db data, there should only be a single published item + if (publishedDataCollection.Contains(publishedDto.NodeId) == false) + publishedDataCollection.Add(publishedDto); + } + + //This is a tuple list identifying if the content item came from the cache or not + var content = new List>(); + var defs = new DocumentDefinitionCollection(includeAllVersions); + var templateIds = new List(); + + //track the looked up content types, even though the content types are cached + // they still need to be deep cloned out of the cache and we don't want to add + // the overhead of deep cloning them on every item in this loop + var contentTypes = new Dictionary(); + + foreach (var dto in dtos) + { + DocumentPublishedReadOnlyDto publishedDto; + publishedDataCollection.TryGetValue(dto.NodeId, out publishedDto); // if the cache contains the published version, use it if (withCache) { - var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - //only use this cached version if the dto returned is also the publish version, they must match - if (cached != null && cached.Published && dto.Published) + var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + //only use this cached version if the dto returned is also the publish version, they must match and be teh same version + if (cached != null && cached.Version == dto.VersionId && cached.Published && dto.Published) { - content[i] = cached; + content.Add(new Tuple(cached, true)); continue; } } // else, need to fetch from the database // content type repository is full-cache so OK to get each one independently - var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); - content[i] = factory.BuildEntity(dto); - // need template - if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - templateIds.Add(dto.TemplateId.Value); + IContentType contentType; + if (contentTypes.ContainsKey(dto.ContentVersionDto.ContentDto.ContentTypeId)) + { + contentType = contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId]; + } + else + { + contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId] = contentType; + } - // need properties - defs.Add(new DocumentDefinition( - dto.NodeId, - dto.VersionId, - dto.ContentVersionDto.VersionDate, - dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, - contentType - )); + // track the definition and if it's successfully added or updated then processed + if (defs.AddOrUpdate(new DocumentDefinition(dto, contentType))) + { + // assign template + if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + templateIds.Add(dto.TemplateId.Value); + + content.Add(new Tuple(ContentFactory.BuildEntity(dto, contentType, publishedDto), false)); + } } // load all required templates in 1 query @@ -886,45 +1057,44 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; .ToDictionary(x => x.Id, x => x); // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(sql, defs); + var propertyData = GetPropertyCollection(pagingSqlQuery, defs); - // assign - var dtoIndex = 0; - foreach (var def in defs) + // assign template and property data + foreach (var contentItem in content) { - // move to corresponding item (which has to exist) - while (dtos[dtoIndex].NodeId != def.Id) dtoIndex++; + var cc = contentItem.Item1; + var fromCache = contentItem.Item2; + + //if this has come from cache, we do not need to build up it's structure + if (fromCache) continue; + + var def = defs[includeAllVersions ? (ValueType)cc.Version : cc.Id]; - // complete the item - var cc = content[dtoIndex]; - var dto = dtos[dtoIndex]; ITemplate template = null; - if (dto.TemplateId.HasValue) - templates.TryGetValue(dto.TemplateId.Value, out template); // else null + if (def.DocumentDto.TemplateId.HasValue) + templates.TryGetValue(def.DocumentDto.TemplateId.Value, out template); // else null cc.Template = template; - cc.Properties = propertyData[cc.Id]; + cc.Properties = propertyData[cc.Version]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 - ((Entity) cc).ResetDirtyProperties(false); + cc.ResetDirtyProperties(false); } - return content; + return content.Select(x => x.Item1).ToArray(); } /// /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. /// /// - /// /// /// - private IContent CreateContentFromDto(DocumentDto dto, Guid versionId, Sql docSql) + private IContent CreateContentFromDto(DocumentDto dto, Sql docSql) { var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); - var content = factory.BuildEntity(dto); + var content = ContentFactory.BuildEntity(dto, contentType); //Check if template id is set on DocumentDto, and get ITemplate if it is. if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) @@ -932,11 +1102,11 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; content.Template = _templateRepository.Get(dto.TemplateId.Value); } - var docDef = new DocumentDefinition(dto.NodeId, versionId, content.UpdateDate, content.CreateDate, contentType); + var docDef = new DocumentDefinition(dto, contentType); var properties = GetPropertyCollection(docSql, new[] { docDef }); - content.Properties = properties[dto.NodeId]; + content.Properties = properties[dto.VersionId]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 @@ -975,7 +1145,7 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; return currentName; } - + /// /// Dispose disposable properties /// @@ -990,5 +1160,26 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; _contentPreviewRepository.Dispose(); _contentXmlRepository.Dispose(); } + + /// + /// A keyed collection for fast lookup when retrieving a separate list of published version data + /// + private class DocumentPublishedReadOnlyDtoCollection : KeyedCollection + { + protected override int GetKeyForItem(DocumentPublishedReadOnlyDto item) + { + return item.NodeId; + } + + public bool TryGetValue(int key, out DocumentPublishedReadOnlyDto val) + { + if (Dictionary == null) + { + val = null; + return false; + } + return Dictionary.TryGetValue(key, out val); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index d86f77168e..13081cb2a4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Persistence.Repositories internal abstract class ContentTypeBaseRepository : PetaPocoRepositoryBase, IReadRepository where TEntity : class, IContentTypeComposition { - protected ContentTypeBaseRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + protected ContentTypeBaseRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } @@ -101,7 +101,8 @@ namespace Umbraco.Core.Persistence.Repositories var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate() - .OrderBy(x => x.PropertyTypeGroupId, SqlSyntax); + //must be sorted this way for the relator to work + .OrderBy(x => x.Id, SqlSyntax); var dtos = Database.Fetch(new GroupPropertyTypeRelator().Map, sql); @@ -468,7 +469,8 @@ AND umbracoNode.id <> @id", .LeftJoin() .On(left => left.DataTypeId, right => right.DataTypeId) .Where(x => x.ContentTypeNodeId == id) - .OrderBy(x => x.Id); + //must be sorted this way for the relator to work + .OrderBy(x => x.Id, SqlSyntax); var dtos = Database.Fetch(new GroupPropertyTypeRelator().Map, sql); @@ -1250,5 +1252,19 @@ WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", while (aliases.Contains(test = alias + i)) i++; return test; } + + /// + /// Given the path of a content item, this will return true if the content item exists underneath a list view content item + /// + /// + /// + public bool HasContainerInPath(string contentPath) + { + var ids = contentPath.Split(',').Select(int.Parse); + var sql = new Sql(@"SELECT COUNT(*) FROM cmsContentType +INNER JOIN cmsContent ON cmsContentType.nodeId=cmsContent.contentType +WHERE cmsContent.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", new { ids, isContainer = true }); + return Database.ExecuteScalar(sql) > 0; + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index b7b4ddd583..985f9446b7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -2,19 +2,13 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Cache; -using Umbraco.Core.Events; -using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; - -using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.Relators; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { @@ -25,23 +19,15 @@ namespace Umbraco.Core.Persistence.Repositories { private readonly ITemplateRepository _templateRepository; - public ContentTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, ITemplateRepository templateRepository) + public ContentTypeRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, ITemplateRepository templateRepository) : base(work, cache, logger, sqlSyntax) { _templateRepository = templateRepository; } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), - //allow this cache to expire - expires:true)); - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ true); } protected override IContentType PerformGet(int id) @@ -118,7 +104,7 @@ namespace Umbraco.Core.Persistence.Repositories if (objectTypes.Any()) { - sql = sql.Where("umbracoNode.nodeObjectType IN (@objectTypes)", objectTypes); + sql = sql.Where("umbracoNode.nodeObjectType IN (@objectTypes)", new {objectTypes = objectTypes}); } return Database.Fetch(sql); diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs index b242bb34f7..ef3ce3eec2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Persistence.Repositories internal class ContentXmlRepository : PetaPocoRepositoryBase> where TContent : IContentBase { - public ContentXmlRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public ContentXmlRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } @@ -70,6 +70,8 @@ namespace Umbraco.Core.Persistence.Repositories { //Remove 'published' xml from the cmsContentXml table for the unpublished content Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + + entity.DeletedDate = DateTime.Now; } protected override void PersistNewItem(ContentXmlEntity entity) diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index a4d71885f8..1690b36148 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -29,12 +29,12 @@ namespace Umbraco.Core.Persistence.Repositories private readonly IContentTypeRepository _contentTypeRepository; private readonly DataTypePreValueRepository _preValRepository; - public DataTypeDefinitionRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, + public DataTypeDefinitionRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentTypeRepository contentTypeRepository) : base(work, cache, logger, sqlSyntax) { _contentTypeRepository = contentTypeRepository; - _preValRepository = new DataTypePreValueRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + _preValRepository = new DataTypePreValueRepository(work, CacheHelper.NoCache, logger, sqlSyntax); } #region Overrides of RepositoryBase @@ -237,7 +237,7 @@ AND umbracoNode.id <> @id", //NOTE: This is a special case, we need to clear the custom cache for pre-values here so they are not stale if devs // are querying for them in the Saved event (before the distributed call cache is clearing it) - RuntimeCache.ClearCacheItem(GetPrefixedCacheKey(entity.Id)); + IsolatedCache.ClearCacheItem(GetPrefixedCacheKey(entity.Id)); entity.ResetDirtyProperties(); } @@ -270,53 +270,41 @@ AND umbracoNode.id <> @id", //Delete (base) node data Database.Delete("WHERE uniqueID = @Id", new { Id = entity.Key }); + + entity.DeletedDate = DateTime.Now; } #endregion public PreValueCollection GetPreValuesCollectionByDataTypeId(int dataTypeId) { - var cached = RuntimeCache.GetCacheItemsByKeySearch(GetPrefixedCacheKey(dataTypeId)); - if (cached != null && cached.Any()) - { - //return from the cache, ensure it's a cloned result - return (PreValueCollection)cached.First().DeepClone(); - } - - return GetAndCachePreValueCollection(dataTypeId); - } - - internal static string GetCacheKeyRegex(int preValueId) - { - return CacheKeys.DataTypePreValuesCacheKey + @"[-\d]+-([\d]*,)*" + preValueId + @"(?!\d)[,\d$]*"; + var collection = GetCachedPreValueCollection(dataTypeId); + return collection; } + /// + /// Gets a specific PreValue by its Id + /// + /// Id of the PreValue to retrieve the value from + /// PreValue as a string public string GetPreValueAsString(int preValueId) { - //We need to see if we can find the cached PreValueCollection based on the cache key above + var collections = IsolatedCache.GetCacheItemsByKeySearch(CacheKeys.DataTypePreValuesCacheKey + "_"); - var cached = RuntimeCache.GetCacheItemsByKeyExpression(GetCacheKeyRegex(preValueId)); - if (cached != null && cached.Any()) - { - //return from the cache - var collection = cached.First(); - var preVal = collection.FormatAsDictionary().Single(x => x.Value.Id == preValueId); - return preVal.Value.Value; - } + var preValue = collections.SelectMany(x => x.FormatAsDictionary().Values).FirstOrDefault(x => x.Id == preValueId); + if (preValue != null) + return preValue.Value; - //go and find the data type id for the pre val id passed in - - var dto = Database.FirstOrDefault("WHERE id = @preValueId", new { preValueId = preValueId }); + var dto = Database.FirstOrDefault("WHERE id = @preValueId", new { preValueId }); if (dto == null) - { return string.Empty; - } - // go cache the collection - var preVals = GetAndCachePreValueCollection(dto.DataTypeNodeId); - //return the single value for this id - var pv = preVals.FormatAsDictionary().Single(x => x.Value.Id == preValueId); - return pv.Value.Value; + var collection = GetCachedPreValueCollection(dto.DataTypeNodeId); + if (collection == null) + return string.Empty; + + preValue = collection.FormatAsDictionary().Values.FirstOrDefault(x => x.Id == preValueId); + return preValue == null ? string.Empty : preValue.Value; } public void AddOrUpdatePreValues(int dataTypeId, IDictionary values) @@ -441,40 +429,28 @@ AND umbracoNode.id <> @id", sortOrder++; } - } - private string GetPrefixedCacheKey(int dataTypeId) + private static string GetPrefixedCacheKey(int dataTypeId) { - return CacheKeys.DataTypePreValuesCacheKey + dataTypeId + "-"; + return CacheKeys.DataTypePreValuesCacheKey + "_" + dataTypeId; } - private PreValueCollection GetAndCachePreValueCollection(int dataTypeId) + private PreValueCollection GetCachedPreValueCollection(int datetypeId) { - //go get the data - var dtos = Database.Fetch("WHERE datatypeNodeId = @Id", new { Id = dataTypeId }); - var list = dtos.Select(x => new Tuple(new PreValue(x.Id, x.Value, x.SortOrder), x.Alias, x.SortOrder)).ToList(); - var collection = PreValueConverter.ConvertToPreValuesCollection(list); - - //now create the cache key, this needs to include all pre-value ids so that we can use this cached item in the GetPreValuesAsString method - //the key will be: "UmbracoPreValDATATYPEID-CSVOFPREVALIDS - - var key = GetPrefixedCacheKey(dataTypeId) - + string.Join(",", collection.FormatAsDictionary().Select(x => x.Value.Id).ToArray()); - - //store into cache - RuntimeCache.InsertCacheItem(key, () => collection, - //30 mins - new TimeSpan(0, 0, 30), - //sliding is true - true); - - return collection; + var key = GetPrefixedCacheKey(datetypeId); + return IsolatedCache.GetCacheItem(key, () => + { + var dtos = Database.Fetch("WHERE datatypeNodeId = @Id", new { Id = datetypeId }); + var list = dtos.Select(x => new Tuple(new PreValue(x.Id, x.Value, x.SortOrder), x.Alias, x.SortOrder)).ToList(); + var collection = PreValueConverter.ConvertToPreValuesCollection(list); + return collection; + }, TimeSpan.FromMinutes(20), isSliding: true); } private string EnsureUniqueNodeName(string nodeName, int id = 0) { - + var sql = new Sql(); sql.Select("*") @@ -519,10 +495,9 @@ AND umbracoNode.id <> @id", /// private class DataTypePreValueRepository : PetaPocoRepositoryBase { - public DataTypePreValueRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public DataTypePreValueRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) - { - } + { } #region Not implemented (don't need to for the purposes of this repo) protected override PreValueEntity PerformGet(int id) @@ -566,6 +541,8 @@ AND umbracoNode.id <> @id", Database.Execute( "DELETE FROM cmsDataTypePreValues WHERE id=@Id", new { Id = entity.Id }); + + entity.DeletedDate = DateTime.Now; } protected override void PersistNewItem(PreValueEntity entity) @@ -576,7 +553,7 @@ AND umbracoNode.id <> @id", } //NOTE: We used to check that the Alias was unique for the given DataTypeNodeId prevalues list, BUT - // in reality there is no need to check the uniqueness of this alias because the only way that this code executes is + // in reality there is no need to check the uniqueness of this alias because the only way that this code executes is // based on an IDictionary dictionary being passed to this repository and a dictionary // must have unique aliases by definition, so there is no need for this additional check @@ -596,10 +573,10 @@ AND umbracoNode.id <> @id", { throw new InvalidOperationException("Cannot update a pre value for a data type that has no identity"); } - + //NOTE: We used to check that the Alias was unique for the given DataTypeNodeId prevalues list, BUT // this causes issues when sorting the pre-values (http://issues.umbraco.org/issue/U4-5670) but in reality - // there is no need to check the uniqueness of this alias because the only way that this code executes is + // there is no need to check the uniqueness of this alias because the only way that this code executes is // based on an IDictionary dictionary being passed to this repository and a dictionary // must have unique aliases by definition, so there is no need for this additional check @@ -614,7 +591,7 @@ AND umbracoNode.id <> @id", Database.Update(dto); } - + } internal static class PreValueConverter diff --git a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs index 971efc4f2d..968c2d9cb0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs @@ -20,25 +20,19 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class DictionaryRepository : PetaPocoRepositoryBase, IDictionaryRepository { - public DictionaryRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider syntax) + public DictionaryRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider syntax) : base(work, cache, logger, syntax) - { - } + { } - private IRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get + var options = new RepositoryCachePolicyOptions { - //custom cache policy which will not cache any results for GetAll - return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( - RuntimeCache, - new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - })); - } + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; + + return new SingleItemsOnlyRepositoryCachePolicy(runtimeCache, options); } #region Overrides of RepositoryBase @@ -47,12 +41,13 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = GetBaseQuery(false) .Where(GetBaseWhereClause(), new { Id = id }) + //must be sorted this way for the relator to work .OrderBy(x => x.UniqueId, SqlSyntax); var dto = Database.Fetch(new DictionaryLanguageTextRelator().Map, sql).FirstOrDefault(); if (dto == null) return null; - + var entity = ConvertFromDto(dto); //on initial construction we don't want to have dirty properties tracked @@ -70,6 +65,9 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where("cmsDictionary.pk in (@ids)", new { ids = ids }); } + //must be sorted this way for the relator to work + sql.OrderBy(x => x.UniqueId, SqlSyntax); + return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql) .Select(dto => ConvertFromDto(dto)); } @@ -79,8 +77,9 @@ namespace Umbraco.Core.Persistence.Repositories var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); + //must be sorted this way for the relator to work sql.OrderBy(x => x.UniqueId, SqlSyntax); - + return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql) .Select(x => ConvertFromDto(x)); } @@ -100,9 +99,9 @@ namespace Umbraco.Core.Persistence.Repositories else { sql.Select("*") - .From(SqlSyntax) - .LeftJoin(SqlSyntax) - .On(SqlSyntax, left => left.UniqueId, right => right.UniqueId); + .From(SqlSyntax) + .LeftJoin(SqlSyntax) + .On(SqlSyntax, left => left.UniqueId, right => right.UniqueId); } return sql; } @@ -149,7 +148,7 @@ namespace Umbraco.Core.Persistence.Repositories translation.Key = dictionaryItem.Key; } - dictionaryItem.ResetDirtyProperties(); + dictionaryItem.ResetDirtyProperties(); } protected override void PersistUpdatedItem(IDictionaryItem entity) @@ -182,8 +181,8 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); //Clear the cache entries that exist by uniqueid/item key - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.ItemKey)); - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Key)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.ItemKey)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.Key)); } protected override void PersistDeletedItem(IDictionaryItem entity) @@ -194,8 +193,10 @@ namespace Umbraco.Core.Persistence.Repositories Database.Delete("WHERE id = @Id", new { Id = entity.Key }); //Clear the cache entries that exist by uniqueid/item key - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.ItemKey)); - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Key)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.ItemKey)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.Key)); + + entity.DeletedDate = DateTime.Now; } private void RecursiveDelete(Guid parentId) @@ -209,8 +210,8 @@ namespace Umbraco.Core.Persistence.Repositories Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId }); //Clear the cache entries that exist by uniqueid/item key - RuntimeCache.ClearCacheItem(GetCacheIdKey(dto.Key)); - RuntimeCache.ClearCacheItem(GetCacheIdKey(dto.UniqueId)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(dto.Key)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(dto.UniqueId)); } } @@ -223,7 +224,7 @@ namespace Umbraco.Core.Persistence.Repositories var list = new List(); foreach (var textDto in dto.LanguageTextDtos) - { + { if (textDto.LanguageId <= 0) continue; @@ -237,7 +238,8 @@ namespace Umbraco.Core.Persistence.Repositories public IDictionaryItem Get(Guid uniqueId) { - using (var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, UnitOfWork, RepositoryCache, Logger, SqlSyntax)) + // note: normal to use GlobalCache here since we're passing it to a new repository + using (var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, UnitOfWork, GlobalCache, Logger, SqlSyntax)) { return uniqueIdRepo.Get(uniqueId); } @@ -245,12 +247,13 @@ namespace Umbraco.Core.Persistence.Repositories public IDictionaryItem Get(string key) { - using (var keyRepo = new DictionaryByKeyRepository(this, UnitOfWork, RepositoryCache, Logger, SqlSyntax)) + // note: normal to use GlobalCache here since we're passing it to a new repository + using (var keyRepo = new DictionaryByKeyRepository(this, UnitOfWork, GlobalCache, Logger, SqlSyntax)) { return keyRepo.Get(key); } } - + private IEnumerable GetRootDictionaryItems() { var query = Query.Builder.Where(x => x.ParentId == null); @@ -274,6 +277,8 @@ namespace Umbraco.Core.Persistence.Repositories var translator = new SqlTranslator(sqlClause, Query.Builder); var sql = translator.Translate(); + + //must be sorted this way for the relator to work sql.OrderBy(x => x.UniqueId, SqlSyntax); return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql) @@ -293,7 +298,7 @@ namespace Umbraco.Core.Persistence.Repositories { private readonly DictionaryRepository _dictionaryRepository; - public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { _dictionaryRepository = dictionaryRepository; @@ -301,6 +306,9 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable PerformFetch(Sql sql) { + //must be sorted this way for the relator to work + sql.OrderBy(x => x.UniqueId, SqlSyntax); + return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql); } @@ -329,20 +337,15 @@ namespace Umbraco.Core.Persistence.Repositories return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; } - private IRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get + var options = new RepositoryCachePolicyOptions { - //custom cache policy which will not cache any results for GetAll - return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( - RuntimeCache, - new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - })); - } + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; + + return new SingleItemsOnlyRepositoryCachePolicy(runtimeCache, options); } } @@ -350,7 +353,7 @@ namespace Umbraco.Core.Persistence.Repositories { private readonly DictionaryRepository _dictionaryRepository; - public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { _dictionaryRepository = dictionaryRepository; @@ -358,6 +361,9 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable PerformFetch(Sql sql) { + //must be sorted this way for the relator to work + sql.OrderBy(x => x.UniqueId, SqlSyntax); + return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql); } @@ -386,23 +392,16 @@ namespace Umbraco.Core.Persistence.Repositories return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; } - private IRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get + var options = new RepositoryCachePolicyOptions { - //custom cache policy which will not cache any results for GetAll - return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( - RuntimeCache, - new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - })); - } + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; + + return new SingleItemsOnlyRepositoryCachePolicy(runtimeCache, options); } } - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs index 7b6cc162a8..3dca2ffdd0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs @@ -18,20 +18,14 @@ namespace Umbraco.Core.Persistence.Repositories internal class DomainRepository : PetaPocoRepositoryBase, IDomainRepository { - public DomainRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public DomainRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), false)); - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ false); } protected override IDomain PerformGet(int id) diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs index cc13275798..bc23d7201c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Repositories { private readonly Guid _containerObjectType; - public EntityContainerRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, Guid containerObjectType) + public EntityContainerRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, Guid containerObjectType) : base(work, cache, logger, sqlSyntax) { var allowedContainers = new[] {Constants.ObjectTypes.DocumentTypeContainerGuid, Constants.ObjectTypes.MediaTypeContainerGuid, Constants.ObjectTypes.DataTypeContainerGuid}; @@ -29,12 +29,11 @@ namespace Umbraco.Core.Persistence.Repositories throw new InvalidOperationException("No container type exists with ID: " + _containerObjectType); } - /// - /// Do not cache anything - /// - protected override IRuntimeCacheProvider RuntimeCache + // never cache + private static readonly IRuntimeCacheProvider NullCache = new NullCacheProvider(); + protected override IRuntimeCacheProvider GetIsolatedCache(IsolatedRuntimeCache provider) { - get { return new NullCacheProvider(); } + return NullCache; } protected override EntityContainer PerformGet(int id) @@ -62,17 +61,27 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable PerformGetAll(params int[] ids) { - //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit - return ids.InGroupsOf(2000).SelectMany(@group => + if (ids.Any()) + { + //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit + return ids.InGroupsOf(2000).SelectMany(@group => + { + var sql = GetBaseQuery(false) + .Where("nodeObjectType=@umbracoObjectTypeId", new {umbracoObjectTypeId = NodeObjectTypeId}) + .Where(string.Format("{0} IN (@ids)", SqlSyntax.GetQuotedColumnName("id")), new {ids = @group}); + + sql.OrderBy(x => x.Level, SqlSyntax); + + return Database.Fetch(sql).Select(CreateEntity); + }); + } + else { var sql = GetBaseQuery(false) - .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) - .Where(string.Format("{0} IN (@ids)", SqlSyntax.GetQuotedColumnName("id")), new { ids = @group }); - + .Where("nodeObjectType=@umbracoObjectTypeId", new {umbracoObjectTypeId = NodeObjectTypeId}); sql.OrderBy(x => x.Level, SqlSyntax); - return Database.Fetch(sql).Select(CreateEntity); - }); + } } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -158,6 +167,8 @@ namespace Umbraco.Core.Persistence.Repositories // delete Database.Delete(nodeDto); + + entity.DeletedDate = DateTime.Now; } protected override void PersistNewItem(EntityContainer entity) diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs index ac7e410a8f..4efa8a63ac 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -1,18 +1,14 @@ using System; using System.Collections.Generic; -using System.Dynamic; -using System.Globalization; +using System.Collections.ObjectModel; using System.Linq; -using System.Reflection; -using System.Text; using Umbraco.Core.Models; -using Umbraco.Core; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Strings; namespace Umbraco.Core.Persistence.Repositories { @@ -49,6 +45,124 @@ namespace Umbraco.Core.Persistence.Repositories #region Query Methods + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectTypeId, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, IQuery filter = null) + { + bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); + bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + var factory = new UmbracoEntityFactory(); + + var sqlClause = GetBaseWhere(GetBase, isContent, isMedia, sql => + { + if (filter != null) + { + foreach (var filterClaus in filter.GetWhereClauses()) + { + sql.Where(filterClaus.Item1, filterClaus.Item2); + } + } + }, objectTypeId); + var translator = new SqlTranslator(sqlClause, query); + var entitySql = translator.Translate(); + var pagedSql = entitySql.Append(GetGroupBy(isContent, isMedia, false)).OrderBy("umbracoNode.id"); + + IEnumerable result; + + if (isMedia) + { + //Treat media differently for now, as an Entity it will be returned with ALL of it's properties in the AdditionalData bag! + var pagedResult = _work.Database.Page(pageIndex + 1, pageSize, pagedSql); + + var ids = pagedResult.Items.Select(x => (int) x.id).InGroupsOf(2000); + var entities = pagedResult.Items.Select(factory.BuildEntityFromDynamic).Cast().ToList(); + + //Now we need to merge in the property data since we need paging and we can't do this the way that the big media query was working before + foreach (var idGroup in ids) + { + var propSql = GetPropertySql(Constants.ObjectTypes.Media) + .Where("contentNodeId IN (@ids)", new {ids = idGroup}) + .OrderBy("contentNodeId"); + + //This does NOT fetch all data into memory in a list, this will read + // over the records as a data reader, this is much better for performance and memory, + // but it means that during the reading of this data set, nothing else can be read + // from SQL server otherwise we'll get an exception. + var allPropertyData = _work.Database.Query(propSql); + + //keep track of the current property data item being enumerated + var propertyDataSetEnumerator = allPropertyData.GetEnumerator(); + var hasCurrent = false; // initially there is no enumerator.Current + + try + { + //This must be sorted by node id (which is done by SQL) because this is how we are sorting the query to lookup property types above, + // which allows us to more efficiently iterate over the large data set of property values. + foreach (var entity in entities) + { + // assemble the dtos for this def + // use the available enumerator.Current if any else move to next + while (hasCurrent || propertyDataSetEnumerator.MoveNext()) + { + if (propertyDataSetEnumerator.Current.contentNodeId == entity.Id) + { + hasCurrent = false; // enumerator.Current is not available + + //the property data goes into the additional data + entity.AdditionalData[propertyDataSetEnumerator.Current.propertyTypeAlias] = new UmbracoEntity.EntityProperty + { + PropertyEditorAlias = propertyDataSetEnumerator.Current.propertyEditorAlias, + Value = StringExtensions.IsNullOrWhiteSpace(propertyDataSetEnumerator.Current.dataNtext) + ? propertyDataSetEnumerator.Current.dataNvarchar + : StringExtensions.ConvertToJsonIfPossible(propertyDataSetEnumerator.Current.dataNtext) + }; + } + else + { + hasCurrent = true; // enumerator.Current is available for another def + break; // no more propertyDataDto for this def + } + } + } + } + finally + { + propertyDataSetEnumerator.Dispose(); + } + } + + result = entities; + } + else + { + var pagedResult = _work.Database.Page(pageIndex + 1, pageSize, pagedSql); + result = pagedResult.Items.Select(factory.BuildEntityFromDynamic).Cast().ToList(); + } + + //The total items from the PetaPoco page query will be wrong due to the Outer join used on parent, depending on the search this will + //return duplicate results when the COUNT is used in conjuction with it, so we need to get the total on our own. + + //generate a query that does not contain the LEFT Join for parent, this would cause + //the COUNT(*) query to return the wrong + var sqlCountClause = GetBaseWhere( + (isC, isM, f) => GetBase(isC, isM, f, true), //true == is a count query + isContent, isMedia, sql => + { + if (filter != null) + { + foreach (var filterClaus in filter.GetWhereClauses()) + { + sql.Where(filterClaus.Item1, filterClaus.Item2); + } + } + }, objectTypeId); + var translatorCount = new SqlTranslator(sqlCountClause, query); + var countSql = translatorCount.Translate(); + + totalRecords = _work.Database.ExecuteScalar(countSql); + + return result; + } + public IUmbracoEntity GetByKey(Guid key) { var sql = GetBaseWhere(GetBase, false, false, key); @@ -68,11 +182,12 @@ namespace Umbraco.Core.Persistence.Repositories bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); var sql = GetFullSqlForEntityType(key, isContent, isMedia, objectTypeId); - + + var factory = new UmbracoEntityFactory(); + if (isMedia) { - //for now treat media differently - //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + //for now treat media differently and include all property data too var entities = _work.Database.Fetch( new UmbracoEntityRelator().Map, sql); @@ -80,14 +195,16 @@ namespace Umbraco.Core.Persistence.Repositories } else { - var nodeDto = _work.Database.FirstOrDefault(sql); - if (nodeDto == null) - return null; - var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntityFromDynamic(nodeDto); - - return entity; + //query = read forward data reader, do not load everything into mem + var dtos = _work.Database.Query(sql); + var collection = new EntityDefinitionCollection(); + foreach (var dto in dtos) + { + collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); + } + var found = collection.FirstOrDefault(); + return found != null ? found.BuildFromDynamic() : null; } @@ -112,29 +229,29 @@ namespace Umbraco.Core.Persistence.Repositories bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); var sql = GetFullSqlForEntityType(id, isContent, isMedia, objectTypeId); - + + var factory = new UmbracoEntityFactory(); + if (isMedia) { - //for now treat media differently - //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + //for now treat media differently and include all property data too var entities = _work.Database.Fetch( new UmbracoEntityRelator().Map, sql); return entities.FirstOrDefault(); } else - { - var nodeDto = _work.Database.FirstOrDefault(sql); - if (nodeDto == null) - return null; - - var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntityFromDynamic(nodeDto); - - return entity; + { + //query = read forward data reader, do not load everything into mem + var dtos = _work.Database.Query(sql); + var collection = new EntityDefinitionCollection(); + foreach (var dto in dtos) + { + collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); + } + var found = collection.FirstOrDefault(); + return found != null ? found.BuildFromDynamic() : null; } - - } public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) @@ -171,22 +288,21 @@ namespace Umbraco.Core.Persistence.Repositories if (isMedia) { - //for now treat media differently - //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + //for now treat media differently and include all property data too var entities = _work.Database.Fetch( new UmbracoEntityRelator().Map, sql); - foreach (var entity in entities) - { - yield return entity; - } + return entities; } else { - var dtos = _work.Database.Fetch(sql); - foreach (var entity in dtos.Select(dto => factory.BuildEntityFromDynamic(dto))) + //query = read forward data reader, do not load everything into mem + var dtos = _work.Database.Query(sql); + var collection = new EntityDefinitionCollection(); + foreach (var dto in dtos) { - yield return entity; + collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); } + return collection.Select(x => x.BuildFromDynamic()).ToList(); } } @@ -231,8 +347,7 @@ namespace Umbraco.Core.Persistence.Repositories } }); - //treat media differently for now - //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + //for now treat media differently and include all property data too var entities = _work.Database.Fetch( new UmbracoEntityRelator().Map, mediaSql); return entities; @@ -241,8 +356,15 @@ namespace Umbraco.Core.Persistence.Repositories { //use dynamic so that we can get ALL properties from the SQL so we can chuck that data into our AdditionalData var finalSql = entitySql.Append(GetGroupBy(isContent, false)); - var dtos = _work.Database.Fetch(finalSql); - return dtos.Select(factory.BuildEntityFromDynamic).Cast().ToList(); + + //query = read forward data reader, do not load everything into mem + var dtos = _work.Database.Query(finalSql); + var collection = new EntityDefinitionCollection(); + foreach (var dto in dtos) + { + collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); + } + return collection.Select(x => x.BuildFromDynamic()).ToList(); } } @@ -278,11 +400,9 @@ namespace Umbraco.Core.Persistence.Repositories return GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false)), filter); } - private Sql GetFullSqlForMedia(Sql entitySql, Action filter = null) + private Sql GetPropertySql(string nodeObjectType) { - //this will add any dataNvarchar property to the output which can be added to the additional properties - - var joinSql = new Sql() + var sql = new Sql() .Select("contentNodeId, versionId, dataNvarchar, dataNtext, propertyEditorAlias, alias as propertyTypeAlias") .From() .InnerJoin() @@ -291,7 +411,16 @@ namespace Umbraco.Core.Persistence.Repositories .On(dto => dto.Id, dto => dto.PropertyTypeId) .InnerJoin() .On(dto => dto.DataTypeId, dto => dto.DataTypeId) - .Where("umbracoNode.nodeObjectType = @nodeObjectType", new {nodeObjectType = Constants.ObjectTypes.Media}); + .Where("umbracoNode.nodeObjectType = @nodeObjectType", new { nodeObjectType = nodeObjectType }); + + return sql; + } + + private Sql GetFullSqlForMedia(Sql entitySql, Action filter = null) + { + //this will add any dataNvarchar property to the output which can be added to the additional properties + + var joinSql = GetPropertySql(Constants.ObjectTypes.Media); if (filter != null) { @@ -314,30 +443,49 @@ namespace Umbraco.Core.Persistence.Repositories protected virtual Sql GetBase(bool isContent, bool isMedia, Action customFilter) { - var columns = new List - { - "umbracoNode.id", - "umbracoNode.trashed", - "umbracoNode.parentID", - "umbracoNode.nodeUser", - "umbracoNode.level", - "umbracoNode.path", - "umbracoNode.sortOrder", - "umbracoNode.uniqueID", - "umbracoNode.text", - "umbracoNode.nodeObjectType", - "umbracoNode.createDate", - "COUNT(parent.parentID) as children" - }; + return GetBase(isContent, isMedia, customFilter, false); + } - if (isContent || isMedia) + protected virtual Sql GetBase(bool isContent, bool isMedia, Action customFilter, bool isCount) + { + var columns = new List(); + if (isCount) { - columns.Add("published.versionId as publishedVersion"); - columns.Add("latest.versionId as newestVersion"); - columns.Add("contenttype.alias"); - columns.Add("contenttype.icon"); - columns.Add("contenttype.thumbnail"); - columns.Add("contenttype.isContainer"); + columns.Add("COUNT(*)"); + } + else + { + columns.AddRange(new List + { + "umbracoNode.id", + "umbracoNode.trashed", + "umbracoNode.parentID", + "umbracoNode.nodeUser", + "umbracoNode.level", + "umbracoNode.path", + "umbracoNode.sortOrder", + "umbracoNode.uniqueID", + "umbracoNode.text", + "umbracoNode.nodeObjectType", + "umbracoNode.createDate", + "COUNT(parent.parentID) as children" + }); + + if (isContent || isMedia) + { + if (isContent) + { + //only content has/needs this info + columns.Add("published.versionId as publishedVersion"); + columns.Add("document.versionId as newestVersion"); + columns.Add("contentversion.id as versionId"); + } + + columns.Add("contenttype.alias"); + columns.Add("contenttype.icon"); + columns.Add("contenttype.thumbnail"); + columns.Add("contenttype.isContainer"); + } } //Creates an SQL query to return a single row for the entity @@ -345,20 +493,28 @@ namespace Umbraco.Core.Persistence.Repositories var entitySql = new Sql() .Select(columns.ToArray()) .From("umbracoNode umbracoNode"); - + if (isContent || isMedia) { - entitySql.InnerJoin("cmsContent content").On("content.nodeId = umbracoNode.id") - .LeftJoin("cmsContentType contenttype").On("contenttype.nodeId = content.contentType") - .LeftJoin( - "(SELECT nodeId, versionId FROM cmsDocument WHERE published = 1 GROUP BY nodeId, versionId) as published") - .On("umbracoNode.id = published.nodeId") - .LeftJoin( - "(SELECT nodeId, versionId FROM cmsDocument WHERE newest = 1 GROUP BY nodeId, versionId) as latest") - .On("umbracoNode.id = latest.nodeId"); + entitySql.InnerJoin("cmsContent content").On("content.nodeId = umbracoNode.id"); + + if (isContent) + { + //only content has/needs this info + entitySql + .InnerJoin("cmsDocument document").On("document.nodeId = umbracoNode.id") + .InnerJoin("cmsContentVersion contentversion").On("contentversion.VersionId = document.versionId") + .LeftJoin("(SELECT nodeId, versionId FROM cmsDocument WHERE published = 1) as published") + .On("umbracoNode.id = published.nodeId"); + } + + entitySql.LeftJoin("cmsContentType contenttype").On("contenttype.nodeId = content.contentType"); } - entitySql.LeftJoin("umbracoNode parent").On("parent.parentID = umbracoNode.id"); + if (isCount == false) + { + entitySql.LeftJoin("umbracoNode parent").On("parent.parentID = umbracoNode.id"); + } if (customFilter != null) { @@ -372,22 +528,42 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = baseQuery(isContent, isMedia, filter) .Where("umbracoNode.nodeObjectType = @NodeObjectType", new { NodeObjectType = nodeObjectType }); + + if (isContent) + { + sql.Where("document.newest = 1"); + } + return sql; } protected virtual Sql GetBaseWhere(Func, Sql> baseQuery, bool isContent, bool isMedia, int id) { var sql = baseQuery(isContent, isMedia, null) - .Where("umbracoNode.id = @Id", new { Id = id }) - .Append(GetGroupBy(isContent, isMedia)); + .Where("umbracoNode.id = @Id", new { Id = id }); + + if (isContent) + { + sql.Where("document.newest = 1"); + } + + sql.Append(GetGroupBy(isContent, isMedia)); + return sql; } protected virtual Sql GetBaseWhere(Func, Sql> baseQuery, bool isContent, bool isMedia, Guid key) { var sql = baseQuery(isContent, isMedia, null) - .Where("umbracoNode.uniqueID = @UniqueID", new { UniqueID = key }) - .Append(GetGroupBy(isContent, isMedia)); + .Where("umbracoNode.uniqueID = @UniqueID", new {UniqueID = key}); + + if (isContent) + { + sql.Where("document.newest = 1"); + } + + sql.Append(GetGroupBy(isContent, isMedia)); + return sql; } @@ -396,6 +572,12 @@ namespace Umbraco.Core.Persistence.Repositories var sql = baseQuery(isContent, isMedia, null) .Where("umbracoNode.id = @Id AND umbracoNode.nodeObjectType = @NodeObjectType", new {Id = id, NodeObjectType = nodeObjectType}); + + if (isContent) + { + sql.Where("document.newest = 1"); + } + return sql; } @@ -404,36 +586,46 @@ namespace Umbraco.Core.Persistence.Repositories var sql = baseQuery(isContent, isMedia, null) .Where("umbracoNode.uniqueID = @UniqueID AND umbracoNode.nodeObjectType = @NodeObjectType", new { UniqueID = key, NodeObjectType = nodeObjectType }); + + if (isContent) + { + sql.Where("document.newest = 1"); + } + return sql; } protected virtual Sql GetGroupBy(bool isContent, bool isMedia, bool includeSort = true) { var columns = new List - { - "umbracoNode.id", - "umbracoNode.trashed", - "umbracoNode.parentID", - "umbracoNode.nodeUser", - "umbracoNode.level", - "umbracoNode.path", - "umbracoNode.sortOrder", - "umbracoNode.uniqueID", - "umbracoNode.text", - "umbracoNode.nodeObjectType", - "umbracoNode.createDate" - }; + { + "umbracoNode.id", + "umbracoNode.trashed", + "umbracoNode.parentID", + "umbracoNode.nodeUser", + "umbracoNode.level", + "umbracoNode.path", + "umbracoNode.sortOrder", + "umbracoNode.uniqueID", + "umbracoNode.text", + "umbracoNode.nodeObjectType", + "umbracoNode.createDate" + }; if (isContent || isMedia) { - columns.Add("published.versionId"); - columns.Add("latest.versionId"); + if (isContent) + { + columns.Add("published.versionId"); + columns.Add("document.versionId"); + columns.Add("contentversion.id"); + } columns.Add("contenttype.alias"); columns.Add("contenttype.icon"); columns.Add("contenttype.thumbnail"); - columns.Add("contenttype.isContainer"); + columns.Add("contenttype.isContainer"); } - + var sql = new Sql() .GroupBy(columns.ToArray()); @@ -444,7 +636,7 @@ namespace Umbraco.Core.Persistence.Repositories return sql; } - + #endregion /// @@ -458,33 +650,19 @@ namespace Umbraco.Core.Persistence.Repositories UnitOfWork.DisposeIfDisposable(); } - #region umbracoNode POCO - Extends NodeDto - [TableName("umbracoNode")] - [PrimaryKey("id")] - [ExplicitColumns] - internal class UmbracoEntityDto : NodeDto + public bool Exists(Guid key) { - [Column("children")] - public int Children { get; set; } - - [Column("publishedVersion")] - public Guid PublishedVersion { get; set; } - - [Column("newestVersion")] - public Guid NewestVersion { get; set; } - - [Column("alias")] - public string Alias { get; set; } - - [Column("icon")] - public string Icon { get; set; } - - [Column("thumbnail")] - public string Thumbnail { get; set; } - - [ResultColumn] - public List UmbracoPropertyDtos { get; set; } + var sql = new Sql().Select("COUNT(*)").From("umbracoNode").Where("uniqueID=@uniqueID", new {uniqueID = key}); + return _work.Database.ExecuteScalar(sql) > 0; } + + public bool Exists(int id) + { + var sql = new Sql().Select("COUNT(*)").From("umbracoNode").Where("id=@id", new { id = id }); + return _work.Database.ExecuteScalar(sql) > 0; + } + + #region private classes [ExplicitColumns] internal class UmbracoPropertyDto @@ -568,6 +746,99 @@ namespace Umbraco.Core.Persistence.Repositories return prev; } } + + private class EntityDefinitionCollection : KeyedCollection + { + protected override int GetKeyForItem(EntityDefinition item) + { + return item.Id; + } + + /// + /// if this key already exists if it does then we need to check + /// if the existing item is 'older' than the new item and if that is the case we'll replace the older one + /// + /// + /// + public bool AddOrUpdate(EntityDefinition item) + { + if (Dictionary == null) + { + base.Add(item); + return true; + } + + var key = GetKeyForItem(item); + EntityDefinition found; + if (TryGetValue(key, out found)) + { + //it already exists and it's older so we need to replace it + if (item.VersionId > found.VersionId) + { + var currIndex = Items.IndexOf(found); + if (currIndex == -1) + throw new IndexOutOfRangeException("Could not find the item in the list: " + found.Id); + + //replace the current one with the newer one + SetItem(currIndex, item); + return true; + } + //could not add or update + return false; + } + + base.Add(item); + return true; + } + + private bool TryGetValue(int key, out EntityDefinition val) + { + if (Dictionary == null) + { + val = null; + return false; + } + return Dictionary.TryGetValue(key, out val); + } + } + + private class EntityDefinition + { + private readonly UmbracoEntityFactory _factory; + private readonly dynamic _entity; + private readonly bool _isContent; + private readonly bool _isMedia; + + public EntityDefinition(UmbracoEntityFactory factory, dynamic entity, bool isContent, bool isMedia) + { + _factory = factory; + _entity = entity; + _isContent = isContent; + _isMedia = isMedia; + } + + public IUmbracoEntity BuildFromDynamic() + { + return _factory.BuildEntityFromDynamic(_entity); + } + + public int Id + { + get { return _entity.id; } + } + + public int VersionId + { + get + { + if (_isContent || _isMedia) + { + return _entity.versionId; + } + return _entity.id; + } + } + } #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs index 7c80bd10b7..fcaee87975 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class ExternalLoginRepository : PetaPocoRepositoryBase, IExternalLoginRepository { - public ExternalLoginRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public ExternalLoginRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs b/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs index e1e4c3105a..03a8b7e824 100644 --- a/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs @@ -217,20 +217,45 @@ namespace Umbraco.Core.Persistence.Repositories protected string GetFileContent(string filename) { - using (var stream = FileSystem.OpenFile(filename)) - using (var reader = new StreamReader(stream, Encoding.UTF8, true)) + if (FileSystem.FileExists(filename) == false) + return null; + + try { - return reader.ReadToEnd(); + using (var stream = FileSystem.OpenFile(filename)) + using (var reader = new StreamReader(stream, Encoding.UTF8, true)) + { + return reader.ReadToEnd(); + } + } + catch + { + return null; // deal with race conds } } - /// - /// Dispose any disposable properties - /// - /// - /// Dispose the unit of work - /// - protected override void DisposeResources() + public long GetFileSize(string filename) + { + if (FileSystem.FileExists(filename) == false) + return -1; + + try + { + return FileSystem.GetSize(filename); + } + catch + { + return -1; // deal with race conds + } + } + + /// + /// Dispose any disposable properties + /// + /// + /// Dispose the unit of work + /// + protected override void DisposeResources() { _work.DisposeIfDisposable(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index e2555995d2..e04608e9c6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { + public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository { /// @@ -82,19 +83,7 @@ namespace Umbraco.Core.Persistence.Repositories /// void AddOrUpdatePreviewXml(IContent content, Func xml); - /// - /// Gets paged content results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// - /// An Enumerable list of objects - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null); + + } } \ 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 61d83645b3..f118be3b76 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs @@ -8,6 +8,13 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IContentTypeRepository : IContentTypeCompositionRepository { + /// + /// Given the path of a content item, this will return true if the content item exists underneath a list view content item + /// + /// + /// + bool HasContainerInPath(string contentPath); + /// /// Gets all entities of the specified query /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs index 005c1d62ba..8a5a99b32a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Persistence.Repositories { + // cannot kill in v7 because it is public, kill in v8 + [Obsolete("Use MediaFileSystem.DeleteMediaFiles instead.", false)] public interface IDeleteMediaFilesRepository { /// @@ -9,6 +12,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// + [Obsolete("Use MediaFileSystem.DeleteMediaFiles instead.", false)] bool DeleteMediaFiles(IEnumerable files); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs index 3b73aabdf4..54e86e64d4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories @@ -15,5 +16,34 @@ namespace Umbraco.Core.Persistence.Repositories IEnumerable GetAll(Guid objectTypeId, params Guid[] keys); IEnumerable GetByQuery(IQuery query); IEnumerable GetByQuery(IQuery query, Guid objectTypeId); + + /// + /// Gets paged results + /// + /// Query to excute + /// + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// + /// An Enumerable list of objects + IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectTypeId, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, IQuery filter = null); + + /// + /// Returns true if the entity exists + /// + /// + /// + bool Exists(Guid key); + + /// + /// Returns true if the entity exists + /// + /// + /// + bool Exists(int id); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMacroRepository.cs index 496012ef10..6600d528ad 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMacroRepository.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { - internal interface IMacroRepository : IRepositoryQueryable + internal interface IMacroRepository : IRepositoryQueryable, IReadRepository { //IEnumerable GetAll(params string[] aliases); diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index 64989f9269..4c9b1d3561 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -17,36 +17,17 @@ namespace Umbraco.Core.Persistence.Repositories /// void AddOrUpdateContentXml(IMedia content, Func xml); + /// + /// Used to remove the content xml for a content item + /// + /// + void DeleteContentXml(IMedia content); + /// /// Used to add/update preview xml for the content item /// /// /// - void AddOrUpdatePreviewXml(IMedia content, Func xml); - - /// - /// Gets paged media results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); - - /// - /// Gets paged media descendants as XML by path - /// - /// Path starts with - /// Page number - /// Page size - /// Total records the query would return without paging - /// A paged enumerable of XML entries of media items - IEnumerable GetPagedXmlEntriesByPath(string path, long pageIndex, int pageSize, out long totalRecords); + void AddOrUpdatePreviewXml(IMedia content, Func xml); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index a24116f0e2..bed96890d9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -41,25 +41,6 @@ namespace Umbraco.Core.Persistence.Repositories /// int GetCountByQuery(IQuery query); - /// - /// Gets paged member results - /// - /// The query. - /// Index of the page. - /// Size of the page. - /// The total records. - /// The order by column - /// The order direction. - /// Flag to indicate when ordering by system field - /// Search query - /// - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); - - //IEnumerable GetPagedResultsByQuery( - // Sql sql, int pageIndex, int pageSize, out int totalRecords, - // Func, int[]> resolveIds); - /// /// Used to add/update published xml for the media item /// @@ -73,6 +54,6 @@ namespace Umbraco.Core.Persistence.Repositories /// /// void AddOrUpdatePreviewXml(IMember content, Func xml); - + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs index 3fbcdd3283..27342fe643 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Models; +using System.IO; +using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { @@ -7,5 +8,8 @@ namespace Umbraco.Core.Persistence.Repositories void AddFolder(string folderPath); void DeleteFolder(string folderPath); bool ValidatePartialView(IPartialView partialView); + Stream GetFileContentStream(string filepath); + void SetFileContent(string filepath, Stream content); + long GetFileSize(string filepath); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationTypeRepository.cs index d28f81bce0..0cf6357d24 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRelationTypeRepository.cs @@ -1,9 +1,8 @@ -using Umbraco.Core.Models; +using System; +using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { - public interface IRelationTypeRepository : IRepositoryQueryable - { - - } + public interface IRelationTypeRepository : IRepositoryQueryable, IReadRepository + { } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs index 3e05d1feaf..ea3194cb4d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Xml.Linq; +using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { @@ -20,7 +23,7 @@ namespace Umbraco.Core.Persistence.Repositories /// The serializer to convert TEntity to Xml /// Structures will be rebuilt in chunks of this size /// - void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null); + void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null); /// /// Get the total count of entities @@ -67,5 +70,31 @@ namespace Umbraco.Core.Persistence.Repositories /// Id of the object to delete versions from /// Latest version date void DeleteVersions(int id, DateTime versionDate); + + /// + /// Gets paged content descendants as XML by path + /// + /// Path starts with + /// Page number + /// Page size + /// + /// Total records the query would return without paging + /// A paged enumerable of XML entries of content items + IEnumerable GetPagedXmlEntriesByPath(string path, long pageIndex, int pageSize, string[] orderBy, out long totalRecords); + + /// + /// Gets paged content results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// + /// An Enumerable list of objects + IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs index 9d7bfd2e36..c2c0a0ae84 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs @@ -1,9 +1,13 @@ -using Umbraco.Core.Models; +using System.IO; +using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { public interface IScriptRepository : IRepository { bool ValidateScript(Script script); + Stream GetFileContentStream(string filepath); + void SetFileContent(string filepath, Stream content); + long GetFileSize(string filepath); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs index 1b2bcbe3eb..5fadf7b01c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.IO; using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories @@ -6,5 +6,8 @@ namespace Umbraco.Core.Persistence.Repositories public interface IStylesheetRepository : IRepository { bool ValidateStylesheet(Stylesheet stylesheet); + Stream GetFileContentStream(string filepath); + void SetFileContent(string filepath, Stream content); + long GetFileSize(string filepath); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs index 3faba9e282..fd04e21d75 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories @@ -57,5 +58,21 @@ namespace Umbraco.Core.Persistence.Repositories /// to validate /// True if Script is valid, otherwise false bool ValidateTemplate(ITemplate template); + + /// + /// Gets the content of a template as a stream. + /// + /// The filesystem path to the template. + /// The content of the template. + Stream GetFileContentStream(string filepath); + + /// + /// Sets the content of a template. + /// + /// The filesystem path to the template. + /// The content of the template. + void SetFileContent(string filepath, Stream content); + + long GetFileSize(string filepath); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IXsltFileRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IXsltFileRepository.cs new file mode 100644 index 0000000000..4c56b6eb18 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IXsltFileRepository.cs @@ -0,0 +1,13 @@ +using System.IO; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IXsltFileRepository : IRepository + { + bool ValidateXsltFile(XsltFile xsltFile); + Stream GetFileContentStream(string filepath); + void SetFileContent(string filepath, Stream content); + long GetFileSize(string filepath); + } +} \ 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 f9a8e59cfa..8b32f4fd1f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs @@ -19,20 +19,13 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class LanguageRepository : PetaPocoRepositoryBase, ILanguageRepository { - public LanguageRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public LanguageRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) - { - } + { } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), false)); - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ false); } #region Overrides of RepositoryBase @@ -131,8 +124,8 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); //Clear the cache entries that exist by key/iso - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.IsoCode)); - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.CultureName)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.IsoCode)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.CultureName)); } protected override void PersistDeletedItem(ILanguage entity) @@ -140,8 +133,8 @@ namespace Umbraco.Core.Persistence.Repositories base.PersistDeletedItem(entity); //Clear the cache entries that exist by key/iso - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.IsoCode)); - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.CultureName)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.IsoCode)); + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.CultureName)); } #endregion @@ -164,7 +157,5 @@ namespace Umbraco.Core.Persistence.Repositories //use the underlying GetAll which will force cache all languages return GetAll().FirstOrDefault(x => x.IsoCode.InvariantEquals(isoCode)); } - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs index 4d21059758..0cabd91006 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Persistence.Repositories internal class MacroRepository : PetaPocoRepositoryBase, IMacroRepository { - public MacroRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public MacroRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } @@ -26,6 +26,19 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); + return GetBySql(sql); + } + + public IMacro Get(Guid id) + { + var sql = GetBaseQuery().Where("uniqueId=@Id", new { Id = id }); + return GetBySql(sql); + } + + private IMacro GetBySql(Sql sql) + { + //must be sorted this way for the relator to work + sql.OrderBy(x => x.Id, SqlSyntax); var macroDto = Database.Fetch(new MacroPropertyRelator().Map, sql).FirstOrDefault(); if (macroDto == null) @@ -41,28 +54,31 @@ namespace Umbraco.Core.Persistence.Repositories return entity; } + public IEnumerable GetAll(params Guid[] ids) + { + return ids.Length > 0 ? ids.Select(Get) : GetAllNoIds(); + } + + public bool Exists(Guid id) + { + return Get(id) != null; + } + protected override IEnumerable PerformGetAll(params int[] ids) { - if (ids.Any()) - { - return PerformGetAllOnIds(ids); - } + return ids.Length > 0 ? ids.Select(Get) : GetAllNoIds(); + } - var sql = GetBaseQuery(false); + private IEnumerable GetAllNoIds() + { + var sql = GetBaseQuery(false) + //must be sorted this way for the relator to work + .OrderBy(x => x.Id, SqlSyntax); return ConvertFromDtos(Database.Fetch(new MacroPropertyRelator().Map, sql)) .ToArray();// we don't want to re-iterate again! } - private IEnumerable PerformGetAllOnIds(params int[] ids) - { - if (ids.Any() == false) yield break; - foreach (var id in ids) - { - yield return Get(id); - } - } - private IEnumerable ConvertFromDtos(IEnumerable dtos) { var factory = new MacroFactory(); @@ -82,6 +98,9 @@ namespace Umbraco.Core.Persistence.Repositories var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); + //must be sorted this way for the relator to work + sql.OrderBy(x => x.Id, SqlSyntax); + var dtos = Database.Fetch(new MacroPropertyRelator().Map, sql); foreach (var dto in dtos) @@ -104,13 +123,13 @@ namespace Umbraco.Core.Persistence.Repositories return sql; } - private static Sql GetBaseQuery() + private Sql GetBaseQuery() { var sql = new Sql(); sql.Select("*") - .From() - .LeftJoin() - .On(left => left.Id, right => right.Macro); + .From() + .LeftJoin() + .On(left => left.Id, right => right.Macro); return sql; } @@ -124,7 +143,7 @@ namespace Umbraco.Core.Persistence.Repositories var list = new List { "DELETE FROM cmsMacroProperty WHERE macro = @Id", - "DELETE FROM cmsMacro WHERE id = @Id" + "DELETE FROM cmsMacro WHERE id = @Id" }; return list; } @@ -146,7 +165,7 @@ namespace Umbraco.Core.Persistence.Repositories foreach (var propDto in dto.MacroPropertyDtos) { - //need to set the id explicitly here + // need to set the id explicitly here propDto.Macro = id; var propId = Convert.ToInt32(Database.Insert(propDto)); entity.Properties[propDto.Alias].Id = propId; @@ -165,71 +184,60 @@ namespace Umbraco.Core.Persistence.Repositories Database.Update(dto); //update the properties if they've changed - var macro = (Macro)entity; + var macro = (Macro) entity; if (macro.IsPropertyDirty("Properties") || macro.Properties.Any(x => x.IsDirty())) { - //now we need to delete any props that have been removed - foreach (var propAlias in macro.RemovedProperties) - { - //delete the property - Database.Delete("WHERE macro=@macroId AND macroPropertyAlias=@propAlias", - new { macroId = macro.Id, propAlias = propAlias }); - } + var ids = dto.MacroPropertyDtos.Where(x => x.Id > 0).Select(x => x.Id).ToArray(); + if (ids.Length > 0) + Database.Delete("WHERE macro=@macro AND id NOT IN (@ids)", new { macro = dto.Id, ids }); + else + Database.Delete("WHERE macro=@macro", new { macro = dto.Id }); - //for any that exist on the object, we need to determine if we need to update or insert + // detect new aliases, replace with temp aliases + // this ensures that we don't have collisions, ever + var aliases = new Dictionary(); foreach (var propDto in dto.MacroPropertyDtos) { - if (macro.AddedProperties.Contains(propDto.Alias)) + var prop = macro.Properties.FirstOrDefault(x => x.Id == propDto.Id); + if (prop == null) throw new Exception("oops: property."); + if (propDto.Id == 0 || prop.IsPropertyDirty("Alias")) { - //we need to insert since this was added and re-assign the new id - var propId = Convert.ToInt32(Database.Insert(propDto)); - macro.Properties[propDto.Alias].Id = propId; + var tempAlias = Guid.NewGuid().ToString("N").Substring(0, 8); + aliases[tempAlias] = propDto.Alias; + propDto.Alias = tempAlias; + } + } + + // insert or update existing properties, with temp aliases + foreach (var propDto in dto.MacroPropertyDtos) + { + if (propDto.Id == 0) + { + // insert + propDto.Id = Convert.ToInt32(Database.Insert(propDto)); + macro.Properties[aliases[propDto.Alias]].Id = propDto.Id; } else { - //This will only work if the Alias hasn't been changed - if (macro.Properties.ContainsKey(propDto.Alias)) - { - //only update if it's dirty - if (macro.Properties[propDto.Alias].IsDirty()) - { - Database.Update(propDto); - } - } - else - { - var property = macro.Properties.FirstOrDefault(x => x.Id == propDto.Id); - if (property != null && property.IsDirty()) - { - Database.Update(propDto); - } - } + // update + var property = macro.Properties.FirstOrDefault(x => x.Id == propDto.Id); + if (property == null) throw new Exception("oops: property."); + if (property.IsDirty()) + Database.Update(propDto); } } - + // replace the temp aliases with the real ones + foreach (var propDto in dto.MacroPropertyDtos) + { + if (aliases.ContainsKey(propDto.Alias) == false) continue; + + propDto.Alias = aliases[propDto.Alias]; + Database.Update(propDto); + } } entity.ResetDirtyProperties(); } - - //public IEnumerable GetAll(params string[] aliases) - //{ - // if (aliases.Any()) - // { - // var q = new Query(); - // foreach (var alias in aliases) - // { - // q.Where(macro => macro.Alias == alias); - // } - - // var wheres = string.Join(" OR ", q.WhereClauses()); - // } - // else - // { - // return GetAll(new int[] {}); - // } - - //} } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 08c2f7e0be..c9f06c6c75 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -28,15 +28,15 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ContentXmlRepository _contentXmlRepository; private readonly ContentPreviewRepository _contentPreviewRepository; - public MediaRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection) + public MediaRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection) : base(work, cache, logger, sqlSyntax, contentSection) { if (mediaTypeRepository == null) throw new ArgumentNullException("mediaTypeRepository"); if (tagRepository == null) throw new ArgumentNullException("tagRepository"); _mediaTypeRepository = mediaTypeRepository; _tagRepository = tagRepository; - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.NoCache, logger, sqlSyntax); + _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.NoCache, logger, sqlSyntax); EnsureUniqueNaming = contentSection.EnsureUniqueNaming; } @@ -55,7 +55,7 @@ namespace Umbraco.Core.Persistence.Repositories if (dto == null) return null; - var content = CreateMediaFromDto(dto, dto.VersionId, sql); + var content = CreateMediaFromDto(dto, sql); return content; } @@ -68,7 +68,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); } - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -76,26 +76,34 @@ namespace Umbraco.Core.Persistence.Repositories var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate() - .OrderBy(x => x.SortOrder); + .OrderBy(x => x.SortOrder, SqlSyntax); - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } #endregion #region Overrides of PetaPocoRepositoryBase + + protected override Sql GetBaseQuery(BaseQueryType queryType) + { + var sql = new Sql(); + sql.Select(queryType == BaseQueryType.Count ? "COUNT(*)" : (queryType == BaseQueryType.Ids ? "cmsContentVersion.contentId" : "*")) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId, SqlSyntax) + //TODO: IF we want to enable querying on content type information this will need to be joined + //.InnerJoin(SqlSyntax) + //.On(SqlSyntax, left => left.ContentTypeId, right => right.NodeId, SqlSyntax); + .Where(x => x.NodeObjectType == NodeObjectTypeId, SqlSyntax); + return sql; + } protected override Sql GetBaseQuery(bool isCount) { - var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); } protected override string GetBaseWhereClause() @@ -138,93 +146,106 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false) .Where(GetBaseWhereClause(), new { Id = id }) .OrderByDescending(x => x.VersionDate, SqlSyntax); - return ProcessQuery(sql, true); + return ProcessQuery(sql, new PagingSqlQuery(sql), true); } - private IEnumerable ProcessQuery(Sql sql, bool withCache = false) + /// + /// This is the underlying method that processes most queries for this repository + /// + /// + /// The full SQL to select all media data + /// + /// + /// The Id SQL to just return all media ids - used to process the properties for the media item + /// + /// + /// + private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false) { // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sql); - var content = new IMedia[dtos.Count]; - var defs = new List(); + var dtos = Database.Fetch(sqlFull); + + //This is a tuple list identifying if the content item came from the cache or not + var content = new List>(); + var defs = new DocumentDefinitionCollection(); - for (var i = 0; i < dtos.Count; i++) + //track the looked up content types, even though the content types are cached + // they still need to be deep cloned out of the cache and we don't want to add + // the overhead of deep cloning them on every item in this loop + var contentTypes = new Dictionary(); + + foreach (var dto in dtos) { - var dto = dtos[i]; - // if the cache contains the item, use it if (withCache) { - var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - if (cached != null) + var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + //only use this cached version if the dto returned is the same version - this is just a safety check, media doesn't + //store different versions, but just in case someone corrupts some data we'll double check to be sure. + if (cached != null && cached.Version == dto.VersionId) { - content[i] = cached; + content.Add(new Tuple(cached, true)); continue; } } // else, need to fetch from the database // content type repository is full-cache so OK to get each one independently - var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - content[i] = factory.BuildEntity(dto); - // need properties - defs.Add(new DocumentDefinition( - dto.NodeId, - dto.VersionId, - dto.VersionDate, - dto.ContentDto.NodeDto.CreateDate, - contentType - )); + IMediaType contentType; + if (contentTypes.ContainsKey(dto.ContentDto.ContentTypeId)) + { + contentType = contentTypes[dto.ContentDto.ContentTypeId]; + } + else + { + contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); + contentTypes[dto.ContentDto.ContentTypeId] = contentType; + } + + // track the definition and if it's successfully added or updated then processed + if (defs.AddOrUpdate(new DocumentDefinition(dto, contentType))) + { + content.Add(new Tuple(MediaFactory.BuildEntity(dto, contentType), false)); + } } // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(sql, defs); + var propertyData = GetPropertyCollection(pagingSqlQuery, defs); - // assign - var dtoIndex = 0; - foreach (var def in defs) + // assign property data + foreach (var contentItem in content) { - // move to corresponding item (which has to exist) - while (dtos[dtoIndex].NodeId != def.Id) dtoIndex++; + var cc = contentItem.Item1; + var fromCache = contentItem.Item2; - // complete the item - var cc = content[dtoIndex]; - cc.Properties = propertyData[cc.Id]; + //if this has come from cache, we do not need to build up it's structure + if (fromCache) continue; + + cc.Properties = propertyData[cc.Version]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 - ((Entity) cc).ResetDirtyProperties(false); + cc.ResetDirtyProperties(false); } - return content; + return content.Select(x => x.Item1).ToArray(); } public override IMedia GetByVersion(Guid versionId) { var sql = GetBaseQuery(false); sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate); + sql.OrderByDescending(x => x.VersionDate, SqlSyntax); var dto = Database.Fetch(sql).FirstOrDefault(); if (dto == null) return null; - var mediaType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); + var content = CreateMediaFromDto(dto, sql); - var factory = new MediaFactory(mediaType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.VersionId, media.UpdateDate, media.CreateDate, mediaType) }); - - media.Properties = properties[dto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; + return content; } public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) @@ -242,12 +263,15 @@ namespace Umbraco.Core.Persistence.Repositories // get the next group of nodes var query = GetBaseQuery(false); if (contentTypeIdsA.Length > 0) - query = query - .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + { + query = query.WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + } query = query - .Where(x => x.NodeId > baseId) + .Where(x => x.NodeId > baseId, SqlSyntax) + .Where(x => x.Trashed == false, SqlSyntax) .OrderBy(x => x.NodeId, SqlSyntax); - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) + var sql = SqlSyntax.SelectTop(query, groupSize); + var xmlItems = ProcessQuery(sql, new PagingSqlQuery(sql)) .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) .ToList(); @@ -268,7 +292,38 @@ namespace Umbraco.Core.Persistence.Repositories Logger.Error("Could not rebuild XML for nodeId=" + xmlItem.NodeId, e); } } - baseId = xmlItems.Last().NodeId; + baseId = xmlItems[xmlItems.Count - 1].NodeId; + } + + //now delete the items that shouldn't be there + var allMediaIds = Database.Fetch(GetBaseQuery(BaseQueryType.Ids).Where(x => x.Trashed == false, SqlSyntax)); + var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); + var xmlIdsQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId); + + if (contentTypeIdsA.Length > 0) + { + xmlIdsQuery.InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.ContentTypeId) + .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + } + + xmlIdsQuery.Where(dto => dto.NodeObjectType == mediaObjectType, SqlSyntax); + + var allXmlIds = Database.Fetch(xmlIdsQuery); + + var toRemove = allXmlIds.Except(allMediaIds).ToArray(); + if (toRemove.Length > 0) + { + foreach (var idGroup in toRemove.InGroupsOf(2000)) + { + Database.Execute("DELETE FROM cmsContentXml WHERE nodeId IN (@ids)", new { ids = idGroup }); + } } } @@ -277,6 +332,11 @@ namespace Umbraco.Core.Persistence.Repositories _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); } + public void DeleteContentXml(IMedia content) + { + _contentXmlRepository.Delete(new ContentXmlEntity(content)); + } + public void AddOrUpdatePreviewXml(IMedia content, Func xml) { _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); @@ -475,76 +535,43 @@ namespace Umbraco.Core.Persistence.Repositories /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null) { - var args = new List(); - var sbWhere = new StringBuilder(); - Func> filterCallback = null; - if (filter.IsNullOrWhiteSpace() == false) + var filterSql = new Sql(); + if (filter != null) { - sbWhere - .Append("AND (") - .Append(SqlSyntax.GetQuotedTableName("umbracoNode")) - .Append(".") - .Append(SqlSyntax.GetQuotedColumnName("text")) - .Append(" LIKE @") - .Append(args.Count) - .Append(")"); - args.Add("%" + filter + "%"); - - filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); + foreach (var filterClaus in filter.GetWhereClauses()) + { + filterSql.Append(string.Format("AND ({0})", filterClaus.Item1), filterClaus.Item2); + } } - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); + + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsContentVersion", "contentId"), - sql => ProcessQuery(sql), orderBy, orderDirection, orderBySystemField, + (sqlFull, pagingSqlQuery) => ProcessQuery(sqlFull, pagingSqlQuery), orderBy, orderDirection, orderBySystemField, filterCallback); } - /// - /// Gets paged media descendants as XML by path - /// - /// Path starts with - /// Page number - /// Page size - /// Total records the query would return without paging - /// A paged enumerable of XML entries of media items - public IEnumerable GetPagedXmlEntriesByPath(string path, long pageIndex, int pageSize, out long totalRecords) - { - Sql query; - if (path == "-1") - { - query = new Sql().Select("nodeId, xml").From("cmsContentXml").Where("nodeId IN (SELECT id FROM umbracoNode WHERE nodeObjectType = @0)", Guid.Parse(Constants.ObjectTypes.Media)).OrderBy("nodeId"); - } - else - { - query = new Sql().Select("nodeId, xml").From("cmsContentXml").Where("nodeId IN (SELECT id FROM umbracoNode WHERE path LIKE @0)", path.EnsureEndsWith(",%")).OrderBy("nodeId"); - } - var pagedResult = Database.Page(pageIndex+1, pageSize, query); - totalRecords = pagedResult.TotalItems; - return pagedResult.Items.Select(dto => XElement.Parse(dto.Xml)); - } - /// /// Private method to create a media object from a ContentDto /// - /// - /// + /// /// /// - private IMedia CreateMediaFromDto(ContentVersionDto dto, Guid versionId, Sql docSql) + private IMedia CreateMediaFromDto(ContentVersionDto dto, Sql docSql) { var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); + + var media = MediaFactory.BuildEntity(dto, contentType); - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); + var docDef = new DocumentDefinition(dto, contentType); - var docDef = new DocumentDefinition(dto.NodeId, versionId, media.UpdateDate, media.CreateDate, contentType); + var properties = GetPropertyCollection(new PagingSqlQuery(docSql), new[] { docDef }); - var properties = GetPropertyCollection(docSql, new[] { docDef }); - - media.Properties = properties[dto.NodeId]; + media.Properties = properties[dto.VersionId]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index 50a89bfd65..11542673be 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -22,23 +22,14 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class MediaTypeRepository : ContentTypeBaseRepository, IMediaTypeRepository { - - public MediaTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public MediaTypeRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), - //allow this cache to expire - expires: true)); - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ true); } protected override IMediaType PerformGet(int id) diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs index 2c0e910428..e9438b56f6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Repositories internal class MemberGroupRepository : PetaPocoRepositoryBase, IMemberGroupRepository { - public MemberGroupRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public MemberGroupRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } @@ -131,7 +131,7 @@ namespace Umbraco.Core.Persistence.Repositories public IMemberGroup GetByName(string name) { - return RuntimeCache.GetCacheItem( + return IsolatedCache.GetCacheItem( string.Format("{0}.{1}", typeof (IMemberGroup).FullName, name), () => { @@ -147,30 +147,24 @@ namespace Umbraco.Core.Persistence.Repositories public IMemberGroup CreateIfNotExists(string roleName) { - using (var transaction = Database.GetTransaction()) + var qry = new Query().Where(group => group.Name.Equals(roleName)); + var result = GetByQuery(qry); + + if (result.Any()) return null; + + var grp = new MemberGroup { - var qry = new Query().Where(group => group.Name.Equals(roleName)); - var result = GetByQuery(qry); + Name = roleName + }; - if (result.Any()) return null; + PersistNewItem(grp); - var grp = new MemberGroup - { - Name = roleName - }; - PersistNewItem(grp); + if (UnitOfWork.Events.DispatchCancelable(SavingMemberGroup, this, new SaveEventArgs(grp))) + return null; - if (SavingMemberGroup.IsRaisedEventCancelled(new SaveEventArgs(grp), this)) - { - return null; - } + UnitOfWork.Events.Dispatch(SavedMemberGroup, this, new SaveEventArgs(grp)); - transaction.Complete(); - - SavedMemberGroup.RaiseEvent(new SaveEventArgs(grp), this); - - return grp; - } + return grp; } public IEnumerable GetMemberGroupsForMember(int memberId) @@ -221,51 +215,39 @@ namespace Umbraco.Core.Persistence.Repositories public void AssignRoles(string[] usernames, string[] roleNames) { - using (var transaction = Database.GetTransaction()) - { - //first get the member ids based on the usernames - var memberSql = new Sql(); - var memberObjectType = new Guid(Constants.ObjectTypes.Member); - memberSql.Select("umbracoNode.id") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(x => x.NodeObjectType == memberObjectType) - .Where("cmsMember.LoginName in (@usernames)", new { usernames = usernames }); - var memberIds = Database.Fetch(memberSql).ToArray(); + //first get the member ids based on the usernames + var memberSql = new Sql(); + var memberObjectType = new Guid(Constants.ObjectTypes.Member); + memberSql.Select("umbracoNode.id") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(x => x.NodeObjectType == memberObjectType) + .Where("cmsMember.LoginName in (@usernames)", new { usernames = usernames }); + var memberIds = Database.Fetch(memberSql).ToArray(); - AssignRolesInternal(memberIds, roleNames); - transaction.Complete(); - } + AssignRolesInternal(memberIds, roleNames); } public void DissociateRoles(string[] usernames, string[] roleNames) { - using (var transaction = Database.GetTransaction()) - { - //first get the member ids based on the usernames - var memberSql = new Sql(); - var memberObjectType = new Guid(Constants.ObjectTypes.Member); - memberSql.Select("umbracoNode.id") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(x => x.NodeObjectType == memberObjectType) - .Where("cmsMember.LoginName in (@usernames)", new { usernames = usernames }); - var memberIds = Database.Fetch(memberSql).ToArray(); + //first get the member ids based on the usernames + var memberSql = new Sql(); + var memberObjectType = new Guid(Constants.ObjectTypes.Member); + memberSql.Select("umbracoNode.id") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(x => x.NodeObjectType == memberObjectType) + .Where("cmsMember.LoginName in (@usernames)", new { usernames = usernames }); + var memberIds = Database.Fetch(memberSql).ToArray(); - DissociateRolesInternal(memberIds, roleNames); - transaction.Complete(); - } + DissociateRolesInternal(memberIds, roleNames); } public void AssignRoles(int[] memberIds, string[] roleNames) { - using (var transaction = Database.GetTransaction()) - { - AssignRolesInternal(memberIds, roleNames); - transaction.Complete(); - } + AssignRolesInternal(memberIds, roleNames); } public void AssignRolesInternal(int[] memberIds, string[] roleNames) @@ -284,15 +266,13 @@ namespace Umbraco.Core.Persistence.Repositories var missingRoles = roleNames.Except(existingRoles); var missingGroups = missingRoles.Select(x => new MemberGroup {Name = x}).ToArray(); - if (SavingMemberGroup.IsRaisedEventCancelled(new SaveEventArgs(missingGroups), this)) - { + if (UnitOfWork.Events.DispatchCancelable(SavingMemberGroup, this, new SaveEventArgs(missingGroups))) return; - } + foreach (var m in missingGroups) - { PersistNewItem(m); - } - SavedMemberGroup.RaiseEvent(new SaveEventArgs(missingGroups), this); + + UnitOfWork.Events.Dispatch(SavedMemberGroup, this, new SaveEventArgs(missingGroups)); //now go get all the dto's for roles with these role names var rolesForNames = Database.Fetch(existingSql).ToArray(); @@ -333,11 +313,7 @@ namespace Umbraco.Core.Persistence.Repositories public void DissociateRoles(int[] memberIds, string[] roleNames) { - using (var transaction = Database.GetTransaction()) - { - DissociateRolesInternal(memberIds, roleNames); - transaction.Complete(); - } + DissociateRolesInternal(memberIds, roleNames); } private void DissociateRolesInternal(int[] memberIds, string[] roleNames) diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index dcab898685..2ef795282b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -29,7 +29,7 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ContentXmlRepository _contentXmlRepository; private readonly ContentPreviewRepository _contentPreviewRepository; - public MemberRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, IContentSection contentSection) + public MemberRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, IContentSection contentSection) : base(work, cache, logger, sqlSyntax, contentSection) { if (memberTypeRepository == null) throw new ArgumentNullException("memberTypeRepository"); @@ -37,8 +37,8 @@ namespace Umbraco.Core.Persistence.Repositories _memberTypeRepository = memberTypeRepository; _tagRepository = tagRepository; _memberGroupRepository = memberGroupRepository; - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.NoCache, logger, sqlSyntax); + _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.NoCache, logger, sqlSyntax); } #region Overrides of RepositoryBase @@ -54,7 +54,7 @@ namespace Umbraco.Core.Persistence.Repositories if (dto == null) return null; - var content = CreateMemberFromDto(dto, dto.ContentVersionDto.VersionId, sql); + var content = CreateMemberFromDto(dto, sql); return content; @@ -68,7 +68,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); } - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } @@ -90,7 +90,7 @@ namespace Umbraco.Core.Persistence.Repositories baseQuery.Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)) .OrderBy(x => x.SortOrder); - return ProcessQuery(baseQuery); + return ProcessQuery(baseQuery, new PagingSqlQuery(baseQuery)); } else { @@ -98,7 +98,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = translator.Translate() .OrderBy(x => x.SortOrder); - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } } @@ -107,24 +107,29 @@ namespace Umbraco.Core.Persistence.Repositories #region Overrides of PetaPocoRepositoryBase - protected override Sql GetBaseQuery(bool isCount) + protected override Sql GetBaseQuery(BaseQueryType queryType) { var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + sql.Select(queryType == BaseQueryType.Count ? "COUNT(*)" : (queryType == BaseQueryType.Ids ? "cmsMember.nodeId" : "*")) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) //We're joining the type so we can do a query against the member type - not sure if this adds much overhead or not? // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content // types by default on the document and media repo's so we can query by content type there too. - .InnerJoin().On(left => left.NodeId, right => right.ContentTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.ContentTypeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId, SqlSyntax); return sql; + } + protected override Sql GetBaseQuery(bool isCount) + { + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); } protected override string GetBaseWhereClause() @@ -380,8 +385,8 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false) .Where(GetBaseWhereClause(), new { Id = id }) .OrderByDescending(x => x.VersionDate, SqlSyntax); - return ProcessQuery(sql, true); - } + return ProcessQuery(sql, new PagingSqlQuery(sql), true); + } public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) { @@ -403,7 +408,8 @@ namespace Umbraco.Core.Persistence.Repositories query = query .Where(x => x.NodeId > baseId) .OrderBy(x => x.NodeId, SqlSyntax); - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) + var sql = SqlSyntax.SelectTop(query, groupSize); + var xmlItems = ProcessQuery(sql, new PagingSqlQuery(sql)) .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) .ToList(); @@ -442,16 +448,16 @@ namespace Umbraco.Core.Persistence.Repositories var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); + var member = factory.BuildEntity(dto); - var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.ContentVersionDto.VersionId, media.UpdateDate, media.CreateDate, memberType) }); + var properties = GetPropertyCollection(new PagingSqlQuery(sql), new[] { new DocumentDefinition(dto.ContentVersionDto, memberType) }); - media.Properties = properties[dto.NodeId]; + member.Properties = properties[dto.ContentVersionDto.VersionId]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; + ((Entity)member).ResetDirtyProperties(false); + return member; } @@ -535,7 +541,7 @@ namespace Umbraco.Core.Persistence.Repositories .OrderByDescending(x => x.VersionDate) .OrderBy(x => x.SortOrder); - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } @@ -581,45 +587,22 @@ namespace Umbraco.Core.Persistence.Repositories /// The query supplied will ONLY work with data specifically on the cmsMember table because we are using PetaPoco paging (SQL paging) /// public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null) { - var args = new List(); - var sbWhere = new StringBuilder(); - Func> filterCallback = null; - if (filter.IsNullOrWhiteSpace() == false) + var filterSql = new Sql(); + if (filter != null) { - //This will build up the where clause - even though the same 'filter' is being - //applied to both columns, the parameters values passed to PetaPoco need to be - //duplicated, otherwise it gets confused :/ - var columnFilters = new List> + foreach (var filterClaus in filter.GetWhereClauses()) { - new Tuple("umbracoNode", "text"), - new Tuple("cmsMember", "LoginName") - }; - sbWhere.Append("AND ("); - for (int i = 0; i < columnFilters.Count; i++) - { - sbWhere - .Append("(") - .Append(SqlSyntax.GetQuotedTableName(columnFilters[i].Item1)) - .Append(".") - .Append(SqlSyntax.GetQuotedColumnName(columnFilters[i].Item2)) - .Append(" LIKE @") - .Append(args.Count) - .Append(") "); - args.Add(string.Format("%{0}%", filter)); - if (i < (columnFilters.Count - 1)) - { - sbWhere.Append("OR "); - } + filterSql.Append(string.Format("AND ({0})", filterClaus.Item1), filterClaus.Item2); } - sbWhere.Append(")"); - filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); } - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); + + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsMember", "nodeId"), - sql => ProcessQuery(sql), orderBy, orderDirection, orderBySystemField, + (sqlFull, sqlIds) => ProcessQuery(sqlFull, sqlIds), orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -659,25 +642,37 @@ namespace Umbraco.Core.Persistence.Repositories return base.GetEntityPropertyNameForOrderBy(orderBy); } - private IEnumerable ProcessQuery(Sql sql, bool withCache = false) + /// + /// This is the underlying method that processes most queries for this repository + /// + /// + /// The full SQL to select all member data + /// + /// + /// The Id SQL to just return all member ids - used to process the properties for the member item + /// + /// + /// + private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false) { // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sql); + var dtos = Database.Fetch(sqlFull); - var content = new IMember[dtos.Count]; - var defs = new List(); + //This is a tuple list identifying if the content item came from the cache or not + var content = new List>(); + var defs = new DocumentDefinitionCollection(); - for (var i = 0; i < dtos.Count; i++) + foreach (var dto in dtos) { - var dto = dtos[i]; - // if the cache contains the item, use it if (withCache) { - var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - if (cached != null) + var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + //only use this cached version if the dto returned is the same version - this is just a safety check, members dont + //store different versions, but just in case someone corrupts some data we'll double check to be sure. + if (cached != null && cached.Version == dto.ContentVersionDto.VersionId) { - content[i] = cached; + content.Add(new Tuple(cached, true)); continue; } } @@ -685,60 +680,54 @@ namespace Umbraco.Core.Persistence.Repositories // else, need to fetch from the database // content type repository is full-cache so OK to get each one independently var contentType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - var factory = new MemberFactory(contentType, NodeObjectTypeId, dto.NodeId); - content[i] = factory.BuildEntity(dto); // need properties - defs.Add(new DocumentDefinition( - dto.NodeId, - dto.ContentVersionDto.VersionId, - dto.ContentVersionDto.VersionDate, - dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, - contentType - )); + if (defs.AddOrUpdate(new DocumentDefinition(dto.ContentVersionDto, contentType))) + { + content.Add(new Tuple(MemberFactory.BuildEntity(dto, contentType), false)); + } } // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(sql, defs); + var propertyData = GetPropertyCollection(pagingSqlQuery, defs); - // assign - var dtoIndex = 0; - foreach (var def in defs) + // assign property data + foreach (var contentItem in content) { - // move to corresponding item (which has to exist) - while (dtos[dtoIndex].NodeId != def.Id) dtoIndex++; + var cc = contentItem.Item1; + var fromCache = contentItem.Item2; - // complete the item - var cc = content[dtoIndex]; - cc.Properties = propertyData[cc.Id]; + //if this has come from cache, we do not need to build up it's structure + if (fromCache) continue; + + cc.Properties = propertyData[cc.Version]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 - ((Entity)cc).ResetDirtyProperties(false); + cc.ResetDirtyProperties(false); } - return content; + return content.Select(x => x.Item1).ToArray(); } /// /// Private method to create a member object from a MemberDto /// /// - /// /// /// - private IMember CreateMemberFromDto(MemberDto dto, Guid versionId, Sql docSql) + private IMember CreateMemberFromDto(MemberDto dto, Sql docSql) { var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.ContentVersionDto.NodeId); var member = factory.BuildEntity(dto); - var docDef = new DocumentDefinition(dto.ContentVersionDto.NodeId, versionId, member.UpdateDate, member.CreateDate, memberType); + var docDef = new DocumentDefinition(dto.ContentVersionDto, memberType); var properties = GetPropertyCollection(docSql, new[] { docDef }); - member.Properties = properties[dto.ContentVersionDto.NodeId]; + member.Properties = properties[dto.ContentVersionDto.VersionId]; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index d06399a85b..8d489644b5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -21,25 +21,16 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class MemberTypeRepository : ContentTypeBaseRepository, IMemberTypeRepository { - - public MemberTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public MemberTypeRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), - //allow this cache to expire - expires: true)); - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ true); } - + protected override IMemberType PerformGet(int id) { //use the underlying GetAll which will force cache all content types @@ -55,7 +46,9 @@ namespace Umbraco.Core.Persistence.Repositories var statement = string.Join(" OR ", ids.Select(x => string.Format("umbracoNode.id='{0}'", x))); sql.Where(statement); } - sql.OrderByDescending(x => x.NodeId, SqlSyntax); + + //must be sorted this way for the relator to work + sql.OrderBy(x => x.UniqueId, SqlSyntax); var dtos = Database.Fetch( @@ -71,7 +64,8 @@ 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, SqlSyntax); + //must be sorted this way for the relator to work + .OrderBy(x => x.UniqueId, SqlSyntax); var dtos = Database.Fetch( @@ -300,13 +294,19 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - private static IEnumerable BuildFromDtos(List dtos) + private IEnumerable BuildFromDtos(List dtos) { if (dtos == null || dtos.Any() == false) return Enumerable.Empty(); var factory = new MemberTypeReadOnlyFactory(); - return dtos.Select(factory.BuildEntity); + return dtos.Select(x => + { + bool needsSaving; + var memberType = factory.BuildEntity(x, out needsSaving); + if (needsSaving) PersistUpdatedItem(memberType); + return memberType; + }).ToList(); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs index b4a21f5d21..bd5db622d6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class MigrationEntryRepository : PetaPocoRepositoryBase, IMigrationEntryRepository { - public MigrationEntryRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public MigrationEntryRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs b/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs index 9e6e3cf47c..aa7ebeacc8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - internal class NotificationsRepository + internal class NotificationsRepository : IDisposable { private readonly IDatabaseUnitOfWork _unitOfWork; @@ -102,5 +102,10 @@ namespace Umbraco.Core.Persistence.Repositories _unitOfWork.Database.Insert(dto); return new Notification(dto.NodeId, dto.UserId, dto.Action, nodeType); } + + public void Dispose() + { + _unitOfWork.Dispose(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs index e055c3dd93..ea79ff6b82 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs @@ -1,5 +1,4 @@ -using System.Threading; -using Umbraco.Core.IO; +using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.UnitOfWork; @@ -7,15 +6,9 @@ namespace Umbraco.Core.Persistence.Repositories { internal class PartialViewMacroRepository : PartialViewRepository { - public PartialViewMacroRepository(IUnitOfWork work) - : this(work, new PhysicalFileSystem(SystemDirectories.MvcViews + "/MacroPartials/")) - { - } - public PartialViewMacroRepository(IUnitOfWork work, IFileSystem fileSystem) : base(work, fileSystem) - { - } + { } protected override PartialViewType ViewType { get { return PartialViewType.PartialViewMacro; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs index e9352a945f..63cfd53833 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -11,14 +10,9 @@ namespace Umbraco.Core.Persistence.Repositories { internal class PartialViewRepository : FileRepository, IPartialViewRepository { - public PartialViewRepository(IUnitOfWork work) - : this(work, new PhysicalFileSystem(SystemDirectories.MvcViews + "/Partials/")) - { - } - - public PartialViewRepository(IUnitOfWork work, IFileSystem fileSystem) : base(work, fileSystem) - { - } + public PartialViewRepository(IUnitOfWork work, IFileSystem fileSystem) + : base(work, fileSystem) + { } protected virtual PartialViewType ViewType { get { return PartialViewType.PartialView; } } @@ -113,6 +107,25 @@ namespace Umbraco.Core.Persistence.Repositories return isValidPath && isValidExtension; } + public Stream GetFileContentStream(string filepath) + { + if (FileSystem.FileExists(filepath) == false) return null; + + try + { + return FileSystem.OpenFile(filepath); + } + catch + { + return null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) + { + FileSystem.AddFile(filepath, content, true); + } + /// /// Gets a stream that is used to write to the file /// diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index aa671dccec..3d6acb777b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -25,11 +25,11 @@ namespace Umbraco.Core.Persistence.Repositories internal class PermissionRepository where TEntity : class, IAggregateRoot { - private readonly IDatabaseUnitOfWork _unitOfWork; + private readonly IScopeUnitOfWork _unitOfWork; private readonly IRuntimeCacheProvider _runtimeCache; private readonly ISqlSyntaxProvider _sqlSyntax; - internal PermissionRepository(IDatabaseUnitOfWork unitOfWork, CacheHelper cache, ISqlSyntaxProvider sqlSyntax) + internal PermissionRepository(IScopeUnitOfWork unitOfWork, CacheHelper cache, ISqlSyntaxProvider sqlSyntax) { _unitOfWork = unitOfWork; //Make this repository use an isolated cache @@ -126,37 +126,32 @@ namespace Umbraco.Core.Persistence.Repositories public void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds) { var db = _unitOfWork.Database; - using (var trans = db.GetTransaction()) + + //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit + foreach (var idGroup in entityIds.InGroupsOf(2000)) { - //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit - foreach (var idGroup in entityIds.InGroupsOf(2000)) - { - db.Execute("DELETE FROM umbracoUser2NodePermission WHERE userId=@userId AND nodeId in (@nodeIds)", - new { userId = userId, nodeIds = idGroup }); - } - - var toInsert = new List(); - foreach (var p in permissions) - { - foreach (var e in entityIds) - { - toInsert.Add(new User2NodePermissionDto - { - NodeId = e, - Permission = p.ToString(CultureInfo.InvariantCulture), - UserId = userId - }); - } - } - - _unitOfWork.Database.BulkInsertRecords(toInsert, trans); - - trans.Complete(); - - //Raise the event - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(toInsert), false), this); + db.Execute("DELETE FROM umbracoUser2NodePermission WHERE userId=@userId AND nodeId in (@nodeIds)", + new { userId = userId, nodeIds = idGroup }); } + + var toInsert = new List(); + foreach (var p in permissions) + { + foreach (var e in entityIds) + { + toInsert.Add(new User2NodePermissionDto + { + NodeId = e, + Permission = p.ToString(CultureInfo.InvariantCulture), + UserId = userId + }); + } + } + + _unitOfWork.Database.BulkInsertRecords(toInsert, _sqlSyntax); + + //Raise the event + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(toInsert), false)); } /// @@ -168,31 +163,25 @@ namespace Umbraco.Core.Persistence.Repositories public void AssignUserPermission(int userId, char permission, params int[] entityIds) { var db = _unitOfWork.Database; - using (var trans = db.GetTransaction()) - { - db.Execute("DELETE FROM umbracoUser2NodePermission WHERE userId=@userId AND permission=@permission AND nodeId in (@entityIds)", - new - { - userId = userId, - permission = permission.ToString(CultureInfo.InvariantCulture), - entityIds = entityIds - }); - - var actions = entityIds.Select(id => new User2NodePermissionDto + db.Execute("DELETE FROM umbracoUser2NodePermission WHERE userId=@userId AND permission=@permission AND nodeId in (@entityIds)", + new { - NodeId = id, - Permission = permission.ToString(CultureInfo.InvariantCulture), - UserId = userId - }).ToArray(); + userId = userId, + permission = permission.ToString(CultureInfo.InvariantCulture), + entityIds = entityIds + }); - _unitOfWork.Database.BulkInsertRecords(actions, trans); + var actions = entityIds.Select(id => new User2NodePermissionDto + { + NodeId = id, + Permission = permission.ToString(CultureInfo.InvariantCulture), + UserId = userId + }).ToArray(); - trans.Complete(); - - //Raise the event - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this); - } + _unitOfWork.Database.BulkInsertRecords(actions, _sqlSyntax); + + //Raise the event + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); } /// @@ -204,31 +193,25 @@ namespace Umbraco.Core.Persistence.Repositories public void AssignEntityPermission(TEntity entity, char permission, IEnumerable userIds) { var db = _unitOfWork.Database; - using (var trans = db.GetTransaction()) - { - db.Execute("DELETE FROM umbracoUser2NodePermission WHERE nodeId=@nodeId AND permission=@permission AND userId in (@userIds)", + db.Execute("DELETE FROM umbracoUser2NodePermission WHERE nodeId=@nodeId AND permission=@permission AND userId in (@userIds)", new { - nodeId = entity.Id, + nodeId = entity.Id, permission = permission.ToString(CultureInfo.InvariantCulture), userIds = userIds }); - var actions = userIds.Select(id => new User2NodePermissionDto - { - NodeId = entity.Id, - Permission = permission.ToString(CultureInfo.InvariantCulture), - UserId = id - }).ToArray(); + var actions = userIds.Select(id => new User2NodePermissionDto + { + NodeId = entity.Id, + Permission = permission.ToString(CultureInfo.InvariantCulture), + UserId = id + }).ToArray(); - _unitOfWork.Database.BulkInsertRecords(actions, trans); + _unitOfWork.Database.BulkInsertRecords(actions, _sqlSyntax); - trans.Complete(); - - //Raise the event - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this); - } + //Raise the event + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); } /// @@ -242,25 +225,19 @@ namespace Umbraco.Core.Persistence.Repositories public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) { var db = _unitOfWork.Database; - using (var trans = db.GetTransaction()) + db.Execute("DELETE FROM umbracoUser2NodePermission WHERE nodeId=@nodeId", new { nodeId = permissionSet.EntityId }); + + var actions = permissionSet.UserPermissionsSet.Select(p => new User2NodePermissionDto { - db.Execute("DELETE FROM umbracoUser2NodePermission WHERE nodeId=@nodeId", new { nodeId = permissionSet.EntityId }); + NodeId = permissionSet.EntityId, + Permission = p.Permission, + UserId = p.UserId + }).ToArray(); - var actions = permissionSet.UserPermissionsSet.Select(p => new User2NodePermissionDto - { - NodeId = permissionSet.EntityId, - Permission = p.Permission, - UserId = p.UserId - }).ToArray(); - - _unitOfWork.Database.BulkInsertRecords(actions, trans); - - trans.Complete(); - - //Raise the event - AssignedPermissions.RaiseEvent( - new SaveEventArgs(ConvertToPermissionList(actions), false), this); - } + _unitOfWork.Database.BulkInsertRecords(actions, _sqlSyntax); + + //Raise the event + _unitOfWork.Events.Dispatch(AssignedPermissions, this, new SaveEventArgs(ConvertToPermissionList(actions), false)); } private static IEnumerable ConvertToPermissionList(IEnumerable result) diff --git a/src/Umbraco.Core/Persistence/Repositories/PetaPocoRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/PetaPocoRepositoryBase.cs index fe363fea16..afe1a78b68 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PetaPocoRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PetaPocoRepositoryBase.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Data.SqlServerCe; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; @@ -20,7 +19,7 @@ namespace Umbraco.Core.Persistence.Repositories { public ISqlSyntaxProvider SqlSyntax { get; private set; } - protected PetaPocoRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + protected PetaPocoRepositoryBase(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger) { if (sqlSyntax == null) throw new ArgumentNullException("sqlSyntax"); @@ -28,11 +27,11 @@ namespace Umbraco.Core.Persistence.Repositories } /// - /// Returns the database Unit of Work added to the repository + /// Returns the Scope Unit of Work added to the repository /// - protected internal new IDatabaseUnitOfWork UnitOfWork + protected internal new IScopeUnitOfWork UnitOfWork { - get { return (IDatabaseUnitOfWork)base.UnitOfWork; } + get { return base.UnitOfWork; } } protected UmbracoDatabase Database @@ -75,11 +74,7 @@ namespace Umbraco.Core.Persistence.Repositories { Database.Execute(delete, new { Id = GetEntityId(entity) }); } - } - - protected virtual TId GetEntityId(TEntity entity) - { - return (TId)(object)entity.Id; + entity.DeletedDate = DateTime.Now; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs index 37200b2172..f38a882e92 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs @@ -15,19 +15,13 @@ namespace Umbraco.Core.Persistence.Repositories { internal class PublicAccessRepository : PetaPocoRepositoryBase, IPublicAccessRepository { - public PublicAccessRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public PublicAccessRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), false)); - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ false); } protected override PublicAccessEntry PerformGet(Guid id) @@ -44,10 +38,12 @@ namespace Umbraco.Core.Persistence.Repositories { sql.Where("umbracoAccess.id IN (@ids)", new { ids = ids }); } - - sql.OrderBy(x => x.NodeId, SqlSyntax); - + var factory = new PublicAccessEntryFactory(); + + //MUST be ordered by this GUID ID for the AccessRulesRelator to work + sql.OrderBy(dto => dto.Id, SqlSyntax); + var dtos = Database.Fetch(new AccessRulesRelator().Map, sql); return dtos.Select(factory.BuildEntity); } @@ -59,6 +55,10 @@ namespace Umbraco.Core.Persistence.Repositories var sql = translator.Translate(); var factory = new PublicAccessEntryFactory(); + + //MUST be ordered by this GUID ID for the AccessRulesRelator to work + sql.OrderBy(dto => dto.Id, SqlSyntax); + var dtos = Database.Fetch(new AccessRulesRelator().Map, sql); return dtos.Select(factory.BuildEntity); } @@ -70,6 +70,7 @@ namespace Umbraco.Core.Persistence.Repositories .From(SqlSyntax) .LeftJoin(SqlSyntax) .On(SqlSyntax, left => left.Id, right => right.AccessId); + return sql; } @@ -110,6 +111,11 @@ namespace Umbraco.Core.Persistence.Repositories { rule.AccessId = entity.Key; Database.Insert(rule); + //update the id so HasEntity is correct + var entityRule = entity.Rules.First(x => x.Key == rule.Id); + entityRule.Id = entityRule.Key.GetHashCode(); + //double make sure that this is set since it is possible to add rules via ctor without AddRule + entityRule.AccessEntryId = entity.Key; } entity.ResetDirtyProperties(); @@ -126,11 +132,16 @@ namespace Umbraco.Core.Persistence.Repositories Database.Update(dto); + foreach (var removedRule in entity.RemovedRules) + { + Database.Delete("WHERE id=@Id", new { Id = removedRule }); + } + foreach (var rule in entity.Rules) { if (rule.HasIdentity) { - var count = Database.Update(dto.Rules.Single(x => x.Id == rule.Key)); + var count = Database.Update(dto.Rules.First(x => x.Id == rule.Key)); if (count == 0) { throw new InvalidOperationException("No rows were updated for the access rule"); @@ -151,10 +162,8 @@ namespace Umbraco.Core.Persistence.Repositories rule.Id = rule.Key.GetHashCode(); } } - foreach (var removedRule in entity.RemovedRules) - { - Database.Delete("WHERE id=@Id", new {Id = removedRule}); - } + + entity.ClearRemovedRules(); entity.ResetDirtyProperties(); } @@ -164,4 +173,4 @@ namespace Umbraco.Core.Persistence.Repositories return entity.Key; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs index f17a0dd0d2..cb5f7c8810 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Persistence.Repositories internal abstract class RecycleBinRepository : VersionableRepositoryBase, IRecycleBinRepository where TEntity : class, IUmbracoEntity { - protected RecycleBinRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) + protected RecycleBinRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) : base(work, cache, logger, sqlSyntax, contentSection) { } @@ -63,6 +63,7 @@ namespace Umbraco.Core.Persistence.Repositories FormatDeleteStatement("cmsContentVersion", "ContentId"), FormatDeleteStatement("cmsContentXml", "nodeId"), FormatDeleteStatement("cmsContent", "nodeId"), + //TODO: Why is this being done? We just delete this exact data in the next line "UPDATE umbracoNode SET parentID = '" + RecycleBinId + "' WHERE trashed = '1' AND nodeObjectType = @NodeObjectType", "DELETE FROM umbracoNode WHERE trashed = '1' AND nodeObjectType = @NodeObjectType" }; @@ -91,14 +92,18 @@ namespace Umbraco.Core.Persistence.Repositories } } + /// + /// A delete statement that will delete anything in the table specified where its PK (keyName) is found in the + /// list of umbracoNode.id that have trashed flag set + /// + /// + /// + /// private string FormatDeleteStatement(string tableName, string keyName) { - //This query works with sql ce and sql server: - //DELETE FROM umbracoUser2NodeNotify WHERE umbracoUser2NodeNotify.nodeId IN - //(SELECT nodeId FROM umbracoUser2NodeNotify as TB1 INNER JOIN umbracoNode as TB2 ON TB1.nodeId = TB2.id WHERE TB2.trashed = '1' AND TB2.nodeObjectType = 'C66BA18E-EAF3-4CFF-8A22-41B16D66A972') return string.Format( - "DELETE FROM {0} WHERE {0}.{1} IN (SELECT TB1.{1} FROM {0} as TB1 INNER JOIN umbracoNode as TB2 ON TB1.{1} = TB2.id WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType)", + "DELETE FROM {0} WHERE {0}.{1} IN (SELECT id FROM umbracoNode WHERE trashed = '1' AND nodeObjectType = @NodeObjectType)", tableName, keyName); } diff --git a/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs index 5452e868a0..acab12a10a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class RedirectUrlRepository : PetaPocoRepositoryBase, IRedirectUrlRepository { - public RedirectUrlRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public RedirectUrlRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs index 4511ebe35d..30bdbaffbe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Repositories { private readonly IRelationTypeRepository _relationTypeRepository; - public RelationRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IRelationTypeRepository relationTypeRepository) + public RelationRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IRelationTypeRepository relationTypeRepository) : base(work, cache, logger, sqlSyntax) { _relationTypeRepository = relationTypeRepository; @@ -73,12 +73,16 @@ namespace Umbraco.Core.Persistence.Repositories RelationFactory factory = null; var relationTypeId = -1; + // the ToList() here is important because we are using _relationTypeRepository and we + // cannot wait until the result is actually enumerated to do so, because that would + // mean we kinda capture the current unit of work and reuse it after it's been disposed + return dtos.Select(x => { if (relationTypeId != x.RelationType) factory = new RelationFactory(_relationTypeRepository.Get(relationTypeId = x.RelationType)); return DtoToEntity(x, factory); - }); + }).ToList(); } private static IRelation DtoToEntity(RelationDto dto, RelationFactory factory) diff --git a/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs index c0a110feca..ba578b93f1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs @@ -19,21 +19,13 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class RelationTypeRepository : PetaPocoRepositoryBase, IRelationTypeRepository { - public RelationTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public RelationTypeRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } - // assuming we don't have tons of relation types, use a FullDataSet policy, ie - // cache the entire GetAll result once in a single collection - which can expire - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - return _cachePolicyFactory - ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), expires: true)); - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ true); } #region Overrides of RepositoryBase @@ -44,6 +36,17 @@ namespace Umbraco.Core.Persistence.Repositories return GetAll().FirstOrDefault(x => x.Id == id); } + public IRelationType Get(Guid id) + { + // use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Key == id); + } + + public bool Exists(Guid id) + { + return Get(id) != null; + } + protected override IEnumerable PerformGetAll(params int[] ids) { var sql = GetBaseQuery(false); @@ -57,6 +60,15 @@ namespace Umbraco.Core.Persistence.Repositories return dtos.Select(x => DtoToEntity(x, factory)); } + public IEnumerable GetAll(params Guid[] ids) + { + // should not happen due to the cache policy + if (ids.Any()) + throw new NotImplementedException(); + + return GetAll(new int[0]); + } + protected override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(false); diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index 41946d48d4..91f8173304 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -1,36 +1,35 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Cache; -using Umbraco.Core.Collections; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Repositories { internal abstract class RepositoryBase : DisposableObject { - private readonly IUnitOfWork _work; - private readonly CacheHelper _cache; + private readonly IScopeUnitOfWork _work; + private readonly CacheHelper _globalCache; - protected RepositoryBase(IUnitOfWork work, CacheHelper cache, ILogger logger) + protected RepositoryBase(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) { if (work == null) throw new ArgumentNullException("work"); if (cache == null) throw new ArgumentNullException("cache"); if (logger == null) throw new ArgumentNullException("logger"); Logger = logger; _work = work; - _cache = cache; + _globalCache = cache; } /// /// Returns the Unit of Work added to the repository /// - protected internal IUnitOfWork UnitOfWork + protected internal IScopeUnitOfWork UnitOfWork { get { return _work; } } @@ -43,18 +42,18 @@ namespace Umbraco.Core.Persistence.Repositories get { return (Guid)_work.Key; } } - protected CacheHelper RepositoryCache + /// + /// Gets the global application cache. + /// + protected CacheHelper GlobalCache { - get { return _cache; } + get { return _globalCache; } } /// - /// The runtime cache used for this repo - by standard this is the runtime cache exposed by the CacheHelper but can be overridden + /// Gets the repository isolated cache. /// - protected virtual IRuntimeCacheProvider RuntimeCache - { - get { return _cache.RuntimeCache; } - } + protected abstract IRuntimeCacheProvider IsolatedCache { get; } public static string GetCacheIdKey(object id) { @@ -77,15 +76,14 @@ namespace Umbraco.Core.Persistence.Repositories internal abstract class RepositoryBase : RepositoryBase, IRepositoryQueryable, IUnitOfWorkRepository where TEntity : class, IAggregateRoot { - protected RepositoryBase(IUnitOfWork work, CacheHelper cache, ILogger logger) + protected RepositoryBase(IScopeUnitOfWork work, CacheHelper cache, ILogger logger) : base(work, cache, logger) { } - #region Static Queries - private IQuery _hasIdQuery; + private static IQuery _hasIdQuery; #endregion @@ -97,38 +95,115 @@ namespace Umbraco.Core.Persistence.Repositories /// /// The runtime cache used for this repo by default is the isolated cache for this type /// - protected override IRuntimeCacheProvider RuntimeCache - { - get { return RepositoryCache.IsolatedRuntimeCache.GetOrCreateCache(); } - } - - private IRepositoryCachePolicyFactory _cachePolicyFactory; - /// - /// Returns the Cache Policy for the repository - /// - /// - /// The Cache Policy determines how each entity or entity collection is cached - /// - protected virtual IRepositoryCachePolicyFactory CachePolicyFactory + private IRuntimeCacheProvider _isolatedCache; + protected override IRuntimeCacheProvider IsolatedCache { get { - return _cachePolicyFactory ?? (_cachePolicyFactory = new DefaultRepositoryCachePolicyFactory( - RuntimeCache, - new RepositoryCachePolicyOptions(() => - { - //create it once if it is needed (no need for locking here) - if (_hasIdQuery == null) - { - _hasIdQuery = Query.Builder.Where(x => x.Id != 0); - } + if (_isolatedCache != null) return _isolatedCache; - //Get count of all entities of current type (TEntity) to ensure cached result is correct - return PerformCount(_hasIdQuery); - }))); + var scope = UnitOfWork.Scope; + IsolatedRuntimeCache provider; + switch (scope.RepositoryCacheMode) + { + case RepositoryCacheMode.Default: + provider = GlobalCache.IsolatedRuntimeCache; + break; + case RepositoryCacheMode.Scoped: + provider = scope.IsolatedRuntimeCache; + break; + default: + throw new Exception("oops: cache mode."); + } + + return _isolatedCache = GetIsolatedCache(provider); } } + protected virtual IRuntimeCacheProvider GetIsolatedCache(IsolatedRuntimeCache provider) + { + return provider.GetOrCreateCache(); + } + + // this is a *bad* idea because PerformCount captures the current repository and its UOW + // + //private static RepositoryCachePolicyOptions _defaultOptions; + //protected virtual RepositoryCachePolicyOptions DefaultOptions + //{ + // get + // { + // return _defaultOptions ?? (_defaultOptions + // = new RepositoryCachePolicyOptions(() => + // { + // // get count of all entities of current type (TEntity) to ensure cached result is correct + // // create query once if it is needed (no need for locking here) - query is static! + // var query = _hasIdQuery ?? (_hasIdQuery = Query.Builder.Where(x => x.Id != 0)); + // return PerformCount(query); + // })); + // } + //} + + protected virtual RepositoryCachePolicyOptions DefaultOptions + { + get + { + return new RepositoryCachePolicyOptions(() => + { + // get count of all entities of current type (TEntity) to ensure cached result is correct + // create query once if it is needed (no need for locking here) - query is static! + var query = _hasIdQuery ?? (_hasIdQuery = Query.Builder.Where(x => x.Id != 0)); + return PerformCount(query); + }); + } + } + + // this would be better for perfs BUT it breaks the tests - l8tr + // + //private static IRepositoryCachePolicy _defaultCachePolicy; + //protected virtual IRepositoryCachePolicy DefaultCachePolicy + //{ + // get + // { + // return _defaultCachePolicy ?? (_defaultCachePolicy + // = new DefaultRepositoryCachePolicy(IsolatedCache, DefaultOptions)); + // } + //} + + private IRepositoryCachePolicy _cachePolicy; + protected IRepositoryCachePolicy CachePolicy + { + get + { + if (_cachePolicy != null) return _cachePolicy; + + if (GlobalCache == CacheHelper.NoCache) + return _cachePolicy = NoRepositoryCachePolicy.Instance; + + // create the cache policy using IsolatedCache which is either global + // or scoped depending on the repository cache mode for the current scope + _cachePolicy = CreateCachePolicy(IsolatedCache); + var scope = UnitOfWork.Scope; + switch (scope.RepositoryCacheMode) + { + case RepositoryCacheMode.Default: + break; + case RepositoryCacheMode.Scoped: + var globalIsolatedCache = GetIsolatedCache(GlobalCache.IsolatedRuntimeCache); + _cachePolicy = _cachePolicy.Scoped(globalIsolatedCache, scope); + break; + default: + throw new Exception("oops: cache mode."); + } + + return _cachePolicy; + } + } + + protected virtual IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) + { + return new DefaultRepositoryCachePolicy(runtimeCache, DefaultOptions); + } + /// /// Adds or Updates an entity of type TEntity /// @@ -166,10 +241,7 @@ namespace Umbraco.Core.Persistence.Repositories /// public TEntity Get(TId id) { - using (var p = CachePolicyFactory.CreatePolicy()) - { - return p.Get(id, PerformGet); - } + return CachePolicy.Get(id, PerformGet, PerformGetAll); } protected abstract IEnumerable PerformGetAll(params TId[] ids); @@ -192,13 +264,9 @@ namespace Umbraco.Core.Persistence.Repositories throw new InvalidOperationException("Cannot perform a query with more than 2000 parameters"); } - using (var p = CachePolicyFactory.CreatePolicy()) - { - var result = p.GetAll(ids, PerformGetAll); - return result; - } + return CachePolicy.GetAll(ids, PerformGetAll); } - + protected abstract IEnumerable PerformGetByQuery(IQuery query); /// /// Gets a list of entities by the passed in query @@ -220,10 +288,7 @@ namespace Umbraco.Core.Persistence.Repositories /// public bool Exists(TId id) { - using (var p = CachePolicyFactory.CreatePolicy()) - { - return p.Exists(id, PerformExists); - } + return CachePolicy.Exists(id, PerformExists, PerformGetAll); } protected abstract int PerformCount(IQuery query); @@ -236,19 +301,14 @@ namespace Umbraco.Core.Persistence.Repositories { return PerformCount(query); } - + /// /// Unit of work method that tells the repository to persist the new entity /// /// public virtual void PersistNewItem(IEntity entity) { - var casted = (TEntity)entity; - - using (var p = CachePolicyFactory.CreatePolicy()) - { - p.CreateOrUpdate(casted, PersistNewItem); - } + CachePolicy.Create((TEntity) entity, PersistNewItem); } /// @@ -257,12 +317,7 @@ namespace Umbraco.Core.Persistence.Repositories /// public virtual void PersistUpdatedItem(IEntity entity) { - var casted = (TEntity)entity; - - using (var p = CachePolicyFactory.CreatePolicy()) - { - p.CreateOrUpdate(casted, PersistUpdatedItem); - } + CachePolicy.Update((TEntity) entity, PersistUpdatedItem); } /// @@ -271,20 +326,13 @@ namespace Umbraco.Core.Persistence.Repositories /// public virtual void PersistDeletedItem(IEntity entity) { - var casted = (TEntity)entity; - - using (var p = CachePolicyFactory.CreatePolicy()) - { - p.Remove(casted, PersistDeletedItem); - } + CachePolicy.Delete((TEntity) entity, PersistDeletedItem); } - protected abstract void PersistNewItem(TEntity item); protected abstract void PersistUpdatedItem(TEntity item); protected abstract void PersistDeletedItem(TEntity item); - /// /// Dispose disposable properties /// diff --git a/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs index 13b2877c50..c7957bf9bd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; @@ -109,6 +110,25 @@ namespace Umbraco.Core.Persistence.Repositories return isValidPath && isValidExtension; } + public Stream GetFileContentStream(string filepath) + { + if (FileSystem.FileExists(filepath) == false) return null; + + try + { + return FileSystem.OpenFile(filepath); + } + catch + { + return null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) + { + FileSystem.AddFile(filepath, content, true); + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs index 02794ef1fb..9cd47ded97 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs @@ -15,12 +15,16 @@ namespace Umbraco.Core.Persistence.Repositories { internal class ServerRegistrationRepository : PetaPocoRepositoryBase, IServerRegistrationRepository { - private readonly ICacheProvider _staticCache; + private readonly ICacheProvider _globalCache; - public ServerRegistrationRepository(IDatabaseUnitOfWork work, ICacheProvider staticCache, ILogger logger, ISqlSyntaxProvider sqlSyntax) - : base(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax) + public ServerRegistrationRepository(IScopeUnitOfWork work, ICacheProvider globalCache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, CacheHelper.NoCache, logger, sqlSyntax) { - _staticCache = staticCache; + // managing the cache our own way (no policy etc) + // note: this means that the ServerRegistrationRepository does *not* implement scoped cache, + // and this is because the repository is special and should not participate in scopes + // (cleanup in v8) + _globalCache = globalCache; } protected override int PerformCount(IQuery query) @@ -49,7 +53,7 @@ namespace Umbraco.Core.Persistence.Repositories // the cache is populated by ReloadCache which should only be called from methods // that ensure proper locking (at least, read-lock in ReadCommited) of the repo. - var all = _staticCache.GetCacheItem>(CacheKey, Enumerable.Empty); + var all = _globalCache.GetCacheItem>(CacheKey, Enumerable.Empty); return ids.Length == 0 ? all : all.Where(x => ids.Contains(x.Id)); } @@ -75,7 +79,7 @@ namespace Umbraco.Core.Persistence.Repositories { var list = new List { - "DELETE FROM umbracoServer WHERE id = @Id" + "DELETE FROM umbracoServer WHERE id = @Id" }; return list; } @@ -127,8 +131,8 @@ namespace Umbraco.Core.Persistence.Repositories .Select(x => factory.BuildEntity(x)) .Cast() .ToArray(); - _staticCache.ClearCacheItem(CacheKey); - _staticCache.GetCacheItem(CacheKey, () => all); + _globalCache.ClearCacheItem(CacheKey); + _globalCache.GetCacheItem(CacheKey, () => all); } public void DeactiveStaleServers(TimeSpan staleTimeout) diff --git a/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs index a88672ea6f..67c9149422 100644 --- a/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/SimpleGetRepository.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Persistence.Repositories where TDto: class { - protected SimpleGetRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + protected SimpleGetRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs index 6f9138de9a..7515b8513a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs @@ -125,6 +125,39 @@ namespace Umbraco.Core.Persistence.Repositories return isValidPath && isValidExtension; } + public Stream GetFileContentStream(string filepath) + { + if (FileSystem.FileExists(filepath) == false) return null; + + try + { + return FileSystem.OpenFile(filepath); + } + catch + { + return null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) + { + FileSystem.AddFile(filepath, content, true); + } + + public long GetFileSize(string filepath) + { + if (FileSystem.FileExists(filepath) == false) return -1; + + try + { + return FileSystem.GetSize(filepath); + } + catch + { + return -1; // deal with race conds + } + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs index 142740e9d8..0456e16bf3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class TagRepository : PetaPocoRepositoryBase, ITagRepository { - internal TagRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + internal TagRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs index c02362f8fd..f90e81428b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class TaskRepository : PetaPocoRepositoryBase, ITaskRepository { - public TaskRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public TaskRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs index a970880669..71c3d2d1a4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Persistence.Repositories { internal class TaskTypeRepository : PetaPocoRepositoryBase, ITaskTypeRepository { - public TaskTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public TaskTypeRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index de52ce643b..7f265aceb7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Text; using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -17,9 +15,7 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services; using Umbraco.Core.Strings; -using Umbraco.Core.Sync; namespace Umbraco.Core.Persistence.Repositories { @@ -34,26 +30,19 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ViewHelper _viewHelper; private readonly MasterPageHelper _masterPageHelper; - internal TemplateRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IFileSystem masterpageFileSystem, IFileSystem viewFileSystem, ITemplatesSection templateConfig) + internal TemplateRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IFileSystem masterpageFileSystem, IFileSystem viewFileSystem, ITemplatesSection templateConfig) : base(work, cache, logger, sqlSyntax) { _masterpagesFileSystem = masterpageFileSystem; _viewsFileSystem = viewFileSystem; _templateConfig = templateConfig; _viewHelper = new ViewHelper(_viewsFileSystem); - _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); + _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); } - - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CreateCachePolicy(IRuntimeCacheProvider runtimeCache) { - get - { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), false)); - } + return new FullDataSetRepositoryCachePolicy(runtimeCache, GetEntityId, /*expires:*/ false); } #region Overrides of RepositoryBase @@ -81,7 +70,7 @@ namespace Umbraco.Core.Persistence.Repositories if (dtos.Count == 0) return Enumerable.Empty(); - //look up the simple template definitions that have a master template assigned, this is used + //look up the simple template definitions that have a master template assigned, this is used // later to populate the template item's properties var childIds = (ids.Any() ? GetAxisDefinitions(dtos.ToArray()) @@ -91,7 +80,7 @@ namespace Umbraco.Core.Persistence.Repositories ParentId = x.NodeDto.ParentId, Name = x.Alias })).ToArray(); - + return dtos.Select(d => MapFromDto(d, childIds)); } @@ -105,7 +94,7 @@ namespace Umbraco.Core.Persistence.Repositories if (dtos.Count == 0) return Enumerable.Empty(); - //look up the simple template definitions that have a master template assigned, this is used + //look up the simple template definitions that have a master template assigned, this is used // later to populate the template item's properties var childIds = GetAxisDefinitions(dtos.ToArray()).ToArray(); @@ -188,29 +177,7 @@ namespace Umbraco.Core.Persistence.Repositories template.Path = nodeDto.Path; //now do the file work - - if (DetermineTemplateRenderingEngine(entity) == RenderingEngine.Mvc) - { - var result = _viewHelper.CreateView(template, true); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } - else - { - var result = _masterPageHelper.CreateMasterPage(template, this, true); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } + SaveFile(template, dto); template.ResetDirtyProperties(); @@ -265,29 +232,7 @@ namespace Umbraco.Core.Persistence.Repositories template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); //now do the file work - - if (DetermineTemplateRenderingEngine(entity) == RenderingEngine.Mvc) - { - var result = _viewHelper.UpdateViewFile(entity, originalAlias); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } - else - { - var result = _masterPageHelper.UpdateMasterPageFile(entity, originalAlias, this); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } + SaveFile((Template) entity, dto, originalAlias); entity.ResetDirtyProperties(); @@ -296,6 +241,42 @@ namespace Umbraco.Core.Persistence.Repositories template.GetFileContent = file => GetFileContent((Template) file, false); } + private void SaveFile(Template template, TemplateDto dto, string originalAlias = null) + { + string content; + + var templateOnDisk = template as TemplateOnDisk; + if (templateOnDisk != null && templateOnDisk.IsOnDisk) + { + // if "template on disk" load content from disk + content = _viewHelper.GetFileContents(template); + } + else + { + // else, create or write template.Content to disk + if (DetermineTemplateRenderingEngine(template) == RenderingEngine.Mvc) + { + content = originalAlias == null + ? _viewHelper.CreateView(template, true) + : _viewHelper.UpdateViewFile(template, originalAlias); + } + else + { + content = originalAlias == null + ? _masterPageHelper.CreateMasterPage(template, this, true) + : _masterPageHelper.UpdateMasterPageFile(template, originalAlias, this); + } + } + + // once content has been set, "template on disk" are not "on disk" anymore + template.Content = content; + SetVirtualPath(template); + + if (dto.Design == content) return; + dto.Design = content; + Database.Update(dto); // though... we don't care about the db value really??!! + } + protected override void PersistDeletedItem(ITemplate entity) { var deletes = GetDeleteClauses().ToArray(); @@ -329,14 +310,16 @@ namespace Umbraco.Core.Persistence.Repositories { var masterpageName = string.Concat(entity.Alias, ".master"); _masterpagesFileSystem.DeleteFile(masterpageName); - } + } + + entity.DeletedDate = DateTime.Now; } #endregion private IEnumerable GetAxisDefinitions(params TemplateDto[] templates) { - //look up the simple template definitions that have a master template assigned, this is used + //look up the simple template definitions that have a master template assigned, this is used // later to populate the template item's properties var childIdsSql = new Sql() .Select("nodeId,alias,parentID") @@ -345,7 +328,7 @@ namespace Umbraco.Core.Persistence.Repositories .On(SqlSyntax, dto => dto.NodeId, dto => dto.NodeId) //lookup axis's .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("id") + " IN (@parentIds) OR umbracoNode.parentID IN (@childIds)", - new {parentIds = templates.Select(x => x.NodeDto.ParentId), childIds = templates.Select(x => x.NodeId)}); + new {parentIds = templates.Select(x => x.NodeDto.ParentId), childIds = templates.Select(x => x.NodeId)}); var childIds = Database.Fetch(childIdsSql) .Select(x => new UmbracoEntity @@ -391,6 +374,50 @@ namespace Umbraco.Core.Persistence.Repositories return template; } + private void SetVirtualPath(ITemplate template) + { + var path = template.OriginalPath; + if (string.IsNullOrWhiteSpace(path)) + { + // we need to discover the path + path = string.Concat(template.Alias, ".cshtml"); + if (_viewsFileSystem.FileExists(path)) + { + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; + } + path = string.Concat(template.Alias, ".vbhtml"); + if (_viewsFileSystem.FileExists(path)) + { + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; + } + path = string.Concat(template.Alias, ".master"); + if (_masterpagesFileSystem.FileExists(path)) + { + template.VirtualPath = _masterpagesFileSystem.GetUrl(path); + return; + } + } + else + { + // we know the path already + var ext = Path.GetExtension(path); + switch (ext) + { + case ".cshtml": + case ".vbhtml": + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; + case ".master": + template.VirtualPath = _masterpagesFileSystem.GetUrl(path); + return; + } + } + + template.VirtualPath = string.Empty; // file not found... + } + private string GetFileContent(ITemplate template, bool init) { var path = template.OriginalPath; @@ -418,20 +445,10 @@ namespace Umbraco.Core.Persistence.Repositories return GetFileContent(template, _viewsFileSystem, path, init); case ".master": return GetFileContent(template, _masterpagesFileSystem, path, init); - default: - return string.Empty; } } - var fsname = string.Concat(template.Alias, ".cshtml"); - if (_viewsFileSystem.FileExists(fsname)) - return GetFileContent(template, _viewsFileSystem, fsname, init); - fsname = string.Concat(template.Alias, ".vbhtml"); - if (_viewsFileSystem.FileExists(fsname)) - return GetFileContent(template, _viewsFileSystem, fsname, init); - fsname = string.Concat(template.Alias, ".master"); - if (_masterpagesFileSystem.FileExists(fsname)) - return GetFileContent(template, _masterpagesFileSystem, fsname, init); + template.VirtualPath = string.Empty; // file not found... return string.Empty; } @@ -466,6 +483,50 @@ namespace Umbraco.Core.Persistence.Repositories } } + public Stream GetFileContentStream(string filepath) + { + var fs = GetFileSystem(filepath); + if (fs.FileExists(filepath) == false) return null; + + try + { + return GetFileSystem(filepath).OpenFile(filepath); + } + catch + { + return null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) + { + GetFileSystem(filepath).AddFile(filepath, content, true); + } + + public long GetFileSize(string filepath) + { + return GetFileSystem(filepath).GetSize(filepath); + } + + private IFileSystem GetFileSystem(string filepath) + { + var ext = Path.GetExtension(filepath); + IFileSystem fs; + switch (ext) + { + case ".cshtml": + case ".vbhtml": + fs = _viewsFileSystem; + break; + case ".master": + fs = _masterpagesFileSystem; + break; + default: + throw new Exception("Unsupported extension " + ext + "."); + } + return fs; + } + #region Implementation of ITemplateRepository public ITemplate Get(string alias) @@ -531,7 +592,7 @@ namespace Umbraco.Core.Persistence.Repositories //return the list - it will be naturally ordered by level return descendants; } - + public IEnumerable GetDescendants(string alias) { var all = base.GetAll().ToArray(); @@ -575,7 +636,7 @@ namespace Umbraco.Core.Persistence.Repositories /// [Obsolete("Use GetDescendants instead")] public TemplateNode GetTemplateNode(string alias) - { + { //first get all template objects var allTemplates = base.GetAll().ToArray(); @@ -584,7 +645,7 @@ namespace Umbraco.Core.Persistence.Repositories { return null; } - + var top = selfTemplate; while (top.MasterTemplateAlias.IsNullOrWhiteSpace() == false) { @@ -635,16 +696,16 @@ namespace Umbraco.Core.Persistence.Repositories } /// - /// This checks what the default rendering engine is set in config but then also ensures that there isn't already - /// a template that exists in the opposite rendering engine's template folder, then returns the appropriate + /// This checks what the default rendering engine is set in config but then also ensures that there isn't already + /// a template that exists in the opposite rendering engine's template folder, then returns the appropriate /// rendering engine to use. - /// + /// /// /// /// The reason this is required is because for example, if you have a master page file already existing under ~/masterpages/Blah.aspx - /// and then you go to create a template in the tree called Blah and the default rendering engine is MVC, it will create a Blah.cshtml - /// empty template in ~/Views. This means every page that is using Blah will go to MVC and render an empty page. - /// This is mostly related to installing packages since packages install file templates to the file system and then create the + /// and then you go to create a template in the tree called Blah and the default rendering engine is MVC, it will create a Blah.cshtml + /// empty template in ~/Views. This means every page that is using Blah will go to MVC and render an empty page. + /// This is mostly related to installing packages since packages install file templates to the file system and then create the /// templates in business logic. Without this, it could cause the wrong rendering engine to be used for a package. /// public RenderingEngine DetermineTemplateRenderingEngine(ITemplate template) @@ -752,7 +813,7 @@ namespace Umbraco.Core.Persistence.Repositories /// private void EnsureValidAlias(ITemplate template) { - //ensure unique alias + //ensure unique alias template.Alias = template.Alias.ToCleanString(CleanStringType.UnderscoreAlias); if (template.Alias.Length > 100) diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 4652ce47be..10758be578 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.Persistence.Repositories private readonly IUserTypeRepository _userTypeRepository; private readonly CacheHelper _cacheHelper; - public UserRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax, IUserTypeRepository userTypeRepository) + public UserRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax, IUserTypeRepository userTypeRepository) : base(work, cacheHelper, logger, sqlSyntax) { _userTypeRepository = userTypeRepository; @@ -39,6 +39,8 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); + //must be sorted this way for the relator to work + sql.OrderBy(x => x.Id, SqlSyntax); var dto = Database.Fetch(new UserSectionRelator().Map, sql).FirstOrDefault(); @@ -60,7 +62,10 @@ namespace Umbraco.Core.Persistence.Repositories { sql.Where("umbracoUser.id in (@ids)", new {ids = ids}); } - + + //must be sorted this way for the relator to work + sql.OrderBy(x => x.Id, SqlSyntax); + return ConvertFromDtos(Database.Fetch(new UserSectionRelator().Map, sql)) .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. } @@ -71,6 +76,9 @@ namespace Umbraco.Core.Persistence.Repositories var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); + //must be sorted this way for the relator to work + sql.OrderBy(x => x.Id, SqlSyntax); + var dtos = Database.Fetch(new UserSectionRelator().Map, sql) .DistinctBy(x => x.Id); @@ -96,13 +104,13 @@ namespace Umbraco.Core.Persistence.Repositories return sql; } - private static Sql GetBaseQuery(string columns) + private Sql GetBaseQuery(string columns) { var sql = new Sql(); sql.Select(columns) - .From() - .LeftJoin() - .On(left => left.Id, right => right.UserId); + .From() + .LeftJoin() + .On(left => left.Id, right => right.UserId); return sql; } @@ -300,6 +308,8 @@ namespace Umbraco.Core.Persistence.Repositories var innerSql = GetBaseQuery("umbracoUser.id"); innerSql.Where("umbracoUser2app.app = " + SqlSyntax.GetQuotedValue(sectionAlias)); sql.Where(string.Format("umbracoUser.id IN ({0})", innerSql.SQL)); + //must be sorted this way for the relator to work + sql.OrderBy(x => x.Id, SqlSyntax); return ConvertFromDtos(Database.Fetch(new UserSectionRelator().Map, sql)); } diff --git a/src/Umbraco.Core/Persistence/Repositories/UserTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserTypeRepository.cs index 1fd94ca357..81e3a5765e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserTypeRepository.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class UserTypeRepository : PetaPocoRepositoryBase, IUserTypeRepository { - public UserTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + public UserTypeRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index ec1623d16a..98743a6a63 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data.SqlTypes; using System.Diagnostics; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using System.Xml.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; @@ -22,17 +25,21 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Dynamics; using Umbraco.Core.IO; +using Umbraco.Core.Media; namespace Umbraco.Core.Persistence.Repositories { - using SqlSyntax; - internal abstract class VersionableRepositoryBase : PetaPocoRepositoryBase where TEntity : class, IAggregateRoot { private readonly IContentSection _contentSection; - protected VersionableRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) + /// + /// This is used for unit tests ONLY + /// + internal static bool ThrowOnWarning = false; + + protected VersionableRepositoryBase(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) : base(work, cache, logger, sqlSyntax) { _contentSection = contentSection; @@ -137,6 +144,40 @@ namespace Umbraco.Core.Persistence.Repositories #endregion + /// + /// Gets paged document descendants as XML by path + /// + /// Path starts with + /// Page number + /// Page size + /// + /// Total records the query would return without paging + /// A paged enumerable of XML entries of content items + public virtual IEnumerable GetPagedXmlEntriesByPath(string path, long pageIndex, int pageSize, string[] orderBy, out long totalRecords) + { + var query = new Sql().Select(string.Format("umbracoNode.id, cmsContentXml.{0}", SqlSyntax.GetQuotedColumnName("xml"))) + .From("umbracoNode") + .InnerJoin("cmsContentXml").On("cmsContentXml.nodeId = umbracoNode.id"); + + if (path == "-1") + { + query.Where("umbracoNode.nodeObjectType = @type", new { type = NodeObjectTypeId }); + } + else + { + query.Where(string.Format("umbracoNode.{0} LIKE (@0)", SqlSyntax.GetQuotedColumnName("path")), path.EnsureEndsWith(",%")); + } + + //each order by param needs to be in a bracket! see: https://github.com/toptensoftware/PetaPoco/issues/177 + query.OrderBy(orderBy == null + ? "(umbracoNode.id)" + : string.Join(",", orderBy.Select(x => string.Format("({0})", SqlSyntax.GetQuotedColumnName(x))))); + + var pagedResult = Database.Page(pageIndex + 1, pageSize, query); + totalRecords = pagedResult.TotalItems; + return pagedResult.Items.Select(dto => XElement.Parse(dto.Xml)); + } + public int CountDescendants(int parentId, string contentTypeAlias = null) { var pathMatch = parentId == -1 @@ -348,12 +389,8 @@ namespace Umbraco.Core.Persistence.Repositories ON CustomPropData.CustomPropValContentId = umbracoNode.id ", sortedInt, sortedDecimal, sortedDate, sortedString, nodeIdSelect.Item2, nodeIdSelect.Item1, versionQuery, sortedSql.Arguments.Length, newestQuery); - //insert this just above the first LEFT OUTER JOIN (for cmsDocument) or the last WHERE (everything else) - string newSql; - if (nodeIdSelect.Item1 == "cmsDocument") - newSql = sortedSql.SQL.Insert(sortedSql.SQL.IndexOf("LEFT OUTER JOIN"), outerJoinTempTable); - else - newSql = sortedSql.SQL.Insert(sortedSql.SQL.LastIndexOf("WHERE"), outerJoinTempTable); + //insert this just above the last WHERE + string newSql = sortedSql.SQL.Insert(sortedSql.SQL.LastIndexOf("WHERE"), outerJoinTempTable); var newArgs = sortedSql.Arguments.ToList(); newArgs.Add(orderBy); @@ -370,11 +407,14 @@ namespace Umbraco.Core.Persistence.Repositories } } - //no matter what we always MUST order the result also by umbracoNode.id to ensure that all records being ordered by are unique. - // if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column - // is empty for many nodes) - // see: http://issues.umbraco.org/issue/U4-8831 - sortedSql.OrderBy("umbracoNode.id"); + if (orderBySystemField && orderBy != "umbracoNode.id") + { + //no matter what we always MUST order the result also by umbracoNode.id to ensure that all records being ordered by are unique. + // if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column + // is empty for many nodes) + // see: http://issues.umbraco.org/issue/U4-8831 + sortedSql.OrderBy("umbracoNode.id"); + } return sortedSql; @@ -384,7 +424,6 @@ namespace Umbraco.Core.Persistence.Repositories /// A helper method for inheritors to get the paged results by query in a way that minimizes queries /// /// The type of the d. - /// The 'true' entity type (i.e. Content, Member, etc...) /// The query. /// Index of the page. /// Size of the page. @@ -397,34 +436,30 @@ namespace Umbraco.Core.Persistence.Repositories /// Flag to indicate when ordering by system field /// /// orderBy - protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Tuple nodeIdSelect, - Func> processQuery, + Func, IEnumerable> processQuery, string orderBy, Direction orderDirection, bool orderBySystemField, Func> defaultFilter = null) - where TContentBase : class, IAggregateRoot, TEntity { if (orderBy == null) throw new ArgumentNullException("orderBy"); - // Get base query - var sqlBase = GetBaseQuery(false); + // Get base query for returning IDs + var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); + // Get base query for returning all data + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); if (query == null) query = new Query(); - var translator = new SqlTranslator(sqlBase, query); - var sqlQuery = translator.Translate(); - - // Note we can't do multi-page for several DTOs like we can multi-fetch and are doing in PerformGetByQuery, - // but actually given we are doing a Get on each one (again as in PerformGetByQuery), we only need the node Id. - // So we'll modify the SQL. - var sqlNodeIds = new Sql( - sqlQuery.SQL.Replace("SELECT *", string.Format("SELECT {0}.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)), - sqlQuery.Arguments); + var translatorIds = new SqlTranslator(sqlBaseIds, query); + var sqlQueryIds = translatorIds.Translate(); + var translatorFull = new SqlTranslator(sqlBaseFull, query); + var sqlQueryFull = translatorFull.Translate(); //get sorted and filtered sql var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(sqlNodeIds, defaultFilter), + GetFilteredSqlForPagedResults(sqlQueryIds, defaultFilter), orderDirection, orderBy, orderBySystemField, nodeIdSelect); // Get page of results and total count @@ -433,38 +468,32 @@ namespace Umbraco.Core.Persistence.Repositories totalRecords = Convert.ToInt32(pagedResult.TotalItems); //NOTE: We need to check the actual items returned, not the 'totalRecords', that is because if you request a page number - // that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in + // that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in // the pageResult, then the GetAll will actually return ALL records in the db. if (pagedResult.Items.Any()) { - //Crete the inner paged query that was used above to get the paged result, we'll use that as the inner sub query + //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query var args = sqlNodeIdsWithSort.Arguments; string sqlStringCount, sqlStringPage; Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); - //if this is for sql server, the sqlPage will start with a SELECT * but we don't want that, we only want to return the nodeId - sqlStringPage = sqlStringPage - .Replace("SELECT *", - //This ensures we only take the field name of the node id select and not the table name - since the resulting select - // will ony work with the field name. - "SELECT " + nodeIdSelect.Item2); - - //We need to make this an inner join on the paged query - var splitQuery = sqlQuery.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); - var withInnerJoinSql = new Sql(splitQuery[0]) + //We need to make this FULL query an inner join on the paged ID query + var splitQuery = sqlQueryFull.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); + var fullQueryWithPagedInnerJoin = new Sql(splitQuery[0]) .Append("INNER JOIN (") //join the paged query with the paged query arguments .Append(sqlStringPage, args) .Append(") temp ") .Append(string.Format("ON {0}.{1} = temp.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)) //add the original where clause back with the original arguments - .Where(splitQuery[1], sqlQuery.Arguments); + .Where(splitQuery[1], sqlQueryIds.Arguments); //get sorted and filtered sql var fullQuery = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(withInnerJoinSql, defaultFilter), + GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, defaultFilter), orderDirection, orderBy, orderBySystemField, nodeIdSelect); - return processQuery(fullQuery); + + return processQuery(fullQuery, new PagingSqlQuery(Database, sqlNodeIdsWithSort, pageIndex, pageSize)); } else { @@ -474,38 +503,56 @@ namespace Umbraco.Core.Persistence.Repositories return result; } - protected IDictionary GetPropertyCollection( - Sql docSql, - IEnumerable documentDefs) + /// + /// Gets the property collection for a non-paged query + /// + /// + /// + /// + protected IDictionary GetPropertyCollection( + Sql sql, + IReadOnlyCollection documentDefs) { - if (documentDefs.Any() == false) return new Dictionary(); + return GetPropertyCollection(new PagingSqlQuery(sql), documentDefs); + } - //we need to parse the original SQL statement and reduce the columns to just cmsContent.nodeId, cmsContentVersion.VersionId so that we can use + /// + /// Gets the property collection for a query + /// + /// + /// + /// + protected IDictionary GetPropertyCollection( + PagingSqlQuery pagingSqlQuery, + IReadOnlyCollection documentDefs) + { + if (documentDefs.Count == 0) return new Dictionary(); + + //initialize to the query passed in + var docSql = pagingSqlQuery.PrePagedSql; + + //we need to parse the original SQL statement and reduce the columns to just cmsContent.nodeId, cmsContentVersion.VersionId so that we can use // the statement to go get the property data for all of the items by using an inner join var parsedOriginalSql = "SELECT {0} " + docSql.SQL.Substring(docSql.SQL.IndexOf("FROM", StringComparison.Ordinal)); - //now remove everything from an Orderby clause and beyond - if (parsedOriginalSql.InvariantContains("ORDER BY ")) + + if (pagingSqlQuery.HasPaging) { + //if this is a paged query, build the paged query with the custom column substitution, then re-assign + docSql = pagingSqlQuery.BuildPagedQuery("{0}"); + parsedOriginalSql = docSql.SQL; + } + else if (parsedOriginalSql.InvariantContains("ORDER BY ")) + { + //now remove everything from an Orderby clause and beyond if this is unpaged data parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); } - var propSql = new Sql(@"SELECT cmsPropertyData.* -FROM cmsPropertyData -INNER JOIN cmsPropertyType -ON cmsPropertyData.propertytypeid = cmsPropertyType.id -INNER JOIN - (" + string.Format(parsedOriginalSql, "cmsContent.nodeId, cmsContentVersion.VersionId") + @") as docData -ON cmsPropertyData.versionId = docData.VersionId AND cmsPropertyData.contentNodeId = docData.nodeId -LEFT OUTER JOIN cmsDataTypePreValues -ON cmsPropertyType.dataTypeId = cmsDataTypePreValues.datatypeNodeId", docSql.Arguments); - - var allPropertyData = Database.Fetch(propSql); - - //This is a lazy access call to get all prevalue data for the data types that make up all of these properties which we use - // below if any property requires tag support - var allPreValues = new Lazy>(() => - { - var preValsSql = new Sql(@"SELECT a.id, a.value, a.sortorder, a.alias, a.datatypeNodeId + //This retrieves all pre-values for all data types that are referenced for all property types + // that exist in the data set. + //Benchmarks show that eagerly loading these so that we can lazily read the property data + // below (with the use of Query intead of Fetch) go about 30% faster, so we'll eagerly load + // this now since we cannot execute another reader inside of reading the property data. + var preValsSql = new Sql(@"SELECT a.id, a.value, a.sortorder, a.alias, a.datatypeNodeId FROM cmsDataTypePreValues a WHERE EXISTS( SELECT DISTINCT b.id as preValIdInner @@ -513,29 +560,86 @@ WHERE EXISTS( INNER JOIN cmsPropertyType ON b.datatypeNodeId = cmsPropertyType.dataTypeId INNER JOIN - (" + string.Format(parsedOriginalSql, "DISTINCT cmsContent.contentType") + @") as docData + (" + string.Format(parsedOriginalSql, "cmsContent.contentType") + @") as docData ON cmsPropertyType.contentTypeId = docData.contentType WHERE a.id = b.id)", docSql.Arguments); - return Database.Fetch(preValsSql); - }); + var allPreValues = Database.Fetch(preValsSql); - var result = new Dictionary(); + //It's Important with the sort order here! We require this to be sorted by node id, + // this is required because this data set can be huge depending on the page size. Due + // to it's size we need to be smart about iterating over the property values to build + // the document. Before we used to use Linq to get the property data for a given content node + // and perform a Distinct() call. This kills performance because that would mean if we had 7000 nodes + // and on each iteration we will perform a lookup on potentially 100,000 property rows against the node + // id which turns out to be a crazy amount of iterations. Instead we know it's sorted by this value we'll + // keep an index stored of the rows being read so we never have to re-iterate the entire data set + // on each document iteration. + var propSql = new Sql(@"SELECT cmsPropertyData.* +FROM cmsPropertyData +INNER JOIN cmsPropertyType +ON cmsPropertyData.propertytypeid = cmsPropertyType.id +INNER JOIN + (" + string.Format(parsedOriginalSql, "cmsContent.nodeId, cmsContentVersion.VersionId") + @") as docData +ON cmsPropertyData.versionId = docData.VersionId AND cmsPropertyData.contentNodeId = docData.nodeId +ORDER BY contentNodeId, versionId, propertytypeid +", docSql.Arguments); + + //This does NOT fetch all data into memory in a list, this will read + // over the records as a data reader, this is much better for performance and memory, + // but it means that during the reading of this data set, nothing else can be read + // from SQL server otherwise we'll get an exception. + var allPropertyData = Database.Query(propSql); + var result = new Dictionary(); var propertiesWithTagSupport = new Dictionary(); + //used to track the resolved composition property types per content type so we don't have to re-resolve (ToArray) the list every time + var resolvedCompositionProperties = new Dictionary(); - //iterate each definition grouped by it's content type - this will mean less property type iterations while building - // up the property collections - foreach (var compositionGroup in documentDefs.GroupBy(x => x.Composition)) + //keep track of the current property data item being enumerated + var propertyDataSetEnumerator = allPropertyData.GetEnumerator(); + var hasCurrent = false; // initially there is no enumerator.Current + + var comparer = new DocumentDefinitionComparer(SqlSyntax); + + try { - var compositionProperties = compositionGroup.Key.CompositionPropertyTypes.ToArray(); - - foreach (var def in compositionGroup) + //This must be sorted by node id because this is how we are sorting the query to lookup property types above, + // which allows us to more efficiently iterate over the large data set of property values + foreach (var def in documentDefs.OrderBy(x => x.Id).ThenBy(x => x.Version, comparer)) { - var propertyDataDtos = allPropertyData.Where(x => x.NodeId == def.Id).Distinct(); + // get the resolved properties from our local cache, or resolve them and put them in cache + PropertyType[] compositionProperties; + if (resolvedCompositionProperties.ContainsKey(def.Composition.Id)) + { + compositionProperties = resolvedCompositionProperties[def.Composition.Id]; + } + else + { + compositionProperties = def.Composition.CompositionPropertyTypes.ToArray(); + resolvedCompositionProperties[def.Composition.Id] = compositionProperties; + } - var propertyFactory = new PropertyFactory(compositionProperties, def.Version, def.Id, def.CreateDate, def.VersionDate); - var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); + // assemble the dtos for this def + // use the available enumerator.Current if any else move to next + var propertyDataDtos = new List(); + while (hasCurrent || propertyDataSetEnumerator.MoveNext()) + { + //Not checking null on VersionId because it can never be null - no idea why it's set to nullable + // ReSharper disable once PossibleInvalidOperationException + if (propertyDataSetEnumerator.Current.VersionId.Value == def.Version) + { + hasCurrent = false; // enumerator.Current is not available + propertyDataDtos.Add(propertyDataSetEnumerator.Current); + } + else + { + hasCurrent = true; // enumerator.Current is available for another def + break; // no more propertyDataDto for this def + } + } + + var properties = PropertyFactory.BuildEntity(propertyDataDtos, compositionProperties, def.CreateDate, def.VersionDate).ToArray(); foreach (var property in properties) { @@ -552,7 +656,7 @@ WHERE EXISTS( propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] = tagSupport; //this property has tags, so we need to extract them and for that we need the prevals which we've already looked up - var preValData = allPreValues.Value.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) + var preValData = allPreValues.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) .Distinct() .ToArray(); @@ -560,46 +664,36 @@ WHERE EXISTS( var preVals = new PreValueCollection(asDictionary); - var contentPropData = new ContentPropertyData(property.Value, - preVals, - new Dictionary()); + var contentPropData = new ContentPropertyData(property.Value, preVals); TagExtractor.SetPropertyTags(property, contentPropData, property.Value, tagSupport); } } - if (result.ContainsKey(def.Id)) + if (result.ContainsKey(def.Version)) { - Logger.Warn>("The query returned multiple property sets for document definition " + def.Id + ", " + def.Composition.Name); + var msg = string.Format("The query returned multiple property sets for document definition {0}, {1}, {2}", def.Id, def.Version, def.Composition.Name); + if (ThrowOnWarning) + { + throw new InvalidOperationException(msg); + } + else + { + Logger.Warn>(msg); + } } - result[def.Id] = new PropertyCollection(properties); + result[def.Version] = new PropertyCollection(properties); } } + finally + { + propertyDataSetEnumerator.Dispose(); + } return result; + } - - public class DocumentDefinition - { - /// - /// Initializes a new instance of the class. - /// - public DocumentDefinition(int id, Guid version, DateTime versionDate, DateTime createDate, IContentTypeComposition composition) - { - Id = id; - Version = version; - VersionDate = versionDate; - CreateDate = createDate; - Composition = composition; - } - - public int Id { get; set; } - public Guid Version { get; set; } - public DateTime VersionDate { get; set; } - public DateTime CreateDate { get; set; } - public IContentTypeComposition Composition { get; set; } - } - + protected virtual string GetDatabaseFieldNameForOrderBy(string orderBy) { // Translate the passed order by field (which were originally defined for in-memory object sorting @@ -611,8 +705,8 @@ WHERE EXISTS( case "NAME": return "umbracoNode.text"; case "PUBLISHED": - return "cmsDocument.published"; case "OWNER": + return "cmsDocument.published"; //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter return "umbracoNode.nodeUser"; // Members only @@ -656,7 +750,7 @@ WHERE EXISTS( var allsuccess = true; - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + var fs = FileSystemProviderManager.Current.MediaFileSystem; Parallel.ForEach(files, file => { try @@ -688,5 +782,233 @@ WHERE EXISTS( return allsuccess; } + + /// + /// For Paging, repositories must support returning different query for the query type specified + /// + /// + /// + protected abstract Sql GetBaseQuery(BaseQueryType queryType); + + internal class DocumentDefinitionCollection : KeyedCollection + { + private readonly bool _includeAllVersions; + + /// + /// Constructor specifying if all versions should be allowed, in that case the key for the collection becomes the versionId (GUID) + /// + /// + public DocumentDefinitionCollection(bool includeAllVersions = false) + { + _includeAllVersions = includeAllVersions; + } + + protected override ValueType GetKeyForItem(DocumentDefinition item) + { + return _includeAllVersions ? (ValueType)item.Version : item.Id; + } + + /// + /// if this key already exists if it does then we need to check + /// if the existing item is 'older' than the new item and if that is the case we'll replace the older one + /// + /// + /// + public bool AddOrUpdate(DocumentDefinition item) + { + //if we are including all versions then just add, we aren't checking for latest + if (_includeAllVersions) + { + base.Add(item); + return true; + } + + if (Dictionary == null) + { + base.Add(item); + return true; + } + + var key = GetKeyForItem(item); + DocumentDefinition found; + if (TryGetValue(key, out found)) + { + //it already exists and it's older so we need to replace it + if (item.VersionId > found.VersionId) + { + var currIndex = Items.IndexOf(found); + if (currIndex == -1) + throw new IndexOutOfRangeException("Could not find the item in the list: " + found.Version); + + //replace the current one with the newer one + SetItem(currIndex, item); + return true; + } + //could not add or update + return false; + } + + base.Add(item); + return true; + } + + public bool TryGetValue(ValueType key, out DocumentDefinition val) + { + if (Dictionary == null) + { + val = null; + return false; + } + return Dictionary.TryGetValue(key, out val); + } + } + + /// + /// A custom comparer required for sorting entities by GUIDs to match how the sorting of GUIDs works on SQL server + /// + /// + /// MySql sorts GUIDs as a string, MSSQL sorts based on byte sections, this comparer will allow sorting GUIDs to be the same as how SQL server does + /// + private class DocumentDefinitionComparer : IComparer + { + private readonly ISqlSyntaxProvider _sqlSyntax; + + public DocumentDefinitionComparer(ISqlSyntaxProvider sqlSyntax) + { + _sqlSyntax = sqlSyntax; + } + + public int Compare(Guid x, Guid y) + { + //MySql sorts on GUIDs as strings (i.e. normal) + if (_sqlSyntax is MySqlSyntaxProvider) + { + return x.CompareTo(y); + } + + //MSSQL doesn't it sorts them on byte sections! + return new SqlGuid(x).CompareTo(new SqlGuid(y)); + } + } + + internal class DocumentDefinition + { + /// + /// Initializes a new instance of the class. + /// + public DocumentDefinition(DocumentDto dto, IContentTypeComposition composition) + { + DocumentDto = dto; + ContentVersionDto = dto.ContentVersionDto; + Composition = composition; + } + + public DocumentDefinition(ContentVersionDto dto, IContentTypeComposition composition) + { + ContentVersionDto = dto; + Composition = composition; + } + + public DocumentDto DocumentDto { get; private set; } + public ContentVersionDto ContentVersionDto { get; private set; } + + public int Id + { + get { return ContentVersionDto.NodeId; } + } + + public Guid Version + { + get { return DocumentDto != null ? DocumentDto.VersionId : ContentVersionDto.VersionId; } + } + + /// + /// This is used to determien which version is the most recent + /// + public int VersionId + { + get { return ContentVersionDto.Id; } + } + + public DateTime VersionDate + { + get { return ContentVersionDto.VersionDate; } + } + + public DateTime CreateDate + { + get { return ContentVersionDto.ContentDto.NodeDto.CreateDate; } + } + + public IContentTypeComposition Composition { get; set; } + + + } + + /// + /// An object representing a query that may contain paging information + /// + internal class PagingSqlQuery + { + public Sql PrePagedSql { get; private set; } + + public PagingSqlQuery(Sql prePagedSql) + { + PrePagedSql = prePagedSql; + } + + public virtual bool HasPaging + { + get { return false; } + } + + public virtual Sql BuildPagedQuery(string selectColumns) + { + throw new InvalidOperationException("This query has no paging information"); + } + } + + /// + /// An object representing a query that contains paging information + /// + /// + internal class PagingSqlQuery : PagingSqlQuery + { + private readonly Database _db; + private readonly long _pageIndex; + private readonly int _pageSize; + + public PagingSqlQuery(Database db, Sql prePagedSql, long pageIndex, int pageSize) : base(prePagedSql) + { + _db = db; + _pageIndex = pageIndex; + _pageSize = pageSize; + } + + public override bool HasPaging + { + get { return _pageSize > 0; } + } + + /// + /// Creates a paged query based on the original query and subtitutes the selectColumns specified + /// + /// + /// + public override Sql BuildPagedQuery(string selectColumns) + { + if (HasPaging == false) throw new InvalidOperationException("This query has no paging information"); + + var resultSql = string.Format("SELECT {0} {1}", selectColumns, PrePagedSql.SQL.Substring(PrePagedSql.SQL.IndexOf("FROM", StringComparison.Ordinal))); + + //this query is meant to be paged so we need to generate the paging syntax + //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query + var args = PrePagedSql.Arguments; + string sqlStringCount, sqlStringPage; + _db.BuildPageQueries(_pageIndex * _pageSize, _pageSize, resultSql, ref args, out sqlStringCount, out sqlStringPage); + + return new Sql(sqlStringPage, args); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/XsltFileRepository.cs b/src/Umbraco.Core/Persistence/Repositories/XsltFileRepository.cs new file mode 100644 index 0000000000..461efce6c3 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/XsltFileRepository.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Umbraco.Core.IO; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents the XsltFile Repository + /// + internal class XsltFileRepository : FileRepository, IXsltFileRepository + { + public XsltFileRepository(IUnitOfWork work, IFileSystem fileSystem) + : base(work, fileSystem) + { + } + + public override XsltFile Get(string id) + { + var path = FileSystem.GetRelativePath(id); + + path = path.EnsureEndsWith(".xslt"); + + if (FileSystem.FileExists(path) == false) + return null; + + var created = FileSystem.GetCreated(path).UtcDateTime; + var updated = FileSystem.GetLastModified(path).UtcDateTime; + + var xsltFile = new XsltFile(path, file => GetFileContent(file.OriginalPath)) + { + Key = path.EncodeAsGuid(), + CreateDate = created, + UpdateDate = updated, + Id = path.GetHashCode(), + VirtualPath = FileSystem.GetUrl(path) + }; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + xsltFile.ResetDirtyProperties(false); + + return xsltFile; + } + + public override void AddOrUpdate(XsltFile entity) + { + base.AddOrUpdate(entity); + + // ensure that from now on, content is lazy-loaded + if (entity.GetFileContent == null) + entity.GetFileContent = file => GetFileContent(file.OriginalPath); + } + + public override IEnumerable GetAll(params string[] ids) + { + ids = ids + .Select(x => x.EnsureEndsWith(".xslt")) + .Distinct() + .ToArray(); + + if (ids.Any()) + { + foreach (var id in ids) + { + yield return Get(id); + } + } + else + { + var files = FindAllFiles("", "*.xslt"); + foreach (var file in files) + { + yield return Get(file); + } + } + } + + /// + /// Gets a list of all that exist at the relative path specified. + /// + /// + /// If null or not specified, will return the XSLT files at the root path relative to the IFileSystem + /// + /// + public IEnumerable GetXsltFilesAtPath(string rootPath = null) + { + return FileSystem.GetFiles(rootPath ?? string.Empty, "*.xslt").Select(Get); + } + + private static readonly List ValidExtensions = new List { "xslt" }; + + public bool ValidateXsltFile(XsltFile xsltFile) + { + // get full path + string fullPath; + try + { + // may throw for security reasons + fullPath = FileSystem.GetFullPath(xsltFile.Path); + } + catch + { + return false; + } + + // validate path and extension + var validDir = SystemDirectories.Xslt; + var isValidPath = IOHelper.VerifyEditPath(fullPath, validDir); + var isValidExtension = IOHelper.VerifyFileExtension(xsltFile.Path, ValidExtensions); + return isValidPath && isValidExtension; + } + + public Stream GetFileContentStream(string filepath) + { + if (FileSystem.FileExists(filepath) == false) return null; + + try + { + return FileSystem.OpenFile(filepath); + } + catch + { + return null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) + { + FileSystem.AddFile(filepath, content, true); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index ff0f9e9028..afb93f620f 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -1,5 +1,6 @@ using Umbraco.Core.Configuration; using System; +using System.ComponentModel; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; @@ -19,7 +20,7 @@ namespace Umbraco.Core.Persistence private readonly ILogger _logger; private readonly ISqlSyntaxProvider _sqlSyntax; private readonly CacheHelper _cacheHelper; - private readonly CacheHelper _noCache; + private readonly CacheHelper _nullCache; private readonly IUmbracoSettingsSection _settings; #region Ctors @@ -31,7 +32,7 @@ namespace Umbraco.Core.Persistence //if (sqlSyntax == null) throw new ArgumentNullException("sqlSyntax"); if (settings == null) throw new ArgumentNullException("settings"); - _cacheHelper = cacheHelper; + _cacheHelper = cacheHelper; //IMPORTANT: We will force the DeepCloneRuntimeCacheProvider to be used here which is a wrapper for the underlying // runtime cache to ensure that anything that can be deep cloned in/out is done so, this also ensures that our tracks @@ -48,11 +49,16 @@ namespace Umbraco.Core.Persistence _cacheHelper.IsolatedRuntimeCache.CacheFactory = type => { var cache = origFactory(type); - return new DeepCloneRuntimeCacheProvider(cache); + + //if the result is already a DeepCloneRuntimeCacheProvider then return it, otherwise + //wrap the result with a DeepCloneRuntimeCacheProvider + return cache is DeepCloneRuntimeCacheProvider + ? cache + : new DeepCloneRuntimeCacheProvider(cache); }; } - _noCache = CacheHelper.CreateDisabledCacheHelper(); + _nullCache = CacheHelper.NoCache; _logger = logger; _sqlSyntax = sqlSyntax; _settings = settings; @@ -70,60 +76,60 @@ namespace Umbraco.Core.Persistence { } - [Obsolete("Use the ctor specifying all dependencies instead, NOTE: disableAllCache has zero effect")] + [Obsolete("Use the ctor specifying all dependencies instead, NOTE: disableAllCache has zero effect")] public RepositoryFactory(bool disableAllCache, CacheHelper cacheHelper) : this(cacheHelper, LoggerResolver.Current.Logger, SqlSyntaxContext.SqlSyntaxProvider, UmbracoConfig.For.UmbracoSettings()) { if (cacheHelper == null) throw new ArgumentNullException("cacheHelper"); _cacheHelper = cacheHelper; - _noCache = CacheHelper.CreateDisabledCacheHelper(); + _nullCache = CacheHelper.NoCache; } [Obsolete("Use the ctor specifying all dependencies instead")] public RepositoryFactory(bool disableAllCache) - : this(disableAllCache ? CacheHelper.CreateDisabledCacheHelper() : ApplicationContext.Current.ApplicationCache, LoggerResolver.Current.Logger, SqlSyntaxContext.SqlSyntaxProvider, UmbracoConfig.For.UmbracoSettings()) + : this(disableAllCache ? CacheHelper.NoCache : ApplicationContext.Current.ApplicationCache, LoggerResolver.Current.Logger, SqlSyntaxContext.SqlSyntaxProvider, UmbracoConfig.For.UmbracoSettings()) { } - + #endregion - public virtual IExternalLoginRepository CreateExternalLoginRepository(IDatabaseUnitOfWork uow) + public virtual IExternalLoginRepository CreateExternalLoginRepository(IScopeUnitOfWork uow) { return new ExternalLoginRepository(uow, _cacheHelper, _logger, _sqlSyntax); } - public virtual IPublicAccessRepository CreatePublicAccessRepository(IDatabaseUnitOfWork uow) + public virtual IPublicAccessRepository CreatePublicAccessRepository(IScopeUnitOfWork uow) { return new PublicAccessRepository(uow, _cacheHelper, _logger, _sqlSyntax); } - public virtual ITaskRepository CreateTaskRepository(IDatabaseUnitOfWork uow) + public virtual ITaskRepository CreateTaskRepository(IScopeUnitOfWork uow) { return new TaskRepository(uow, - _noCache, //never cache + _nullCache, //never cache _logger, _sqlSyntax); } - public virtual IAuditRepository CreateAuditRepository(IDatabaseUnitOfWork uow) + public virtual IAuditRepository CreateAuditRepository(IScopeUnitOfWork uow) { return new AuditRepository(uow, - _noCache, //never cache + _nullCache, //never cache _logger, _sqlSyntax); } - public virtual ITagRepository CreateTagRepository(IDatabaseUnitOfWork uow) + public virtual ITagRepository CreateTagRepository(IScopeUnitOfWork uow) { return new TagRepository( uow, _cacheHelper, _logger, _sqlSyntax); } - public virtual IContentRepository CreateContentRepository(IDatabaseUnitOfWork uow) + public virtual IContentRepository CreateContentRepository(IScopeUnitOfWork uow) { return new ContentRepository( uow, @@ -139,7 +145,7 @@ namespace Umbraco.Core.Persistence }; } - public virtual IContentTypeRepository CreateContentTypeRepository(IDatabaseUnitOfWork uow) + public virtual IContentTypeRepository CreateContentTypeRepository(IScopeUnitOfWork uow) { return new ContentTypeRepository( uow, @@ -148,7 +154,7 @@ namespace Umbraco.Core.Persistence CreateTemplateRepository(uow)); } - public virtual IDataTypeDefinitionRepository CreateDataTypeDefinitionRepository(IDatabaseUnitOfWork uow) + public virtual IDataTypeDefinitionRepository CreateDataTypeDefinitionRepository(IScopeUnitOfWork uow) { return new DataTypeDefinitionRepository( uow, @@ -157,7 +163,7 @@ namespace Umbraco.Core.Persistence CreateContentTypeRepository(uow)); } - public virtual IDictionaryRepository CreateDictionaryRepository(IDatabaseUnitOfWork uow) + public virtual IDictionaryRepository CreateDictionaryRepository(IScopeUnitOfWork uow) { return new DictionaryRepository( uow, @@ -166,7 +172,7 @@ namespace Umbraco.Core.Persistence _sqlSyntax); } - public virtual ILanguageRepository CreateLanguageRepository(IDatabaseUnitOfWork uow) + public virtual ILanguageRepository CreateLanguageRepository(IScopeUnitOfWork uow) { return new LanguageRepository( uow, @@ -174,7 +180,7 @@ namespace Umbraco.Core.Persistence _logger, _sqlSyntax); } - public virtual IMediaRepository CreateMediaRepository(IDatabaseUnitOfWork uow) + public virtual IMediaRepository CreateMediaRepository(IScopeUnitOfWork uow) { return new MediaRepository( uow, @@ -185,7 +191,7 @@ namespace Umbraco.Core.Persistence _settings.Content); } - public virtual IMediaTypeRepository CreateMediaTypeRepository(IDatabaseUnitOfWork uow) + public virtual IMediaTypeRepository CreateMediaTypeRepository(IScopeUnitOfWork uow) { return new MediaTypeRepository( uow, @@ -193,16 +199,16 @@ namespace Umbraco.Core.Persistence _logger, _sqlSyntax); } - public virtual IRelationRepository CreateRelationRepository(IDatabaseUnitOfWork uow) + public virtual IRelationRepository CreateRelationRepository(IScopeUnitOfWork uow) { return new RelationRepository( uow, - _noCache, + _nullCache, _logger, _sqlSyntax, CreateRelationTypeRepository(uow)); } - public virtual IRelationTypeRepository CreateRelationTypeRepository(IDatabaseUnitOfWork uow) + public virtual IRelationTypeRepository CreateRelationTypeRepository(IScopeUnitOfWork uow) { return new RelationTypeRepository( uow, @@ -212,43 +218,56 @@ namespace Umbraco.Core.Persistence public virtual IScriptRepository CreateScriptRepository(IUnitOfWork uow) { - return new ScriptRepository(uow, new PhysicalFileSystem(SystemDirectories.Scripts), _settings.Content); + return new ScriptRepository(uow, FileSystemProviderManager.Current.ScriptsFileSystem, _settings.Content); } internal virtual IPartialViewRepository CreatePartialViewRepository(IUnitOfWork uow) { - return new PartialViewRepository(uow); + return new PartialViewRepository(uow, FileSystemProviderManager.Current.PartialViewsFileSystem); } internal virtual IPartialViewRepository CreatePartialViewMacroRepository(IUnitOfWork uow) { - return new PartialViewMacroRepository(uow); + return new PartialViewMacroRepository(uow, FileSystemProviderManager.Current.MacroPartialsFileSystem); } + public virtual IStylesheetRepository CreateStylesheetRepository(IUnitOfWork uow) + { + return new StylesheetRepository(uow, FileSystemProviderManager.Current.StylesheetsFileSystem); + } + + [Obsolete("Do not use this method, use the method with only the single unit of work parameter")] + [EditorBrowsable(EditorBrowsableState.Never)] public virtual IStylesheetRepository CreateStylesheetRepository(IUnitOfWork uow, IDatabaseUnitOfWork db) { - return new StylesheetRepository(uow, new PhysicalFileSystem(SystemDirectories.Css)); + // note: the second unit of work is ignored. + return new StylesheetRepository(uow, FileSystemProviderManager.Current.StylesheetsFileSystem); } - public virtual ITemplateRepository CreateTemplateRepository(IDatabaseUnitOfWork uow) + public virtual ITemplateRepository CreateTemplateRepository(IScopeUnitOfWork uow) { - return new TemplateRepository(uow, + return new TemplateRepository(uow, _cacheHelper, _logger, _sqlSyntax, - new PhysicalFileSystem(SystemDirectories.Masterpages), - new PhysicalFileSystem(SystemDirectories.MvcViews), + FileSystemProviderManager.Current.MasterPagesFileSystem, + FileSystemProviderManager.Current.MvcViewsFileSystem, _settings.Templates); } - public virtual IMigrationEntryRepository CreateMigrationEntryRepository(IDatabaseUnitOfWork uow) + public virtual IXsltFileRepository CreateXsltFileRepository(IUnitOfWork uow) + { + return new XsltFileRepository(uow, FileSystemProviderManager.Current.XsltFileSystem); + } + + public virtual IMigrationEntryRepository CreateMigrationEntryRepository(IScopeUnitOfWork uow) { return new MigrationEntryRepository( uow, - _noCache, //never cache + _nullCache, //never cache _logger, _sqlSyntax); } - public virtual IServerRegistrationRepository CreateServerRegistrationRepository(IDatabaseUnitOfWork uow) + public virtual IServerRegistrationRepository CreateServerRegistrationRepository(IScopeUnitOfWork uow) { return new ServerRegistrationRepository( uow, @@ -256,7 +275,7 @@ namespace Umbraco.Core.Persistence _logger, _sqlSyntax); } - public virtual IUserTypeRepository CreateUserTypeRepository(IDatabaseUnitOfWork uow) + public virtual IUserTypeRepository CreateUserTypeRepository(IScopeUnitOfWork uow) { return new UserTypeRepository( uow, @@ -265,8 +284,8 @@ namespace Umbraco.Core.Persistence _logger, _sqlSyntax); } - public virtual IUserRepository CreateUserRepository(IDatabaseUnitOfWork uow) - { + public virtual IUserRepository CreateUserRepository(IScopeUnitOfWork uow) + { return new UserRepository( uow, //Need to cache users - we look up user information more than anything in the back office! @@ -275,14 +294,14 @@ namespace Umbraco.Core.Persistence CreateUserTypeRepository(uow)); } - internal virtual IMacroRepository CreateMacroRepository(IDatabaseUnitOfWork uow) + internal virtual IMacroRepository CreateMacroRepository(IScopeUnitOfWork uow) { return new MacroRepository(uow, _cacheHelper, _logger, _sqlSyntax); } - public virtual IMemberRepository CreateMemberRepository(IDatabaseUnitOfWork uow) + public virtual IMemberRepository CreateMemberRepository(IScopeUnitOfWork uow) { return new MemberRepository( uow, @@ -294,38 +313,38 @@ namespace Umbraco.Core.Persistence _settings.Content); } - public virtual IMemberTypeRepository CreateMemberTypeRepository(IDatabaseUnitOfWork uow) + public virtual IMemberTypeRepository CreateMemberTypeRepository(IScopeUnitOfWork uow) { return new MemberTypeRepository(uow, _cacheHelper, _logger, _sqlSyntax); } - public virtual IMemberGroupRepository CreateMemberGroupRepository(IDatabaseUnitOfWork uow) + public virtual IMemberGroupRepository CreateMemberGroupRepository(IScopeUnitOfWork uow) { return new MemberGroupRepository(uow, _cacheHelper, _logger, _sqlSyntax); } - public virtual IEntityRepository CreateEntityRepository(IDatabaseUnitOfWork uow) + public virtual IEntityRepository CreateEntityRepository(IScopeUnitOfWork uow) { return new EntityRepository(uow); } - public virtual IDomainRepository CreateDomainRepository(IDatabaseUnitOfWork uow) + public virtual IDomainRepository CreateDomainRepository(IScopeUnitOfWork uow) { return new DomainRepository(uow, _cacheHelper, _logger, _sqlSyntax); } - public ITaskTypeRepository CreateTaskTypeRepository(IDatabaseUnitOfWork uow) + public ITaskTypeRepository CreateTaskTypeRepository(IScopeUnitOfWork uow) { return new TaskTypeRepository(uow, - _noCache, //never cache + _nullCache, //never cache _logger, _sqlSyntax); } - internal virtual EntityContainerRepository CreateEntityContainerRepository(IDatabaseUnitOfWork uow, Guid containerObjectType) + internal virtual EntityContainerRepository CreateEntityContainerRepository(IScopeUnitOfWork uow, Guid containerObjectType) { return new EntityContainerRepository( uow, @@ -334,7 +353,7 @@ namespace Umbraco.Core.Persistence containerObjectType); } - public IRedirectUrlRepository CreateRedirectUrlRepository(IDatabaseUnitOfWork uow) + public IRedirectUrlRepository CreateRedirectUrlRepository(IScopeUnitOfWork uow) { return new RedirectUrlRepository( uow, diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs index 1231765f20..f0bafdacf7 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs @@ -1,7 +1,23 @@ -namespace Umbraco.Core.Persistence.SqlSyntax +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Core.Persistence.SqlSyntax { internal static class SqlSyntaxProviderExtensions { + public static IEnumerable GetDefinedIndexesDefinitions(this ISqlSyntaxProvider sql, Database db) + { + return sql.GetDefinedIndexes(db) + .Select(x => new DbIndexDefinition() + { + TableName = x.Item1, + IndexName = x.Item2, + ColumnName = x.Item3, + IsUnique = x.Item4 + }).ToArray(); + } + /// /// Returns the quotes tableName.columnName combo /// diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 5ca30f2860..c451117847 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -1,13 +1,15 @@ using System; -using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Linq; using System.Text; using StackExchange.Profiling; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.SqlSyntax; +#if DEBUG_DATABASES +using System.Threading; +#endif + namespace Umbraco.Core.Persistence { /// @@ -18,11 +20,16 @@ namespace Umbraco.Core.Persistence /// 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. /// - public class UmbracoDatabase : Database, IDisposeOnRequestEnd + public class UmbracoDatabase : Database { private readonly ILogger _logger; private readonly Guid _instanceId = Guid.NewGuid(); private bool _enableCount; +#if DEBUG_DATABASES + private int _spid = -1; +#endif + + internal DefaultDatabaseFactory DatabaseFactory = null; /// /// Used for testing @@ -32,11 +39,43 @@ namespace Umbraco.Core.Persistence get { return _instanceId; } } + public string InstanceSid + { + get + { +#if DEBUG_DATABASES + return _instanceId.ToString("N").Substring(0, 8) + ":" + _spid; +#else + return _instanceId.ToString("N").Substring(0, 8); +#endif + } + } + /// /// Generally used for testing, will output all SQL statements executed to the logger /// internal bool EnableSqlTrace { get; set; } + public bool InTransaction { get; private set; } + + public override void OnBeginTransaction() + { + base.OnBeginTransaction(); + InTransaction = true; + } + + public override void OnEndTransaction() + { + base.OnEndTransaction(); + InTransaction = false; + } + +#if DEBUG_DATABASES + private const bool EnableSqlTraceDefault = true; +#else + private const bool EnableSqlTraceDefault = false; +#endif + /// /// Used for testing /// @@ -87,41 +126,74 @@ namespace Umbraco.Core.Persistence : base(connection) { _logger = logger; - EnableSqlTrace = false; + EnableSqlTrace = EnableSqlTraceDefault; } public UmbracoDatabase(string connectionString, string providerName, ILogger logger) : base(connectionString, providerName) { _logger = logger; - EnableSqlTrace = false; + EnableSqlTrace = EnableSqlTraceDefault; } public UmbracoDatabase(string connectionString, DbProviderFactory provider, ILogger logger) : base(connectionString, provider) { _logger = logger; - EnableSqlTrace = false; + EnableSqlTrace = EnableSqlTraceDefault; } public UmbracoDatabase(string connectionStringName, ILogger logger) : base(connectionStringName) { _logger = logger; - EnableSqlTrace = false; + EnableSqlTrace = EnableSqlTraceDefault; } public override IDbConnection OnConnectionOpened(IDbConnection connection) { // propagate timeout if none yet +#if DEBUG_DATABASES + // determines the database connection SPID for debugging + + if (DatabaseType == DBType.MySql) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = "SELECT CONNECTION_ID()"; + _spid = Convert.ToInt32(command.ExecuteScalar()); + } + } + else if (DatabaseType == DBType.SqlServer) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = "SELECT @@SPID"; + _spid = Convert.ToInt32(command.ExecuteScalar()); + } + } + else + { + // includes SqlCE + _spid = 0; + } +#endif + // wrap the connection with a profiling connection that tracks timings return new StackExchange.Profiling.Data.ProfiledDbConnection(connection as DbConnection, MiniProfiler.Current); } +#if DEBUG_DATABASES + public override void OnConnectionClosing(IDbConnection conn) + { + _spid = -1; + } +#endif + public override void OnException(Exception x) { - _logger.Error("Database exception occurred", x); + _logger.Error("Exception (" + InstanceSid + ").", x); base.OnException(x); } @@ -134,16 +206,27 @@ namespace Umbraco.Core.Persistence if (EnableSqlTrace) { var sb = new StringBuilder(); +#if DEBUG_DATABASES + sb.Append(InstanceSid); + sb.Append(": "); +#endif sb.Append(cmd.CommandText); foreach (DbParameter p in cmd.Parameters) { sb.Append(" - "); sb.Append(p.Value); } - - _logger.Debug(sb.ToString()); + + _logger.Debug(sb.ToString().Replace("{", "{{").Replace("}", "}}")); } +#if DEBUG_DATABASES + // ensures the database does not have an open reader, for debugging + DatabaseDebugHelper.SetCommand(cmd, InstanceSid + " [T" + Thread.CurrentThread.ManagedThreadId + "]"); + var refsobj = DatabaseDebugHelper.GetReferencedObjects(cmd.Connection); + if (refsobj != null) _logger.Debug("Oops!" + Environment.NewLine + refsobj); +#endif + base.OnExecutingCommand(cmd); } @@ -187,7 +270,7 @@ namespace Umbraco.Core.Persistence } } } - + //use the defaults base.BuildSqlDbSpecificPagingQuery(databaseType, skip, take, sql, sqlSelectRemoved, sqlOrderBy, ref args, out sqlPage); } diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWork.cs deleted file mode 100644 index c4c31849eb..0000000000 --- a/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWork.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Transactions; -using Umbraco.Core.Models.EntityBase; - -namespace Umbraco.Core.Persistence.UnitOfWork -{ - /// - /// Represents the Unit of Work implementation for working with files - /// - internal class FileUnitOfWork : IUnitOfWork - { - private Guid _key; - private readonly Queue _operations = new Queue(); - - public FileUnitOfWork() - { - _key = Guid.NewGuid(); - } - - #region Implementation of IUnitOfWork - - /// - /// Registers an instance to be added through this - /// - /// The - /// The participating in the transaction - public void RegisterAdded(IEntity entity, IUnitOfWorkRepository repository) - { - _operations.Enqueue( - new Operation - { - Entity = entity, - Repository = repository, - Type = TransactionType.Insert - }); - } - - /// - /// Registers an instance to be changed through this - /// - /// The - /// The participating in the transaction - public void RegisterChanged(IEntity entity, IUnitOfWorkRepository repository) - { - _operations.Enqueue( - new Operation - { - Entity = entity, - Repository = repository, - Type = TransactionType.Update - }); - } - - /// - /// Registers an instance to be removed through this - /// - /// The - /// The participating in the transaction - public void RegisterRemoved(IEntity entity, IUnitOfWorkRepository repository) - { - _operations.Enqueue( - new Operation - { - Entity = entity, - Repository = repository, - Type = TransactionType.Delete - }); - } - - public void Commit() - { - //NOTE: I'm leaving this in here for reference, but this is useless, transaction scope + Files doesn't do anything, - // the closest you can get is transactional NTFS, but that requires distributed transaction coordinator and some other libs/wrappers, - // plus MS has not deprecated it anyways. To do transactional IO we'd have to write this ourselves using temporary files and then - // on committing move them to their correct place. - //using(var scope = new TransactionScope()) - //{ - // // Commit the transaction - // scope.Complete(); - //} - - while (_operations.Count > 0) - { - var operation = _operations.Dequeue(); - switch (operation.Type) - { - case TransactionType.Insert: - operation.Repository.PersistNewItem(operation.Entity); - break; - case TransactionType.Delete: - operation.Repository.PersistDeletedItem(operation.Entity); - break; - case TransactionType.Update: - operation.Repository.PersistUpdatedItem(operation.Entity); - break; - } - } - - // Clear everything - _operations.Clear(); - _key = Guid.NewGuid(); - } - - public object Key - { - get { return _key; } - } - - #endregion - - #region Operation - - /// - /// Provides a snapshot of an entity and the repository reference it belongs to. - /// - private sealed class Operation - { - /// - /// Gets or sets the entity. - /// - /// The entity. - public IEntity Entity { get; set; } - - /// - /// Gets or sets the repository. - /// - /// The repository. - public IUnitOfWorkRepository Repository { get; set; } - - /// - /// Gets or sets the type of operation. - /// - /// The type of operation. - public TransactionType Type { get; set; } - } - - #endregion - - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWorkProvider.cs b/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWorkProvider.cs index c27e86a515..4d069726c1 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWorkProvider.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/FileUnitOfWorkProvider.cs @@ -1,17 +1,31 @@ -namespace Umbraco.Core.Persistence.UnitOfWork +using System; +using Umbraco.Core.Logging; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.UnitOfWork { + // note: the concept of "file unit of work" does not make sense anymore in v7.6 + // and we should probably remove this class, which is not used anywhere, but for + // the time being we keep it here - just not to break too many things. + /// - /// Represents a Unit of Work Provider for creating a + /// Represents a Unit of Work Provider for creating a file unit of work /// - public class FileUnitOfWorkProvider : IUnitOfWorkProvider + [Obsolete("Use the ScopeUnitOfWorkProvider instead.", false)] + public class FileUnitOfWorkProvider : ScopeUnitOfWorkProvider { - #region Implementation of IUnitOfWorkProvider + [Obsolete("Use the ctor specifying a IScopeProvider instead")] + public FileUnitOfWorkProvider() + : this(new ScopeProvider(new DefaultDatabaseFactory(Constants.System.UmbracoConnectionName, LoggerResolver.Current.Logger))) + { } - public IUnitOfWork GetUnitOfWork() + public FileUnitOfWorkProvider(IScopeProvider scopeProvider) + : base(scopeProvider) + { } + + public override IScopeUnitOfWork GetUnitOfWork() { - return new FileUnitOfWork(); + return new ScopeUnitOfWork(ScopeProvider); } - - #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWorkProvider.cs b/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWorkProvider.cs index c18c08d8bb..a63299d0a6 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWorkProvider.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWorkProvider.cs @@ -6,5 +6,5 @@ namespace Umbraco.Core.Persistence.UnitOfWork public interface IDatabaseUnitOfWorkProvider { IDatabaseUnitOfWork GetUnitOfWork(); - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWork.cs new file mode 100644 index 0000000000..9c655d5d48 --- /dev/null +++ b/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWork.cs @@ -0,0 +1,13 @@ +using Umbraco.Core.Events; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.UnitOfWork +{ + public interface IScopeUnitOfWork : IDatabaseUnitOfWork + { + IScope Scope { get; } + EventMessages Messages { get; } + IEventDispatcher Events { get; } + void Flush(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWorkProvider.cs b/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWorkProvider.cs new file mode 100644 index 0000000000..89c8a8bb36 --- /dev/null +++ b/src/Umbraco.Core/Persistence/UnitOfWork/IScopeUnitOfWorkProvider.cs @@ -0,0 +1,26 @@ +using System.Data; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.UnitOfWork +{ + /// + /// Provides scoped units of work. + /// + public interface IScopeUnitOfWorkProvider : IDatabaseUnitOfWorkProvider + { + // gets the scope provider + IScopeProvider ScopeProvider { get; } + + // creates a unit of work + // redefine the method to indicate it returns an IScopeUnitOfWork and + // not anymore only an IDatabaseUnitOfWork as IDatabaseUnitOfWorkProvider does + new IScopeUnitOfWork GetUnitOfWork(); + + // creates a unit of work + // support specifying an isolation level + // support auto-commit - but beware! it will be committed, whatever happens + // TODO in v8 this should all be merged as one single method with optional args + IScopeUnitOfWork GetUnitOfWork(bool readOnly); + IScopeUnitOfWork GetUnitOfWork(IsolationLevel isolationLevel, bool readOnly = false); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWork.cs deleted file mode 100644 index a5337e854c..0000000000 --- a/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWork.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Models.EntityBase; - -namespace Umbraco.Core.Persistence.UnitOfWork -{ - /// - /// Represents the Unit of Work implementation for PetaPoco - /// - internal class PetaPocoUnitOfWork : DisposableObject, IDatabaseUnitOfWork - { - - /// - /// Used for testing - /// - internal Guid InstanceId { get; private set; } - - private Guid _key; - private readonly Queue _operations = new Queue(); - - /// - /// Creates a new unit of work instance - /// - /// - /// - /// This should normally not be used directly and should be created with the UnitOfWorkProvider - /// - internal PetaPocoUnitOfWork(UmbracoDatabase database) - { - Database = database; - _key = Guid.NewGuid(); - InstanceId = Guid.NewGuid(); - } - - /// - /// Registers an instance to be added through this - /// - /// The - /// The participating in the transaction - public void RegisterAdded(IEntity entity, IUnitOfWorkRepository repository) - { - _operations.Enqueue(new Operation - { - Entity = entity, - Repository = repository, - Type = TransactionType.Insert - }); - } - - /// - /// Registers an instance to be changed through this - /// - /// The - /// The participating in the transaction - public void RegisterChanged(IEntity entity, IUnitOfWorkRepository repository) - { - _operations.Enqueue( - new Operation - { - Entity = entity, - Repository = repository, - Type = TransactionType.Update - }); - } - - /// - /// Registers an instance to be removed through this - /// - /// The - /// The participating in the transaction - public void RegisterRemoved(IEntity entity, IUnitOfWorkRepository repository) - { - _operations.Enqueue( - new Operation - { - Entity = entity, - Repository = repository, - Type = TransactionType.Delete - }); - } - - /// - /// Commits all batched changes within the scope of a PetaPoco transaction - /// - /// - /// Unlike a typical unit of work, this UOW will let you commit more than once since a new transaction is creaed per - /// Commit() call instead of having one Transaction per UOW. - /// - public void Commit() - { - Commit(null); - } - - /// - /// Commits all batched changes within the scope of a PetaPoco transaction - /// - /// - /// Allows you to set a callback which is executed before the transaction is committed, allow you to add additional SQL - /// operations to the overall commit process after the queue has been processed. - /// - internal void Commit(Action transactionCompleting) - { - using (var transaction = Database.GetTransaction()) - { - while (_operations.Count > 0) - { - var operation = _operations.Dequeue(); - switch (operation.Type) - { - case TransactionType.Insert: - operation.Repository.PersistNewItem(operation.Entity); - break; - case TransactionType.Delete: - operation.Repository.PersistDeletedItem(operation.Entity); - break; - case TransactionType.Update: - operation.Repository.PersistUpdatedItem(operation.Entity); - break; - } - } - - //Execute the callback if there is one - if (transactionCompleting != null) - { - transactionCompleting(Database); - } - - transaction.Complete(); - } - - // Clear everything - _operations.Clear(); - _key = Guid.NewGuid(); - } - - public object Key - { - get { return _key; } - } - - public UmbracoDatabase Database { get; private set; } - - #region Operation - - /// - /// Provides a snapshot of an entity and the repository reference it belongs to. - /// - private sealed class Operation - { - /// - /// Gets or sets the entity. - /// - /// The entity. - public IEntity Entity { get; set; } - - /// - /// Gets or sets the repository. - /// - /// The repository. - public IUnitOfWorkRepository Repository { get; set; } - - /// - /// Gets or sets the type of operation. - /// - /// The type of operation. - public TransactionType Type { get; set; } - } - - #endregion - - /// - /// Ensures disposable objects are disposed - /// - /// - /// Ensures that the Transaction instance is disposed of - /// - protected override void DisposeResources() - { - _operations.Clear(); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWorkProvider.cs b/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWorkProvider.cs index 8c909fc554..56f6c6af98 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWorkProvider.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWorkProvider.cs @@ -1,83 +1,40 @@ using System; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.UnitOfWork { /// - /// Represents a Unit of Work Provider for creating a + /// Represents a Unit of Work Provider for creating a /// - public class PetaPocoUnitOfWorkProvider : IDatabaseUnitOfWorkProvider - { - private readonly IDatabaseFactory _dbFactory; - + public class PetaPocoUnitOfWorkProvider : ScopeUnitOfWorkProvider + { [Obsolete("Use the constructor specifying an ILogger instead")] public PetaPocoUnitOfWorkProvider() - : this(new DefaultDatabaseFactory(GlobalSettings.UmbracoConnectionName, LoggerResolver.Current.Logger)) - { - - } + : base(new ScopeProvider(new DefaultDatabaseFactory(Constants.System.UmbracoConnectionName, LoggerResolver.Current.Logger))) + { } [Obsolete("Use the constructor specifying an ILogger instead")] public PetaPocoUnitOfWorkProvider(string connectionString, string providerName) - : this(new DefaultDatabaseFactory(connectionString, providerName, LoggerResolver.Current.Logger)) + : base(new ScopeProvider(new DefaultDatabaseFactory(connectionString, providerName, LoggerResolver.Current.Logger))) + { } + + public PetaPocoUnitOfWorkProvider(ILogger logger) + : base(new ScopeProvider(new DefaultDatabaseFactory(Constants.System.UmbracoConnectionName, logger))) { } /// - /// Parameterless constructor uses defaults - /// - public PetaPocoUnitOfWorkProvider(ILogger logger) - : this(new DefaultDatabaseFactory(GlobalSettings.UmbracoConnectionName, logger)) - { - - } - - /// - /// Constructor accepting custom connectino string and provider name + /// Constructor accepting custom connection string and provider name /// /// /// Connection String to use with Database /// Database Provider for the Connection String public PetaPocoUnitOfWorkProvider(ILogger logger, string connectionString, string providerName) - : this(new DefaultDatabaseFactory(connectionString, providerName, logger)) + : base(new ScopeProvider(new DefaultDatabaseFactory(connectionString, providerName, logger))) { } - /// - /// Constructor accepting an IDatabaseFactory instance - /// - /// - public PetaPocoUnitOfWorkProvider(IDatabaseFactory dbFactory) - { - Mandate.ParameterNotNull(dbFactory, "dbFactory"); - _dbFactory = dbFactory; - } - - #region Implementation of IUnitOfWorkProvider - - /// - /// Creates a Unit of work with a new UmbracoDatabase instance for the work item/transaction. - /// - /// - /// - /// Each PetaPoco UOW uses it's own Database object, not the shared Database object that comes from - /// the ApplicationContext.Current.DatabaseContext.Database. This is because each transaction should use it's own Database - /// and we Dispose of this Database object when the UOW is disposed. - /// - public IDatabaseUnitOfWork GetUnitOfWork() - { - return new PetaPocoUnitOfWork(_dbFactory.CreateDatabase()); - } - - #endregion - - /// - /// Static helper method to return a new unit of work - /// - /// - internal static IDatabaseUnitOfWork CreateUnitOfWork(ILogger logger) - { - var provider = new PetaPocoUnitOfWorkProvider(logger); - return provider.GetUnitOfWork(); - } + public PetaPocoUnitOfWorkProvider(IScopeProvider scopeProvider) + : base(scopeProvider) + { } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWork.cs new file mode 100644 index 0000000000..f8d168cdaa --- /dev/null +++ b/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWork.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Data; +using Umbraco.Core.Events; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.UnitOfWork +{ + /// + /// Represents a scoped unit of work. + /// + internal class ScopeUnitOfWork : DisposableObject, IScopeUnitOfWork + { + private readonly Queue _operations = new Queue(); + private readonly IsolationLevel _isolationLevel; + private readonly IScopeProvider _scopeProvider; + private bool _completeScope; + private readonly bool _readOnly; + private IScope _scope; + private Guid _key; + + /// + /// Used for testing + /// + internal Guid InstanceId { get; private set; } + + /// + /// Creates a new unit of work instance + /// + /// + /// + /// + /// + /// This should normally not be used directly and should be created with the UnitOfWorkProvider + /// + internal ScopeUnitOfWork(IScopeProvider scopeProvider, IsolationLevel isolationLevel = IsolationLevel.Unspecified, bool readOnly = false) + { + _scopeProvider = scopeProvider; + _isolationLevel = isolationLevel; + _key = Guid.NewGuid(); + InstanceId = Guid.NewGuid(); + + // be false by default + // if set to true... the UnitOfWork is "auto-commit" which means that even in the case of + // an exception, the scope would still be completed - ppl should use it with great care! + _completeScope = _readOnly = readOnly; + } + + /// + /// Registers an instance to be added through this + /// + /// The + /// The participating in the transaction + public void RegisterAdded(IEntity entity, IUnitOfWorkRepository repository) + { + if (_readOnly) + throw new NotSupportedException("This unit of work is read-only."); + + _operations.Enqueue(new Operation + { + Entity = entity, + Repository = repository, + Type = TransactionType.Insert + }); + } + + /// + /// Registers an instance to be changed through this + /// + /// The + /// The participating in the transaction + public void RegisterChanged(IEntity entity, IUnitOfWorkRepository repository) + { + if (_readOnly) + throw new NotSupportedException("This unit of work is read-only."); + + _operations.Enqueue( + new Operation + { + Entity = entity, + Repository = repository, + Type = TransactionType.Update + }); + } + + /// + /// Registers an instance to be removed through this + /// + /// The + /// The participating in the transaction + public void RegisterRemoved(IEntity entity, IUnitOfWorkRepository repository) + { + if (_readOnly) + throw new NotSupportedException("This unit of work is read-only."); + + _operations.Enqueue( + new Operation + { + Entity = entity, + Repository = repository, + Type = TransactionType.Delete + }); + } + + /// + /// Commits all batched changes within the scope of a PetaPoco transaction + /// + /// + /// Unlike a typical unit of work, this UOW will let you commit more than once since a new transaction is creaed per + /// Commit() call instead of having one Transaction per UOW. + /// + public void Commit() + { + Commit(null); + } + + /// + /// Commits all batched changes within the scope of a PetaPoco transaction + /// + /// + /// Allows you to set a callback which is executed before the transaction is committed, allow you to add additional SQL + /// operations to the overall commit process after the queue has been processed. + /// + internal void Commit(Action transactionCompleting) + { + // this happens in a scope-managed transaction + + if (_readOnly) + throw new NotSupportedException("This unit of work is read-only."); + + // in case anything goes wrong + _completeScope = false; + + while (_operations.Count > 0) + { + var operation = _operations.Dequeue(); + switch (operation.Type) + { + case TransactionType.Insert: + operation.Repository.PersistNewItem(operation.Entity); + break; + case TransactionType.Delete: + operation.Repository.PersistDeletedItem(operation.Entity); + break; + case TransactionType.Update: + operation.Repository.PersistUpdatedItem(operation.Entity); + break; + } + } + + if (transactionCompleting != null) + transactionCompleting(Database); + + // all is ok + _completeScope = true; + + // Clear everything + _operations.Clear(); + _key = Guid.NewGuid(); + } + + public void Flush() + { + if (_readOnly) + throw new NotSupportedException("This unit of work is read-only."); + + while (_operations.Count > 0) + { + var operation = _operations.Dequeue(); + switch (operation.Type) + { + case TransactionType.Insert: + operation.Repository.PersistNewItem(operation.Entity); + break; + case TransactionType.Delete: + operation.Repository.PersistDeletedItem(operation.Entity); + break; + case TransactionType.Update: + operation.Repository.PersistUpdatedItem(operation.Entity); + break; + } + } + + _operations.Clear(); + _key = Guid.NewGuid(); + } + + public object Key + { + get { return _key; } + } + + public IScope Scope + { + // TODO + // once we are absolutely sure that our UOW cannot be disposed more than once, + // this should throw if the UOW has already been disposed, NOT recreate a scope! + get { return _scope ?? (_scope = _scopeProvider.CreateScope(_isolationLevel)); } + } + + public UmbracoDatabase Database + { + get { return Scope.Database; } + } + + public EventMessages Messages + { + get { return Scope.Messages; } + } + + public IEventDispatcher Events + { + get { return Scope.Events; } + } + + #region Operation + + /// + /// Provides a snapshot of an entity and the repository reference it belongs to. + /// + private sealed class Operation + { + /// + /// Gets or sets the entity. + /// + /// The entity. + public IEntity Entity { get; set; } + + /// + /// Gets or sets the repository. + /// + /// The repository. + public IUnitOfWorkRepository Repository { get; set; } + + /// + /// Gets or sets the type of operation. + /// + /// The type of operation. + public TransactionType Type { get; set; } + } + + #endregion + + /// + /// Ensures disposable objects are disposed + /// + /// + /// Ensures that the Transaction instance is disposed of + /// + protected override void DisposeResources() + { + _operations.Clear(); + if (_scope == null) return; + + if (_completeScope) _scope.Complete(); + _scope.Dispose(); + _scope = null; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWorkProvider.cs b/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWorkProvider.cs new file mode 100644 index 0000000000..9800b2bc55 --- /dev/null +++ b/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWorkProvider.cs @@ -0,0 +1,51 @@ +using System.Data; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.UnitOfWork +{ + public abstract class ScopeUnitOfWorkProvider : IScopeUnitOfWorkProvider + { + /// + /// Constructor accepting a instance + /// + /// + protected ScopeUnitOfWorkProvider(IScopeProvider scopeProvider) + { + Mandate.ParameterNotNull(scopeProvider, "scopeProvider"); + ScopeProvider = scopeProvider; + } + + /// + public IScopeProvider ScopeProvider { get; private set; } + + // explicit implementation + IDatabaseUnitOfWork IDatabaseUnitOfWorkProvider.GetUnitOfWork() + { + return new ScopeUnitOfWork(ScopeProvider); + } + + /// + public virtual IScopeUnitOfWork GetUnitOfWork() + { + return new ScopeUnitOfWork(ScopeProvider); + } + + /// + public IScopeUnitOfWork GetUnitOfWork(IsolationLevel isolationLevel) + { + return new ScopeUnitOfWork(ScopeProvider, isolationLevel); + } + + /// + public IScopeUnitOfWork GetUnitOfWork(bool readOnly) + { + return new ScopeUnitOfWork(ScopeProvider, readOnly: readOnly); + } + + /// + public IScopeUnitOfWork GetUnitOfWork(IsolationLevel isolationLevel, bool readOnly) + { + return new ScopeUnitOfWork(ScopeProvider, isolationLevel, readOnly: readOnly); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PluginManager.cs b/src/Umbraco.Core/PluginManager.cs index 102f0ee88f..3eb0f2a018 100644 --- a/src/Umbraco.Core/PluginManager.cs +++ b/src/Umbraco.Core/PluginManager.cs @@ -7,45 +7,55 @@ using System.Reflection; using System.Text; using System.Threading; using System.Web.Compilation; -using System.Xml.Linq; -using Umbraco.Core.Configuration; +using Umbraco.Core.Cache; using Umbraco.Core.IO; using Umbraco.Core.Logging; -using Umbraco.Core.Models; using Umbraco.Core.Persistence.Mappers; -using Umbraco.Core.Persistence.Migrations; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Profiling; using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Cache; using umbraco.interfaces; using File = System.IO.File; namespace Umbraco.Core { /// - /// Used to resolve all plugin types and cache them and is also used to instantiate plugin types + /// Provides methods to find and instanciate types. /// /// - /// - /// This class should be used to resolve all plugin types, the TypeFinder should not be used directly! - /// - /// This class can expose extension methods to resolve custom plugins - /// - /// Before this class resolves any plugins it checks if the hash has changed for the DLLs in the /bin folder, if it hasn't - /// it will use the cached resolved plugins that it has already found which means that no assembly scanning is necessary. This leads - /// to much faster startup times. + /// This class should be used to resolve all types, the class should never be used directly. + /// In most cases this class is not used directly but through extension methods that retrieve specific types. + /// This class caches the types it knows to avoid excessive assembly scanning and shorten startup times, relying + /// on a hash of the DLLs in the ~/bin folder to check for cache expiration. /// public class PluginManager { + private const string CacheKey = "umbraco-plugins.list"; + + private static PluginManager _current; + private static bool _hasCurrent; + private static object _currentLock = new object(); + + private readonly IServiceProvider _serviceProvider; + private readonly IRuntimeCacheProvider _runtimeCache; + private readonly ProfilingLogger _logger; + private readonly string _tempFolder; + + private readonly object _typesLock = new object(); + private readonly Dictionary _types = new Dictionary(); + + private long _cachedAssembliesHash = -1; + private long _currentAssembliesHash = -1; + private IEnumerable _assemblies; + private bool _reportedChange; + /// - /// Creates a new PluginManager with an ApplicationContext instance which ensures that the plugin xml - /// file is cached temporarily until app startup completes. + /// Initializes a new instance of the class. /// - /// - /// - /// - /// + /// A mechanism for retrieving service objects. + /// The application runtime cache. + /// A profiling logger. + /// Whether to detect changes using hashes. internal PluginManager(IServiceProvider serviceProvider, IRuntimeCacheProvider runtimeCache, ProfilingLogger logger, bool detectChanges = true) { if (serviceProvider == null) throw new ArgumentNullException("serviceProvider"); @@ -56,22 +66,13 @@ namespace Umbraco.Core _runtimeCache = runtimeCache; _logger = logger; + // the temp folder where the cache file lives _tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache"); - //create the folder if it doesn't exist if (Directory.Exists(_tempFolder) == false) - { Directory.CreateDirectory(_tempFolder); - } var pluginListFile = GetPluginListFilePath(); - //this is a check for legacy changes, before we didn't store the TypeResolutionKind in the file which was a mistake, - //so we need to detect if the old file is there without this attribute, if it is then we delete it - if (DetectLegacyPluginListFile()) - { - File.Delete(pluginListFile); - } - if (detectChanges) { //first check if the cached hash is 0, if it is then we ne @@ -80,7 +81,7 @@ namespace Umbraco.Core //if they have changed, we need to write the new file if (RequiresRescanning) { - //if the hash has changed, clear out the persisted list no matter what, this will force + // if the hash has changed, clear out the persisted list no matter what, this will force // rescanning of all plugin types including lazy ones. // http://issues.umbraco.org/issue/U4-4789 File.Delete(pluginListFile); @@ -90,75 +91,96 @@ namespace Umbraco.Core } else { - - //if the hash has changed, clear out the persisted list no matter what, this will force + // if the hash has changed, clear out the persisted list no matter what, this will force // rescanning of all plugin types including lazy ones. // http://issues.umbraco.org/issue/U4-4789 File.Delete(pluginListFile); - //always set to true if we're not detecting (generally only for testing) + // always set to true if we're not detecting (generally only for testing) RequiresRescanning = true; } } - private readonly IServiceProvider _serviceProvider; - private readonly IRuntimeCacheProvider _runtimeCache; - private readonly ProfilingLogger _logger; - private const string CacheKey = "umbraco-plugins.list"; - static PluginManager _resolver; - private readonly string _tempFolder; - private long _cachedAssembliesHash = -1; - private long _currentAssembliesHash = -1; - private static bool _initialized = false; - private static object _singletonLock = new object(); - /// - /// We will ensure that no matter what, only one of these is created, this is to ensure that caching always takes place + /// Gets or sets the set of assemblies to scan. /// /// - /// The setter is generally only used for unit tests + /// If not explicitely set, defaults to all assemblies except those that are know to not have any of the + /// types we might scan. Because we only scan for application types, this means we can safely exclude GAC assemblies + /// for example. + /// This is for unit tests. /// + internal IEnumerable AssembliesToScan + { + get { return _assemblies ?? (_assemblies = TypeFinder.GetAssembliesWithKnownExclusions()); } + set { _assemblies = value; } + } + + /// + /// Gets the type lists. + /// + /// For unit tests. + internal IEnumerable TypeLists + { + get { return _types.Values; } + } + + /// + /// Sets a type list. + /// + /// For unit tests. + internal void AddTypeList(TypeList typeList) + { + _types[new TypeListKey(typeList.BaseType, typeList.AttributeType)] = typeList; + } + + /// + /// Gets or sets the singleton instance. + /// + /// The setter exists for unit tests. public static PluginManager Current { get { - return LazyInitializer.EnsureInitialized(ref _resolver, ref _initialized, ref _singletonLock, () => + return LazyInitializer.EnsureInitialized(ref _current, ref _hasCurrent, ref _currentLock, () => { + IRuntimeCacheProvider runtimeCache; + ProfilingLogger profilingLogger; + if (ApplicationContext.Current == null) { + runtimeCache = new NullCacheProvider(); var logger = LoggerResolver.HasCurrent ? LoggerResolver.Current.Logger : new DebugDiagnosticsLogger(); var profiler = ProfilerResolver.HasCurrent ? ProfilerResolver.Current.Profiler : new LogProfiler(logger); - return new PluginManager( - new ActivatorServiceProvider(), - new NullCacheProvider(), - new ProfilingLogger(logger, profiler)); + profilingLogger = new ProfilingLogger(logger, profiler); } - return new PluginManager( - new ActivatorServiceProvider(), - ApplicationContext.Current.ApplicationCache.RuntimeCache, - ApplicationContext.Current.ProfilingLogger); + else + { + runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + profilingLogger = ApplicationContext.Current.ProfilingLogger; + } + + return new PluginManager(new ActivatorServiceProvider(), runtimeCache, profilingLogger); }); } set { - _initialized = true; - _resolver = value; + _hasCurrent = true; + _current = value; } } - #region Hash checking methods - + #region Hashing /// - /// Returns a bool if the assemblies in the /bin, app_code, global.asax, etc... have changed since they were last hashed. + /// Gets a value indicating whether the assemblies in bin, app_code, global.asax, etc... have changed since they were last hashed. /// internal bool RequiresRescanning { get; private set; } /// - /// Returns the currently cached hash value of the scanned assemblies in the /bin folder. Returns 0 - /// if no cache is found. + /// Gets the currently cached hash value of the scanned assemblies. /// - /// + /// The cached hash value, or 0 if no cache is found. internal long CachedAssembliesHash { get @@ -167,24 +189,22 @@ namespace Umbraco.Core return _cachedAssembliesHash; var filePath = GetPluginHashFilePath(); - if (!File.Exists(filePath)) - return 0; + if (File.Exists(filePath) == false) return 0; + var hash = File.ReadAllText(filePath, Encoding.UTF8); - Int64 val; - if (Int64.TryParse(hash, out val)) - { - _cachedAssembliesHash = val; - return _cachedAssembliesHash; - } - //it could not parse for some reason so we'll return 0. - return 0; + + long val; + if (long.TryParse(hash, out val) == false) return 0; + + _cachedAssembliesHash = val; + return _cachedAssembliesHash; } } /// - /// Returns the current assemblies hash based on creating a hash from the assemblies in the /bin + /// Gets the current assemblies hash based on creating a hash from the assemblies in various places. /// - /// + /// The current hash. internal long CurrentAssembliesHash { get @@ -192,26 +212,24 @@ namespace Umbraco.Core if (_currentAssembliesHash != -1) return _currentAssembliesHash; - _currentAssembliesHash = GetFileHash( - new List> - { - //add the bin folder and everything in it - new Tuple(new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Bin)), false), - //add the app code folder and everything in it - new Tuple(new DirectoryInfo(IOHelper.MapPath("~/App_Code")), false), - //add the global.asax (the app domain also monitors this, if it changes will do a full restart) - new Tuple(new FileInfo(IOHelper.MapPath("~/global.asax")), false), + _currentAssembliesHash = GetFileHash(new List> + { + // the bin folder and everything in it + new Tuple(new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Bin)), false), + // the app code folder and everything in it + new Tuple(new DirectoryInfo(IOHelper.MapPath("~/App_Code")), false), + // global.asax (the app domain also monitors this, if it changes will do a full restart) + new Tuple(new FileInfo(IOHelper.MapPath("~/global.asax")), false), + // trees.config - use the contents to create the hash since this gets resaved on every app startup! + new Tuple(new FileInfo(IOHelper.MapPath(SystemDirectories.Config + "/trees.config")), true) + }, _logger); - //add the trees.config - use the contents to create the has since this gets resaved on every app startup! - new Tuple(new FileInfo(IOHelper.MapPath(SystemDirectories.Config + "/trees.config")), true) - }, _logger - ); return _currentAssembliesHash; } } /// - /// Writes the assembly hash file + /// Writes the assembly hash file. /// private void WriteCachePluginsHash() { @@ -220,652 +238,725 @@ namespace Umbraco.Core } /// - /// Returns a unique hash for the combination of FileInfo objects passed in + /// Returns a unique hash for a combination of FileInfo objects. /// - /// - /// A collection of files and whether or not to use their file contents to determine the hash or the file's properties - /// (true will make a hash based on it's contents) - /// - /// + /// A collection of files. + /// A profiling logger. + /// The hash. + /// Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the + /// file properties (false) or the file contents (true). internal static long GetFileHash(IEnumerable> filesAndFolders, ProfilingLogger logger) { using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined")) { var hashCombiner = new HashCodeCombiner(); - //get the file info's to check - var fileInfos = filesAndFolders.Where(x => x.Item2 == false).ToArray(); - var fileContents = filesAndFolders.Except(fileInfos); - - //add each unique folder/file to the hash - foreach (var i in fileInfos.Select(x => x.Item1).DistinctBy(x => x.FullName)) - { - hashCombiner.AddFileSystemItem(i); - } + // get the distinct file infos to hash + var uniqInfos = new HashSet(); + var uniqContent = new HashSet(); - //add each unique file's contents to the hash - foreach (var i in fileContents.Select(x => x.Item1).DistinctBy(x => x.FullName)) + foreach (var fileOrFolder in filesAndFolders) { - if (File.Exists(i.FullName)) + var info = fileOrFolder.Item1; + if (fileOrFolder.Item2) { - var content = File.ReadAllText(i.FullName).Replace("\r\n", string.Empty).Replace("\n", string.Empty).Replace("\r", string.Empty); - hashCombiner.AddCaseInsensitiveString(content); - } - - } + // add each unique file's contents to the hash + // normalize the content for cr/lf and case-sensitivity - return ConvertPluginsHashFromHex(hashCombiner.GetCombinedHashCode()); + if (uniqContent.Contains(info.FullName)) continue; + uniqContent.Add(info.FullName); + if (File.Exists(info.FullName) == false) continue; + var content = RemoveCrLf(File.ReadAllText(info.FullName)); + hashCombiner.AddCaseInsensitiveString(content); + } + else + { + // add each unique folder/file to the hash + + if (uniqInfos.Contains(info.FullName)) continue; + uniqInfos.Add(info.FullName); + hashCombiner.AddFileSystemItem(info); + } + } + + return ConvertHashToInt64(hashCombiner.GetCombinedHashCode()); } } + // fast! (yes, according to benchmarks) + private static string RemoveCrLf(string s) + { + var buffer = new char[s.Length]; + var count = 0; + // ReSharper disable once ForCanBeConvertedToForeach - no! + for (var i = 0; i < s.Length; i++) + { + if (s[i] != '\r' && s[i] != '\n') + buffer[count++] = s[i]; + } + return new string(buffer, 0, count); + } + + /// + /// Returns a unique hash for a combination of FileInfo objects. + /// + /// A collection of files. + /// A profiling logger. + /// The hash. internal static long GetFileHash(IEnumerable filesAndFolders, ProfilingLogger logger) { using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined")) { var hashCombiner = new HashCodeCombiner(); - //add each unique folder/file to the hash - foreach (var i in filesAndFolders.DistinctBy(x => x.FullName)) + // get the distinct file infos to hash + var uniqInfos = new HashSet(); + + foreach (var fileOrFolder in filesAndFolders) { - hashCombiner.AddFileSystemItem(i); + if (uniqInfos.Contains(fileOrFolder.FullName)) continue; + uniqInfos.Add(fileOrFolder.FullName); + hashCombiner.AddFileSystemItem(fileOrFolder); } - return ConvertPluginsHashFromHex(hashCombiner.GetCombinedHashCode()); + + return ConvertHashToInt64(hashCombiner.GetCombinedHashCode()); } } /// - /// Converts the hash value of current plugins to long from string + /// Converts a string hash value into an Int64. /// - /// - /// - internal static long ConvertPluginsHashFromHex(string val) + internal static long ConvertHashToInt64(string val) { long outVal; - if (Int64.TryParse(val, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out outVal)) - { - return outVal; - } - return 0; + return long.TryParse(val, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out outVal) ? outVal : 0; } - /// - /// Attempts to resolve the list of plugin + assemblies found in the runtime for the base type 'T' passed in. - /// If the cache file doesn't exist, fails to load, is corrupt or the type 'T' element is not found then - /// a false attempt is returned. - /// - /// - /// - internal Attempt> TryGetCachedPluginsFromFile(TypeResolutionKind resolutionType) - { - var filePath = GetPluginListFilePath(); - if (!File.Exists(filePath)) - return Attempt>.Fail(); + #endregion + #region Cache + + /// + /// Attemps to retrieve the list of types from the cache. + /// + /// Fails if the cache is missing or corrupt in any way. + internal Attempt> TryGetCached(Type baseType, Type attributeType) + { + var cache = _runtimeCache.GetCacheItem, IEnumerable>>(CacheKey, ReadCacheSafe, TimeSpan.FromMinutes(4)); + + IEnumerable types; + cache.TryGetValue(Tuple.Create(baseType == null ? string.Empty : baseType.FullName, attributeType == null ? string.Empty : attributeType.FullName), out types); + return types == null + ? Attempt>.Fail() + : Attempt.Succeed(types); + } + + internal Dictionary, IEnumerable> ReadCacheSafe() + { try { - //we will load the xml document, if the app context exist, we will load it from the cache (which is only around for 5 minutes) - //while the app boots up, this should save some IO time on app startup when the app context is there (which is always unless in unit tests) - var xml = _runtimeCache.GetCacheItem(CacheKey, - () => XDocument.Load(filePath), - new TimeSpan(0, 0, 5, 0)); - - if (xml.Root == null) - return Attempt>.Fail(); - - var typeElement = xml.Root.Elements() - .SingleOrDefault(x => - x.Name.LocalName == "baseType" - && ((string)x.Attribute("type")) == typeof(T).FullName - && ((string)x.Attribute("resolutionType")) == resolutionType.ToString()); - - //return false but specify this exception type so we can detect it - if (typeElement == null) - return Attempt>.Fail(new CachedPluginNotFoundInFileException()); - - //return success - return Attempt.Succeed(typeElement.Elements("add") - .Select(x => (string)x.Attribute("type"))); + return ReadCache(); } - catch (Exception ex) + catch { - //if the file is corrupted, etc... return false - return Attempt>.Fail(ex); + try + { + var filePath = GetPluginListFilePath(); + File.Delete(filePath); + } + catch + { + // on-purpose, does not matter + } } + + return new Dictionary, IEnumerable>(); + } + + internal Dictionary, IEnumerable> ReadCache() + { + var cache = new Dictionary, IEnumerable>(); + + var filePath = GetPluginListFilePath(); + if (File.Exists(filePath) == false) + return cache; + + using (var stream = File.OpenRead(filePath)) + using (var reader = new StreamReader(stream)) + { + while (true) + { + var baseType = reader.ReadLine(); + if (baseType == null) return cache; // exit + if (baseType.StartsWith("<")) break; // old xml + + var attributeType = reader.ReadLine(); + if (attributeType == null) break; + + var types = new List(); + while (true) + { + var type = reader.ReadLine(); + if (type == null) + { + types = null; // break 2 levels + break; + } + if (type == string.Empty) + { + cache[Tuple.Create(baseType, attributeType)] = types; + break; + } + types.Add(type); + } + + if (types == null) break; + } + } + + cache.Clear(); + return cache; } /// - /// Removes cache files and internal cache as well + /// Removes cache files and internal cache. /// - /// - /// Generally only used for resetting cache, for example during the install process - /// + /// Generally only used for resetting cache, for example during the install process. public void ClearPluginCache() { var path = GetPluginListFilePath(); if (File.Exists(path)) File.Delete(path); + path = GetPluginHashFilePath(); if (File.Exists(path)) File.Delete(path); _runtimeCache.ClearCacheItem(CacheKey); } - + private string GetPluginListFilePath() { - return Path.Combine(_tempFolder, string.Format("umbraco-plugins.{0}.list", NetworkHelper.FileSafeMachineName)); + var filename = "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".list"; + return Path.Combine(_tempFolder, filename); } private string GetPluginHashFilePath() { - return Path.Combine(_tempFolder, string.Format("umbraco-plugins.{0}.hash", NetworkHelper.FileSafeMachineName)); + var filename = "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".hash"; + return Path.Combine(_tempFolder, filename); } - /// - /// This will return true if the plugin list file is a legacy one - /// - /// - /// - /// This method exists purely due to an error in 4.11. We were writing the plugin list file without the - /// type resolution kind which will have caused some problems. Now we detect this legacy file and if it is detected - /// we remove it so it can be recreated properly. - /// - internal bool DetectLegacyPluginListFile() + internal void WriteCache() { var filePath = GetPluginListFilePath(); - if (!File.Exists(filePath)) - return false; - try + using (var stream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite)) + using (var writer = new StreamWriter(stream)) { - var xml = XDocument.Load(filePath); - if (xml.Root == null) - return false; - - var typeElement = xml.Root.Elements() - .FirstOrDefault(x => x.Name.LocalName == "baseType"); - - if (typeElement == null) - return false; - - //now check if the typeElement is missing the resolutionType attribute - return typeElement.Attributes().All(x => x.Name.LocalName != "resolutionType"); - } - catch (Exception) - { - //if the file is corrupted, etc... return true so it is removed - return true; + foreach (var typeList in _types.Values) + { + writer.WriteLine(typeList.BaseType == null ? string.Empty : typeList.BaseType.FullName); + writer.WriteLine(typeList.AttributeType == null ? string.Empty : typeList.AttributeType.FullName); + foreach (var type in typeList.Types) + writer.WriteLine(type.AssemblyQualifiedName); + writer.WriteLine(); + } } } - /// - /// Adds/Updates the type list for the base type 'T' in the cached file - /// - /// - /// - /// - /// - /// THIS METHOD IS NOT THREAD SAFE - /// - /// - /// - /// - /// - /// - /// - /// - /// ]]> - /// - internal void UpdateCachedPluginsFile(IEnumerable typesFound, TypeResolutionKind resolutionType) + internal void UpdateCache() { - var filePath = GetPluginListFilePath(); - XDocument xml; - try - { - xml = XDocument.Load(filePath); - } - catch - { - //if there's an exception loading then this is somehow corrupt, we'll just replace it. - File.Delete(filePath); - //create the document and the root - xml = new XDocument(new XElement("plugins")); - } - if (xml.Root == null) - { - //if for some reason there is no root, create it - xml.Add(new XElement("plugins")); - } - //find the type 'T' element to add or update - var typeElement = xml.Root.Elements() - .SingleOrDefault(x => - x.Name.LocalName == "baseType" - && ((string)x.Attribute("type")) == typeof(T).FullName - && ((string)x.Attribute("resolutionType")) == resolutionType.ToString()); - - if (typeElement == null) - { - //create the type element - typeElement = new XElement("baseType", - new XAttribute("type", typeof(T).FullName), - new XAttribute("resolutionType", resolutionType.ToString())); - //then add it to the root - xml.Root.Add(typeElement); - } - - - //now we have the type element, we need to clear any previous types as children and add/update it with new ones - typeElement.ReplaceNodes(typesFound.Select(x => new XElement("add", new XAttribute("type", x.AssemblyQualifiedName)))); - //save the xml file - xml.Save(filePath); + // note + // at the moment we write the cache to disk every time we update it. ideally we defer the writing + // since all the updates are going to happen in a row when Umbraco starts. that being said, the + // file is small enough, so it is not a priority. + WriteCache(); } #endregion - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); - private readonly HashSet _types = new HashSet(); - private IEnumerable _assemblies; + #region Create Instances /// - /// Returns all found property editors (based on the resolved Iparameter editors - this saves a scan) + /// Resolves and creates instances. /// - internal IEnumerable ResolvePropertyEditors() + /// The type to use for resolution. + /// Indicates whether to throw if an instance cannot be created. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// The created instances. + /// + /// By default is false and instances that cannot be created are just skipped. + /// By default is true and cache is used for type resolution. + /// By default is null and is used. + /// Caching is disabled when using specific assemblies. + /// + internal IEnumerable FindAndCreateInstances(bool throwException = false, bool cache = true, IEnumerable specificAssemblies = null) { - //return all proeprty editor types found except for the base property editor type - return ResolveTypes() - .Where(x => x.IsType()) - .Except(new[] { typeof(PropertyEditor) }); - } - - /// - /// Returns all found parameter editors (which includes property editors) - /// - internal IEnumerable ResolveParameterEditors() - { - //return all paramter editor types found except for the base property editor type - return ResolveTypes() - .Except(new[] { typeof(ParameterEditor), typeof(PropertyEditor) }); - } - - /// - /// Returns all available IApplicationStartupHandler objects - /// - /// - internal IEnumerable ResolveApplicationStartupHandlers() - { - return ResolveTypes(); - } - - /// - /// Returns all classes of type ICacheRefresher - /// - /// - internal IEnumerable ResolveCacheRefreshers() - { - return ResolveTypes(); - } - - /// - /// Returns all available IPropertyEditorValueConverter - /// - /// - internal IEnumerable ResolvePropertyEditorValueConverters() - { - return ResolveTypes(); - } - - /// - /// Returns all available IDataType in application - /// - /// - internal IEnumerable ResolveDataTypes() - { - return ResolveTypes(); - } - - /// - /// Returns all available IMacroGuiRendering in application - /// - /// - internal IEnumerable ResolveMacroRenderings() - { - return ResolveTypes(); - } - - /// - /// Returns all available IPackageAction in application - /// - /// - internal IEnumerable ResolvePackageActions() - { - return ResolveTypes(); - } - - /// - /// Returns all available IAction in application - /// - /// - internal IEnumerable ResolveActions() - { - return ResolveTypes(); - } - - /// - /// Returns all mapper types that have a MapperFor attribute defined - /// - /// - internal IEnumerable ResolveAssignedMapperTypes() - { - return ResolveTypesWithAttribute(); - } - - /// - /// Returns all SqlSyntaxProviders with the SqlSyntaxProviderAttribute - /// - /// - internal IEnumerable ResolveSqlSyntaxProviders() - { - return ResolveTypesWithAttribute(); - } - - /// - /// Gets/sets which assemblies to scan when type finding, generally used for unit testing, if not explicitly set - /// this will search all assemblies known to have plugins and exclude ones known to not have them. - /// - internal IEnumerable AssembliesToScan - { - get { return _assemblies ?? (_assemblies = TypeFinder.GetAssembliesWithKnownExclusions()); } - set { _assemblies = value; } - } - - /// - /// Used to resolve and create instances of the specified type based on the resolved/cached plugin types - /// - /// - /// set to true if an exception is to be thrown if there is an error during instantiation - /// - /// - /// - internal IEnumerable FindAndCreateInstances(bool throwException = false, bool cacheResult = true, IEnumerable specificAssemblies = null) - { - var types = ResolveTypes(cacheResult, specificAssemblies); + var types = ResolveTypes(cache, specificAssemblies); return CreateInstances(types, throwException); } /// - /// Used to create instances of the specified type based on the resolved/cached plugin types + /// Creates instances of the specified types. /// - /// - /// - /// set to true if an exception is to be thrown if there is an error during instantiation - /// + /// The base type for all instances. + /// The instance types. + /// Indicates whether to throw if an instance cannot be created. + /// The created instances. + /// By default is false and instances that cannot be created are just skipped. internal IEnumerable CreateInstances(IEnumerable types, bool throwException = false) { return _serviceProvider.CreateInstances(types, _logger.Logger, throwException); } /// - /// Used to create an instance of the specified type based on the resolved/cached plugin types + /// Creates an instance of the specified type. /// - /// - /// + /// The base type of the instance. + /// The type of the instance. /// - /// + /// The created instance. internal T CreateInstance(Type type, bool throwException = false) { var instances = CreateInstances(new[] { type }, throwException); return instances.FirstOrDefault(); } - private IEnumerable ResolveTypes( - Func> finder, - TypeResolutionKind resolutionType, - bool cacheResult) - { - using (var readLock = new UpgradeableReadLock(Locker)) - { - var typesFound = new List(); - - using (_logger.TraceDuration( - String.Format("Starting resolution types of {0}", typeof(T).FullName), - String.Format("Completed resolution of types of {0}, found {1}", typeof(T).FullName, typesFound.Count))) - { - //check if the TypeList already exists, if so return it, if not we'll create it - var typeList = _types.SingleOrDefault(x => x.IsTypeList(resolutionType)); - - //need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 - if (cacheResult && typeList != null) - { - _logger.Logger.Debug("Existing typeList found for {0} with resolution type {1}", () => typeof(T), () => resolutionType); - } - - //if we're not caching the result then proceed, or if the type list doesn't exist then proceed - if (cacheResult == false || typeList == null) - { - //upgrade to a write lock since we're adding to the collection - readLock.UpgradeToWriteLock(); - - typeList = new TypeList(resolutionType); - - //we first need to look into our cache file (this has nothing to do with the 'cacheResult' parameter which caches in memory). - //if assemblies have not changed and the cache file actually exists, then proceed to try to lookup by the cache file. - if (RequiresRescanning == false && File.Exists(GetPluginListFilePath())) - { - var fileCacheResult = TryGetCachedPluginsFromFile(resolutionType); - - //here we need to identify if the CachedPluginNotFoundInFile was the exception, if it was then we need to re-scan - //in some cases the plugin will not have been scanned for on application startup, but the assemblies haven't changed - //so in this instance there will never be a result. - if (fileCacheResult.Exception != null && fileCacheResult.Exception is CachedPluginNotFoundInFileException) - { - _logger.Logger.Debug("Tried to find typelist for type {0} and resolution {1} in file cache but the type was not found so loading types by assembly scan ", () => typeof(T), () => resolutionType); - - //we don't have a cache for this so proceed to look them up by scanning - LoadViaScanningAndUpdateCacheFile(typeList, resolutionType, finder); - } - else - { - if (fileCacheResult.Success) - { - var successfullyLoadedFromCache = true; - //we have a previous cache for this so we don't need to scan we just load what has been found in the file - foreach (var t in fileCacheResult.Result) - { - try - { - //we use the build manager to ensure we get all types loaded, this is slightly slower than - //Type.GetType but if the types in the assembly aren't loaded yet then we have problems with that. - var type = BuildManager.GetType(t, true); - typeList.AddType(type); - } - catch (Exception ex) - { - //if there are any exceptions loading types, we have to exist, this should never happen so - //we will need to revert to scanning for types. - successfullyLoadedFromCache = false; - _logger.Logger.Error("Could not load a cached plugin type: " + t + " now reverting to re-scanning assemblies for the base type: " + typeof(T).FullName, ex); - break; - } - } - if (successfullyLoadedFromCache == false) - { - //we need to manually load by scanning if loading from the file was not successful. - LoadViaScanningAndUpdateCacheFile(typeList, resolutionType, finder); - } - else - { - _logger.Logger.Debug("Loaded plugin types {0} with resolution {1} from persisted cache", () => typeof(T), () => resolutionType); - } - } - } - } - else - { - _logger.Logger.Debug("Assembly changes detected, loading types {0} for resolution {1} by assembly scan", () => typeof(T), () => resolutionType); - - //we don't have a cache for this so proceed to look them up by scanning - LoadViaScanningAndUpdateCacheFile(typeList, resolutionType, finder); - } - - //only add the cache if we are to cache the results - if (cacheResult) - { - //add the type list to the collection - var added = _types.Add(typeList); - - _logger.Logger.Debug("Caching of typelist for type {0} and resolution {1} was successful = {2}", () => typeof(T), () => resolutionType, () => added); - - } - } - typesFound = typeList.GetTypes().ToList(); - } - - return typesFound; - } - } - - /// - /// This method invokes the finder which scans the assemblies for the types and then loads the result into the type finder. - /// Once the results are loaded, we update the cached type xml file - /// - /// - /// - /// - /// - /// THIS METHODS IS NOT THREAD SAFE - /// - private void LoadViaScanningAndUpdateCacheFile(TypeList typeList, TypeResolutionKind resolutionKind, Func> finder) - { - //we don't have a cache for this so proceed to look them up by scanning - foreach (var t in finder()) - { - typeList.AddType(t); - } - UpdateCachedPluginsFile(typeList.GetTypes(), resolutionKind); - } - - #region Public Methods - /// - /// Generic method to find the specified type and cache the result - /// - /// - /// - public IEnumerable ResolveTypes(bool cacheResult = true, IEnumerable specificAssemblies = null) - { - return ResolveTypes( - () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), - TypeResolutionKind.FindAllTypes, - cacheResult); - } - - /// - /// Generic method to find the specified type that has an attribute and cache the result - /// - /// - /// - /// - public IEnumerable ResolveTypesWithAttribute(bool cacheResult = true, IEnumerable specificAssemblies = null) - where TAttribute : Attribute - { - return ResolveTypes( - () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), - TypeResolutionKind.FindTypesWithAttribute, - cacheResult); - } - - /// - /// Generic method to find any type that has the specified attribute - /// - /// - /// - public IEnumerable ResolveAttributedTypes(bool cacheResult = true, IEnumerable specificAssemblies = null) - where TAttribute : Attribute - { - return ResolveTypes( - () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), - TypeResolutionKind.FindAttributedTypes, - cacheResult); - } #endregion - /// - /// Used for unit tests - /// - /// - internal HashSet GetTypeLists() - { - return _types; - } - - - - #region Private classes/Enums + #region Resolve Types /// - /// The type of resolution being invoked + /// Resolves class types inheriting from or implementing the specified type /// - internal enum TypeResolutionKind + /// The type to inherit from or implement. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types inheriting from or implementing the specified type. + /// Caching is disabled when using specific assemblies. + public IEnumerable ResolveTypes(bool cache = true, IEnumerable specificAssemblies = null) { - FindAllTypes, - FindAttributedTypes, - FindTypesWithAttribute - } + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; - internal abstract class TypeList - { - public abstract void AddType(Type t); - public abstract bool IsTypeList(TypeResolutionKind resolutionType); - public abstract IEnumerable GetTypes(); - } - - internal class TypeList : TypeList - { - private readonly TypeResolutionKind _resolutionType; - - public TypeList(TypeResolutionKind resolutionType) + // if not caching, or not IDiscoverable, directly resolve types + if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) { - _resolutionType = resolutionType; + return ResolveTypesInternal( + typeof (T), null, + () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), + cache); } - private readonly List _types = new List(); + // if caching and IDiscoverable + // filter the cached discovered types (and cache the result) - public override void AddType(Type t) + var discovered = ResolveTypesInternal( + typeof (IDiscoverable), null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + true); + + return ResolveTypesInternal( + typeof (T), null, + () => discovered + .Where(x => typeof (T).IsAssignableFrom(x)), + true); + } + + /// + /// Resolves class types inheriting from or implementing the specified type and marked with the specified attribute. + /// + /// The type to inherit from or implement. + /// The type of the attribute. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types inheriting from or implementing the specified type and marked with the specified attribute. + /// Caching is disabled when using specific assemblies. + public IEnumerable ResolveTypesWithAttribute(bool cache = true, IEnumerable specificAssemblies = null) + where TAttribute : Attribute + { + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; + + // if not caching, or not IDiscoverable, directly resolve types + if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) { - //if the type is an attribute type we won't do the type check because typeof is going to be the - //attribute type whereas the 't' type is the object type found with the attribute. - if (_resolutionType == TypeResolutionKind.FindAttributedTypes || t.IsType()) + return ResolveTypesInternal( + typeof (T), typeof (TAttribute), + () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), + cache); + } + + // if caching and IDiscoverable + // filter the cached discovered types (and cache the result) + + var discovered = ResolveTypesInternal( + typeof (IDiscoverable), null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + true); + + return ResolveTypesInternal( + typeof (T), typeof (TAttribute), + () => discovered + .Where(x => typeof(T).IsAssignableFrom(x)) + .Where(x => x.GetCustomAttributes(false).Any()), + true); + } + + /// + /// Resolves class types marked with the specified attribute. + /// + /// The type of the attribute. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types marked with the specified attribute. + /// Caching is disabled when using specific assemblies. + public IEnumerable ResolveAttributedTypes(bool cache = true, IEnumerable specificAssemblies = null) + where TAttribute : Attribute + { + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; + + return ResolveTypesInternal( + typeof (object), typeof (TAttribute), + () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), + cache); + } + + private IEnumerable ResolveTypesInternal( + Type baseType, Type attributeType, + Func> finder, + bool cache) + { + // using an upgradeable lock makes little sense here as only one thread can enter the upgradeable + // lock at a time, and we don't have non-upgradeable readers, and quite probably the plugin + // manager is mostly not going to be used in any kind of massively multi-threaded scenario - so, + // a plain lock is enough + + var name = ResolvedName(baseType, attributeType); + + lock (_typesLock) + using (_logger.TraceDuration( + "Resolving " + name, + "Resolved " + name)) // cannot contain typesFound.Count as it's evaluated before the find + { + // resolve within a lock & timer + return ResolveTypesInternalLocked(baseType, attributeType, finder, cache); + } + } + + private static string ResolvedName(Type baseType, Type attributeType) + { + var s = attributeType == null ? string.Empty : ("[" + attributeType + "]"); + s += baseType; + return s; + } + + private IEnumerable ResolveTypesInternalLocked( + Type baseType, Type attributeType, + Func> finder, + bool cache) + { + // check if the TypeList already exists, if so return it, if not we'll create it + var listKey = new TypeListKey(baseType, attributeType); + TypeList typeList = null; + if (cache) + _types.TryGetValue(listKey, out typeList); // else null + + // if caching and found, return + if (typeList != null) + { + // need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 + _logger.Logger.Debug("Resolving {0}: found a cached type list.", () => ResolvedName(baseType, attributeType)); + return typeList.Types; + } + + // else proceed, + typeList = new TypeList(baseType, attributeType); + + var scan = RequiresRescanning || File.Exists(GetPluginListFilePath()) == false; + + if (scan) + { + // either we have to rescan, or we could not find the cache file: + // report (only once) and scan and update the cache file - this variable is purely used to check if we need to log + if (_reportedChange == false) { - _types.Add(t); + _logger.Logger.Debug("Assembly changes detected, need to rescan everything."); + _reportedChange = true; } } + if (scan == false) + { + // if we don't have to scan, try the cache + var cacheResult = TryGetCached(baseType, attributeType); + + // here we need to identify if the CachedPluginNotFoundInFile was the exception, if it was then we need to re-scan + // in some cases the plugin will not have been scanned for on application startup, but the assemblies haven't changed + // so in this instance there will never be a result. + if (cacheResult.Exception is CachedPluginNotFoundInFileException || cacheResult.Success == false) + { + _logger.Logger.Debug("Resolving {0}: failed to load from cache file, must scan assemblies.", () => ResolvedName(baseType, attributeType)); + scan = true; + } + else + { + // successfully retrieved types from the file cache: load + foreach (var type in cacheResult.Result) + { + try + { + // we use the build manager to ensure we get all types loaded, this is slightly slower than + // Type.GetType but if the types in the assembly aren't loaded yet it would fail whereas + // BuildManager will load them - this is how eg MVC loads types, etc - no need to make it + // more complicated + typeList.Add(BuildManager.GetType(type, true)); + } + catch (Exception ex) + { + // in case of any exception, we have to exit, and revert to scanning + _logger.Logger.Error("Resolving " + ResolvedName(baseType, attributeType) + ": failed to load cache file type " + type + ", reverting to scanning assemblies.", ex); + scan = true; + break; + } + } + + if (scan == false) + { + _logger.Logger.Debug("Resolving {0}: loaded types from cache file.", () => ResolvedName(baseType, attributeType)); + } + } + } + + if (scan) + { + // either we had to scan, or we could not resolve the types from the cache file - scan now + _logger.Logger.Debug("Resolving {0}: scanning assemblies.", () => ResolvedName(baseType, attributeType)); + + foreach (var t in finder()) + typeList.Add(t); + } + + // if we are to cache the results, do so + if (cache) + { + var added = _types.ContainsKey(listKey) == false; + if (added) + { + _types[listKey] = typeList; + //if we are scanning then update the cache file + if (scan) + { + UpdateCache(); + } + } + + _logger.Logger.Debug("Resolved {0}, caching ({1}).", () => ResolvedName(baseType, attributeType), () => added.ToString().ToLowerInvariant()); + } + else + { + _logger.Logger.Debug("Resolved {0}.", () => ResolvedName(baseType, attributeType)); + } + + return typeList.Types; + } + + #endregion + + #region Nested classes and stuff + + /// + /// Groups a type and a resolution kind into a key. + /// + private struct TypeListKey + { + // ReSharper disable MemberCanBePrivate.Local + public readonly Type BaseType; + public readonly Type AttributeType; + // ReSharper restore MemberCanBePrivate.Local + + public TypeListKey(Type baseType, Type attributeType) + { + BaseType = baseType ?? typeof (object); + AttributeType = attributeType; + } + + public override bool Equals(object obj) + { + if (obj == null || obj is TypeListKey == false) return false; + var o = (TypeListKey)obj; + return BaseType == o.BaseType && AttributeType == o.AttributeType; + } + + public override int GetHashCode() + { + // in case AttributeType is null we need something else, using typeof (TypeListKey) + // which does not really "mean" anything, it's just a value... + + var hash = 5381; + hash = ((hash << 5) + hash) ^ BaseType.GetHashCode(); + hash = ((hash << 5) + hash) ^ (AttributeType ?? typeof (TypeListKey)).GetHashCode(); + return hash; + } + } + + /// + /// Represents a list of types obtained by looking for types inheriting/implementing a + /// specified type, and/or marked with a specified attribute type. + /// + internal class TypeList + { + private readonly HashSet _types = new HashSet(); + + public TypeList(Type baseType, Type attributeType) + { + BaseType = baseType; + AttributeType = attributeType; + } + + public Type BaseType { get; private set; } + public Type AttributeType { get; private set; } + + /// + /// Adds a type. + /// + public void Add(Type type) + { + if (BaseType.IsAssignableFrom(type) == false) + throw new ArgumentException("Base type " + BaseType + " is not assignable from type " + type + ".", "type"); + _types.Add(type); + } + /// - /// Returns true if the current TypeList is of the same lookup type + /// Gets the types. /// - /// - /// - /// - public override bool IsTypeList(TypeResolutionKind resolutionType) + public IEnumerable Types { - return _resolutionType == resolutionType && (typeof(T)) == typeof(TLookup); - } - - public override IEnumerable GetTypes() - { - return _types; + get { return _types; } } } /// - /// This class is used simply to determine that a plugin was not found in the cache plugin list with the specified - /// TypeResolutionKind. + /// Represents the error that occurs when a plugin was not found in the cache plugin + /// list with the specified TypeResolutionKind. /// internal class CachedPluginNotFoundInFileException : Exception - { - - } + { } #endregion } + + internal static class PluginManagerExtensions + { + /// + /// Gets all classes inheriting from PropertyEditor. + /// + /// + /// Excludes the actual PropertyEditor base type. + /// + public static IEnumerable ResolvePropertyEditors(this PluginManager mgr) + { + // look for IParameterEditor (fast, IDiscoverable) then filter + + var propertyEditor = typeof (PropertyEditor); + + return mgr.ResolveTypes() + .Where(x => propertyEditor.IsAssignableFrom(x) && x != propertyEditor); + } + + /// + /// Gets all classes implementing IParameterEditor. + /// + /// + /// Includes property editors. + /// Excludes the actual ParameterEditor and PropertyEditor base types. + /// + public static IEnumerable ResolveParameterEditors(this PluginManager mgr) + { + var propertyEditor = typeof (PropertyEditor); + var parameterEditor = typeof (ParameterEditor); + + return mgr.ResolveTypes() + .Where(x => x != propertyEditor && x != parameterEditor); + } + + /// + /// Gets all classes implementing IApplicationStartupHandler. + /// + [Obsolete("IApplicationStartupHandler is obsolete.")] + public static IEnumerable ResolveApplicationStartupHandlers(this PluginManager mgr) + { + return mgr.ResolveTypes(); + } + + /// + /// Gets all classes implementing ICacheRefresher. + /// + public static IEnumerable ResolveCacheRefreshers(this PluginManager mgr) + { + return mgr.ResolveTypes(); + } + + /// + /// Gets all classes implementing IPropertyEditorValueConverter. + /// + [Obsolete("IPropertyEditorValueConverter is obsolete.")] + public static IEnumerable ResolvePropertyEditorValueConverters(this PluginManager mgr) + { + return mgr.ResolveTypes(); + } + + /// + /// Gets all classes implementing IDataType. + /// + [Obsolete("IDataType is obsolete.")] + public static IEnumerable ResolveDataTypes(this PluginManager mgr) + { + return mgr.ResolveTypes(); + } + + /// + /// Gets all classes implementing IMacroGuiRendering. + /// + [Obsolete("IMacroGuiRendering is obsolete.")] + public static IEnumerable ResolveMacroRenderings(this PluginManager mgr) + { + return mgr.ResolveTypes(); + } + + /// + /// Gets all classes implementing IPackageAction. + /// + public static IEnumerable ResolvePackageActions(this PluginManager mgr) + { + return mgr.ResolveTypes(); + } + + /// + /// Gets all classes implementing IAction. + /// + public static IEnumerable ResolveActions(this PluginManager mgr) + { + return mgr.ResolveTypes(); + } + + /// + /// Gets all classes inheriting from BaseMapper and marked with the MapperForAttribute. + /// + public static IEnumerable ResolveAssignedMapperTypes(this PluginManager mgr) + { + return mgr.ResolveTypesWithAttribute(); + } + + /// + /// Gets all classes implementing ISqlSyntaxProvider and marked with the SqlSyntaxProviderAttribute. + /// + public static IEnumerable ResolveSqlSyntaxProviders(this PluginManager mgr) + { + return mgr.ResolveTypesWithAttribute(); + } + } } diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index dbc1ab6c93..7d94f51fbe 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -29,7 +29,7 @@ using System.Security.Permissions; [assembly: InternalsVisibleTo("umbraco.webservices")] [assembly: InternalsVisibleTo("umbraco.datalayer")] [assembly: InternalsVisibleTo("umbraco.MacroEngines")] - +[assembly: InternalsVisibleTo("umbraco.providers")] [assembly: InternalsVisibleTo("umbraco.editorControls")] [assembly: InternalsVisibleTo("Umbraco.Tests")] [assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] @@ -42,7 +42,14 @@ using System.Security.Permissions; [assembly: InternalsVisibleTo("Umbraco.VisualStudio")] [assembly: InternalsVisibleTo("Umbraco.Courier.Core")] [assembly: InternalsVisibleTo("Umbraco.Courier.Persistence")] -[assembly: InternalsVisibleTo("umbraco.providers")] + +[assembly: InternalsVisibleTo("Umbraco.Deploy")] +[assembly: InternalsVisibleTo("Umbraco.Deploy.UI")] +[assembly: InternalsVisibleTo("Umbraco.Deploy.Cloud")] + +[assembly: InternalsVisibleTo("Umbraco.Forms.Core")] +[assembly: InternalsVisibleTo("Umbraco.Forms.Core.Providers")] +[assembly: InternalsVisibleTo("Umbraco.Forms.Web")] //allow this to be mocked in our unit tests [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/IParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/IParameterEditor.cs index 44320c9464..cbff5e45ab 100644 --- a/src/Umbraco.Core/PropertyEditors/IParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IParameterEditor.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; +using umbraco.interfaces; namespace Umbraco.Core.PropertyEditors { - public interface IParameterEditor + public interface IParameterEditor : IDiscoverable { /// /// The id of the property editor diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs index c7cbc0f473..b2e41ad628 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs @@ -1,4 +1,5 @@ using System; +using umbraco.interfaces; namespace Umbraco.Core.PropertyEditors { @@ -7,8 +8,8 @@ namespace Umbraco.Core.PropertyEditors /// // todo: drop IPropertyEditorValueConverter support (when?). [Obsolete("Use IPropertyValueConverter.")] - public interface IPropertyEditorValueConverter - { + public interface IPropertyEditorValueConverter : IDiscoverable + { /// /// Returns a value indicating whether this provider applies to the specified property. /// diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs index 79887843a0..0666aca087 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs @@ -1,11 +1,12 @@ -using Umbraco.Core.Models.PublishedContent; +using umbraco.interfaces; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors { /// /// Provides published content properties conversion service. /// - public interface IPropertyValueConverter + public interface IPropertyValueConverter : IDiscoverable { /// /// Gets a value indicating whether the converter supports a property type. diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditorResolver.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditorResolver.cs index fa9feb3347..3b37460741 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditorResolver.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditorResolver.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.ObjectResolution; @@ -12,16 +14,18 @@ namespace Umbraco.Core.PropertyEditors /// /// /// This resolver will contain any parameter editors defined in manifests as well as any property editors defined in manifests - /// that have the IsParameterEditorFlag = true and any PropertyEditors found in c# that have this flag as well. + /// that have the IsParameterEditorFlag = true and any PropertyEditors found in c# that have this flag as well. /// internal class ParameterEditorResolver : LazyManyObjectsResolverBase { private readonly ManifestBuilder _builder; - + private readonly IContentSection _contentSection; + public ParameterEditorResolver(IServiceProvider serviceProvider, ILogger logger, Func> typeListProducerList, ManifestBuilder builder) : base(serviceProvider, logger, typeListProducerList, ObjectLifetimeScope.Application) { _builder = builder; + _contentSection = UmbracoConfig.For.UmbracoSettings().Content; } /// @@ -29,40 +33,51 @@ namespace Umbraco.Core.PropertyEditors /// public IEnumerable ParameterEditors { - get + get { return GetParameterEditors(); } + } + + public IEnumerable GetParameterEditors(bool includeDeprecated = false) + { + // all property editors and parameter editors + // except property editors where !IsParameterEditor + var values = Values + .Where(x => x is PropertyEditor == false || ((PropertyEditor) x).IsParameterEditor); + + // union all manifest parameter editors + values = values + .Union(_builder.ParameterEditors); + + // union all manifest property editors where IsParameterEditor + values = values + .Union(_builder.PropertyEditors.Where(x => x.IsParameterEditor)); + + if (includeDeprecated == false && _contentSection.ShowDeprecatedPropertyEditors == false) { - //This will by default include all property editors and parameter editors but we need to filter this - //list to ensure that none of the property editors that do not have the IsParameterEditor flag set to true - //are filtered. - var filtered = Values.Select(x => x as PropertyEditor) - .WhereNotNull() - .Where(x => x.IsParameterEditor == false); - - return Values - //exclude the non parameter editor c# property editors - .Except(filtered) - //include the manifest parameter editors - .Union(_builder.ParameterEditors) - //include the manifest prop editors that are parameter editors - .Union(_builder.PropertyEditors.Where(x => x.IsParameterEditor)); + // except deprecated property editors + values = values + .Where(x => x is PropertyEditor == false || ((PropertyEditor) x).IsDeprecated == false); } + + return values; } /// /// Returns a property editor by alias /// /// + /// /// - public IParameterEditor GetByAlias(string alias) + public IParameterEditor GetByAlias(string alias, bool includeDeprecated = false) { - var found = ParameterEditors.SingleOrDefault(x => x.Alias == alias); + var paramEditors = GetParameterEditors(includeDeprecated).ToArray(); + var found = paramEditors.SingleOrDefault(x => x.Alias == alias); if (found != null) return found; - + //couldn't find one, so try the map var mapped = LegacyParameterEditorAliasConverter.GetNewAliasFromLegacyAlias(alias); - return mapped == null - ? null - : ParameterEditors.SingleOrDefault(x => x.Alias == mapped); + return mapped == null + ? null + : paramEditors.SingleOrDefault(x => x.Alias == mapped); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/PreValueField.cs b/src/Umbraco.Core/PropertyEditors/PreValueField.cs index 3cf4e960ea..2b66f7a6a8 100644 --- a/src/Umbraco.Core/PropertyEditors/PreValueField.cs +++ b/src/Umbraco.Core/PropertyEditors/PreValueField.cs @@ -15,6 +15,7 @@ namespace Umbraco.Core.PropertyEditors public PreValueField() { Validators = new List(); + Config = new Dictionary(); //check for an attribute and fill the values var att = GetType().GetCustomAttribute(false); @@ -79,5 +80,11 @@ namespace Umbraco.Core.PropertyEditors /// [JsonProperty("validation", ItemConverterType = typeof(ManifestValidatorConverter))] public List Validators { get; private set; } + + /// + /// This allows for custom configuration to be injected into the pre-value editor + /// + [JsonProperty("config")] + public IDictionary Config { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs index 6cecc9dff5..c2498ecc7a 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs @@ -38,6 +38,7 @@ namespace Umbraco.Core.PropertyEditors IsParameterEditor = _attribute.IsParameterEditor; Icon = _attribute.Icon; Group = _attribute.Group; + IsDeprecated = _attribute.IsDeprecated; } } @@ -90,6 +91,9 @@ namespace Umbraco.Core.PropertyEditors get { return CreateValueEditor(); } } + [JsonIgnore] + public bool IsDeprecated { get; internal set; } + [JsonIgnore] IValueEditor IParameterEditor.ValueEditor { diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs index d120753185..41e4ccc74e 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs @@ -60,6 +60,12 @@ namespace Umbraco.Core.PropertyEditors public string ValueType { get; set; } public bool IsParameterEditor { get; set; } + /// + /// If set to true, this property editor will not show up in the DataType's drop down list + /// if there is not already one of them chosen for a DataType + /// + public bool IsDeprecated { get; set; } + /// /// If this is is true than the editor will be displayed full width without a label /// diff --git a/src/Umbraco.Core/PropertyEditors/TagValueType.cs b/src/Umbraco.Core/PropertyEditors/TagValueType.cs index e5f65933ab..e837d1e8bf 100644 --- a/src/Umbraco.Core/PropertyEditors/TagValueType.cs +++ b/src/Umbraco.Core/PropertyEditors/TagValueType.cs @@ -14,7 +14,8 @@ /// The list of tags will be supplied by the property editor's ConvertEditorToDb method result which will need to return an IEnumerable{string} value /// /// - /// if the ConvertEditorToDb doesn't return an IEnumerable{string} then an exception will be thrown. + /// if the ConvertEditorToDb doesn't return an IEnumerable{string} then it will automatically try to be detected as either CSV or JSON and if neither of those match + /// an exception will be thrown. /// CustomTagList } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs new file mode 100644 index 0000000000..3b97b5e3e6 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(IEnumerable))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class CheckboxListValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.CheckBoxListAlias); + } + return false; + } + + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + var sourceString = (source ?? string.Empty).ToString(); + + if (string.IsNullOrEmpty(sourceString)) + return Enumerable.Empty(); + + var values = + sourceString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => v.Trim()); + + return values; + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs index bc943b7da0..987640716b 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs @@ -1,18 +1,28 @@ -using Umbraco.Core.Models.PublishedContent; +using System; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(string))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] public class ColorPickerValueConverter : PropertyValueConverterBase { public override bool IsConverter(PublishedPropertyType propertyType) { - return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.ColorPickerAlias); + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.ColorPickerAlias); + } + return false; } public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) { // make sure it's a string - return source.ToString(); + return source == null ? string.Empty : source.ToString(); } + } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs index 769f3928a7..ce10dd202e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] [PropertyValueType(typeof(DateTime))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] public class DatePickerValueConverter : PropertyValueConverterBase diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs index e32ff7af02..89af76ea12 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] [PropertyValueType(typeof(decimal))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] public class DecimalValueConverter : PropertyValueConverterBase diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleValueConverter.cs new file mode 100644 index 0000000000..80a8a18a5d --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleValueConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(IEnumerable))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class DropdownListMultipleValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.DropDownListMultipleAlias); + } + return false; + } + + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + var sourceString = (source ?? "").ToString(); + + if (string.IsNullOrEmpty(sourceString)) + return Enumerable.Empty(); + + var values = + sourceString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => v.Trim()); + + return values; + } + + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleWithKeysValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleWithKeysValueConverter.cs new file mode 100644 index 0000000000..649d50746f --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListMultipleWithKeysValueConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(IEnumerable))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class DropdownListMultipleWithKeysValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.DropdownlistMultiplePublishKeysAlias); + } + return false; + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) + return new int[] { }; + + var prevalueIds = source.ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(p => p.Trim()) + .Select(int.Parse) + .ToArray(); + + return prevalueIds; + } + + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListValueConverter.cs new file mode 100644 index 0000000000..69b2ff1adb --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListValueConverter.cs @@ -0,0 +1,28 @@ +using System; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(string))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class DropdownListValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.DropDownListAlias); + } + return false; + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + return source == null ? string.Empty : source.ToString(); + } + + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListWithKeysValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListWithKeysValueConverter.cs new file mode 100644 index 0000000000..de95906469 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DropdownListWithKeysValueConverter.cs @@ -0,0 +1,31 @@ +using System; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(int))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class DropdownListWithKeysValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.DropdownlistPublishingKeysAlias); + } + return false; + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + var intAttempt = source.TryConvertTo(); + if (intAttempt.Success) + return intAttempt.Result; + + return null; + } + + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs new file mode 100644 index 0000000000..7a2a06cf67 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs @@ -0,0 +1,27 @@ +using System; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(string))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class EmailAddressValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.EmailAddressAlias); + } + return false; + } + + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + return source.ToString(); + } + + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index b3db026c89..029d1c3546 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -5,7 +5,6 @@ using System.Web; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Grid; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs index c9b0306e32..34b1ebffb1 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs @@ -2,6 +2,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] [PropertyValueType(typeof(int))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] public class IntegerValueConverter : PropertyValueConverterBase diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index aae8da4be7..1d6425133e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] [PropertyValueType(typeof(IHtmlString))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] public class MarkdownEditorValueConverter : PropertyValueConverterBase diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs new file mode 100644 index 0000000000..2d56dcba58 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs @@ -0,0 +1,27 @@ +using System; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(string))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class MemberGroupPickerValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.MemberGroupPickerAlias); + } + return false; + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + return source == null ? string.Empty : source.ToString(); + } + + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs index a3b12a6688..fcbdb1cdaf 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] [PropertyValueType(typeof(IEnumerable))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] public class MultipleTextStringValueConverter : PropertyValueConverterBase diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs new file mode 100644 index 0000000000..9427ccecdb --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs @@ -0,0 +1,31 @@ +using System; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(int))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class RadioButtonListValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.RadioButtonListAlias); + } + return false; + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + var intAttempt = source.TryConvertTo(); + if (intAttempt.Success) + return intAttempt.Result; + + return null; + } + + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs new file mode 100644 index 0000000000..93cdd95cab --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class SliderValueConverter : PropertyValueConverterBase, IPropertyValueConverterMeta + { + private readonly IDataTypeService _dataTypeService; + + //TODO: Remove this ctor in v8 since the other one will use IoC + public SliderValueConverter() + : this(ApplicationContext.Current.Services.DataTypeService) + { + } + + public SliderValueConverter(IDataTypeService dataTypeService) + { + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + _dataTypeService = dataTypeService; + } + + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.SliderAlias); + } + return false; + } + + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) + { + return null; + } + + if (IsRangeDataType(propertyType.DataTypeId)) + { + var rangeRawValues = source.ToString().Split(','); + var minimumAttempt = rangeRawValues[0].TryConvertTo(); + var maximumAttempt = rangeRawValues[1].TryConvertTo(); + + if (minimumAttempt.Success && maximumAttempt.Success) + { + return new Range() { Maximum = maximumAttempt.Result, Minimum = minimumAttempt.Result }; + } + } + + var valueAttempt = source.ToString().TryConvertTo(); + if (valueAttempt.Success) + { + return valueAttempt.Result; + } + + // Something failed in the conversion of the strings to decimals + return null; + + } + + public Type GetPropertyValueType(PublishedPropertyType propertyType) + { + if (IsRangeDataType(propertyType.DataTypeId)) + { + return typeof(Range); + } + return typeof(decimal); + } + + public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue) + { + return PropertyCacheLevel.Content; + } + + /// + /// Discovers if the slider is set to range mode. + /// + /// + /// The data type id. + /// + /// + /// The . + /// + private bool IsRangeDataType(int dataTypeId) + { + // GetPreValuesCollectionByDataTypeId is cached at repository level; + // still, the collection is deep-cloned so this is kinda expensive, + // better to cache here + trigger refresh in DataTypeCacheRefresher + + return Storages.GetOrAdd(dataTypeId, id => + { + var preValue = _dataTypeService.GetPreValuesCollectionByDataTypeId(id) + .PreValuesAsDictionary + .FirstOrDefault(x => string.Equals(x.Key, "enableRange", StringComparison.InvariantCultureIgnoreCase)) + .Value; + + return preValue != null && preValue.Value.TryConvertTo().Result; + }); + } + + private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); + + internal static void ClearCaches() + { + Storages.Clear(); + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs new file mode 100644 index 0000000000..b085748487 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(IEnumerable))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class TagsValueConverter : PropertyValueConverterBase + { + private readonly IDataTypeService _dataTypeService; + + //TODO: Remove this ctor in v8 since the other one will use IoC + public TagsValueConverter() + : this(ApplicationContext.Current.Services.DataTypeService) + { + } + + public TagsValueConverter(IDataTypeService dataTypeService) + { + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + _dataTypeService = dataTypeService; + } + + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.TagsAlias); + } + return false; + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + // if Json storage type deserialzie and return as string array + if (JsonStorageType(propertyType.DataTypeId)) + { + var jArray = JsonConvert.DeserializeObject(source.ToString()); + return jArray.ToObject(); + } + + // Otherwise assume CSV storage type and return as string array + var csvTags = + source.ToString() + .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .ToArray(); + return csvTags; + } + + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) + { + return null; + } + return (string[]) source; + } + + /// + /// Discovers if the tags data type is storing its data in a Json format + /// + /// + /// The data type id. + /// + /// + /// The . + /// + private bool JsonStorageType(int dataTypeId) + { + // GetPreValuesCollectionByDataTypeId is cached at repository level; + // still, the collection is deep-cloned so this is kinda expensive, + // better to cache here + trigger refresh in DataTypeCacheRefresher + + return Storages.GetOrAdd(dataTypeId, id => + { + var preValue = _dataTypeService.GetPreValuesCollectionByDataTypeId(id) + .PreValuesAsDictionary + .FirstOrDefault(x => string.Equals(x.Key, "storageType", StringComparison.InvariantCultureIgnoreCase)) + .Value; + + return preValue != null && preValue.Value.InvariantEquals("json"); + }); + } + + private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); + + internal static void ClearCaches() + { + Storages.Clear(); + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs index b64065c801..bffb073df2 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -6,11 +6,12 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] [PropertyValueType(typeof(string))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] public class TextStringValueConverter : PropertyValueConverterBase { - private readonly static string[] PropertyTypeAliases = + private static readonly string[] PropertyTypeAliases = { Constants.PropertyEditors.TextboxAlias, Constants.PropertyEditors.TextboxMultipleAlias diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs index 937808b96b..89fc5fb268 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs @@ -4,10 +4,11 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { - /// - /// Value converter for the RTE so that it always returns IHtmlString so that Html.Raw doesn't have to be used. - /// + /// + /// Value converter for the RTE so that it always returns IHtmlString so that Html.Raw doesn't have to be used. + /// // PropertyCacheLevel.Content is ok here because that version of RTE converter does not parse {locallink} nor executes macros + [DefaultPropertyValueConverter] [PropertyValueType(typeof(IHtmlString))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] public class TinyMceValueConverter : PropertyValueConverterBase diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs new file mode 100644 index 0000000000..4953cf3d5e --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + /// + /// The upload property value converter. + /// + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(string))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class UploadPropertyConverter : PropertyValueConverterBase + { + /// + /// Checks if this converter can convert the property editor and registers if it can. + /// + /// + /// The published property type. + /// + /// + /// The . + /// + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.UploadFieldAlias); + } + return false; + } + + /// + /// Convert the source object to a string + /// + /// + /// The published property type. + /// + /// + /// The value of the property + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + return (source ?? "").ToString(); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs index 1e3b684b08..b9e8639d72 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] [PropertyValueType(typeof(bool))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] public class YesNoValueConverter : PropertyValueConverterBase diff --git a/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs b/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs index 63f32c41c5..6c08822a71 100644 --- a/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs +++ b/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs @@ -1,11 +1,12 @@ +using System; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models; namespace Umbraco.Core.Publishing { - /// - /// Abstract class for the implementation of an , which provides the events used for publishing/unpublishing. - /// + [Obsolete("This class is not intended to be used and will be removed in future versions, see IPublishingStrategy instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public abstract class BasePublishingStrategy : IPublishingStrategy { diff --git a/src/Umbraco.Core/Publishing/IPublishingStrategy.cs b/src/Umbraco.Core/Publishing/IPublishingStrategy.cs index 0408409488..93be145779 100644 --- a/src/Umbraco.Core/Publishing/IPublishingStrategy.cs +++ b/src/Umbraco.Core/Publishing/IPublishingStrategy.cs @@ -1,9 +1,80 @@ using System.Collections.Generic; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Publishing { /// + /// TODO: This is a compatibility hack, we want to get rid of IPublishingStrategy all together or just have it as an internal + /// helper class but we can't just remove it now, we also cannot just change it, so the current IPublishingStrategy one will simply not be used + /// in our own code, if for some odd reason someone else is using it, then fine it will continue to work with hacks but won't be used by us. + /// + internal interface IPublishingStrategy2 + { + /// + /// Publishes a single piece of Content + /// + /// + /// to publish + /// Id of the User issueing the publish operation + /// True if the publish operation was successfull and not cancelled, otherwise false + Attempt Publish(IScopeUnitOfWork uow, IContent content, int userId); + + /// + /// Publishes a list of Content + /// + /// + /// An enumerable list of + /// Id of the User issueing the publish operation + /// + /// True if the publish operation was successfull and not cancelled, otherwise false + IEnumerable> PublishWithChildren(IScopeUnitOfWork uow, IEnumerable content, int userId, bool includeUnpublishedDocuments); + + /// + /// Unpublishes a single piece of Content + /// + /// + /// to unpublish + /// Id of the User issueing the unpublish operation + /// True if the unpublish operation was successfull and not cancelled, otherwise false + Attempt UnPublish(IScopeUnitOfWork uow, IContent content, int userId); + + /// + /// Call to fire event that updating the published content has finalized. + /// + /// + /// This seperation of the OnPublished event is done to ensure that the Content + /// has been properly updated (committed unit of work) and xml saved in the db. + /// + /// + /// thats being published + void PublishingFinalized(IScopeUnitOfWork uow, IContent content); + + /// + /// Call to fire event that updating the published content has finalized. + /// + /// + /// An enumerable list of thats being published + /// Boolean indicating whether its all content that is republished + void PublishingFinalized(IScopeUnitOfWork uow, IEnumerable content, bool isAllRepublished); + + /// + /// Call to fire event that updating the unpublished content has finalized. + /// + /// + /// thats being unpublished + void UnPublishingFinalized(IScopeUnitOfWork uow, IContent content); + + /// + /// Call to fire event that updating the unpublished content has finalized. + /// + /// + /// An enumerable list of thats being unpublished + void UnPublishingFinalized(IScopeUnitOfWork uow, IEnumerable content); + } + + /// + /// TODO: This should be obsoleted but if we did that then the Publish/Unpublish events on the content service would show that the param is obsoleted /// Defines the Publishing Strategy /// public interface IPublishingStrategy diff --git a/src/Umbraco.Core/Publishing/PublishStatus.cs b/src/Umbraco.Core/Publishing/PublishStatus.cs index 3436e9070e..a2fdf1d6cd 100644 --- a/src/Umbraco.Core/Publishing/PublishStatus.cs +++ b/src/Umbraco.Core/Publishing/PublishStatus.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Services; @@ -12,7 +13,8 @@ namespace Umbraco.Core.Publishing { public PublishStatus(IContent content, PublishStatusType statusType, EventMessages eventMessages) : base(content, statusType, eventMessages) - { + { + InvalidProperties = Enumerable.Empty(); } /// @@ -20,8 +22,7 @@ namespace Umbraco.Core.Publishing /// public PublishStatus(IContent content, EventMessages eventMessages) : this(content, PublishStatusType.Success, eventMessages) - { - } + { } public IContent ContentItem { diff --git a/src/Umbraco.Core/Publishing/PublishingStrategy.cs b/src/Umbraco.Core/Publishing/PublishingStrategy.cs index c6d4f19baf..2c603a40ed 100644 --- a/src/Umbraco.Core/Publishing/PublishingStrategy.cs +++ b/src/Umbraco.Core/Publishing/PublishingStrategy.cs @@ -1,27 +1,47 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; namespace Umbraco.Core.Publishing { - //TODO: Do we need this anymore?? + //TODO: Do we need this anymore?? - get rid of it! + /// /// Currently acts as an interconnection between the new public api and the legacy api for publishing /// - public class PublishingStrategy : BasePublishingStrategy + [EditorBrowsable(EditorBrowsableState.Never)] + public class PublishingStrategy : BasePublishingStrategy, IPublishingStrategy2 { + private readonly IScopeProvider _scopeProvider; private readonly IEventMessagesFactory _eventMessagesFactory; private readonly ILogger _logger; + [Obsolete("This class is not intended to be used, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] public PublishingStrategy(IEventMessagesFactory eventMessagesFactory, ILogger logger) { if (eventMessagesFactory == null) throw new ArgumentNullException("eventMessagesFactory"); if (logger == null) throw new ArgumentNullException("logger"); + _scopeProvider = new ScopeProvider(new DefaultDatabaseFactory(Constants.System.UmbracoConnectionName, logger)); + _eventMessagesFactory = eventMessagesFactory; + _logger = logger; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public PublishingStrategy(IScopeProvider scopeProvider, IEventMessagesFactory eventMessagesFactory, ILogger logger) + { + if (eventMessagesFactory == null) throw new ArgumentNullException("eventMessagesFactory"); + if (logger == null) throw new ArgumentNullException("logger"); + _scopeProvider = scopeProvider; _eventMessagesFactory = eventMessagesFactory; _logger = logger; } @@ -29,14 +49,14 @@ namespace Umbraco.Core.Publishing /// /// Publishes a single piece of Content /// + /// /// to publish - /// Id of the User issueing the publish operation - internal Attempt PublishInternal(IContent content, int userId) + /// Id of the User issueing the publish operation + Attempt IPublishingStrategy2.Publish(IScopeUnitOfWork uow, IContent content, int userId) { var evtMsgs = _eventMessagesFactory.Get(); - if (Publishing.IsRaisedEventCancelled( - new PublishEventArgs(content, evtMsgs), this)) + if (uow.Events.DispatchCancelable(Publishing, this, new PublishEventArgs(content, evtMsgs), "Publishing")) { _logger.Info( string.Format("Content '{0}' with Id '{1}' will not be published, the event was cancelled.", content.Name, content.Id)); @@ -87,41 +107,46 @@ namespace Umbraco.Core.Publishing /// True if the publish operation was successfull and not cancelled, otherwise false public override bool Publish(IContent content, int userId) { - return PublishInternal(content, userId).Success; + using (var uow = new ScopeUnitOfWork(_scopeProvider)) + { + uow.Commit(); + return ((IPublishingStrategy2)this).Publish(uow, content, userId).Success; + } } /// /// Publishes a list of content items /// + /// /// /// /// /// By default this is set to true which means that it will publish any content item in the list that is completely unpublished and /// not visible on the front-end. If set to false, this will only publish content that is live on the front-end but has new versions /// that have yet to be published. - /// + /// /// /// - /// + /// /// This method becomes complex once we start to be able to cancel events or stop publishing a content item in any way because if a /// content item is not published then it's children shouldn't be published either. This rule will apply for the following conditions: /// * If a document fails to be published, do not proceed to publish it's children if: /// ** The document does not have a publish version /// ** The document does have a published version but the includeUnpublishedDocuments = false - /// + /// /// In order to do this, we will order the content by level and begin by publishing each item at that level, then proceed to the next - /// level and so on. If we detect that the above rule applies when the document publishing is cancelled we'll add it to the list of + /// level and so on. If we detect that the above rule applies when the document publishing is cancelled we'll add it to the list of /// parentsIdsCancelled so that it's children don't get published. - /// + /// /// Its important to note that all 'root' documents included in the list *will* be published regardless of the rules mentioned - /// above (unless it is invalid)!! By 'root' documents we are referring to documents in the list with the minimum value for their 'level'. - /// In most cases the 'root' documents will only be one document since under normal circumstance we only publish one document and + /// above (unless it is invalid)!! By 'root' documents we are referring to documents in the list with the minimum value for their 'level'. + /// In most cases the 'root' documents will only be one document since under normal circumstance we only publish one document and /// its children. The reason we have to do this is because if a user is publishing a document and it's children, it is implied that /// the user definitely wants to publish it even if it has never been published before. - /// + /// /// - internal IEnumerable> PublishWithChildrenInternal( - IEnumerable content, int userId, bool includeUnpublishedDocuments = true) + IEnumerable> IPublishingStrategy2.PublishWithChildren(IScopeUnitOfWork uow, + IEnumerable content, int userId, bool includeUnpublishedDocuments) { var statuses = new List>(); @@ -132,7 +157,7 @@ namespace Umbraco.Core.Publishing //group by levels and iterate over the sorted ascending level. //TODO: This will cause all queries to execute, they will not be lazy but I'm not really sure being lazy actually made - // much difference because we iterate over them all anyways?? Morten? + // much difference because we iterate over them all anyways?? Morten? // Because we're grouping I think this will execute all the queries anyways so need to fetch it all first. var fetchedContent = content.ToArray(); @@ -149,7 +174,7 @@ namespace Umbraco.Core.Publishing var levelGroups = fetchedContent.GroupBy(x => x.Level); foreach (var level in levelGroups.OrderBy(x => x.Key)) { - //set the first level flag, used to ensure that all documents at the first level will + //set the first level flag, used to ensure that all documents at the first level will //be published regardless of the rules mentioned in the remarks. if (!firstLevel.HasValue) { @@ -180,8 +205,7 @@ namespace Umbraco.Core.Publishing } //Fire Publishing event - if (Publishing.IsRaisedEventCancelled( - new PublishEventArgs(item, evtMsgs), this)) + if (uow.Events.DispatchCancelable(Publishing, this, new PublishEventArgs(item, evtMsgs), "Publishing")) { //the publishing has been cancelled. _logger.Info( @@ -200,7 +224,10 @@ namespace Umbraco.Core.Publishing _logger.Info( string.Format("Content '{0}' with Id '{1}' will not be published because some of it's content is not passing validation rules.", item.Name, item.Id)); - statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedContentInvalid, evtMsgs))); + statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedContentInvalid, evtMsgs) + { + InvalidProperties = ((ContentBase)item).LastInvalidProperties + })); //Does this document apply to our rule to cancel it's children being published? CheckCancellingOfChildPublishing(item, parentsIdsCancelled, includeUnpublishedDocuments); @@ -272,21 +299,21 @@ namespace Umbraco.Core.Publishing /// /// /// See remarks on method: PublishWithChildrenInternal - /// + /// private void CheckCancellingOfChildPublishing(IContent content, List parentsIdsCancelled, bool includeUnpublishedDocuments) { //Does this document apply to our rule to cancel it's children being published? - //TODO: We're going back to the service layer here... not sure how to avoid this? And this will add extra overhead to + //TODO: We're going back to the service layer here... not sure how to avoid this? And this will add extra overhead to // any document that fails to publish... var hasPublishedVersion = ApplicationContext.Current.Services.ContentService.HasPublishedVersion(content.Id); - if (hasPublishedVersion && !includeUnpublishedDocuments) + if (hasPublishedVersion && includeUnpublishedDocuments == false) { //it has a published version but our flag tells us to not include un-published documents and therefore we should // not be forcing decendant/child documents to be published if their parent fails. parentsIdsCancelled.Add(content.Id); } - else if (!hasPublishedVersion) + else if (hasPublishedVersion == false) { //it doesn't have a published version so we certainly cannot publish it's children. parentsIdsCancelled.Add(content.Id); @@ -301,15 +328,21 @@ namespace Umbraco.Core.Publishing /// True if the publish operation was successfull and not cancelled, otherwise false public override bool PublishWithChildren(IEnumerable content, int userId) { - var result = PublishWithChildrenInternal(content, userId); + using (var uow = new ScopeUnitOfWork(_scopeProvider)) + { + var result = ((IPublishingStrategy2)this).PublishWithChildren(uow, content, userId, true); - //NOTE: This previously always returned true so I've left it that way. It returned true because (from Morten)... - // ... if one item couldn't be published it wouldn't be correct to return false. - // in retrospect it should have returned a list of with Ids and Publish Status - // come to think of it ... the cache would still be updated for a failed item or at least tried updated. - // It would call the Published event for the entire list, but if the Published property isn't set to True it - // wouldn't actually update the cache for that item. But not really ideal nevertheless... - return true; + uow.Commit(); + + //NOTE: This previously always returned true so I've left it that way. It returned true because (from Morten)... + // ... if one item couldn't be published it wouldn't be correct to return false. + // in retrospect it should have returned a list of with Ids and Publish Status + // come to think of it ... the cache would still be updated for a failed item or at least tried updated. + // It would call the Published event for the entire list, but if the Published property isn't set to True it + // wouldn't actually update the cache for that item. But not really ideal nevertheless... + return true; + } + } /// @@ -320,21 +353,26 @@ namespace Umbraco.Core.Publishing /// True if the unpublish operation was successfull and not cancelled, otherwise false public override bool UnPublish(IContent content, int userId) { - return UnPublishInternal(content, userId).Success; + using (var uow = new ScopeUnitOfWork(_scopeProvider)) + { + uow.Commit(); + return ((IPublishingStrategy2)this).UnPublish(uow, content, userId).Success; + } } /// /// Unpublishes a list of Content /// + /// /// An enumerable list of /// Id of the User issueing the unpublish operation /// A list of publish statuses - private IEnumerable> UnPublishInternal(IEnumerable content, int userId) + private IEnumerable> UnPublishInternal(IScopeUnitOfWork uow, IEnumerable content, int userId) { - return content.Select(x => UnPublishInternal(x, userId)); + return content.Select(x => ((IPublishingStrategy2)this).UnPublish(uow, x, userId)); } - private Attempt UnPublishInternal(IContent content, int userId) + Attempt IPublishingStrategy2.UnPublish(IScopeUnitOfWork uow, IContent content, int userId) { // content should (is assumed to ) be the newest version, which may not be published // don't know how to test this, so it's not verified @@ -345,8 +383,7 @@ namespace Umbraco.Core.Publishing var evtMsgs = _eventMessagesFactory.Get(); //Fire UnPublishing event - if (UnPublishing.IsRaisedEventCancelled( - new PublishEventArgs(content, evtMsgs), this)) + if (uow.Events.DispatchCancelable(UnPublishing, this, new PublishEventArgs(content, evtMsgs), "UnPublishing")) { _logger.Info( string.Format("Content '{0}' with Id '{1}' will not be unpublished, the event was cancelled.", content.Name, content.Id)); @@ -383,15 +420,21 @@ namespace Umbraco.Core.Publishing /// True if the unpublish operation was successfull and not cancelled, otherwise false public override bool UnPublish(IEnumerable content, int userId) { - var result = UnPublishInternal(content, userId); + using (var uow = new ScopeUnitOfWork(_scopeProvider)) + { + var result = UnPublishInternal(uow, content, userId); + uow.Commit(); - //NOTE: This previously always returned true so I've left it that way. It returned true because (from Morten)... - // ... if one item couldn't be published it wouldn't be correct to return false. - // in retrospect it should have returned a list of with Ids and Publish Status - // come to think of it ... the cache would still be updated for a failed item or at least tried updated. - // It would call the Published event for the entire list, but if the Published property isn't set to True it - // wouldn't actually update the cache for that item. But not really ideal nevertheless... - return true; + //NOTE: This previously always returned true so I've left it that way. It returned true because (from Morten)... + // ... if one item couldn't be published it wouldn't be correct to return false. + // in retrospect it should have returned a list of with Ids and Publish Status + // come to think of it ... the cache would still be updated for a failed item or at least tried updated. + // It would call the Published event for the entire list, but if the Published property isn't set to True it + // wouldn't actually update the cache for that item. But not really ideal nevertheless... + return true; + } + + } /// @@ -404,9 +447,12 @@ namespace Umbraco.Core.Publishing /// thats being published public override void PublishingFinalized(IContent content) { - var evtMsgs = _eventMessagesFactory.Get(); - Published.RaiseEvent( - new PublishEventArgs(content, false, false, evtMsgs), this); + using (var uow = new ScopeUnitOfWork(_scopeProvider)) + { + ((IPublishingStrategy2) this).PublishingFinalized(uow, content); + uow.Commit(); + } + } /// @@ -416,10 +462,11 @@ namespace Umbraco.Core.Publishing /// Boolean indicating whether its all content that is republished public override void PublishingFinalized(IEnumerable content, bool isAllRepublished) { - var evtMsgs = _eventMessagesFactory.Get(); - Published.RaiseEvent( - new PublishEventArgs(content, false, isAllRepublished, evtMsgs), this); - + using (var uow = new ScopeUnitOfWork(_scopeProvider)) + { + ((IPublishingStrategy2)this).PublishingFinalized(uow, content, isAllRepublished); + uow.Commit(); + } } /// @@ -428,9 +475,11 @@ namespace Umbraco.Core.Publishing /// thats being unpublished public override void UnPublishingFinalized(IContent content) { - var evtMsgs = _eventMessagesFactory.Get(); - UnPublished.RaiseEvent( - new PublishEventArgs(content, false, false, evtMsgs), this); + using (var uow = new ScopeUnitOfWork(_scopeProvider)) + { + ((IPublishingStrategy2)this).UnPublishingFinalized(uow, content); + uow.Commit(); + } } /// @@ -439,31 +488,64 @@ namespace Umbraco.Core.Publishing /// An enumerable list of thats being unpublished public override void UnPublishingFinalized(IEnumerable content) { - var evtMsgs = _eventMessagesFactory.Get(); - UnPublished.RaiseEvent( - new PublishEventArgs(content, false, false, evtMsgs), this); + using (var uow = new ScopeUnitOfWork(_scopeProvider)) + { + ((IPublishingStrategy2)this).UnPublishingFinalized(uow, content); + uow.Commit(); + } } /// /// Occurs before publish /// + [Obsolete("Use events on the ContentService")] + [EditorBrowsable(EditorBrowsableState.Never)] public static event TypedEventHandler> Publishing; /// /// Occurs after publish /// + [Obsolete("Use events on the ContentService")] + [EditorBrowsable(EditorBrowsableState.Never)] public static event TypedEventHandler> Published; /// /// Occurs before unpublish /// + [Obsolete("Use events on the ContentService")] + [EditorBrowsable(EditorBrowsableState.Never)] public static event TypedEventHandler> UnPublishing; /// /// Occurs after unpublish /// - public static event TypedEventHandler> UnPublished; + [Obsolete("Use events on the ContentService")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static event TypedEventHandler> UnPublished; + + void IPublishingStrategy2.PublishingFinalized(IScopeUnitOfWork uow, IContent content) + { + var evtMsgs = _eventMessagesFactory.Get(); + uow.Events.Dispatch(Published, this, new PublishEventArgs(content, false, false, evtMsgs), "Published"); + } + void IPublishingStrategy2.PublishingFinalized(IScopeUnitOfWork uow, IEnumerable content, bool isAllRepublished) + { + var evtMsgs = _eventMessagesFactory.Get(); + uow.Events.Dispatch(Published, this, new PublishEventArgs(content, false, isAllRepublished, evtMsgs), "Published"); + } + void IPublishingStrategy2.UnPublishingFinalized(IScopeUnitOfWork uow, IContent content) + { + var evtMsgs = _eventMessagesFactory.Get(); + uow.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false, evtMsgs), "UnPublished"); + } + + void IPublishingStrategy2.UnPublishingFinalized(IScopeUnitOfWork uow, IEnumerable content) + { + var evtMsgs = _eventMessagesFactory.Get(); + uow.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false, evtMsgs), "UnPublished"); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs index 94df1b88f1..b57bb1451a 100644 --- a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs +++ b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Services; @@ -26,12 +27,16 @@ namespace Umbraco.Core.Publishing public int CheckPendingAndProcess() { var counter = 0; - foreach (var d in _contentService.GetContentForRelease()) + var contentForRelease = _contentService.GetContentForRelease().ToArray(); + if (contentForRelease.Length > 0) + LogHelper.Debug(string.Format("There's {0} item(s) of content to be published", contentForRelease.Length)); + foreach (var d in contentForRelease) { try { d.ReleaseDate = null; var result = _contentService.SaveAndPublishWithStatus(d, (int)d.GetWriterProfile().Id); + LogHelper.Debug(string.Format("Result of publish attempt: {0}", result.Result.StatusType)); if (result.Success == false) { if (result.Exception != null) @@ -54,7 +59,11 @@ namespace Umbraco.Core.Publishing throw; } } - foreach (var d in _contentService.GetContentForExpiration()) + + var contentForExpiration = _contentService.GetContentForExpiration().ToArray(); + if (contentForExpiration.Length > 0) + LogHelper.Debug(string.Format("There's {0} item(s) of content to be unpublished", contentForExpiration.Length)); + foreach (var d in contentForExpiration) { try { diff --git a/src/Umbraco.Core/SafeCallContext.cs b/src/Umbraco.Core/SafeCallContext.cs new file mode 100644 index 0000000000..5ed41d389f --- /dev/null +++ b/src/Umbraco.Core/SafeCallContext.cs @@ -0,0 +1,94 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +namespace Umbraco.Core +{ + internal class SafeCallContext : IDisposable + { + private static readonly List> EnterFuncs = new List>(); + private static readonly List> ExitActions = new List>(); + private static int _count; + private readonly List _objects; + private bool _disposed; + + public static void Register(Func enterFunc, Action exitAction) + { + if (enterFunc == null) throw new ArgumentNullException("enterFunc"); + if (exitAction == null) throw new ArgumentNullException("exitAction"); + + lock (EnterFuncs) + { + if (_count > 0) throw new InvalidOperationException("Cannot register while some SafeCallContext instances exist."); + EnterFuncs.Add(enterFunc); + ExitActions.Add(exitAction); + } + } + + // tried to make the UmbracoDatabase serializable but then it leaks to weird places + // in ReSharper and so on, where Umbraco.Core is not available. Tried to serialize + // as an object instead but then it comes *back* deserialized into the original context + // as an object and of course it breaks everything. Cannot prevent this from flowing, + // and ExecutionContext.SuppressFlow() works for threads but not domains. and we'll + // have the same issue with anything that toys with logical call context... + // + // so this class lets anything that uses the logical call context register itself, + // providing two methods: + // - an enter func that removes and returns whatever is in the logical call context + // - an exit action that restores the value into the logical call context + // whenever a SafeCallContext instance is created, it uses these methods to capture + // and clear the logical call context, and restore it when disposed. + // + // in addition, a static Clear method is provided - which uses the enter funcs to + // remove everything from logical call context - not to be used when the app runs, + // but can be useful during tests + // + // note + // see System.Transactions + // they are using a conditional weak table to store the data, and what they store in + // LLC is the key - which is just an empty MarshalByRefObject that is created with + // the transaction scope - that way, they can "clear current data" provided that + // they have the key - but they need to hold onto a ref to the scope... not ok for us + + public static void Clear() + { + lock (EnterFuncs) + { + foreach (var enter in EnterFuncs) + enter(); + } + } + + public SafeCallContext() + { + lock (EnterFuncs) + { + _count++; + _objects = EnterFuncs.Select(x => x()).ToList(); + } + } + + public void Dispose() + { + if (_disposed) throw new ObjectDisposedException("this"); + _disposed = true; + lock (EnterFuncs) + { + for (var i = 0; i < ExitActions.Count; i++) + ExitActions[i](_objects[i]); + _count--; + } + } + + // for unit tests ONLY + internal static void Reset() + { + lock (EnterFuncs) + { + if (_count > 0) throw new InvalidOperationException("Cannot reset while some SafeCallContext instances exist."); + EnterFuncs.Clear(); + ExitActions.Clear(); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs b/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs new file mode 100644 index 0000000000..4c88e1c1b5 --- /dev/null +++ b/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Core.Scoping +{ + public interface IInstanceIdentifiable + { + Guid InstanceId { get; } + } +} diff --git a/src/Umbraco.Core/Scoping/IScope.cs b/src/Umbraco.Core/Scoping/IScope.cs new file mode 100644 index 0000000000..4f178b80bc --- /dev/null +++ b/src/Umbraco.Core/Scoping/IScope.cs @@ -0,0 +1,45 @@ +using System; +using Umbraco.Core.Cache; +using Umbraco.Core.Events; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Scoping +{ + /// + /// Represents a scope. + /// + public interface IScope : IDisposable, IInstanceIdentifiable + { + /// + /// Gets the scope database. + /// + UmbracoDatabase Database { get; } + + /// + /// Gets the scope event messages. + /// + EventMessages Messages { get; } + + /// + /// Gets the event manager + /// + IEventDispatcher Events { get; } + + /// + /// Gets the repository cache mode. + /// + RepositoryCacheMode RepositoryCacheMode { get; } + + /// + /// Gets the isolated cache. + /// + IsolatedRuntimeCache IsolatedRuntimeCache { get; } + + /// + /// Completes the scope. + /// + /// A value indicating whether the scope has been successfully completed. + /// Can return false if any child scope has not completed. + bool Complete(); + } +} diff --git a/src/Umbraco.Core/Scoping/IScopeInternal.cs b/src/Umbraco.Core/Scoping/IScopeInternal.cs new file mode 100644 index 0000000000..c1c28b41fe --- /dev/null +++ b/src/Umbraco.Core/Scoping/IScopeInternal.cs @@ -0,0 +1,18 @@ +using System.Data; +using Umbraco.Core.Events; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Scoping +{ + internal interface IScopeInternal : IScope + { + IScopeInternal ParentScope { get; } + bool CallContext { get; } + IsolationLevel IsolationLevel { get; } + UmbracoDatabase DatabaseOrNull { get; } + EventMessages MessagesOrNull { get; } + bool ScopedFileSystems { get; } + void ChildCompleted(bool? completed); + void Reset(); + } +} diff --git a/src/Umbraco.Core/Scoping/IScopeProvider.cs b/src/Umbraco.Core/Scoping/IScopeProvider.cs new file mode 100644 index 0000000000..754fb63aa7 --- /dev/null +++ b/src/Umbraco.Core/Scoping/IScopeProvider.cs @@ -0,0 +1,75 @@ +using System; +using System.Data; +using Umbraco.Core.Events; +#if DEBUG_SCOPES +using System.Collections.Generic; +#endif + +namespace Umbraco.Core.Scoping +{ + /// + /// Provides scopes. + /// + public interface IScopeProvider + { + /// + /// Creates an ambient scope. + /// + /// The created ambient scope. + /// + /// The created scope becomes the ambient scope. + /// If an ambient scope already exists, it becomes the parent of the created scope. + /// When the created scope is disposed, the parent scope becomes the ambient scope again. + /// + IScope CreateScope( + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false); + + /// + /// Creates a detached scope. + /// + /// A detached scope. + /// + /// A detached scope is not ambient and has no parent. + /// It is meant to be attached by . + /// + IScope CreateDetachedScope( + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null); + + /// + /// Attaches a scope. + /// + /// The scope to attach. + /// A value indicating whether to force usage of call context. + /// + /// Only a scope created by can be attached. + /// + void AttachScope(IScope scope, bool callContext = false); + + /// + /// Detaches a scope. + /// + /// The detached scope. + /// + /// Only a scope previously attached by can be detached. + /// + IScope DetachScope(); + + /// + /// Gets the scope context. + /// + ScopeContext Context { get; } + +#if DEBUG_SCOPES + Dictionary CallContextObjects { get; } + IEnumerable ScopeInfos { get; } + ScopeInfo GetScopeInfo(IScope scope); +#endif + } +} diff --git a/src/Umbraco.Core/Scoping/IScopeProviderInternal.cs b/src/Umbraco.Core/Scoping/IScopeProviderInternal.cs new file mode 100644 index 0000000000..0cd03117eb --- /dev/null +++ b/src/Umbraco.Core/Scoping/IScopeProviderInternal.cs @@ -0,0 +1,31 @@ +namespace Umbraco.Core.Scoping +{ + /// + /// Provides scopes. + /// + /// Extends with internal features. + internal interface IScopeProviderInternal : IScopeProvider + { + /// + /// Gets the ambient context. + /// + ScopeContext AmbientContext { get; } + + /// + /// Gets the ambient scope. + /// + IScopeInternal AmbientScope { get; } + + /// + /// Gets the ambient scope if any, else creates and returns a . + /// + IScopeInternal GetAmbientOrNoScope(); + + /// + /// Resets the ambient scope. + /// + /// Resets the ambient scope (not completed anymore) and disposes the + /// entire scopes chain until there is no more scopes. + void Reset(); + } +} diff --git a/src/Umbraco.Core/Scoping/NoScope.cs b/src/Umbraco.Core/Scoping/NoScope.cs new file mode 100644 index 0000000000..a21815173c --- /dev/null +++ b/src/Umbraco.Core/Scoping/NoScope.cs @@ -0,0 +1,147 @@ +using System; +using System.Data; +using Umbraco.Core.Cache; +using Umbraco.Core.Events; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Scoping +{ + /// + /// Implements when there is no scope. + /// + internal class NoScope : IScopeInternal + { + private readonly ScopeProvider _scopeProvider; + private bool _disposed; + + private UmbracoDatabase _database; + private EventMessages _messages; + + public NoScope(ScopeProvider scopeProvider) + { + _scopeProvider = scopeProvider; +#if DEBUG_SCOPES + _scopeProvider.RegisterScope(this); +#endif + } + + private readonly Guid _instanceId = Guid.NewGuid(); + public Guid InstanceId { get { return _instanceId; } } + + /// + public bool CallContext { get { return false; } } + + /// + public RepositoryCacheMode RepositoryCacheMode + { + get { return RepositoryCacheMode.Default; } + } + + /// + public IsolatedRuntimeCache IsolatedRuntimeCache { get { throw new NotSupportedException(); } } + + /// + public UmbracoDatabase Database + { + get + { + EnsureNotDisposed(); + return _database ?? (_database = _scopeProvider.DatabaseFactory.CreateNewDatabase()); + } + } + + public UmbracoDatabase DatabaseOrNull + { + get + { + EnsureNotDisposed(); + return _database; + } + } + + /// + public EventMessages Messages + { + get + { + EnsureNotDisposed(); + if (_messages != null) return _messages; + + // see comments in Scope + + var factory = ScopeLifespanMessagesFactory.Current; + if (factory == null) + { + _messages = new EventMessages(); + } + else + { + _messages = factory.GetFromHttpContext(); + if (_messages == null) + factory.Set(_messages = new EventMessages()); + } + + return _messages; + } + } + + public EventMessages MessagesOrNull + { + get + { + EnsureNotDisposed(); + + // see comments in Scope + + if (_messages != null) return _messages; + + var factory = ScopeLifespanMessagesFactory.Current; + return factory == null ? null : factory.GetFromHttpContext(); + } + } + + /// + public IEventDispatcher Events + { + get { throw new NotSupportedException(); } + } + + /// + public bool Complete() + { + throw new NotSupportedException(); + } + + private void EnsureNotDisposed() + { + if (_disposed) + throw new ObjectDisposedException("this"); + } + + public void Dispose() + { + EnsureNotDisposed(); + + if (this != _scopeProvider.AmbientScope) + throw new InvalidOperationException("Not the ambient scope."); + +#if DEBUG_SCOPES + _scopeProvider.Disposed(this); +#endif + + if (_database != null) + _database.Dispose(); + + _scopeProvider.SetAmbient(null); + + _disposed = true; + GC.SuppressFinalize(this); + } + + public IScopeInternal ParentScope { get { return null; } } + public IsolationLevel IsolationLevel { get {return IsolationLevel.Unspecified; } } + public bool ScopedFileSystems { get { return false; } } + public void ChildCompleted(bool? completed) { } + public void Reset() { } + } +} diff --git a/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs b/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs new file mode 100644 index 0000000000..2b25f2eb59 --- /dev/null +++ b/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Scoping +{ + public enum RepositoryCacheMode + { + // ? + Unspecified = 0, + + // the default, full L2 cache + Default = 1, + + // a scoped cache + // reads from and writes to a local cache + // clears the global cache on completion + Scoped = 2 + } +} diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs new file mode 100644 index 0000000000..bfee772872 --- /dev/null +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -0,0 +1,513 @@ +using System; +using System.Data; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Events; +using Umbraco.Core.IO; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Scoping +{ + /// + /// Implements . + /// + /// Not thread-safe obviously. + internal class Scope : IScopeInternal + { + private readonly ScopeProvider _scopeProvider; + private readonly IsolationLevel _isolationLevel; + private readonly RepositoryCacheMode _repositoryCacheMode; + private readonly bool? _scopeFileSystem; + private readonly ScopeContext _scopeContext; + private bool _callContext; + private bool _disposed; + private bool? _completed; + + private IsolatedRuntimeCache _isolatedRuntimeCache; + private UmbracoDatabase _database; + private EventMessages _messages; + private ICompletable _fscope; + private IEventDispatcher _eventDispatcher; + + // this is v7, in v8 this has to change to RepeatableRead + private const IsolationLevel DefaultIsolationLevel = IsolationLevel.ReadCommitted; + + // initializes a new scope + private Scope(ScopeProvider scopeProvider, + Scope parent, ScopeContext scopeContext, bool detachable, + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + { + _scopeProvider = scopeProvider; + _scopeContext = scopeContext; + _isolationLevel = isolationLevel; + _repositoryCacheMode = repositoryCacheMode; + _eventDispatcher = eventDispatcher; + _scopeFileSystem = scopeFileSystems; + _callContext = callContext; + Detachable = detachable; + +#if DEBUG_SCOPES + _scopeProvider.RegisterScope(this); + Console.WriteLine("create " + _instanceId.ToString("N").Substring(0, 8)); +#endif + + if (detachable) + { + if (parent != null) throw new ArgumentException("Cannot set parent on detachable scope.", "parent"); + if (scopeContext != null) throw new ArgumentException("Cannot set context on detachable scope.", "scopeContext"); + + // detachable creates its own scope context + _scopeContext = new ScopeContext(); + + // see note below + if (scopeFileSystems == true) + _fscope = FileSystemProviderManager.Current.Shadow(Guid.NewGuid()); + + return; + } + + if (parent != null) + { + ParentScope = parent; + + // cannot specify a different mode! + if (repositoryCacheMode != RepositoryCacheMode.Unspecified && parent.RepositoryCacheMode != repositoryCacheMode) + throw new ArgumentException("Cannot be different from parent.", "repositoryCacheMode"); + + // cannot specify a dispatcher! + if (_eventDispatcher != null) + throw new ArgumentException("Cannot be specified on nested scope.", "eventDispatcher"); + + // cannot specify a different fs scope! + if (scopeFileSystems != null && parent._scopeFileSystem != scopeFileSystems) + throw new ArgumentException("Cannot be different from parent.", "scopeFileSystems"); + } + else + { + // the FS scope cannot be "on demand" like the rest, because we would need to hook into + // every scoped FS to trigger the creation of shadow FS "on demand", and that would be + // pretty pointless since if scopeFileSystems is true, we *know* we want to shadow + if (scopeFileSystems == true) + _fscope = FileSystemProviderManager.Current.Shadow(Guid.NewGuid()); + } + } + + // initializes a new scope + public Scope(ScopeProvider scopeProvider, bool detachable, + ScopeContext scopeContext, + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + : this(scopeProvider, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext) + { } + + // initializes a new scope in a nested scopes chain, with its parent + public Scope(ScopeProvider scopeProvider, Scope parent, + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + : this(scopeProvider, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext) + { } + + // initializes a new scope, replacing a NoScope instance + public Scope(ScopeProvider scopeProvider, NoScope noScope, + ScopeContext scopeContext, + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + : this(scopeProvider, null, scopeContext, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext) + { + // steal everything from NoScope + _database = noScope.DatabaseOrNull; + _messages = noScope.MessagesOrNull; + + // make sure the NoScope can be replaced ie not in a transaction + if (_database != null && _database.InTransaction) + throw new Exception("NoScope instance is not free."); + } + + private readonly Guid _instanceId = Guid.NewGuid(); + public Guid InstanceId { get { return _instanceId; } } + + // a value indicating whether to force call-context + public bool CallContext + { + get + { + if (_callContext) return true; + if (ParentScope != null) return ParentScope.CallContext; + return false; + } + set { _callContext = value; } + } + + public bool ScopedFileSystems + { + get + { + if (ParentScope != null) return ParentScope.ScopedFileSystems; + return _fscope != null; + } + } + + /// + public RepositoryCacheMode RepositoryCacheMode + { + get + { + if (_repositoryCacheMode != RepositoryCacheMode.Unspecified) return _repositoryCacheMode; + if (ParentScope != null) return ParentScope.RepositoryCacheMode; + return RepositoryCacheMode.Default; + } + } + + /// + public IsolatedRuntimeCache IsolatedRuntimeCache + { + get + { + if (ParentScope != null) return ParentScope.IsolatedRuntimeCache; + + return _isolatedRuntimeCache ?? (_isolatedRuntimeCache + = new IsolatedRuntimeCache(type => new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); + } + } + + // a value indicating whether the scope is detachable + // ie whether it was created by CreateDetachedScope + public bool Detachable { get; private set; } + + // the parent scope (in a nested scopes chain) + public IScopeInternal ParentScope { get; set; } + + public bool Attached { get; set; } + + // the original scope (when attaching a detachable scope) + public IScopeInternal OrigScope { get; set; } + + // the original context (when attaching a detachable scope) + public ScopeContext OrigContext { get; set; } + + // the context (for attaching & detaching only) + public ScopeContext Context + { + get { return _scopeContext; } + } + + public IsolationLevel IsolationLevel + { + get + { + if (_isolationLevel != IsolationLevel.Unspecified) return _isolationLevel; + if (ParentScope != null) return ParentScope.IsolationLevel; + return DefaultIsolationLevel; + } + } + + /// + public UmbracoDatabase Database + { + get + { + EnsureNotDisposed(); + if (ParentScope != null) + { + var database = ParentScope.Database; + if (_isolationLevel > IsolationLevel.Unspecified && database.CurrentTransactionIsolationLevel < _isolationLevel) + throw new Exception("Scope requires isolation level " + _isolationLevel + ", but got " + database.CurrentTransactionIsolationLevel + " from parent."); + _database = database; + } + + if (_database != null) + { + // if the database has been created by a Scope instance it has to be + // in a transaction, however it can be a database that was stolen from + // a NoScope instance, in which case we need to enter a transaction, as + // a scope implies a transaction, always + if (_database.InTransaction) + return _database; + } + else + { + // create a new database + _database = _scopeProvider.DatabaseFactory.CreateNewDatabase(); + } + + // enter a transaction, as a scope implies a transaction, always + try + { + _database.BeginTransaction(IsolationLevel); + return _database; + } + catch + { + _database.Dispose(); + _database = null; + throw; + } + } + } + + public UmbracoDatabase DatabaseOrNull + { + get + { + EnsureNotDisposed(); + return ParentScope == null ? _database : ParentScope.DatabaseOrNull; + } + } + + /// + public EventMessages Messages + { + get + { + EnsureNotDisposed(); + if (ParentScope != null) return ParentScope.Messages; + + if (_messages != null) return _messages; + + // this is ugly - in v7 for backward compatibility reasons, EventMessages need + // to survive way longer that the scopes, and kinda resides on its own in http + // context, but must also be in scopes for when we do async and lose http context + // TODO refactor in v8 + + var factory = ScopeLifespanMessagesFactory.Current; + if (factory == null) + { + _messages = new EventMessages(); + } + else + { + _messages = factory.GetFromHttpContext(); + if (_messages == null) + factory.Set(_messages = new EventMessages()); + } + + return _messages; + } + } + + public EventMessages MessagesOrNull + { + get + { + EnsureNotDisposed(); + if (ParentScope != null) return ParentScope.MessagesOrNull; + + // see comments in Messages + + if (_messages != null) return _messages; + + var factory = ScopeLifespanMessagesFactory.Current; + return factory == null ? null : factory.GetFromHttpContext(); + } + } + + /// + public IEventDispatcher Events + { + get + { + EnsureNotDisposed(); + if (ParentScope != null) return ParentScope.Events; + return _eventDispatcher ?? (_eventDispatcher = new ScopeEventDispatcher()); + } + } + + /// + public bool Complete() + { + if (_completed.HasValue == false) + _completed = true; + return _completed.Value; + } + + public void Reset() + { + _completed = null; + } + + public void ChildCompleted(bool? completed) + { + // if child did not complete we cannot complete + if (completed.HasValue == false || completed.Value == false) + { + if (LogUncompletedScopes) + Logging.LogHelper.Debug("Uncompleted Child Scope at\r\n" + Environment.StackTrace); + _completed = false; + } + } + + private void EnsureNotDisposed() + { + if (_disposed) + throw new ObjectDisposedException("this"); + } + + public void Dispose() + { + EnsureNotDisposed(); + + if (this != _scopeProvider.AmbientScope) + { +#if DEBUG_SCOPES + var ambient = _scopeProvider.AmbientScope; + Logging.LogHelper.Debug("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)"); + if (ambient == null) + throw new InvalidOperationException("Not the ambient scope (no ambient scope)."); + var infos = _scopeProvider.GetScopeInfo(ambient); + throw new InvalidOperationException("Not the ambient scope (see current ambient ctor stack trace).\r\n" + infos.CtorStack); +#else + throw new InvalidOperationException("Not the ambient scope."); +#endif + } + + var parent = ParentScope; + _scopeProvider.AmbientScope = parent; + +#if DEBUG_SCOPES + _scopeProvider.Disposed(this); +#endif + + if (parent != null) + parent.ChildCompleted(_completed); + else + DisposeLastScope(); + + _disposed = true; + GC.SuppressFinalize(this); + } + + private void DisposeLastScope() + { + // figure out completed + var completed = _completed.HasValue && _completed.Value; + + // deal with database + var databaseException = false; + if (_database != null) + { + try + { + if (completed) + _database.CompleteTransaction(); + else + _database.AbortTransaction(); + } + catch + { + databaseException = true; + throw; + } + finally + { + _database.Dispose(); + _database = null; + + if (databaseException) + RobustExit(false, true); + } + } + + RobustExit(completed, false); + } + + // this chains some try/finally blocks to + // - complete and dispose the scoped filesystems + // - deal with events if appropriate + // - remove the scope context if it belongs to this scope + // - deal with detachable scopes + // here, + // - completed indicates whether the scope has been completed + // can be true or false, but in both cases the scope is exiting + // in a normal way + // - onException indicates whether completing/aborting the database + // transaction threw an exception, in which case 'completed' has + // to be false + events don't trigger and we just to some cleanup + // to ensure we don't leave a scope around, etc + private void RobustExit(bool completed, bool onException) + { + if (onException) completed = false; + + TryFinally(() => + { + if (_scopeFileSystem == true) + { + if (completed) + _fscope.Complete(); + _fscope.Dispose(); + _fscope = null; + } + }, () => + { + // deal with events + if (onException == false && _eventDispatcher != null) + _eventDispatcher.ScopeExit(completed); + }, () => + { + // if *we* created it, then get rid of it + if (_scopeProvider.AmbientContext == _scopeContext) + { + try + { + _scopeProvider.AmbientContext.ScopeExit(completed); + } + finally + { + // removes the ambient context (ambient scope already gone) + _scopeProvider.SetAmbient(null); + } + } + }, () => + { + if (Detachable) + { + // get out of the way, restore original + _scopeProvider.SetAmbient(OrigScope, OrigContext); + Attached = false; + OrigScope = null; + OrigContext = null; + } + }); + } + + private static void TryFinally(params Action[] actions) + { + TryFinally(0, actions); + } + + private static void TryFinally(int index, Action[] actions) + { + if (index == actions.Length) return; + try + { + actions[index](); + } + finally + { + TryFinally(index + 1, actions); + } + } + + // backing field for LogUncompletedScopes + private static bool? _logUncompletedScopes; + + // caching config + // true if Umbraco.CoreDebug.LogUncompletedScope appSetting is set to "true" + private static bool LogUncompletedScopes + { + get { return (_logUncompletedScopes ?? (_logUncompletedScopes = UmbracoConfig.For.CoreDebug().LogUncompletedScopes)).Value; } + } + } +} diff --git a/src/Umbraco.Core/Scoping/ScopeContext.cs b/src/Umbraco.Core/Scoping/ScopeContext.cs new file mode 100644 index 0000000000..c79f02a0fa --- /dev/null +++ b/src/Umbraco.Core/Scoping/ScopeContext.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Scoping +{ + public class ScopeContext : IInstanceIdentifiable + { + private Dictionary _enlisted; + private bool _exiting; + + public void ScopeExit(bool completed) + { + if (_enlisted == null) + return; + + _exiting = true; + + List exceptions = null; + foreach (var enlisted in _enlisted.Values.OrderBy(x => x.Priority)) + { + try + { + enlisted.Execute(completed); + } + catch (Exception e) + { + if (exceptions == null) + exceptions = new List(); + exceptions.Add(e); + } + } + + if (exceptions != null) + throw new AggregateException("Exceptions were thrown by listed actions.", exceptions); + } + + private readonly Guid _instanceId = Guid.NewGuid(); + public Guid InstanceId { get { return _instanceId; } } + + private IDictionary Enlisted + { + get + { + return _enlisted ?? (_enlisted + = new Dictionary()); + } + } + + private interface IEnlistedObject + { + void Execute(bool completed); + int Priority { get; } + } + + private class EnlistedObject : IEnlistedObject + { + private readonly Action _action; + + public EnlistedObject(T item, Action action, int priority) + { + Item = item; + Priority = priority; + _action = action; + } + + public T Item { get; private set; } + + public int Priority { get; private set; } + + public void Execute(bool completed) + { + _action(completed, Item); + } + } + + // todo: replace with optional parameters when we can break things + public T Enlist(string key, Func creator) + { + return Enlist(key, creator, null, 100); + } + + // todo: replace with optional parameters when we can break things + public T Enlist(string key, Func creator, Action action) + { + return Enlist(key, creator, action, 100); + } + + // todo: replace with optional parameters when we can break things + public void Enlist(string key, Action action) + { + Enlist(key, null, (completed, item) => action(completed), 100); + } + + public void Enlist(string key, Action action, int priority) + { + Enlist(key, null, (completed, item) => action(completed), priority); + } + + public T Enlist(string key, Func creator, Action action, int priority) + { + if (_exiting) + throw new InvalidOperationException("Cannot enlist now, context is exiting."); + + var enlistedObjects = _enlisted ?? (_enlisted = new Dictionary()); + + IEnlistedObject enlisted; + if (enlistedObjects.TryGetValue(key, out enlisted)) + { + var enlistedAs = enlisted as EnlistedObject; + if (enlistedAs == null) throw new InvalidOperationException("An item with the key already exists, but with a different type."); + if (enlistedAs.Priority != priority) throw new InvalidOperationException("An item with the key already exits, but with a different priority."); + return enlistedAs.Item; + } + var enlistedOfT = new EnlistedObject(creator == null ? default(T) : creator(), action, priority); + Enlisted[key] = enlistedOfT; + return enlistedOfT.Item; + } + } +} diff --git a/src/Umbraco.Core/Scoping/ScopeProvider.cs b/src/Umbraco.Core/Scoping/ScopeProvider.cs new file mode 100644 index 0000000000..dfcf7985ae --- /dev/null +++ b/src/Umbraco.Core/Scoping/ScopeProvider.cs @@ -0,0 +1,610 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Runtime.Remoting.Messaging; +using System.Text; +using System.Web; +using Umbraco.Core.Events; +using Umbraco.Core.Persistence; +#if DEBUG_SCOPES +using System.Linq; +#endif + +namespace Umbraco.Core.Scoping +{ + /// + /// Implements . + /// + internal class ScopeProvider : IScopeProviderInternal + { + public ScopeProvider(IDatabaseFactory2 databaseFactory) + { + DatabaseFactory = databaseFactory; + } + + static ScopeProvider() + { + SafeCallContext.Register( + () => + { + var scope = GetCallContextObject(ScopeItemKey); + var context = GetCallContextObject(ContextItemKey); + SetCallContextObject(ScopeItemKey, null); + SetCallContextObject(ContextItemKey, null); + return Tuple.Create(scope, context); + }, + o => + { + // cannot re-attached over leaked scope/context + // except of course over NoScope (which leaks) + var ambientScope = GetCallContextObject(ScopeItemKey); + if (ambientScope != null) + { + var ambientNoScope = ambientScope as NoScope; + if (ambientNoScope == null) + throw new Exception("Found leaked scope when restoring call context."); + + // this should rollback any pending transaction + ambientNoScope.Dispose(); + } + if (GetCallContextObject(ContextItemKey) != null) + throw new Exception("Found leaked context when restoring call context."); + + var t = (Tuple) o; + SetCallContextObject(ScopeItemKey, t.Item1); + SetCallContextObject(ContextItemKey, t.Item2); + }); + } + + public IDatabaseFactory2 DatabaseFactory { get; private set; } + + #region Context + + // objects that go into the logical call context better be serializable else they'll eventually + // cause issues whenever some cross-AppDomain code executes - could be due to ReSharper running + // tests, any other things (see https://msdn.microsoft.com/en-us/library/dn458353(v=vs.110).aspx), + // but we don't want to make all of our objects serializable since they are *not* meant to be + // used in cross-AppDomain scenario anyways. + // in addition, whatever goes into the logical call context is serialized back and forth any + // time cross-AppDomain code executes, so if we put an "object" there, we'll can *another* + // "object" instance - and so we cannot use a random object as a key. + // so what we do is: we register a guid in the call context, and we keep a table mapping those + // guids to the actual objects. the guid serializes back and forth without causing any issue, + // and we can retrieve the actual objects from the table. + // only issue: how are we supposed to clear the table? we can't, really. objects should take + // care of de-registering themselves from context. + // everything we use does, except the NoScope scope, which just stays there + // + // during tests, NoScope can to into call context... nothing much we can do about it + + private static readonly object StaticCallContextObjectsLock = new object(); + private static readonly Dictionary StaticCallContextObjects + = new Dictionary(); + +#if DEBUG_SCOPES + public Dictionary CallContextObjects + { + get + { + lock (StaticCallContextObjectsLock) + { + // capture in a dictionary + return StaticCallContextObjects.ToDictionary(x => x.Key, x => x.Value); + } + } + } +#endif + + private static T GetCallContextObject(string key) + where T : class + { + var objectKey = CallContext.LogicalGetData(key).AsGuid(); + if (objectKey == Guid.Empty) return null; + + lock (StaticCallContextObjectsLock) + { + object callContextObject; + if (StaticCallContextObjects.TryGetValue(objectKey, out callContextObject)) + { +#if DEBUG_SCOPES + Logging.LogHelper.Debug("Got " + typeof(T).Name + " Object " + objectKey.ToString("N").Substring(0, 8)); + //Logging.LogHelper.Debug("At:\r\n" + Head(Environment.StackTrace, 24)); +#endif + return (T) callContextObject; + } + + Logging.LogHelper.Warn("Missed " + typeof(T).Name + " Object " + objectKey.ToString("N").Substring(0, 8)); +#if DEBUG_SCOPES + //Logging.LogHelper.Debug("At:\r\n" + Head(Environment.StackTrace, 24)); +#endif + return null; + } + } + + private static void SetCallContextObject(string key, IInstanceIdentifiable value) + { +#if DEBUG_SCOPES + // manage the 'context' that contains the scope (null, "http" or "call") + // only for scopes of course! + if (key == ScopeItemKey) + { + // first, null-register the existing value + var ambientKey = CallContext.LogicalGetData(ScopeItemKey).AsGuid(); + object o = null; + lock (StaticCallContextObjectsLock) + { + if (ambientKey != default(Guid)) + StaticCallContextObjects.TryGetValue(ambientKey, out o); + } + var ambientScope = o as IScope; + if (ambientScope != null) RegisterContext(ambientScope, null); + // then register the new value + var scope = value as IScope; + if (scope != null) RegisterContext(scope, "call"); + } +#endif + if (value == null) + { + var objectKey = CallContext.LogicalGetData(key).AsGuid(); + CallContext.FreeNamedDataSlot(key); + if (objectKey == default (Guid)) return; + lock (StaticCallContextObjectsLock) + { +#if DEBUG_SCOPES + Logging.LogHelper.Debug("Remove Object " + objectKey.ToString("N").Substring(0, 8)); + //Logging.LogHelper.Debug("At:\r\n" + Head(Environment.StackTrace, 24)); +#endif + StaticCallContextObjects.Remove(objectKey); + } + } + else + { + // note - we are *not* detecting an already-existing value + // because our code in this class *always* sets to null before + // setting to a real value + var objectKey = value.InstanceId; + lock (StaticCallContextObjectsLock) + { +#if DEBUG_SCOPES + Logging.LogHelper.Debug("AddObject " + objectKey.ToString("N").Substring(0, 8)); + //Logging.LogHelper.Debug("At:\r\n" + Head(Environment.StackTrace, 24)); +#endif + StaticCallContextObjects.Add(objectKey, value); + } + CallContext.LogicalSetData(key, objectKey); + } + } + + // this is for tests exclusively until we have a proper accessor in v8 + internal static Func HttpContextItemsGetter { get; set; } + + private static IDictionary HttpContextItems + { + get + { + return HttpContextItemsGetter == null + ? (HttpContext.Current == null ? null : HttpContext.Current.Items) + : HttpContextItemsGetter(); + } + } + + public static T GetHttpContextObject(string key, bool required = true) + where T : class + { + var httpContextItems = HttpContextItems; + if (httpContextItems != null) + return (T)httpContextItems[key]; + if (required) + throw new Exception("HttpContext.Current is null."); + return null; + } + + private static bool SetHttpContextObject(string key, object value, bool required = true) + { + var httpContextItems = HttpContextItems; + if (httpContextItems == null) + { + if (required) + throw new Exception("HttpContext.Current is null."); + return false; + } +#if DEBUG_SCOPES + // manage the 'context' that contains the scope (null, "http" or "call") + // only for scopes of course! + if (key == ScopeItemKey) + { + // first, null-register the existing value + var ambientScope = (IScope)httpContextItems[ScopeItemKey]; + if (ambientScope != null) RegisterContext(ambientScope, null); + // then register the new value + var scope = value as IScope; + if (scope != null) RegisterContext(scope, "http"); + } +#endif + if (value == null) + httpContextItems.Remove(key); + else + httpContextItems[key] = value; + return true; + } + +#endregion + + #region Ambient Context + + internal const string ContextItemKey = "Umbraco.Core.Scoping.ScopeContext"; + + internal static ScopeContext AmbientContextInternal + { + get + { + // try http context, fallback onto call context + var value = GetHttpContextObject(ContextItemKey, false); + return value ?? GetCallContextObject(ContextItemKey); + } + set + { + // clear both + SetHttpContextObject(ContextItemKey, null, false); + SetCallContextObject(ContextItemKey, null); + if (value == null) return; + + // set http/call context + if (SetHttpContextObject(ContextItemKey, value, false) == false) + SetCallContextObject(ContextItemKey, value); + } + } + + /// + public ScopeContext AmbientContext + { + get { return AmbientContextInternal; } + } + + #endregion + + #region Ambient Scope + + internal const string ScopeItemKey = "Umbraco.Core.Scoping.Scope"; + internal const string ScopeRefItemKey = "Umbraco.Core.Scoping.ScopeReference"; + + // only 1 instance which can be disposed and disposed again + private static readonly ScopeReference StaticScopeReference = new ScopeReference(new ScopeProvider(null)); + + internal static IScopeInternal AmbientScopeInternal + { + get + { + // try http context, fallback onto call context + var value = GetHttpContextObject(ScopeItemKey, false); + return value ?? GetCallContextObject(ScopeItemKey); + } + set + { + // clear both + SetHttpContextObject(ScopeItemKey, null, false); + SetHttpContextObject(ScopeRefItemKey, null, false); + SetCallContextObject(ScopeItemKey, null); + if (value == null) return; + + // set http/call context + if (value.CallContext == false && SetHttpContextObject(ScopeItemKey, value, false)) + SetHttpContextObject(ScopeRefItemKey, StaticScopeReference); + else + SetCallContextObject(ScopeItemKey, value); + } + } + + /// + public IScopeInternal AmbientScope + { + get { return AmbientScopeInternal; } + internal set { AmbientScopeInternal = value; } + } + + /// + public IScopeInternal GetAmbientOrNoScope() + { + return AmbientScope ?? (AmbientScope = new NoScope(this)); + } + + #endregion + + public void SetAmbient(IScopeInternal scope, ScopeContext context = null) + { + // clear all + SetHttpContextObject(ScopeItemKey, null, false); + SetHttpContextObject(ScopeRefItemKey, null, false); + SetCallContextObject(ScopeItemKey, null); + SetHttpContextObject(ContextItemKey, null, false); + SetCallContextObject(ContextItemKey, null); + if (scope == null) + { + if (context != null) + throw new ArgumentException("Must be null if scope is null.", "context"); + return; + } + + if (scope.CallContext == false && SetHttpContextObject(ScopeItemKey, scope, false)) + { + SetHttpContextObject(ScopeRefItemKey, StaticScopeReference); + SetHttpContextObject(ContextItemKey, context); + } + else + { + SetCallContextObject(ScopeItemKey, scope); + SetCallContextObject(ContextItemKey, context); + } + } + + /// + public IScope CreateDetachedScope( + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null) + { + return new Scope(this, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems); + } + + /// + public void AttachScope(IScope other, bool callContext = false) + { + var otherScope = other as Scope; + if (otherScope == null) + throw new ArgumentException("Not a Scope instance."); + + if (otherScope.Detachable == false) + throw new ArgumentException("Not a detachable scope."); + + if (otherScope.Attached) + throw new InvalidOperationException("Already attached."); + + otherScope.Attached = true; + otherScope.OrigScope = AmbientScope; + otherScope.OrigContext = AmbientContext; + + otherScope.CallContext = callContext; + SetAmbient(otherScope, otherScope.Context); + } + + /// + public IScope DetachScope() + { + var ambient = AmbientScope; + if (ambient == null) + throw new InvalidOperationException("There is no ambient scope."); + + var noScope = ambient as NoScope; + if (noScope != null) + throw new InvalidOperationException("Cannot detach NoScope."); + + var scope = ambient as Scope; + if (scope == null) + throw new Exception("Ambient scope is not a Scope instance."); + + if (scope.Detachable == false) + throw new InvalidOperationException("Ambient scope is not detachable."); + + SetAmbient(scope.OrigScope, scope.OrigContext); + scope.OrigScope = null; + scope.OrigContext = null; + scope.Attached = false; + return scope; + } + + /// + public IScope CreateScope( + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + { + var ambient = AmbientScope; + if (ambient == null) + { + var ambientContext = AmbientContext; + var newContext = ambientContext == null ? new ScopeContext() : null; + var scope = new Scope(this, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext); + // assign only if scope creation did not throw! + SetAmbient(scope, newContext ?? ambientContext); + return scope; + } + + // replace noScope with a real one + var noScope = ambient as NoScope; + if (noScope != null) + { +#if DEBUG_SCOPES + Disposed(noScope); +#endif + // peta poco nulls the shared connection after each command unless there's a trx + var database = noScope.DatabaseOrNull; + if (database != null && database.InTransaction) + throw new Exception("NoScope is in a transaction."); + var ambientContext = AmbientContext; + var newContext = ambientContext == null ? new ScopeContext() : null; + var scope = new Scope(this, noScope, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext); + // assign only if scope creation did not throw! + SetAmbient(scope, newContext ?? ambientContext); + return scope; + } + + var ambientScope = ambient as Scope; + if (ambientScope == null) throw new Exception("Ambient scope is not a Scope instance."); + + var nested = new Scope(this, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext); + SetAmbient(nested, AmbientContext); + return nested; + } + + /// + public void Reset() + { + var scope = AmbientScope as Scope; + if (scope != null) + scope.Reset(); + + StaticScopeReference.Dispose(); + } + + /// + public ScopeContext Context + { + get { return AmbientContext; } + } + +#if DEBUG_SCOPES + // this code needs TLC + // + // the idea here is to keep in a list all the scopes that have been created, and to remove them + // when they are disposed, so we can track leaks, ie scopes that would not be properly taken + // care of by our code + // + // note: the code could probably be optimized... but this is NOT supposed to go into any real + // live build, either production or debug - it's just a debugging tool for the time being + + // helps identifying when non-httpContext scopes are created by logging the stack trace + //private void LogCallContextStack() + //{ + // var trace = Environment.StackTrace; + // if (trace.IndexOf("ScheduledPublishing") > 0) + // LogHelper.Debug("CallContext: Scheduled Publishing"); + // else if (trace.IndexOf("TouchServerTask") > 0) + // LogHelper.Debug("CallContext: Server Registration"); + // else if (trace.IndexOf("LogScrubber") > 0) + // LogHelper.Debug("CallContext: Log Scrubber"); + // else + // LogHelper.Debug("CallContext: " + Environment.StackTrace); + //} + + // all scope instances that are currently beeing tracked + private static readonly object StaticScopeInfosLock = new object(); + private static readonly Dictionary StaticScopeInfos = new Dictionary(); + + public IEnumerable ScopeInfos + { + get + { + lock (StaticScopeInfosLock) + { + return StaticScopeInfos.Values.ToArray(); // capture in an array + } + } + } + + public ScopeInfo GetScopeInfo(IScope scope) + { + lock (StaticScopeInfosLock) + { + ScopeInfo scopeInfo; + return StaticScopeInfos.TryGetValue(scope, out scopeInfo) ? scopeInfo : null; + } + } + + //private static void Log(string message, UmbracoDatabase database) + //{ + // LogHelper.Debug(message + " (" + (database == null ? "" : database.InstanceSid) + ")."); + //} + + // register a scope and capture its ctor stacktrace + public void RegisterScope(IScope scope) + { + lock (StaticScopeInfosLock) + { + if (StaticScopeInfos.ContainsKey(scope)) throw new Exception("oops: already registered."); + Logging.LogHelper.Debug("Register " + scope.InstanceId.ToString("N").Substring(0, 8)); + StaticScopeInfos[scope] = new ScopeInfo(scope, Environment.StackTrace); + } + } + + // register that a scope is in a 'context' + // 'context' that contains the scope (null, "http" or "call") + public static void RegisterContext(IScope scope, string context) + { + lock (StaticScopeInfosLock) + { + ScopeInfo info; + if (StaticScopeInfos.TryGetValue(scope, out info) == false) info = null; + if (info == null) + { + if (context == null) return; + throw new Exception("oops: unregistered scope."); + } + var sb = new StringBuilder(); + var s = scope; + while (s != null) + { + if (sb.Length > 0) sb.Append(" < "); + sb.Append(s.InstanceId.ToString("N").Substring(0, 8)); + var ss = s as IScopeInternal; + s = ss == null ? null : ss.ParentScope; + } + Logging.LogHelper.Debug("Register " + (context ?? "null") + " context " + sb); + if (context == null) info.NullStack = Environment.StackTrace; + //Logging.LogHelper.Debug("At:\r\n" + Head(Environment.StackTrace, 16)); + info.Context = context; + } + } + + private static string Head(string s, int count) + { + var pos = 0; + var i = 0; + while (i < count && pos >= 0) + { + pos = s.IndexOf("\r\n", pos + 1, StringComparison.OrdinalIgnoreCase); + i++; + } + if (pos < 0) return s; + return s.Substring(0, pos); + } + + public void Disposed(IScope scope) + { + lock (StaticScopeInfosLock) + { + if (StaticScopeInfos.ContainsKey(scope)) + { + // enable this by default + //Console.WriteLine("unregister " + scope.InstanceId.ToString("N").Substring(0, 8)); + StaticScopeInfos.Remove(scope); + Logging.LogHelper.Debug("Remove " + scope.InstanceId.ToString("N").Substring(0, 8)); + + // instead, enable this to keep *all* scopes + // beware, there can be a lot of scopes! + //info.Disposed = true; + //info.DisposedStack = Environment.StackTrace; + } + } + } +#endif + } + +#if DEBUG_SCOPES + public class ScopeInfo + { + public ScopeInfo(IScope scope, string ctorStack) + { + Scope = scope; + Created = DateTime.Now; + CtorStack = ctorStack; + } + + public IScope Scope { get; private set; } // the scope itself + + // the scope's parent identifier + public Guid Parent { get { return (Scope is NoScope || ((Scope) Scope).ParentScope == null) ? Guid.Empty : ((Scope) Scope).ParentScope.InstanceId; } } + + public DateTime Created { get; private set; } // the date time the scope was created + public bool Disposed { get; set; } // whether the scope has been disposed already + public string Context { get; set; } // the current 'context' that contains the scope (null, "http" or "lcc") + + public string CtorStack { get; private set; } // the stacktrace of the scope ctor + public string DisposedStack { get; set; } // the stacktrace when disposed + public string NullStack { get; set; } // the stacktrace when the 'context' that contains the scope went null + } +#endif +} diff --git a/src/Umbraco.Core/Scoping/ScopeReference.cs b/src/Umbraco.Core/Scoping/ScopeReference.cs new file mode 100644 index 0000000000..998f21c587 --- /dev/null +++ b/src/Umbraco.Core/Scoping/ScopeReference.cs @@ -0,0 +1,30 @@ +namespace Umbraco.Core.Scoping +{ + /// + /// References a scope. + /// + /// Should go into HttpContext to indicate there is also an IScope in context + /// that needs to be disposed at the end of the request (the scope, and the entire scopes + /// chain). + internal class ScopeReference : IDisposeOnRequestEnd // implies IDisposable + { + private readonly IScopeProviderInternal _scopeProvider; + + public ScopeReference(IScopeProviderInternal scopeProvider) + { + _scopeProvider = scopeProvider; + } + + public void Dispose() + { + // dispose the entire chain (if any) + // reset (don't commit by default) + IScopeInternal scope; + while ((scope = _scopeProvider.AmbientScope) != null) + { + scope.Reset(); + scope.Dispose(); + } + } + } +} diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 3c88c07edf..17cea2976c 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -80,7 +80,41 @@ namespace Umbraco.Core.Security return false; } + + /// + /// This will return the current back office identity if the IPrincipal is the correct type + /// + /// + /// + internal static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user) + { + //If it's already a UmbracoBackOfficeIdentity + var backOfficeIdentity = 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 = 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 = user.Identity as ClaimsIdentity; + if (claimsIdentity != null && claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType)) + { + try + { + return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); + } + catch (InvalidOperationException) + { + } + } + + return null; + } /// /// This will return the current back office identity. @@ -100,31 +134,8 @@ namespace Umbraco.Core.Security if (http.User == null) return null; //there's no user at all so no identity //If it's already a UmbracoBackOfficeIdentity - 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 && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType)) - { - try - { - return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); - } - catch (InvalidOperationException ex) - { - //This will occur if the required claim types are missing which would mean something strange is going on - LogHelper.Error(typeof(AuthenticationExtensions), "The current identity cannot be converted to " + typeof(UmbracoBackOfficeIdentity), ex); - } - } + var backOfficeIdentity = GetUmbracoIdentity(http.User); + if (backOfficeIdentity != null) return backOfficeIdentity; if (authenticateRequestIfNotFound == false) return null; diff --git a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs index 7c65b43291..9a6f9cceb3 100644 --- a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs @@ -35,7 +35,7 @@ namespace Umbraco.Core.Security public static BackOfficeSignInManager Create(IdentityFactoryOptions options, IOwinContext context, ILogger logger) { return new BackOfficeSignInManager( - context.GetBackOfficeUserManager(), + context.GetBackOfficeUserManager(), context.Authentication, logger, context.Request); @@ -48,8 +48,8 @@ namespace Umbraco.Core.Security /// public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout) { - var result = await base.PasswordSignInAsync(userName, password, isPersistent, shouldLockout); - + var result = await PasswordSignInAsyncImpl(userName, password, isPersistent, shouldLockout); + switch (result) { case SignInStatus.Success: @@ -69,7 +69,7 @@ namespace Umbraco.Core.Security case SignInStatus.RequiresVerification: _logger.WriteCore(TraceEventType.Information, 0, string.Format( - "Login attempt failed for username {0} from IP address {1}, the user requires verification", + "Login attempt requires verification for username {0} from IP address {1}", userName, _request.RemoteIpAddress), null, null); break; @@ -87,6 +87,68 @@ namespace Umbraco.Core.Security return result; } + /// + /// Borrowed from Micorosoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type + /// + /// + /// + /// + /// + /// + private async Task PasswordSignInAsyncImpl(string userName, string password, bool isPersistent, bool shouldLockout) + { + if (UserManager == null) + { + return SignInStatus.Failure; + } + var user = await UserManager.FindByNameAsync(userName); + if (user == null) + { + return SignInStatus.Failure; + } + if (await UserManager.IsLockedOutAsync(user.Id)) + { + return SignInStatus.LockedOut; + } + if (await UserManager.CheckPasswordAsync(user, password)) + { + await UserManager.ResetAccessFailedCountAsync(user.Id); + return await SignInOrTwoFactor(user, isPersistent); + } + if (shouldLockout) + { + // If lockout is requested, increment access failed count which might lock out the user + await UserManager.AccessFailedAsync(user.Id); + if (await UserManager.IsLockedOutAsync(user.Id)) + { + return SignInStatus.LockedOut; + } + } + return SignInStatus.Failure; + } + + /// + /// Borrowed from Micorosoft's underlying sign in manager which is not flexible enough to tell it to use a different cookie type + /// + /// + /// + /// + private async Task SignInOrTwoFactor(BackOfficeIdentityUser user, bool isPersistent) + { + var id = Convert.ToString(user.Id); + if (await UserManager.GetTwoFactorEnabledAsync(user.Id) + && (await UserManager.GetValidTwoFactorProvidersAsync(user.Id)).Count > 0) + { + var identity = new ClaimsIdentity(Constants.Security.BackOfficeTwoFactorAuthenticationType); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id)); + identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, user.UserName)); + AuthenticationManager.SignIn(identity); + return SignInStatus.RequiresVerification; + } + await SignInAsync(user, isPersistent, false); + return SignInStatus.Success; + } + /// /// Creates a user identity and then signs the identity using the AuthenticationManager /// @@ -100,11 +162,11 @@ namespace Umbraco.Core.Security // Clear any partial cookies from external or two factor partial sign ins AuthenticationManager.SignOut( - Constants.Security.BackOfficeExternalAuthenticationType, + Constants.Security.BackOfficeExternalAuthenticationType, Constants.Security.BackOfficeTwoFactorAuthenticationType); var nowUtc = DateTime.Now.ToUniversalTime(); - + if (rememberBrowser) { var rememberBrowserIdentity = AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(ConvertIdToString(user.Id)); @@ -114,7 +176,7 @@ namespace Umbraco.Core.Security AllowRefresh = true, IssuedUtc = nowUtc, ExpiresUtc = nowUtc.AddMinutes(GlobalSettings.TimeOutInMinutes) - }, userIdentity, rememberBrowserIdentity); + }, userIdentity, rememberBrowserIdentity); } else { @@ -127,11 +189,47 @@ namespace Umbraco.Core.Security }, userIdentity); } + //track the last login date + user.LastLoginDateUtc = DateTime.UtcNow; + user.AccessFailedCount = 0; + await UserManager.UpdateAsync(user); + _logger.WriteCore(TraceEventType.Information, 0, string.Format( "Login attempt succeeded for username {0} from IP address {1}", user.UserName, _request.RemoteIpAddress), null, null); } + + /// + /// Get the user id that has been verified already or -1. + /// + /// + /// + /// Replaces the underlying call which is not flexible and doesn't support a custom cookie + /// + public new async Task GetVerifiedUserIdAsync() + { + var result = await AuthenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); + if (result != null && result.Identity != null && string.IsNullOrEmpty(result.Identity.GetUserId()) == false) + { + return ConvertIdFromString(result.Identity.GetUserId()); + } + return -1; + } + + /// + /// Get the username that has been verified already or null. + /// + /// + public async Task GetVerifiedUserNameAsync() + { + var result = await AuthenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); + if (result != null && result.Identity != null && string.IsNullOrEmpty(result.Identity.GetUserName()) == false) + { + return result.Identity.GetUserName(); + } + return null; + } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 08c6f54dcf..889c7004d7 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -625,6 +625,12 @@ namespace Umbraco.Core.Security var anythingChanged = false; //don't assign anything if nothing has changed as this will trigger //the track changes of the model + if ((user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) + || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) + { + anythingChanged = true; + user.LastLoginDate = identityUser.LastLoginDateUtc.Value.ToLocalTime(); + } if (user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index 3e02d6aec2..4d4dad3fe8 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -9,6 +9,7 @@ using System.Web.Configuration; using System.Web.Hosting; using System.Web.Security; using Umbraco.Core.Logging; +using Umbraco.Core.Models; namespace Umbraco.Core.Security { @@ -76,7 +77,8 @@ namespace Umbraco.Core.Security private bool _requiresQuestionAndAnswer; private bool _requiresUniqueEmail; private string _customHashAlgorithmType ; - internal bool UseLegacyEncoding; + + public bool UseLegacyEncoding { get; private set; } #region Properties @@ -323,6 +325,14 @@ namespace Umbraco.Core.Security throw new MembershipPasswordException("Change password canceled due to password validation failure."); } + //Special case to allow changing password without validating existing credentials + //This is used during installation only + if (AllowManuallyChangingPassword == false && ApplicationContext.Current != null + && ApplicationContext.Current.IsConfigured == false && oldPassword == "default") + { + return PerformChangePassword(username, oldPassword, newPassword); + } + if (AllowManuallyChangingPassword == false) { if (ValidateUser(username, oldPassword) == false) return false; @@ -511,7 +521,11 @@ namespace Umbraco.Core.Security public override string ResetPassword(string username, string answer) { - if (EnablePasswordReset == false) + var userService = ApplicationContext.Current == null ? null : ApplicationContext.Current.Services.UserService; + + var canReset = this.CanResetPassword(userService); + + if (canReset == false) { throw new NotSupportedException("Password reset is not supported"); } diff --git a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs index bdd3174960..645de22ab8 100644 --- a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs +++ b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs @@ -9,12 +9,45 @@ using System.Web; using System.Web.Hosting; using System.Web.Security; using Umbraco.Core.Configuration; +using Umbraco.Core.Models; using Umbraco.Core.Security; +using Umbraco.Core.Services; namespace Umbraco.Core.Security { public static class MembershipProviderExtensions { + /// + /// Extension method to check if a password can be reset based on a given provider and the current request (logged in user) + /// + /// + /// + /// + internal static bool CanResetPassword(this MembershipProvider provider, IUserService userService) + { + if (provider == null) throw new ArgumentNullException("provider"); + + var canReset = provider.EnablePasswordReset; + + if (userService == null) return canReset; + + //we need to check for the special case in which a user is an admin - in which acse they can reset the password even if EnablePasswordReset == false + if (provider.EnablePasswordReset == false) + { + var identity = Thread.CurrentPrincipal.GetUmbracoIdentity(); + if (identity != null) + { + var user = userService.GetByUsername(identity.Username); + var userIsAdmin = user.IsAdmin(); + if (userIsAdmin) + { + canReset = true; + } + } + } + return canReset; + } + internal static MembershipUserCollection FindUsersByName(this MembershipProvider provider, string usernameToMatch) { int totalRecords = 0; diff --git a/src/Umbraco.Core/Serialization/StreamResultExtensions.cs b/src/Umbraco.Core/Serialization/StreamResultExtensions.cs new file mode 100644 index 0000000000..96490a933c --- /dev/null +++ b/src/Umbraco.Core/Serialization/StreamResultExtensions.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Text; +using System.Xml.Linq; + +namespace Umbraco.Core.Serialization +{ + public static class StreamResultExtensions + { + public static string ToJsonString(this Stream stream) + { + byte[] bytes = new byte[stream.Length]; + stream.Position = 0; + stream.Read(bytes, 0, (int)stream.Length); + return Encoding.UTF8.GetString(bytes); + } + + public static XDocument ToXDoc(this Stream stream) + { + return XDocument.Load(stream); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Serialization/StreamedResult.cs b/src/Umbraco.Core/Serialization/StreamedResult.cs index 9b73fd06bf..0a50751229 100644 --- a/src/Umbraco.Core/Serialization/StreamedResult.cs +++ b/src/Umbraco.Core/Serialization/StreamedResult.cs @@ -1,6 +1,4 @@ using System.IO; -using System.Text; -using System.Xml.Linq; namespace Umbraco.Core.Serialization { @@ -20,20 +18,4 @@ namespace Umbraco.Core.Serialization #endregion } - - public static class StreamResultExtensions - { - public static string ToJsonString(this Stream stream) - { - byte[] bytes = new byte[stream.Length]; - stream.Position = 0; - stream.Read(bytes, 0, (int)stream.Length); - return Encoding.UTF8.GetString(bytes); - } - - public static XDocument ToXDoc(this Stream stream) - { - return XDocument.Load(stream); - } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Serialization/UdiJsonConverter.cs b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs new file mode 100644 index 0000000000..ff62535825 --- /dev/null +++ b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs @@ -0,0 +1,27 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Serialization +{ + + public class UdiJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(Udi).IsAssignableFrom(objectType); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jo = JToken.ReadFrom(reader); + var val = jo.ToObject(); + return val == null ? null : Udi.Parse(val); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Serialization/UdiRangeJsonConverter.cs b/src/Umbraco.Core/Serialization/UdiRangeJsonConverter.cs new file mode 100644 index 0000000000..099c46f29d --- /dev/null +++ b/src/Umbraco.Core/Serialization/UdiRangeJsonConverter.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Serialization +{ + public class UdiRangeJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(UdiRange).IsAssignableFrom(objectType); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jo = JToken.ReadFrom(reader); + var val = jo.ToObject(); + return val == null ? null : UdiRange.Parse(val); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ApplicationTreeService.cs b/src/Umbraco.Core/Services/ApplicationTreeService.cs index a0a5d8cb82..b40eac106e 100644 --- a/src/Umbraco.Core/Services/ApplicationTreeService.cs +++ b/src/Umbraco.Core/Services/ApplicationTreeService.cs @@ -358,9 +358,8 @@ namespace Umbraco.Core.Services { var applicationAlias = (string)addElement.Attribute("application"); var type = (string)addElement.Attribute("type"); - var assembly = (string)addElement.Attribute("assembly"); - - var clrType = Type.GetType(type); + + var clrType = ApplicationTree.TryGetType(type); if (clrType == null) { _logger.Warn("The tree definition: " + addElement.ToString() + " could not be resolved to a .Net object type"); diff --git a/src/Umbraco.Core/Services/AuditService.cs b/src/Umbraco.Core/Services/AuditService.cs index 3e076084f2..642d89cb39 100644 --- a/src/Umbraco.Core/Services/AuditService.cs +++ b/src/Umbraco.Core/Services/AuditService.cs @@ -7,18 +7,17 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { - public sealed class AuditService : RepositoryService, IAuditService + public sealed class AuditService : ScopeRepositoryService, IAuditService { public AuditService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, repositoryFactory, logger, eventMessagesFactory) - { - } + { } public void Add(AuditType type, string comment, int userId, int objectId) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateAuditRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repo = RepositoryFactory.CreateAuditRepository(uow); repo.AddOrUpdate(new AuditItem(objectId, comment, type, userId)); uow.Commit(); } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index e0edaa38c4..c00b226d3e 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -7,12 +7,9 @@ using System.Linq; using System.Threading; using System.Xml; using System.Xml.Linq; -using Umbraco.Core.Auditing; -using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; @@ -20,23 +17,23 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Publishing; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Services { /// /// Represents the Content Service, which is an easy access to operations involving /// - public class ContentService : RepositoryService, IContentService, IContentServiceOperations + public class ContentService : ScopeRepositoryService, IContentService, IContentServiceOperations { - private readonly IPublishingStrategy _publishingStrategy; + private readonly IPublishingStrategy2 _publishingStrategy; private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); private readonly IDataTypeService _dataTypeService; private readonly IUserService _userService; - //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); @@ -45,15 +42,13 @@ namespace Umbraco.Core.Services RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, - IPublishingStrategy publishingStrategy, IDataTypeService dataTypeService, IUserService userService) : base(provider, repositoryFactory, logger, eventMessagesFactory) { - if (publishingStrategy == null) throw new ArgumentNullException("publishingStrategy"); if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); if (userService == null) throw new ArgumentNullException("userService"); - _publishingStrategy = publishingStrategy; + _publishingStrategy = new PublishingStrategy(UowProvider.ScopeProvider, eventMessagesFactory, logger); _dataTypeService = dataTypeService; _userService = userService; } @@ -66,36 +61,36 @@ namespace Umbraco.Core.Services public int CountPublished(string contentTypeAlias = null) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); return repository.CountPublished(); } } public int Count(string contentTypeAlias = null) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); return repository.Count(contentTypeAlias); } } public int CountChildren(int parentId, string contentTypeAlias = null) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); return repository.CountChildren(parentId, contentTypeAlias); } } public int CountDescendants(int parentId, string contentTypeAlias = null) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); return repository.CountDescendants(parentId, contentTypeAlias); } } @@ -107,10 +102,11 @@ namespace Umbraco.Core.Services /// public void ReplaceContentPermissions(EntityPermissionSet permissionSet) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repository = RepositoryFactory.CreateContentRepository(uow); repository.ReplaceContentPermissions(permissionSet); + uow.Commit(); } } @@ -122,10 +118,11 @@ namespace Umbraco.Core.Services /// public void AssignContentPermission(IContent entity, char permission, IEnumerable userIds) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repository = RepositoryFactory.CreateContentRepository(uow); repository.AssignEntityPermission(entity, permission, userIds); + uow.Commit(); } } @@ -136,9 +133,9 @@ namespace Umbraco.Core.Services /// public IEnumerable GetPermissionsForEntity(IContent content) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); return repository.GetPermissionsForEntity(content.Id); } } @@ -164,22 +161,21 @@ namespace Umbraco.Core.Services var parent = GetById(content.ParentId); content.Path = string.Concat(parent.IfNotNull(x => x.Path, content.ParentId.ToString()), ",", content.Id); - - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) + using (var uow = UowProvider.GetUnitOfWork()) { - content.WasCancelled = true; - return content; - } + if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(content, contentTypeAlias, parentId))) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } - content.CreatorId = userId; - content.WriterId = userId; + content.CreatorId = userId; + content.WriterId = userId; - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); + uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parentId)); - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(content.Id, string.Format("Content '{0}' was created", name), AuditType.New, content.CreatorId)); + Audit(uow, AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); uow.Commit(); } @@ -208,19 +204,24 @@ namespace Umbraco.Core.Services var content = new Content(name, parent, contentType); content.Path = string.Concat(parent.Path, ",", content.Id); - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) + using (var uow = UowProvider.GetUnitOfWork()) { - content.WasCancelled = true; - return content; + if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(content, contentTypeAlias, parent))) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } + + content.CreatorId = userId; + content.WriterId = userId; + + uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parent)); + + Audit(uow, AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); + uow.Commit(); } - content.CreatorId = userId; - content.WriterId = userId; - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); - - Audit(AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); - return content; } @@ -242,37 +243,38 @@ namespace Umbraco.Core.Services var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parentId, contentType); - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) + using (var uow = UowProvider.GetUnitOfWork()) { - content.WasCancelled = true; - return content; - } + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(content, contentTypeAlias, parentId))) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) - { - content.WasCancelled = true; - return content; - } + if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content))) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { + var repository = RepositoryFactory.CreateContentRepository(uow); content.CreatorId = userId; content.WriterId = userId; + repository.AddOrUpdate(content); - //Generate a new preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + + uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false)); + uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parentId)); + + Audit(uow, AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); uow.Commit(); } - Saved.RaiseEvent(new SaveEventArgs(content, false), this); - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); - - Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); - return content; } @@ -296,37 +298,38 @@ namespace Umbraco.Core.Services var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parent, contentType); - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) + using (var uow = UowProvider.GetUnitOfWork()) { - content.WasCancelled = true; - return content; - } + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(content, contentTypeAlias, parent))) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) - { - content.WasCancelled = true; - return content; - } + if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content))) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { + var repository = RepositoryFactory.CreateContentRepository(uow); content.CreatorId = userId; content.WriterId = userId; + repository.AddOrUpdate(content); - //Generate a new preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + + uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false)); + uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parent)); + + Audit(uow, AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); uow.Commit(); } - Saved.RaiseEvent(new SaveEventArgs(content, false), this); - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); - - Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); - return content; } @@ -337,8 +340,9 @@ namespace Umbraco.Core.Services /// public IContent GetById(int id) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); return repository.Get(id); } } @@ -353,11 +357,12 @@ namespace Umbraco.Core.Services var idsArray = ids.ToArray(); if (idsArray.Length == 0) return Enumerable.Empty(); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - //ensure that the result has the order based on the ids passed in - var result = repository.GetAll(idsArray); + var repository = RepositoryFactory.CreateContentRepository(uow); + // ensure that the result has the order based on the ids passed in + var result = repository.GetAll(idsArray); var content = result.ToDictionary(x => x.Id, x => x); var sortedResult = idsArray.Select(x => @@ -377,8 +382,9 @@ namespace Umbraco.Core.Services /// public IContent GetById(Guid key) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); var query = Query.Builder.Where(x => x.Key == key); var contents = repository.GetByQuery(query); return contents.SingleOrDefault(); @@ -392,23 +398,21 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetContentOfContentType(int id) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); var query = Query.Builder.Where(x => x.ContentTypeId == id); - var contents = repository.GetByQuery(query); - - return contents; + return repository.GetByQuery(query); } } internal IEnumerable GetPublishedContentOfContentType(int id) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); var query = Query.Builder.Where(x => x.ContentTypeId == id); - var contents = repository.GetByPublishedVersion(query); - - return contents; + return repository.GetByPublishedVersion(query); } } @@ -419,12 +423,11 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetByLevel(int level) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var query = Query.Builder.Where(x => x.Level == level && !x.Path.StartsWith(Constants.System.RecycleBinContent.ToInvariantString())); - var contents = repository.GetByQuery(query); - - return contents; + var repository = RepositoryFactory.CreateContentRepository(uow); + var query = Query.Builder.Where(x => x.Level == level && x.Path.StartsWith(Constants.System.RecycleBinContent.ToInvariantString()) == false); + return repository.GetByQuery(query); } } @@ -435,8 +438,9 @@ namespace Umbraco.Core.Services /// An item public IContent GetByVersion(Guid versionId) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); return repository.GetByVersion(versionId); } } @@ -449,10 +453,10 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetVersions(int id) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var versions = repository.GetAllVersions(id); - return versions; + var repository = RepositoryFactory.CreateContentRepository(uow); + return repository.GetAllVersions(id); } } @@ -464,10 +468,10 @@ namespace Umbraco.Core.Services /// public IEnumerable GetVersionIds(int id, int maxRows) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var versions = repository.GetVersionIds(id, maxRows); - return versions; + var repository = RepositoryFactory.CreateContentRepository(uow); + return repository.GetVersionIds(id, maxRows); } } @@ -496,8 +500,9 @@ namespace Umbraco.Core.Services if (ids.Any() == false) return new List(); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); return repository.GetAll(ids); } } @@ -509,12 +514,11 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetChildren(int id) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); var query = Query.Builder.Where(x => x.ParentId == id); - var contents = repository.GetByQuery(query).OrderBy(x => x.SortOrder); - - return contents; + return repository.GetByQuery(query).OrderBy(x => x.SortOrder); } } @@ -563,29 +567,27 @@ namespace Umbraco.Core.Services { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.ParentId == id); - } + // always check for a parent - else it will also get decendants (and then you should use the GetPagedDescendants method) + query.Where(x => x.ParentId == id); + IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) { filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filterQuery); - - return contents; + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filterQuery); } } [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") { long total; var result = GetPagedDescendants(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); @@ -603,8 +605,8 @@ namespace Umbraco.Core.Services /// Field to order by /// Direction to order by /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") { return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); } @@ -620,28 +622,26 @@ namespace Umbraco.Core.Services /// Direction to order by /// Flag to indicate when ordering by system field /// Search text filter - /// An Enumerable list of objects + /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); + // get query - if the id is System Root, then just get all var query = Query.Builder; - //if the id is System Root, then just get all if (id != Constants.System.Root) - { query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } + + // get filter IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) - { filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); - } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filterQuery); - return contents; + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filterQuery); } } @@ -662,18 +662,16 @@ namespace Umbraco.Core.Services Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); + + // get query - if the id is System Root, then just get all var query = Query.Builder; - - //if the id is System Root, then just get all if (id != Constants.System.Root) - { query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); - return contents; + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); } } @@ -685,12 +683,12 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetChildrenByName(int parentId, string name) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var query = Query.Builder.Where(x => x.ParentId == parentId && x.Name.Contains(name)); - var contents = repository.GetByQuery(query); + var repository = RepositoryFactory.CreateContentRepository(uow); - return contents; + var query = Query.Builder.Where(x => x.ParentId == parentId && x.Name.Contains(name)); + return repository.GetByQuery(query); } } @@ -702,11 +700,7 @@ namespace Umbraco.Core.Services public IEnumerable GetDescendants(int id) { var content = GetById(id); - if (content == null) - { - return Enumerable.Empty(); - } - return GetDescendants(content); + return content == null ? Enumerable.Empty() : GetDescendants(content); } /// @@ -720,14 +714,13 @@ namespace Umbraco.Core.Services if (content.ValidatePath() == false) throw new InvalidDataException(string.Format("The content item {0} has an invalid path: {1} with parentID: {2}", content.Id, content.Path, content.ParentId)); - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); + var pathMatch = content.Path + ","; var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != content.Id); - var contents = repository.GetByQuery(query); - - return contents; + return repository.GetByQuery(query); } } @@ -763,7 +756,7 @@ namespace Umbraco.Core.Services public IContent GetPublishedVersion(int id) { var version = GetVersions(id); - return version.FirstOrDefault(x => x.Published == true); + return version.FirstOrDefault(x => x.Published); } /// @@ -785,12 +778,12 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetRootContent() { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var query = Query.Builder.Where(x => x.ParentId == Constants.System.Root); - var contents = repository.GetByQuery(query); + var repository = RepositoryFactory.CreateContentRepository(uow); - return contents; + var query = Query.Builder.Where(x => x.ParentId == Constants.System.Root); + return repository.GetByQuery(query); } } @@ -806,8 +799,9 @@ namespace Umbraco.Core.Services _notTrashedQuery = Query.Builder.Where(x => x.Trashed == false); } - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); return repository.GetByPublishedVersion(_notTrashedQuery); } } @@ -818,12 +812,11 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetContentForExpiration() { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var query = Query.Builder.Where(x => x.Published == true && x.ExpireDate <= DateTime.Now); - var contents = repository.GetByQuery(query); - - return contents; + var repository = RepositoryFactory.CreateContentRepository(uow); + var query = Query.Builder.Where(x => x.Published && x.ExpireDate <= DateTime.Now); + return repository.GetByQuery(query); } } @@ -833,12 +826,11 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetContentForRelease() { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); var query = Query.Builder.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); - var contents = repository.GetByQuery(query); - - return contents; + return repository.GetByQuery(query); } } @@ -848,12 +840,11 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetContentInRecycleBin() { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); var query = Query.Builder.Where(x => x.Path.Contains(Constants.System.RecycleBinContent.ToInvariantString())); - var contents = repository.GetByQuery(query); - - return contents; + return repository.GetByQuery(query); } } @@ -871,11 +862,11 @@ namespace Umbraco.Core.Services internal int CountChildren(int id) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); var query = Query.Builder.Where(x => x.ParentId == id); - var count = repository.Count(query); - return count; + return repository.Count(query); } } @@ -886,11 +877,11 @@ namespace Umbraco.Core.Services /// True if the content has any published version otherwise False public bool HasPublishedVersion(int id) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentRepository(uow); var query = Query.Builder.Where(x => x.Published == true && x.Id == id && x.Trashed == false); - int count = repository.Count(query); - return count > 0; + return repository.Count(query) > 0; } } @@ -905,7 +896,7 @@ namespace Umbraco.Core.Services //because if the Parent is publishable then the current content can be Saved and Published if (content.HasIdentity == false) { - IContent parent = GetById(content.ParentId); + var parent = GetById(content.ParentId); return IsPublishable(parent, true); } @@ -913,12 +904,12 @@ namespace Umbraco.Core.Services } /// - /// This will rebuild the xml structures for content in the database. + /// This will rebuild the xml structures for content in the database. /// /// This is not used for anything /// True if publishing succeeded, otherwise False /// - /// This is used for when a document type alias or a document type property is changed, the xml will need to + /// This is used for when a document type alias or a document type property is changed, the xml will need to /// be regenerated. /// public bool RePublishAll(int userId = 0) @@ -936,7 +927,7 @@ namespace Umbraco.Core.Services } /// - /// This will rebuild the xml structures for content in the database. + /// This will rebuild the xml structures for content in the database. /// /// /// If specified will only rebuild the xml for the content type's specified, otherwise will update the structure @@ -999,46 +990,53 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting the Content Attempt IContentServiceOperations.MoveToRecycleBin(IContent content, int userId) { - var evtMsgs = EventMessagesFactory.Get(); + return MoveToRecycleBinDo(content, userId, false); + } + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + /// + /// A boolean indicating to ignore this item's descendant list from also being moved to the recycle bin. This is required for the DeleteContentOfTypes method + /// because it has already looked up all descendant nodes that will need to be recycled + /// TODO: Fix all of this, it will require a reasonable refactor and most of this stuff should be done at the repo level instead of service sub operations + /// + private Attempt MoveToRecycleBinDo(IContent content, int userId, bool ignoreDescendants) + { + var evtMsgs = EventMessagesFactory.Get(); using (new WriteLock(Locker)) { - //Hack: this ensures that the entity's path is valid and if not it fixes/persists it - //see: http://issues.umbraco.org/issue/U4-9336 - content.EnsureValidPath(Logger, entity => GetById(entity.ParentId), QuickUpdate); - - var originalPath = content.Path; - - if (Trashing.IsRaisedEventCancelled( - new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), - this)) + using (var uow = UowProvider.GetUnitOfWork()) { - return OperationStatus.Cancelled(evtMsgs); - } + //Hack: this ensures that the entity's path is valid and if not it fixes/persists it + //see: http://issues.umbraco.org/issue/U4-9336 + content.EnsureValidPath(Logger, entity => GetById(entity.ParentId), QuickUpdate); + var originalPath = content.Path; + if (uow.Events.DispatchCancelable(Trashing, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), "Trashing")) + { + uow.Commit(); + return OperationStatus.Cancelled(evtMsgs); + } + var moveInfo = new List> + { + new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent) + }; - var moveInfo = new List> - { - new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent) - }; + //get descendents to process of the content item that is being moved to trash - must be done before changing the state below + //must be processed with shallowest levels first + var descendants = ignoreDescendants ? Enumerable.Empty() : GetDescendants(content).OrderBy(x => x.Level); - //Make sure that published content is unpublished before being moved to the Recycle Bin - if (HasPublishedVersion(content.Id)) - { - //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! - UnPublish(content, userId); - } - - //Unpublish descendents of the content item that is being moved to trash - var descendants = GetDescendants(content).OrderBy(x => x.Level).ToList(); - foreach (var descendant in descendants) - { - //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! - UnPublish(descendant, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { + //Do the updates for this item + var repository = RepositoryFactory.CreateContentRepository(uow); + //Make sure that published content is unpublished before being moved to the Recycle Bin + if (HasPublishedVersion(content.Id)) + { + //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! + UnPublish(content, userId); + } content.WriterId = userId; content.ChangeTrashedState(true); repository.AddOrUpdate(content); @@ -1046,20 +1044,21 @@ namespace Umbraco.Core.Services //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId foreach (var descendant in descendants) { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - + //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! + UnPublish(descendant, userId); descendant.WriterId = userId; descendant.ChangeTrashedState(true, descendant.ParentId); repository.AddOrUpdate(descendant); + + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); } + uow.Events.Dispatch(Trashed, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), "Trashed"); + + Audit(uow, AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); uow.Commit(); } - Trashed.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); - return OperationStatus.Success(evtMsgs); } } @@ -1125,7 +1124,9 @@ namespace Umbraco.Core.Services /// True if unpublishing succeeded, otherwise False public bool UnPublish(IContent content, int userId = 0) { - return ((IContentServiceOperations)this).UnPublish(content, userId).Success; + var attempt = ((IContentServiceOperations)this).UnPublish(content, userId); + LogHelper.Debug(string.Format("Result of unpublish attempt: {0}", attempt.Result.StatusType)); + return attempt.Success; } /// @@ -1167,32 +1168,31 @@ namespace Umbraco.Core.Services /// /// Saves a collection of objects. - /// + /// /// Collection of to save /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. + /// Optional boolean indicating whether or not to raise events. Attempt IContentServiceOperations.Save(IEnumerable contents, int userId, bool raiseEvents) { var asArray = contents.ToArray(); var evtMsgs = EventMessagesFactory.Get(); - if (raiseEvents) + using (var uow = UowProvider.GetUnitOfWork()) { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(asArray, evtMsgs), - this)) + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(asArray, evtMsgs))) { + uow.Commit(); return OperationStatus.Cancelled(evtMsgs); } - } - using (new WriteLock(Locker)) - { - var containsNew = asArray.Any(x => x.HasIdentity == false); - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + // todo - understand what's a lock in a scope? + // (though, these locks are refactored in v8) + using (new WriteLock(Locker)) { + var containsNew = asArray.Any(x => x.HasIdentity == false); + var repository = RepositoryFactory.CreateContentRepository(uow); + if (containsNew) { foreach (var content in asArray) @@ -1218,14 +1218,13 @@ namespace Umbraco.Core.Services repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); } } - - uow.Commit(); } if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false, evtMsgs), this); + uow.Events.Dispatch(Saved, this, new SaveEventArgs(asArray, false, evtMsgs)); - Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); + Audit(uow, AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); + uow.Commit(); return OperationStatus.Success(evtMsgs); } @@ -1246,41 +1245,38 @@ namespace Umbraco.Core.Services using (new WriteLock(Locker)) { - if (Deleting.IsRaisedEventCancelled( - new DeleteEventArgs(content, evtMsgs), - this)) + using (var uow = UowProvider.GetUnitOfWork()) { - return OperationStatus.Cancelled(evtMsgs); - } + if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(content, evtMsgs), "Deleting")) + { + uow.Commit(); + return OperationStatus.Cancelled(evtMsgs); + } - //Make sure that published content is unpublished before being deleted - if (HasPublishedVersion(content.Id)) - { - UnPublish(content, userId); - } + //Make sure that published content is unpublished before being deleted + if (HasPublishedVersion(content.Id)) + { + UnPublish(content, userId); + } - //Delete children before deleting the 'possible parent' - var children = GetChildren(content.Id); - foreach (var child in children) - { - Delete(child, userId); - } + //Delete children before deleting the 'possible parent' + var children = GetChildren(content.Id); + foreach (var child in children) + { + Delete(child, userId); + } + + var repository = RepositoryFactory.CreateContentRepository(uow); - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { repository.Delete(content); - uow.Commit(); var args = new DeleteEventArgs(content, false, evtMsgs); - Deleted.RaiseEvent(args, this); + uow.Events.Dispatch(Deleted, this, args, "Deleted"); - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); + Audit(uow, AuditType.Delete, "Delete Content performed by user", userId, content.Id); + uow.Commit(); } - Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); - return OperationStatus.Success(evtMsgs); } } @@ -1322,6 +1318,51 @@ namespace Umbraco.Core.Services ((IContentServiceOperations)this).Save(contents, userId, raiseEvents); } + /// + /// Deletes all content of the specified types. All Descendants of deleted content that is not of these types is moved to Recycle Bin. + /// + /// Id of the + /// Optional Id of the user issueing the delete operation + public void DeleteContentOfTypes(IEnumerable contentTypeIds, int userId = 0) + { + using (new WriteLock(Locker)) + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + //track the 'root' items of the collection of nodes discovered to delete, we need to use + //these items to lookup descendants that are not of this doc type so they can be transfered + //to the recycle bin + IDictionary rootItems; + var contentToDelete = this.TrackDeletionsForDeleteContentOfTypes(contentTypeIds, repository, out rootItems).ToArray(); + + if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(contentToDelete), "Deleting")) + { + uow.Commit(); + return; + } + + //Determine the items that will need to be recycled (that are children of these content items but not of these content types) + var contentToRecycle = this.TrackTrashedForDeleteContentOfTypes(contentTypeIds, rootItems, repository); + + //move each item to the bin starting with the deepest items + foreach (var child in contentToRecycle.OrderByDescending(x => x.Level)) + { + MoveToRecycleBinDo(child, userId, true); + } + + foreach (var content in contentToDelete) + { + Delete(content, userId); + } + + Audit(uow, AuditType.Delete, + string.Format("Delete Content of Types {0} performed by user", string.Join(",", contentTypeIds)), + userId, Constants.System.Root); + uow.Commit(); + } + } + /// /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. /// @@ -1330,47 +1371,7 @@ 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()) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - //NOTE What about content that has the contenttype as part of its composition? - var query = Query.Builder.Where(x => x.ContentTypeId == contentTypeId); - var contents = repository.GetByQuery(query).ToArray(); - - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) - return; - - foreach (var content in contents.OrderByDescending(x => x.ParentId)) - { - //Look for children of current content and move that to trash before the current content is deleted - var c = content; - var childQuery = Query.Builder.Where(x => x.Path.StartsWith(c.Path)); - var children = repository.GetByQuery(childQuery); - - foreach (var child in children) - { - if (child.ContentType.Id != contentTypeId) - MoveToRecycleBin(child, userId); - } - - //Permantly delete the content - Delete(content, userId); - } - } - - Audit(AuditType.Delete, - string.Format("Delete Content of Type {0} performed by user", contentTypeId), - userId, Constants.System.Root); - } + DeleteContentOfTypes(new[] {contentTypeId}, userId); } /// @@ -1396,19 +1397,22 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting versions of a Content object public void DeleteVersions(int id, DateTime versionDate, int userId = 0) { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) - return; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), "DeletingVersions")) + { + uow.Commit(); + return; + } + + var repository = RepositoryFactory.CreateContentRepository(uow); repository.DeleteVersions(id, versionDate); + + uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), "DeletedVersions"); + + Audit(uow, AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); uow.Commit(); } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); - - Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); } /// @@ -1423,25 +1427,28 @@ namespace Umbraco.Core.Services { using (new WriteLock(Locker)) { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) - return; - - if (deletePriorVersions) + using (var uow = UowProvider.GetUnitOfWork()) { - var content = GetByVersion(versionId); - DeleteVersions(id, content.UpdateDate, userId); - } + if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, specificVersion: versionId), "DeletingVersions")) + { + uow.Commit(); + return; + } - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { + if (deletePriorVersions) + { + var content = GetByVersion(versionId); + DeleteVersions(id, content.UpdateDate, userId); + } + + var repository = RepositoryFactory.CreateContentRepository(uow); repository.DeleteVersion(versionId); + + uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), "DeletedVersions"); + + Audit(uow, AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); uow.Commit(); } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); - - Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); } } @@ -1478,22 +1485,25 @@ namespace Umbraco.Core.Services return; } - if (Moving.IsRaisedEventCancelled( - new MoveEventArgs( - new MoveEventInfo(content, content.Path, parentId)), this)) + using (var uow = UowProvider.GetUnitOfWork()) { - return; + if (uow.Events.DispatchCancelable(Moving, this, new MoveEventArgs(new MoveEventInfo(content, content.Path, parentId)), "Moving")) + { + uow.Commit(); + return; + } + + //used to track all the moved entities to be given to the event + var moveInfo = new List>(); + + //call private method that does the recursive moving + PerformMove(content, parentId, userId, moveInfo); + + uow.Events.Dispatch(Moved, this, new MoveEventArgs(false, moveInfo.ToArray()), "Moved"); + + Audit(uow, AuditType.Move, "Move Content performed by user", userId, content.Id); + uow.Commit(); } - - //used to track all the moved entities to be given to the event - var moveInfo = new List>(); - - //call private method that does the recursive moving - PerformMove(content, parentId, userId, moveInfo); - - Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); } } @@ -1504,37 +1514,38 @@ namespace Umbraco.Core.Services { using (new WriteLock(Locker)) { - Dictionary> entities; - List files; - bool success; var nodeObjectType = new Guid(Constants.ObjectTypes.Document); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork()) { + var repository = RepositoryFactory.CreateContentRepository(uow); + //Create a dictionary of ids -> dictionary of property aliases + values - entities = repository.GetEntitiesInRecycleBin() + var entities = repository.GetEntitiesInRecycleBin() .ToDictionary( key => key.Id, val => (IEnumerable)val.Properties); - files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField(); + var files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField(); - if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType, entities, files), this)) + if (uow.Events.DispatchCancelable(EmptyingRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, entities, files))) + { + uow.Commit(); return; + } - success = repository.EmptyRecycleBin(); + var success = repository.EmptyRecycleBin(); - EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); + uow.Events.Dispatch(EmptiedRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, entities, files, success)); - if (success) - repository.DeleteMediaFiles(files); + Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); + uow.Commit(); } } - Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); } /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// Copies an object by creating a new Content object of the same type and copies all data from the current /// to the new copy which is returned. Recursively copies all children. /// /// The to copy @@ -1548,7 +1559,7 @@ namespace Umbraco.Core.Services } /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// Copies an object by creating a new Content object of the same type and copies all data from the current /// to the new copy which is returned. /// /// The to copy @@ -1570,51 +1581,54 @@ namespace Umbraco.Core.Services // A copy should never be set to published automatically even if the original was. copy.ChangePublishedState(PublishedState.Unpublished); - if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) - return null; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(Copying, this, new CopyEventArgs(content, copy, parentId))) + { + uow.Commit(); + return null; + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + // Update the create author and last edit author copy.CreatorId = userId; copy.WriterId = userId; repository.AddOrUpdate(copy); - //add or update a preview repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - uow.Commit(); - + uow.Commit(); // todo - this should flush, not commit //Special case for the associated tags //TODO: Move this to the repository layer in a single transaction! //don't copy tags data in tags table if the item is in the recycle bin if (parentId != Constants.System.RecycleBinContent) { - var tags = uow.Database.Fetch("WHERE nodeId = @Id", new { Id = content.Id }); foreach (var tag in tags) + uow.Database.Insert(new TagRelationshipDto + { + NodeId = copy.Id, TagId = tag.TagId, PropertyTypeId = tag.PropertyTypeId + }); + } + uow.Commit(); // todo - this should flush, not commit + + if (recursive) + { + //Look for children and copy those as well + var children = GetChildren(content.Id); + foreach (var child in children) { - uow.Database.Insert(new TagRelationshipDto { NodeId = copy.Id, TagId = tag.TagId, PropertyTypeId = tag.PropertyTypeId }); + //TODO: This shouldn't recurse back to this method, it should be done in a private method + // that doesn't have a nested lock and so we can perform the entire operation in one commit. + Copy(child, copy.Id, relateToOriginal, true, userId); } } + uow.Events.Dispatch(Copied, this, new CopyEventArgs(content, copy, false, parentId, relateToOriginal)); + Audit(uow, AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); + uow.Commit(); } - if (recursive) - { - //Look for children and copy those as well - var children = GetChildren(content.Id); - foreach (var child in children) - { - //TODO: This shouldn't recurse back to this method, it should be done in a private method - // that doesn't have a nested lock and so we can perform the entire operation in one commit. - Copy(child, copy.Id, relateToOriginal, true, userId); - } - } - - Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId, relateToOriginal), this); - - Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); return copy; } } @@ -1628,17 +1642,24 @@ namespace Umbraco.Core.Services /// True if sending publication was succesfull otherwise false public bool SendToPublication(IContent content, int userId = 0) { - if (SendingToPublish.IsRaisedEventCancelled(new SendToPublishEventArgs(content), this)) - return false; + using (var uow = UowProvider.GetUnitOfWork()) + { + if (uow.Events.DispatchCancelable(SendingToPublish, this, new SendToPublishEventArgs(content))) + { + uow.Commit(); + return false; + } - //Save before raising event - Save(content, userId); + //Save before raising event + Save(content, userId); - SentToPublish.RaiseEvent(new SendToPublishEventArgs(content, false), this); + uow.Events.Dispatch(SentToPublish, this, new SendToPublishEventArgs(content, false)); - Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); + Audit(uow, AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); + uow.Commit(); - return true; + return true; + } } /// @@ -1657,26 +1678,29 @@ namespace Umbraco.Core.Services { var content = GetByVersion(versionId); - if (RollingBack.IsRaisedEventCancelled(new RollbackEventArgs(content), this)) - return content; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(RollingBack, this, new RollbackEventArgs(content))) + { + uow.Commit(); + return content; + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + content.WriterId = userId; content.CreatorId = userId; content.ChangePublishedState(PublishedState.Unpublished); repository.AddOrUpdate(content); - //add or update a preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + + uow.Events.Dispatch(RolledBack, this, new RollbackEventArgs(content, false)); + + Audit(uow, AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); uow.Commit(); } - RolledBack.RaiseEvent(new RollbackEventArgs(content, false), this); - - Audit(AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); - return content; } @@ -1694,22 +1718,23 @@ namespace Umbraco.Core.Services /// True if sorting succeeded, otherwise False public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) { - var asArray = items.ToArray(); - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) - return false; - } - var shouldBePublished = new List(); var shouldBeSaved = new List(); - + using (new WriteLock(Locker)) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { - int i = 0; + var asArray = items.ToArray(); + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(asArray))) + { + uow.Commit(); + return false; + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + + var i = 0; foreach (var content in asArray) { //If the current sort order equals that of the content @@ -1728,7 +1753,7 @@ namespace Umbraco.Core.Services if (content.Published) { //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! - var published = _publishingStrategy.Publish(content, userId); + var published = _publishingStrategy.Publish(uow, content, userId).Success; shouldBePublished.Add(content); } else @@ -1745,35 +1770,59 @@ namespace Umbraco.Core.Services repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); } + if (raiseEvents) + uow.Events.Dispatch(Saved, this, new SaveEventArgs(asArray, false)); + + if (shouldBePublished.Any()) + { + //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! + _publishingStrategy.PublishingFinalized(uow, shouldBePublished, false); + } + + Audit(uow, AuditType.Sort, "Sorting content performed by user", userId, 0); uow.Commit(); } } - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); - - if (shouldBePublished.Any()) - { - //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! - _publishingStrategy.PublishingFinalized(shouldBePublished, false); - } - - - Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); - return true; } + /// + /// Gets paged content descendants as XML by path + /// + /// Path starts with + /// Page number + /// Page size + /// Total records the query would return without paging + /// A paged enumerable of XML entries of content items + public IEnumerable GetPagedXmlEntries(string path, long pageIndex, int pageSize, out long totalRecords) + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var contents = repository.GetPagedXmlEntriesByPath(path, pageIndex, pageSize, + //This order by is VERY important! This allows us to figure out what is implicitly not published, see ContentRepository.BuildXmlCache and + // UmbracoContentIndexer.PerformIndexAll which uses the logic based on this sort order + new[] { "level", "parentID", "sortOrder" }, + out totalRecords); + return contents; + } + } + /// /// This builds the Xml document used for the XML cache /// /// public XmlDocument BuildXmlCache() { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repository = RepositoryFactory.CreateContentRepository(uow); var result = repository.BuildXmlCache(); + uow.Commit(); return result; } } @@ -1787,18 +1836,17 @@ namespace Umbraco.Core.Services /// public void RebuildXmlStructures(params int[] contentTypeIds) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repository = RepositoryFactory.CreateContentRepository(uow); + repository.RebuildXmlStructures( content => _entitySerializer.Serialize(this, _dataTypeService, _userService, content), contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); + Audit(uow, AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); uow.Commit(); } - - Audit(AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); - } #region Internal Methods @@ -1810,12 +1858,12 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects internal IEnumerable GetPublishedDescendants(IContent content) { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var query = Query.Builder.Where(x => x.Id != content.Id && x.Path.StartsWith(content.Path) && x.Trashed == false); - var contents = repository.GetByPublishedVersion(query); + var repository = RepositoryFactory.CreateContentRepository(uow); - return contents; + var query = Query.Builder.Where(x => x.Id != content.Id && x.Path.StartsWith(content.Path) && x.Trashed == false); + return repository.GetByPublishedVersion(query); } } @@ -1832,22 +1880,18 @@ namespace Umbraco.Core.Services if (content == null) throw new ArgumentNullException("content"); if (content.HasIdentity == false) throw new InvalidOperationException("Cannot update an entity without an Identity"); - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { - repository.AddOrUpdate(content); + var repository = RepositoryFactory.CreateContentRepository(uow); + repository.AddOrUpdate(content); uow.Commit(); } } - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(IScopeUnitOfWork uow, AuditType type, string message, int userId, int objectId) { - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Commit(); - } + var auditRepo = RepositoryFactory.CreateAuditRepository(uow); + auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); } //TODO: All of this needs to be moved to the repository @@ -1894,7 +1938,7 @@ namespace Umbraco.Core.Services //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine Save(content, false, userId); - //TODO: This shouldn't be here! This needs to be part of the repository logic but in order to fix this we need to + //TODO: This shouldn't be here! This needs to be part of the repository logic but in order to fix this we need to // change how this method calls "Save" as it needs to save using an internal method using (var uow = UowProvider.GetUnitOfWork()) { @@ -1907,6 +1951,7 @@ namespace Umbraco.Core.Services int result = exists ? uow.Database.Update(poco) : Convert.ToInt32(uow.Database.Insert(poco)); + uow.Commit(); } } } @@ -1932,7 +1977,7 @@ namespace Umbraco.Core.Services /// /// The to publish along with its children /// Optional Id of the User issueing the publishing - /// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published + /// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published /// /// A list of publish statues. If the parent document is not valid or cannot be published because it's parent(s) is not published /// then the list will only contain one status item, otherwise it will contain status items for it and all of it's descendants that @@ -1987,19 +2032,20 @@ namespace Umbraco.Core.Services list.Add(content); //include parent item list.AddRange(GetDescendants(content)); - var internalStrategy = (PublishingStrategy)_publishingStrategy; + var internalStrategy = _publishingStrategy; - //Publish and then update the database with new status - var publishedOutcome = internalStrategy.PublishWithChildrenInternal(list, userId, includeUnpublished).ToArray(); - var published = publishedOutcome - .Where(x => x.Success || x.Result.StatusType == PublishStatusType.SuccessAlreadyPublished) - // ensure proper order (for events) - cannot publish a child before its parent! - .OrderBy(x => x.Result.ContentItem.Level) - .ThenBy(x => x.Result.ContentItem.SortOrder); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + //Publish and then update the database with new status + var publishedOutcome = internalStrategy.PublishWithChildren(uow, list, userId, includeUnpublished).ToArray(); + var published = publishedOutcome + .Where(x => x.Success || x.Result.StatusType == PublishStatusType.SuccessAlreadyPublished) + // ensure proper order (for events) - cannot publish a child before its parent! + .OrderBy(x => x.Result.ContentItem.Level) + .ThenBy(x => x.Result.ContentItem.SortOrder); + + var repository = RepositoryFactory.CreateContentRepository(uow); + //NOTE The Publish with subpages-dialog was used more as a republish-type-thing, so we'll have to include PublishStatusType.SuccessAlreadyPublished //in the updated-list, so the Published event is triggered with the expected set of pages and the xml is updated. foreach (var item in published) @@ -2013,16 +2059,14 @@ namespace Umbraco.Core.Services updated.Add(item.Result.ContentItem); } + //Save xml to db and call following method to fire event: + _publishingStrategy.PublishingFinalized(uow, updated, false); + + Audit(uow, AuditType.Publish, "Publish with Children performed by user", userId, content.Id); uow.Commit(); + return publishedOutcome; } - //Save xml to db and call following method to fire event: - _publishingStrategy.PublishingFinalized(updated, false); - - Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id); - - - return publishedOutcome; } } @@ -2047,12 +2091,17 @@ namespace Umbraco.Core.Services return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs)); // already unpublished } - var unpublished = _publishingStrategy.UnPublish(content, userId); - if (unpublished == false) return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + var unpublished = _publishingStrategy.UnPublish(uow, content, userId); + if (unpublished == false) + { + uow.Commit(); + return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + content.WriterId = userId; repository.AddOrUpdate(content); // is published is not newest, reset the published flag on published version @@ -2060,13 +2109,13 @@ namespace Umbraco.Core.Services repository.ClearPublished(published); repository.DeleteContentXml(content); + //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache + if (omitCacheRefresh == false) + _publishingStrategy.UnPublishingFinalized(uow, content); + + Audit(uow, AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); uow.Commit(); } - //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache - if (omitCacheRefresh == false) - _publishingStrategy.UnPublishingFinalized(content); - - Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.Success, evtMsgs)); } @@ -2082,47 +2131,44 @@ namespace Umbraco.Core.Services { var evtMsgs = EventMessagesFactory.Get(); - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(content, evtMsgs), this)) - { - return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); - } - } - using (new WriteLock(Locker)) { - //Has this content item previously been published? If so, we don't need to refresh the children - var previouslyPublished = content.HasIdentity && HasPublishedVersion(content.Id); //content might not have an id - var publishStatus = new PublishStatus(content, PublishStatusType.Success, evtMsgs); //initially set to success - - //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published - publishStatus.StatusType = CheckAndLogIsPublishable(content); - //if it is not successful, then check if the props are valid - if ((int)publishStatus.StatusType < 10) + using (var uow = UowProvider.GetUnitOfWork()) { - //Content contains invalid property values and can therefore not be published - fire event? - publishStatus.StatusType = CheckAndLogIsValid(content); - //set the invalid properties (if there are any) - publishStatus.InvalidProperties = ((ContentBase)content).LastInvalidProperties; - } - //if we're still successful, then publish using the strategy - if (publishStatus.StatusType == PublishStatusType.Success) - { - var internalStrategy = (PublishingStrategy)_publishingStrategy; - //Publish and then update the database with new status - var publishResult = internalStrategy.PublishInternal(content, userId); - //set the status type to the publish result - publishStatus.StatusType = publishResult.Result.StatusType; - } + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content, evtMsgs))) + { + uow.Commit(); + return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); + } - //we are successfully published if our publishStatus is still Successful - bool published = publishStatus.StatusType == PublishStatusType.Success; + //Has this content item previously been published? If so, we don't need to refresh the children + var previouslyPublished = content.HasIdentity && HasPublishedVersion(content.Id); //content might not have an id + var publishStatus = new PublishStatus(content, PublishStatusType.Success, evtMsgs); //initially set to success + + //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published + publishStatus.StatusType = CheckAndLogIsPublishable(content); + //if it is not successful, then check if the props are valid + if ((int)publishStatus.StatusType < 10) + { + //Content contains invalid property values and can therefore not be published - fire event? + publishStatus.StatusType = CheckAndLogIsValid(content); + //set the invalid properties (if there are any) + publishStatus.InvalidProperties = ((ContentBase)content).LastInvalidProperties; + } + //if we're still successful, then publish using the strategy + if (publishStatus.StatusType == PublishStatusType.Success) + { + //Publish and then update the database with new status + var publishResult = _publishingStrategy.Publish(uow, content, userId); + //set the status type to the publish result + publishStatus.StatusType = publishResult.Result.StatusType; + } + + //we are successfully published if our publishStatus is still Successful + bool published = publishStatus.StatusType == PublishStatusType.Success; + + var repository = RepositoryFactory.CreateContentRepository(uow); - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { if (published == false) { content.ChangePublishedState(PublishedState.Saved); @@ -2135,8 +2181,6 @@ namespace Umbraco.Core.Services content.WriterId = userId; repository.AddOrUpdate(content); - - //Generate a new preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); if (published) @@ -2145,29 +2189,28 @@ namespace Umbraco.Core.Services repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); } + if (raiseEvents) + uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false, evtMsgs)); + + //Save xml to db and call following method to fire event through PublishingStrategy to update cache + if (published) + { + _publishingStrategy.PublishingFinalized(uow, content); + } + + //We need to check if children and their publish state to ensure that we 'republish' content that was previously published + if (published && previouslyPublished == false && HasChildren(content.Id)) + { + //TODO: Horrible for performance if there are lots of descendents! We should page if anything but this is crazy + var descendants = GetPublishedDescendants(content); + _publishingStrategy.PublishingFinalized(uow, descendants, false); + } + + Audit(uow, AuditType.Publish, "Save and Publish performed by user", userId, content.Id); uow.Commit(); + + return Attempt.If(publishStatus.StatusType == PublishStatusType.Success, publishStatus); } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); - - //Save xml to db and call following method to fire event through PublishingStrategy to update cache - if (published) - { - _publishingStrategy.PublishingFinalized(content); - } - - //We need to check if children and their publish state to ensure that we 'republish' content that was previously published - if (published && previouslyPublished == false && HasChildren(content.Id)) - { - var descendants = GetPublishedDescendants(content); - - _publishingStrategy.PublishingFinalized(descendants, false); - } - - Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); - - return Attempt.If(publishStatus.StatusType == PublishStatusType.Success, publishStatus); } } @@ -2182,21 +2225,23 @@ namespace Umbraco.Core.Services { var evtMsgs = EventMessagesFactory.Get(); - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(content, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - } - using (new WriteLock(Locker)) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content, evtMsgs))) + { + uow.Commit(); + return OperationStatus.Cancelled(evtMsgs); + } + + if (string.IsNullOrWhiteSpace(content.Name)) + { + throw new ArgumentException("Cannot save content with empty name."); + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + if (content.HasIdentity == false) { content.CreatorId = userId; @@ -2212,14 +2257,13 @@ namespace Umbraco.Core.Services //Generate a new preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + if (raiseEvents) + uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false, evtMsgs)); + + Audit(uow, AuditType.Save, "Save Content performed by user", userId, content.Id); uow.Commit(); } - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); - - Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); - return OperationStatus.Success(evtMsgs); } } @@ -2269,7 +2313,8 @@ namespace Umbraco.Core.Services content.Name, content.Id)); return PublishStatusType.FailedPathNotPublished; } - else if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) + + if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) { Logger.Info( string.Format( @@ -2277,7 +2322,8 @@ namespace Umbraco.Core.Services content.Name, content.Id)); return PublishStatusType.FailedHasExpired; } - else if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) + + if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) { Logger.Info( string.Format( @@ -2306,8 +2352,10 @@ namespace Umbraco.Core.Services private IContentType FindContentTypeByAlias(string contentTypeAlias) { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + var query = Query.Builder.Where(x => x.Alias == contentTypeAlias); var types = repository.GetByQuery(query); @@ -2321,7 +2369,6 @@ namespace Umbraco.Core.Services if (contentType == null) throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null", contentTypeAlias)); - return contentType; } } @@ -2372,7 +2419,7 @@ namespace Umbraco.Core.Services #region Event Handlers /// /// Occurs before Delete - /// + /// public static event TypedEventHandler> Deleting; /// @@ -2382,7 +2429,7 @@ namespace Umbraco.Core.Services /// /// Occurs before Delete Versions - /// + /// public static event TypedEventHandler DeletingVersions; /// diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index 4919a81217..d2ce1de2f8 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -1,5 +1,10 @@ +using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Repositories; namespace Umbraco.Core.Services { @@ -34,5 +39,145 @@ namespace Umbraco.Core.Services { return mediaService.CountChildren(Constants.System.RecycleBinMedia) > 0; } + + /// + /// Used for the DeleteContentOfType(s) methods to find content items to be deleted based on the content type ids passed in + /// + /// + /// + /// The content type ids being deleted + /// + /// + /// Returns a dictionary (path, TContent) of the root items discovered in the data set of items to be deleted, this can then be used + /// to search for content that needs to be trashed as a result of this. + /// + /// + /// The content items to be deleted + /// + /// + /// An internal extension method used for the DeleteContentOfTypes (DeleteMediaOfTypes) methods so that logic can be shared to avoid code duplication. + /// + internal static IEnumerable TrackDeletionsForDeleteContentOfTypes(this IContentServiceBase contentService, + IEnumerable contentTypeIds, + IRepositoryVersionable repository, + out IDictionary rootItems) + where TContent: IContentBase + { + var contentToDelete = new List(); + + //track the 'root' items of the collection of nodes discovered to delete, we need to use + //these items to lookup descendants that are not of this doc type so they can be transfered + //to the recycle bin + rootItems = new Dictionary(); + + var query = Query.Builder.Where(x => contentTypeIds.Contains(x.ContentTypeId)); + + //TODO: What about content that has the contenttype as part of its composition? + + long pageIndex = 0; + const int pageSize = 10000; + int currentPageSize; + do + { + long total; + + //start at the highest level + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "umbracoNode.level", Direction.Ascending, true).ToArray(); + + // need to store decendants count before filtering, in order for loop to work correctly + currentPageSize = contents.Length; + + //loop through the items, check if if the item exists already in the hierarchy of items tracked + //and if not, we need to add it as a 'root' item to be used to lookup later + foreach (var content in contents) + { + var pathParts = content.Path.Split(','); + var found = false; + + for (int i = 1; i < pathParts.Length; i++) + { + var currPath = "-1," + string.Join(",", Enumerable.Range(1, i).Select(x => pathParts[x])); + if (rootItems.Keys.Contains(currPath)) + { + //this content item's ancestor already exists in the root collection + found = true; + break; + } + } + + if (found == false) + { + rootItems[content.Path] = content; + } + + //track content for deletion + contentToDelete.Add(content); + } + + pageIndex++; + } while (currentPageSize == pageSize); + + return contentToDelete; + } + + /// + /// Used for the DeleteContentOfType(s) methods to find content items to be trashed based on the content type ids passed in + /// + /// + /// + /// The content type ids being deleted + /// + /// + /// + /// The content items to be trashed + /// + /// + /// An internal extension method used for the DeleteContentOfTypes (DeleteMediaOfTypes) methods so that logic can be shared to avoid code duplication. + /// + internal static IEnumerable TrackTrashedForDeleteContentOfTypes(this IContentServiceBase contentService, + IEnumerable contentTypeIds, + IDictionary rootItems, + IRepositoryVersionable repository) + where TContent : IContentBase + { + const int pageSize = 10000; + var contentToRecycle = new List(); + + //iterate over the root items found in the collection to be deleted, then discover which descendant items + //need to be moved to the recycle bin + foreach (var content in rootItems) + { + //Look for children of current content and move that to trash before the current content is deleted + var c = content; + var pathMatch = string.Format("{0},", c.Value.Path); + var descendantQuery = Query.Builder.Where(x => x.Path.StartsWith(pathMatch)); + + long pageIndex = 0; + int currentPageSize; + + do + { + long total; + + var descendants = repository.GetPagedResultsByQuery(descendantQuery, pageIndex, pageSize, out total, "umbracoNode.id", Direction.Ascending, true).ToArray(); + + foreach (var d in descendants) + { + //track for recycling if this item is not of a contenttype that is being deleted + if (contentTypeIds.Contains(d.ContentTypeId) == false) + { + contentToRecycle.Add(d); + } + } + + // need to store decendants count before filtering, in order for loop to work correctly + currentPageSize = descendants.Length; + + pageIndex++; + } while (currentPageSize == pageSize); + } + + return contentToRecycle; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 31e86cd128..1b6735153d 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -1,22 +1,15 @@ using System; using System.Collections.Generic; -using System.Data; -using System.Diagnostics; using System.Linq; using System.Text; -using System.Xml.Linq; using System.Threading; -using AutoMapper; -using Umbraco.Core.Auditing; using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services @@ -26,14 +19,15 @@ namespace Umbraco.Core.Services /// public class ContentTypeService : ContentTypeServiceBase, IContentTypeService { - private readonly IContentService _contentService; + 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. //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); - public ContentTypeService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IContentService contentService, IMediaService mediaService) + public ContentTypeService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IContentService contentService, + IMediaService mediaService) : base(provider, repositoryFactory, logger, eventMessagesFactory) { if (contentService == null) throw new ArgumentNullException("contentService"); @@ -47,9 +41,10 @@ namespace Umbraco.Core.Services 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)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid); + try { var container = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid) @@ -59,23 +54,23 @@ namespace Umbraco.Core.Services CreatorId = userId }; - if (SavingContentTypeContainer.IsRaisedEventCancelled( - new SaveEventArgs(container, evtMsgs), - this)) + if (uow.Events.DispatchCancelable(SavingContentTypeContainer, this, new SaveEventArgs(container, evtMsgs))) { + uow.Commit(); return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); } repo.AddOrUpdate(container); uow.Commit(); - SavedContentTypeContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + uow.Events.Dispatch(SavedContentTypeContainer, this, new SaveEventArgs(container, evtMsgs), "SavedContentTypeContainer"); //TODO: Audit trail ? return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); } catch (Exception ex) { + uow.Commit(); return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex); } } @@ -84,9 +79,10 @@ namespace Umbraco.Core.Services 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)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid); + try { var container = new EntityContainer(Constants.ObjectTypes.MediaTypeGuid) @@ -96,23 +92,23 @@ namespace Umbraco.Core.Services CreatorId = userId }; - if (SavingMediaTypeContainer.IsRaisedEventCancelled( - new SaveEventArgs(container, evtMsgs), - this)) + if (uow.Events.DispatchCancelable(SavingMediaTypeContainer, this, new SaveEventArgs(container, evtMsgs))) { + uow.Commit(); return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); } repo.AddOrUpdate(container); uow.Commit(); - SavedMediaTypeContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + uow.Events.Dispatch(SavedMediaTypeContainer, this, new SaveEventArgs(container, evtMsgs), "SavedMediaTypeContainer"); //TODO: Audit trail ? return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); } catch (Exception ex) { + uow.Commit(); return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex); } } @@ -122,14 +118,16 @@ namespace Umbraco.Core.Services { return SaveContainer( SavingContentTypeContainer, SavedContentTypeContainer, - container, Constants.ObjectTypes.DocumentTypeContainerGuid, "document type", userId); + container, Constants.ObjectTypes.DocumentTypeContainerGuid, "document type", + "SavedContentTypeContainer", userId); } public Attempt SaveMediaTypeContainer(EntityContainer container, int userId = 0) { return SaveContainer( SavingMediaTypeContainer, SavedMediaTypeContainer, - container, Constants.ObjectTypes.MediaTypeContainerGuid, "media type", userId); + container, Constants.ObjectTypes.MediaTypeContainerGuid, "media type", + "SavedMediaTypeContainer", userId); } private Attempt SaveContainer( @@ -137,7 +135,9 @@ namespace Umbraco.Core.Services TypedEventHandler> savedEvent, EntityContainer container, Guid containerObjectType, - string objectTypeName, int userId) + string objectTypeName, + string savedEventName, + int userId) { var evtMsgs = EventMessagesFactory.Get(); @@ -153,22 +153,20 @@ namespace Umbraco.Core.Services return OperationStatus.Exception(evtMsgs, ex); } - if (savingEvent.IsRaisedEventCancelled( - new SaveEventArgs(container, evtMsgs), - this)) + using (var uow = UowProvider.GetUnitOfWork()) { - return OperationStatus.Cancelled(evtMsgs); - } + if (uow.Events.DispatchCancelable(savingEvent, this, new SaveEventArgs(container, evtMsgs))) + { + uow.Commit(); + return OperationStatus.Cancelled(evtMsgs); + } - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType)) - { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType); repo.AddOrUpdate(container); uow.Commit(); + uow.Events.Dispatch(savedEvent, this, new SaveEventArgs(container, evtMsgs), savedEventName); } - savedEvent.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); - //TODO: Audit trail ? return OperationStatus.Success(evtMsgs); @@ -186,35 +184,34 @@ namespace Umbraco.Core.Services private EntityContainer GetContainer(int containerId, Guid containerObjectType) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var container = repo.Get(containerId); - return container; + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType); + return repo.Get(containerId); } } public IEnumerable GetMediaTypeContainers(int[] containerIds) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid); return repo.GetAll(containerIds); } } public IEnumerable GetMediaTypeContainers(string name, int level) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid); return repo.Get(name, level); } } public IEnumerable GetMediaTypeContainers(IMediaType mediaType) { - var ancestorIds = mediaType.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + var ancestorIds = mediaType.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) .Select(x => { var asInt = x.TryConvertTo(); @@ -234,9 +231,9 @@ namespace Umbraco.Core.Services public IEnumerable GetContentTypeContainers(int[] containerIds) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid); return repo.GetAll(containerIds); } } @@ -263,19 +260,18 @@ namespace Umbraco.Core.Services private EntityContainer GetContainer(Guid containerId, Guid containerObjectType) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var container = repo.Get(containerId); - return container; + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType); + return repo.Get(containerId); } } public IEnumerable GetContentTypeContainers(string name, int level) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid); return repo.Get(name, level); } } @@ -283,23 +279,26 @@ namespace Umbraco.Core.Services 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)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid); var container = repo.Get(containerId); - if (container == null) return OperationStatus.NoOperation(evtMsgs); - - if (DeletingContentTypeContainer.IsRaisedEventCancelled( - new DeleteEventArgs(container, evtMsgs), - this)) + if (container == null) { + uow.Commit(); + return OperationStatus.NoOperation(evtMsgs); + } + + if (uow.Events.DispatchCancelable(DeletingContentTypeContainer, this, new DeleteEventArgs(container, evtMsgs))) + { + uow.Commit(); return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); } repo.Delete(container); uow.Commit(); - DeletedContentTypeContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); + uow.Events.Dispatch(DeletedContentTypeContainer, this, new DeleteEventArgs(container, evtMsgs), "DeletedContentTypeContainer"); return OperationStatus.Success(evtMsgs); //TODO: Audit trail ? @@ -309,23 +308,27 @@ namespace Umbraco.Core.Services 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)) + using (var uow = UowProvider.GetUnitOfWork()) { - var container = repo.Get(containerId); - if (container == null) return OperationStatus.NoOperation(evtMsgs); + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid); - if (DeletingMediaTypeContainer.IsRaisedEventCancelled( - new DeleteEventArgs(container, evtMsgs), - this)) + var container = repo.Get(containerId); + if (container == null) { + uow.Commit(); + return OperationStatus.NoOperation(evtMsgs); + } + + if (uow.Events.DispatchCancelable(DeletingMediaTypeContainer, this, new DeleteEventArgs(container, evtMsgs))) + { + uow.Commit(); return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); } repo.Delete(container); uow.Commit(); - DeletedMediaTypeContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); + uow.Events.Dispatch(DeletedMediaTypeContainer, this, new DeleteEventArgs(container, evtMsgs)); return OperationStatus.Success(evtMsgs); //TODO: Audit trail ? @@ -340,8 +343,9 @@ namespace Umbraco.Core.Services /// public IEnumerable GetAllPropertyTypeAliases() { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); return repository.GetAllPropertyTypeAliases(); } } @@ -356,8 +360,9 @@ namespace Umbraco.Core.Services /// public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); return repository.GetAllContentTypeAliases(objectTypes); } } @@ -421,7 +426,7 @@ namespace Umbraco.Core.Services clone.Name = name; - var compositionAliases = clone.CompositionAliases().Except(new[] { alias }).ToList(); + var compositionAliases = clone.CompositionAliases().Except(new[] {alias}).ToList(); //remove all composition that is not it's current alias foreach (var a in compositionAliases) { @@ -452,8 +457,9 @@ namespace Umbraco.Core.Services /// public IContentType GetContentType(int id) { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); return repository.Get(id); } } @@ -465,8 +471,9 @@ namespace Umbraco.Core.Services /// public IContentType GetContentType(string alias) { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); return repository.Get(alias); } } @@ -478,8 +485,9 @@ namespace Umbraco.Core.Services /// public IContentType GetContentType(Guid id) { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); return repository.Get(id); } } @@ -491,8 +499,9 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetAllContentTypes(params int[] ids) { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); return repository.GetAll(ids); } } @@ -504,8 +513,9 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetAllContentTypes(IEnumerable ids) { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); return repository.GetAll(ids.ToArray()); } } @@ -517,11 +527,11 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetContentTypeChildren(int id) { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); var query = Query.Builder.Where(x => x.ParentId == id); - var contentTypes = repository.GetByQuery(query); - return contentTypes; + return repository.GetByQuery(query); } } @@ -532,13 +542,14 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetContentTypeChildren(Guid id) { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + var found = GetContentType(id); if (found == null) return Enumerable.Empty(); var query = Query.Builder.Where(x => x.ParentId == found.Id); - var contentTypes = repository.GetByQuery(query); - return contentTypes; + return repository.GetByQuery(query); } } @@ -549,11 +560,12 @@ namespace Umbraco.Core.Services /// True if the content type has any children otherwise False public bool HasChildren(int id) { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + var query = Query.Builder.Where(x => x.ParentId == id); - int count = repository.Count(query); - return count > 0; + return repository.Count(query) > 0; } } @@ -564,16 +576,38 @@ namespace Umbraco.Core.Services /// True if the content type has any children otherwise False public bool HasChildren(Guid id) { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + var found = GetContentType(id); if (found == null) return false; var query = Query.Builder.Where(x => x.ParentId == found.Id); - int count = repository.Count(query); - return count > 0; + return repository.Count(query) > 0; } } + public override IEnumerable GetDescendants(IContentTypeBase contentType) + { + var ctype = contentType as IContentType; + if (ctype != null) return GetDescendants(ctype); + var mtype = contentType as IMediaType; + if (mtype != null) return GetDescendants(mtype); + return Enumerable.Empty(); + } + + public IEnumerable GetDescendants(IContentType contentType) + { + return GetContentTypeChildren(contentType.Id) + .SelectRecursive(type => GetContentTypeChildren(type.Id)); + } + + public IEnumerable GetDescendants(IMediaType contentType) + { + return GetMediaTypeChildren(contentType.Id) + .SelectRecursive(type => GetMediaTypeChildren(type.Id)); + } + /// /// This is called after an IContentType is saved and is used to update the content xml structures in the database /// if they are required to be updated. @@ -581,51 +615,50 @@ namespace Umbraco.Core.Services /// A tuple of a content type and a boolean indicating if it is new (HasIdentity was false before committing) private void UpdateContentXmlStructure(params IContentTypeBase[] contentTypes) { - var toUpdate = GetContentTypesForXmlUpdates(contentTypes).ToArray(); - if (toUpdate.Any()) + if (toUpdate.Any() == false) return; + + var firstType = toUpdate.First(); + //if it is a content type then call the rebuilding methods or content + if (firstType is IContentType) { - var firstType = toUpdate.First(); - //if it is a content type then call the rebuilding methods or content - if (firstType is IContentType) + var typedContentService = _contentService as ContentService; + if (typedContentService != null) { - var typedContentService = _contentService as ContentService; - if (typedContentService != null) - { - typedContentService.RePublishAll(toUpdate.Select(x => x.Id).ToArray()); - } - else - { - //this should never occur, the content service should always be typed but we'll check anyways. - _contentService.RePublishAll(); - } + typedContentService.RePublishAll(toUpdate.Select(x => x.Id).ToArray()); } - else if (firstType is IMediaType) + else { - //if it is a media type then call the rebuilding methods for media - var typedContentService = _mediaService as MediaService; - if (typedContentService != null) - { - typedContentService.RebuildXmlStructures(toUpdate.Select(x => x.Id).ToArray()); - } + //this should never occur, the content service should always be typed but we'll check anyways. + _contentService.RePublishAll(); + } + } + else if (firstType is IMediaType) + { + //if it is a media type then call the rebuilding methods for media + var typedContentService = _mediaService as MediaService; + if (typedContentService != null) + { + typedContentService.RebuildXmlStructures(toUpdate.Select(x => x.Id).ToArray()); } } - } public int CountContentTypes() { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); return repository.Count(Query.Builder); } } public int CountMediaTypes() { - using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); return repository.Count(Query.Builder); } } @@ -680,7 +713,7 @@ namespace Umbraco.Core.Services var comparer = new DelegateEqualityComparer((x, y) => x.Id == y.Id, x => x.Id); var dependencies = new HashSet(compositions, comparer); var stack = new Stack(); - indirectReferences.ForEach(stack.Push);//Push indirect references to a stack, so we can add recursively + indirectReferences.ForEach(stack.Push); //Push indirect references to a stack, so we can add recursively while (stack.Count > 0) { var indirectReference = stack.Pop(); @@ -719,25 +752,39 @@ 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)) - return; - using (new WriteLock(Locker)) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(SavingContentType, this, new SaveEventArgs(contentType))) + { + uow.Commit(); + return; + } + + if (string.IsNullOrWhiteSpace(contentType.Name)) + { + throw new ArgumentException("Cannot save content type with empty name."); + } + + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + ValidateLocked(contentType); // throws if invalid contentType.CreatorId = userId; + if (contentType.Description == string.Empty) + contentType.Description = null; repository.AddOrUpdate(contentType); uow.Commit(); - } - UpdateContentXmlStructure(contentType); + UpdateContentXmlStructure(contentType); + + uow.Events.Dispatch(SavedContentType, this, new SaveEventArgs(contentType, false)); + + Audit(uow, AuditType.Save, "Save ContentType performed by user", userId, contentType.Id); + uow.Commit(); + } } - SavedContentType.RaiseEvent(new SaveEventArgs(contentType, false), this); - Audit(AuditType.Save, string.Format("Save ContentType performed by user"), userId, contentType.Id); } /// @@ -749,15 +796,18 @@ namespace Umbraco.Core.Services { var asArray = contentTypes.ToArray(); - if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) - return; - using (new WriteLock(Locker)) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { - // all-or-nothing, validate them all first + if (uow.Events.DispatchCancelable(SavingContentType, this, new SaveEventArgs(asArray))) + { + uow.Commit(); + return; + } + + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + foreach (var contentType in asArray) { ValidateLocked(contentType); // throws if invalid @@ -765,17 +815,22 @@ namespace Umbraco.Core.Services foreach (var contentType in asArray) { contentType.CreatorId = userId; + if (contentType.Description == string.Empty) + contentType.Description = null; repository.AddOrUpdate(contentType); } //save it all in one go uow.Commit(); - } - UpdateContentXmlStructure(asArray.Cast().ToArray()); + UpdateContentXmlStructure(asArray.Cast().ToArray()); + + uow.Events.Dispatch(SavedContentType, this, new SaveEventArgs(asArray, false)); + + Audit(uow, AuditType.Save, "Save ContentTypes performed by user", userId, -1); + uow.Commit(); + } } - SavedContentType.RaiseEvent(new SaveEventArgs(asArray, false), this); - Audit(AuditType.Save, string.Format("Save ContentTypes performed by user"), userId, -1); } /// @@ -786,35 +841,31 @@ namespace Umbraco.Core.Services /// 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)) - return; - using (new WriteLock(Locker)) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { - //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. - - var deletedContentTypes = new List() {contentType}; - deletedContentTypes.AddRange(contentType.Descendants().OfType()); - - foreach (var deletedContentType in deletedContentTypes) + if (uow.Events.DispatchCancelable(DeletingContentType, this, new DeleteEventArgs(contentType))) { - _contentService.DeleteContentOfType(deletedContentType.Id); + uow.Commit(); + return; } + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + + //If we are deleting this content type, we are also deleting it's descendents! + var deletedContentTypes = new List {contentType}; + deletedContentTypes.AddRange(GetDescendants(contentType)); + + _contentService.DeleteContentOfTypes(deletedContentTypes.Select(x => x.Id), userId); + repository.Delete(contentType); + + uow.Events.Dispatch(DeletedContentType, this, new DeleteEventArgs(deletedContentTypes.DistinctBy(x => x.Id), false)); + + Audit(uow, AuditType.Delete, string.Format("Delete ContentType performed by user"), userId, contentType.Id); uow.Commit(); - - DeletedContentType.RaiseEvent(new DeleteEventArgs(deletedContentTypes.DistinctBy(x => x.Id), false), this); } - - Audit(AuditType.Delete, string.Format("Delete ContentType performed by user"), userId, contentType.Id); } } @@ -830,38 +881,37 @@ namespace Umbraco.Core.Services { var asArray = contentTypes.ToArray(); - if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) - return; - using (new WriteLock(Locker)) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { - var deletedContentTypes = new List(); - deletedContentTypes.AddRange(asArray); + if (uow.Events.DispatchCancelable(DeletingContentType, this, new DeleteEventArgs(asArray))) + { + uow.Commit(); + return; + } + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + + //If we are deleting this content type, we are also deleting it's descendents! + var deletedContentTypes = new List(asArray); foreach (var contentType in asArray) { - deletedContentTypes.AddRange(contentType.Descendants().OfType()); + deletedContentTypes.AddRange(GetDescendants(contentType)); } - foreach (var deletedContentType in deletedContentTypes) - { - _contentService.DeleteContentOfType(deletedContentType.Id); - } + _contentService.DeleteContentOfTypes(deletedContentTypes.Select(x => x.Id), userId); foreach (var contentType in asArray) { repository.Delete(contentType); } + uow.Events.Dispatch(DeletedContentType, this, new DeleteEventArgs(deletedContentTypes.DistinctBy(x => x.Id), false)); + + Audit(uow, AuditType.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1); uow.Commit(); - - DeletedContentType.RaiseEvent(new DeleteEventArgs(deletedContentTypes.DistinctBy(x => x.Id), false), this); } - - Audit(AuditType.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1); } } @@ -872,8 +922,9 @@ namespace Umbraco.Core.Services /// public IMediaType GetMediaType(int id) { - using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); return repository.Get(id); } } @@ -885,8 +936,9 @@ namespace Umbraco.Core.Services /// public IMediaType GetMediaType(string alias) { - using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); return repository.Get(alias); } } @@ -898,8 +950,9 @@ namespace Umbraco.Core.Services /// public IMediaType GetMediaType(Guid id) { - using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); return repository.Get(id); } } @@ -911,8 +964,9 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetAllMediaTypes(params int[] ids) { - using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); return repository.GetAll(ids); } } @@ -924,8 +978,9 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetAllMediaTypes(IEnumerable ids) { - using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); return repository.GetAll(ids.ToArray()); } } @@ -937,11 +992,11 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetMediaTypeChildren(int id) { - using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); var query = Query.Builder.Where(x => x.ParentId == id); - var contentTypes = repository.GetByQuery(query); - return contentTypes; + return repository.GetByQuery(query); } } @@ -952,13 +1007,13 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetMediaTypeChildren(Guid id) { - using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); var found = GetMediaType(id); if (found == null) return Enumerable.Empty(); var query = Query.Builder.Where(x => x.ParentId == found.Id); - var contentTypes = repository.GetByQuery(query); - return contentTypes; + return repository.GetByQuery(query); } } @@ -969,11 +1024,11 @@ namespace Umbraco.Core.Services /// True if the media type has any children otherwise False public bool MediaTypeHasChildren(int id) { - using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); var query = Query.Builder.Where(x => x.ParentId == id); - int count = repository.Count(query); - return count > 0; + return repository.Count(query) > 0; } } @@ -984,13 +1039,13 @@ namespace Umbraco.Core.Services /// True if the media type has any children otherwise False public bool MediaTypeHasChildren(Guid id) { - using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); var found = GetMediaType(id); if (found == null) return false; var query = Query.Builder.Where(x => x.ParentId == found.Id); - int count = repository.Count(query); - return count > 0; + return repository.Count(query) > 0; } } @@ -998,20 +1053,17 @@ namespace Umbraco.Core.Services { var evtMsgs = EventMessagesFactory.Get(); - if (MovingMediaType.IsRaisedEventCancelled( - new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId)), - this)) - { - return Attempt.Fail( - new OperationStatus( - MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); - } - var moveInfo = new List>(); - var uow = UowProvider.GetUnitOfWork(); - using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) - using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(MovingMediaType, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId)))) + { + uow.Commit(); + return Attempt.Fail(new OperationStatus(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid); + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); try { EntityContainer container = null; @@ -1025,14 +1077,13 @@ namespace Umbraco.Core.Services } catch (DataOperationException ex) { - return Attempt.Fail( - new OperationStatus(ex.Operation, evtMsgs)); + uow.Commit(); + return Attempt.Fail(new OperationStatus(ex.Operation, evtMsgs)); } uow.Commit(); + uow.Events.Dispatch(MovedMediaType, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray())); } - MovedMediaType.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - return Attempt.Succeed( new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); } @@ -1041,20 +1092,17 @@ namespace Umbraco.Core.Services { var evtMsgs = EventMessagesFactory.Get(); - if (MovingContentType.IsRaisedEventCancelled( - new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId)), - this)) - { - return Attempt.Fail( - new OperationStatus( - MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); - } - var moveInfo = new List>(); - var uow = UowProvider.GetUnitOfWork(); - using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) - using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(MovingContentType, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId)))) + { + uow.Commit(); + return Attempt.Fail(new OperationStatus(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid); + var repository = RepositoryFactory.CreateContentTypeRepository(uow); try { EntityContainer container = null; @@ -1068,14 +1116,13 @@ namespace Umbraco.Core.Services } catch (DataOperationException ex) { - return Attempt.Fail( - new OperationStatus(ex.Operation, evtMsgs)); + uow.Commit(); + return Attempt.Fail(new OperationStatus(ex.Operation, evtMsgs)); } uow.Commit(); + uow.Events.Dispatch(MovedContentType, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray())); } - MovedContentType.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - return Attempt.Succeed( new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); } @@ -1085,10 +1132,11 @@ namespace Umbraco.Core.Services 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)) + using (var uow = UowProvider.GetUnitOfWork()) { + var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid); + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); + try { if (containerId > 0) @@ -1115,6 +1163,7 @@ namespace Umbraco.Core.Services } catch (DataOperationException ex) { + uow.Commit(); return Attempt.Fail(new OperationStatus(null, ex.Operation, evtMsgs)); } uow.Commit(); @@ -1128,10 +1177,11 @@ namespace Umbraco.Core.Services 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)) + using (var uow = UowProvider.GetUnitOfWork()) { + var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid); + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + try { if (containerId > 0) @@ -1158,6 +1208,7 @@ namespace Umbraco.Core.Services } catch (DataOperationException ex) { + uow.Commit(); return Attempt.Fail(new OperationStatus(null, ex.Operation, evtMsgs)); } uow.Commit(); @@ -1173,26 +1224,33 @@ 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)) - return; - using (new WriteLock(Locker)) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(SavingMediaType, this, new SaveEventArgs(mediaType))) + { + uow.Commit(); + return; + } + + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); + ValidateLocked(mediaType); // throws if invalid mediaType.CreatorId = userId; + if (mediaType.Description == string.Empty) + mediaType.Description = null; repository.AddOrUpdate(mediaType); uow.Commit(); + UpdateContentXmlStructure(mediaType); + + uow.Events.Dispatch(SavedMediaType, this, new SaveEventArgs(mediaType, false)); + + Audit(uow, AuditType.Save, "Save MediaType performed by user", userId, mediaType.Id); + uow.Commit(); } - - UpdateContentXmlStructure(mediaType); } - - SavedMediaType.RaiseEvent(new SaveEventArgs(mediaType, false), this); - Audit(AuditType.Save, string.Format("Save MediaType performed by user"), userId, mediaType.Id); } /// @@ -1204,15 +1262,18 @@ namespace Umbraco.Core.Services { var asArray = mediaTypes.ToArray(); - if (SavingMediaType.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) - return; - using (new WriteLock(Locker)) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { - // all-or-nothing, validate them all first + if (uow.Events.DispatchCancelable(SavingMediaType, this, new SaveEventArgs(asArray))) + { + uow.Commit(); + return; + } + + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); + foreach (var mediaType in asArray) { ValidateLocked(mediaType); // throws if invalid @@ -1220,18 +1281,22 @@ namespace Umbraco.Core.Services foreach (var mediaType in asArray) { mediaType.CreatorId = userId; + if (mediaType.Description == string.Empty) + mediaType.Description = null; repository.AddOrUpdate(mediaType); } //save it all in one go uow.Commit(); + + UpdateContentXmlStructure(asArray.Cast().ToArray()); + + uow.Events.Dispatch(SavedMediaType, this, new SaveEventArgs(asArray, false)); + + Audit(uow, AuditType.Save, "Save MediaTypes performed by user", userId, -1); + uow.Commit(); } - - UpdateContentXmlStructure(asArray.Cast().ToArray()); } - - SavedMediaType.RaiseEvent(new SaveEventArgs(asArray, false), this); - Audit(AuditType.Save, string.Format("Save MediaTypes performed by user"), userId, -1); } /// @@ -1242,25 +1307,33 @@ 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)) - return; + //TODO: Share all of this logic with the Delete IContentType methods, no need for code duplication + using (new WriteLock(Locker)) { - _mediaService.DeleteMediaOfType(mediaType.Id, userId); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { - var deletedMediaTypes = new List() {mediaType}; - deletedMediaTypes.AddRange(mediaType.Descendants().OfType()); + if (uow.Events.DispatchCancelable(DeletingMediaType, this, new DeleteEventArgs(mediaType))) + { + uow.Commit(); + return; + } + + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); + + //If we are deleting this content type, we are also deleting it's descendents! + var deletedMediaTypes = new List { mediaType }; + deletedMediaTypes.AddRange(GetDescendants(mediaType)); + + _mediaService.DeleteMediaOfTypes(deletedMediaTypes.Select(x => x.Id), userId); repository.Delete(mediaType); + + uow.Events.Dispatch(DeletedMediaType, this, new DeleteEventArgs(deletedMediaTypes.DistinctBy(x => x.Id), false)); + + Audit(uow, AuditType.Delete, "Delete MediaType performed by user", userId, mediaType.Id); uow.Commit(); - - DeletedMediaType.RaiseEvent(new DeleteEventArgs(deletedMediaTypes.DistinctBy(x => x.Id), false), this); } - - Audit(AuditType.Delete, string.Format("Delete MediaType performed by user"), userId, mediaType.Id); } } @@ -1272,34 +1345,41 @@ namespace Umbraco.Core.Services /// Deleting a will delete all the objects based on this public void Delete(IEnumerable mediaTypes, int userId = 0) { + //TODO: Share all of this logic with the Delete IContentType methods, no need for code duplication + var asArray = mediaTypes.ToArray(); - if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) - return; using (new WriteLock(Locker)) { - foreach (var mediaType in asArray) + using (var uow = UowProvider.GetUnitOfWork()) { - _mediaService.DeleteMediaOfType(mediaType.Id); - } + if (uow.Events.DispatchCancelable(DeletingMediaType, this, new DeleteEventArgs(asArray))) + { + uow.Commit(); + return; + } - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) - { - var deletedMediaTypes = new List(); - deletedMediaTypes.AddRange(asArray); + var repository = RepositoryFactory.CreateMediaTypeRepository(uow); + + //If we are deleting this content type, we are also deleting it's descendents! + var deletedMediaTypes = new List(asArray); + foreach (var mediaType in asArray) + { + deletedMediaTypes.AddRange(GetDescendants(mediaType)); + } + + _mediaService.DeleteMediaOfTypes(deletedMediaTypes.Select(x => x.Id), userId); foreach (var mediaType in asArray) { - deletedMediaTypes.AddRange(mediaType.Descendants().OfType()); repository.Delete(mediaType); } + + uow.Events.Dispatch(DeletedMediaType, this, new DeleteEventArgs(deletedMediaTypes.DistinctBy(x => x.Id), false)); + + Audit(uow, AuditType.Delete, "Delete MediaTypes performed by user", userId, -1); uow.Commit(); - - DeletedMediaType.RaiseEvent(new DeleteEventArgs(deletedMediaTypes.DistinctBy(x => x.Id), false), this); } - - Audit(AuditType.Delete, string.Format("Delete MediaTypes performed by user"), userId, -1); } } @@ -1341,8 +1421,8 @@ namespace Umbraco.Core.Services string safeAlias = contentType.Alias.ToUmbracoAlias(); if (safeAlias != null) { - strictSchemaBuilder.AppendLine(String.Format("", safeAlias)); - strictSchemaBuilder.AppendLine(String.Format("", safeAlias)); + strictSchemaBuilder.AppendLine(string.Format("", safeAlias)); + strictSchemaBuilder.AppendLine(string.Format("", safeAlias)); } } @@ -1358,14 +1438,10 @@ namespace Umbraco.Core.Services return dtd.ToString(); } - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(IScopeUnitOfWork uow, AuditType type, string message, int userId, int objectId) { - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Commit(); - } + var auditRepo = RepositoryFactory.CreateAuditRepository(uow); + auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); } #region Event Handlers @@ -1385,40 +1461,40 @@ namespace Umbraco.Core.Services /// public static event TypedEventHandler> DeletingContentType; - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> DeletedContentType; + /// + /// Occurs after Delete + /// + public static event TypedEventHandler> DeletedContentType; - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> DeletingMediaType; + /// + /// Occurs before Delete + /// + public static event TypedEventHandler> DeletingMediaType; - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> DeletedMediaType; + /// + /// Occurs after Delete + /// + public static event TypedEventHandler> DeletedMediaType; /// /// Occurs before Save /// - public static event TypedEventHandler> SavingContentType; + public static event TypedEventHandler> SavingContentType; /// /// Occurs after Save /// - public static event TypedEventHandler> SavedContentType; + public static event TypedEventHandler> SavedContentType; - /// - /// Occurs before Save - /// - public static event TypedEventHandler> SavingMediaType; + /// + /// Occurs before Save + /// + public static event TypedEventHandler> SavingMediaType; - /// - /// Occurs after Save - /// - public static event TypedEventHandler> SavedMediaType; + /// + /// Occurs after Save + /// + public static event TypedEventHandler> SavedMediaType; /// /// Occurs before Move diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs index df29012b90..c2dfd687dd 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs @@ -5,17 +5,17 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { - public class ContentTypeServiceBase : RepositoryService + public class ContentTypeServiceBase : ScopeRepositoryService { public ContentTypeServiceBase(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, repositoryFactory, logger, eventMessagesFactory) - { - } - + { } + /// /// This is called after an content type is saved and is used to update the content xml structures in the database /// if they are required to be updated. @@ -33,7 +33,7 @@ namespace Umbraco.Core.Services // - a content type changes it's alias OR // - if a content type has it's property removed OR // - if a content type has a property whose alias has changed - //here we need to check if the alias of the content type changed or if one of the properties was removed. + //here we need to check if the alias of the content type changed or if one of the properties was removed. var dirty = contentType as IRememberBeingDirty; if (dirty == null) continue; @@ -50,25 +50,45 @@ namespace Umbraco.Core.Services && (dirty.WasPropertyDirty("Alias") || dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved") || hasAnyPropertiesChangedAlias)) { //If the alias was changed then we only need to update the xml structures for content of the current content type. - //If a property was deleted or a property alias was changed then we need to update the xml structures for any + //If a property was deleted or a property alias was changed then we need to update the xml structures for any // content of the current content type and any of the content type's child content types. if (dirty.WasPropertyDirty("Alias") && dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved") == false && hasAnyPropertiesChangedAlias == false) { - //if only the alias changed then only update the current content type + //if only the alias changed then only update the current content type toUpdate.Add(contentType); } else { //if a property was deleted or alias changed, then update all content of the current content type - // and all of it's desscendant doc types. - toUpdate.AddRange(contentType.DescendantsAndSelf()); + // and all of it's desscendant doc types. + toUpdate.Add(contentType); + toUpdate.AddRange(GetDescendants(contentType)); } } } return toUpdate; + } + public virtual IEnumerable GetDescendants(IContentTypeBase contentType) + { + return Enumerable.Empty(); + } + + /// + /// Given the path of a content item, this will return true if the content item exists underneath a list view content item + /// + /// + /// + public bool HasContainerInPath(string contentPath) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + // can use same repo for both content and media + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + return repository.HasContainerInPath(contentPath); + } } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 2cdcfc76d5..c01b13136e 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; -using Umbraco.Core.Auditing; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -19,7 +17,7 @@ namespace Umbraco.Core.Services /// /// Represents the DataType Service, which is an easy access to operations involving /// - public class DataTypeService : RepositoryService, IDataTypeService + public class DataTypeService : ScopeRepositoryService, IDataTypeService { public DataTypeService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory) @@ -32,9 +30,10 @@ namespace Umbraco.Core.Services 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)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid); + try { var container = new EntityContainer(Constants.ObjectTypes.DataTypeGuid) @@ -44,17 +43,16 @@ namespace Umbraco.Core.Services CreatorId = userId }; - if (SavingContainer.IsRaisedEventCancelled( - new SaveEventArgs(container, evtMsgs), - this)) + if (uow.Events.DispatchCancelable(SavingContainer, this, new SaveEventArgs(container, evtMsgs))) { + uow.Commit(); return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); } repo.AddOrUpdate(container); uow.Commit(); - SavedContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + uow.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs)); //TODO: Audit trail ? return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); @@ -68,29 +66,27 @@ namespace Umbraco.Core.Services public EntityContainer GetContainer(int containerId) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var container = repo.Get(containerId); - return container; + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid); + return repo.Get(containerId); } } public EntityContainer GetContainer(Guid containerId) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - var container = repo.Get(containerId); - return container; + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid); + return repo.Get(containerId); } } public IEnumerable GetContainers(string name, int level) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid); return repo.Get(name, level); } } @@ -112,9 +108,9 @@ namespace Umbraco.Core.Services public IEnumerable GetContainers(int[] containerIds) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid); return repo.GetAll(containerIds); } } @@ -135,22 +131,17 @@ namespace Umbraco.Core.Services return OperationStatus.Exception(evtMsgs, ex); } - if (SavingContainer.IsRaisedEventCancelled( - new SaveEventArgs(container, evtMsgs), - this)) + using (var uow = UowProvider.GetUnitOfWork()) { - return OperationStatus.Cancelled(evtMsgs); - } + if (uow.Events.DispatchCancelable(SavingContainer, this, new SaveEventArgs(container, evtMsgs))) + return OperationStatus.Cancelled(evtMsgs); - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) - { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid); repo.AddOrUpdate(container); uow.Commit(); + uow.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs)); } - SavedContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); - //TODO: Audit trail ? return OperationStatus.Success(evtMsgs); @@ -159,23 +150,22 @@ namespace Umbraco.Core.Services 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)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid); var container = repo.Get(containerId); if (container == null) return OperationStatus.NoOperation(evtMsgs); - if (DeletingContainer.IsRaisedEventCancelled( - new DeleteEventArgs(container, evtMsgs), - this)) + if (uow.Events.DispatchCancelable(DeletingContainer, this, new DeleteEventArgs(container, evtMsgs))) { + uow.Commit(); return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); } repo.Delete(container); uow.Commit(); - DeletedContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); + uow.Events.Dispatch(DeletedContainer, this, new DeleteEventArgs(container, evtMsgs)); return OperationStatus.Success(evtMsgs); //TODO: Audit trail ? @@ -191,8 +181,9 @@ namespace Umbraco.Core.Services /// public IDataTypeDefinition GetDataTypeDefinitionByName(string name) { - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); return repository.GetByQuery(new Query().Where(x => x.Name == name)).FirstOrDefault(); } } @@ -204,8 +195,9 @@ namespace Umbraco.Core.Services /// public IDataTypeDefinition GetDataTypeDefinitionById(int id) { - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); return repository.Get(id); } } @@ -217,11 +209,12 @@ namespace Umbraco.Core.Services /// public IDataTypeDefinition GetDataTypeDefinitionById(Guid id) { - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); var query = Query.Builder.Where(x => x.Key == id); - var definitions = repository.GetByQuery(query); + var definitions = repository.GetByQuery(query); return definitions.FirstOrDefault(); } } @@ -245,12 +238,11 @@ namespace Umbraco.Core.Services /// Collection of objects with a matching contorl id public IEnumerable GetDataTypeDefinitionByPropertyEditorAlias(string propertyEditorAlias) { - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); var query = Query.Builder.Where(x => x.PropertyEditorAlias == propertyEditorAlias); - var definitions = repository.GetByQuery(query); - - return definitions; + return repository.GetByQuery(query); } } @@ -261,8 +253,9 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public IEnumerable GetAllDataTypeDefinitions(params int[] ids) { - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); return repository.GetAll(ids); } } @@ -274,17 +267,16 @@ namespace Umbraco.Core.Services /// An enumerable list of string values public IEnumerable GetPreValuesByDataTypeId(int id) { - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); + //now convert the collection to a string list var collection = repository.GetPreValuesCollectionByDataTypeId(id); //now convert the collection to a string list - var list = collection.FormatAsDictionary() - .Select(x => x.Value.Value) - .ToList(); - return list; + return collection.FormatAsDictionary().Select(x => x.Value.Value).ToList(); } } - + /// /// Returns the PreValueCollection for the specified data type /// @@ -292,8 +284,9 @@ namespace Umbraco.Core.Services /// public PreValueCollection GetPreValuesCollectionByDataTypeId(int id) { - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); return repository.GetPreValuesCollectionByDataTypeId(id); } } @@ -305,8 +298,9 @@ namespace Umbraco.Core.Services /// PreValue as a string public string GetPreValueAsString(int id) { - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); return repository.GetPreValueAsString(id); } } @@ -315,20 +309,18 @@ namespace Umbraco.Core.Services { var evtMsgs = EventMessagesFactory.Get(); - if (Moving.IsRaisedEventCancelled( - new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, parentId)), - this)) - { - return Attempt.Fail( - new OperationStatus( - MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); - } - var moveInfo = new List>(); - var uow = UowProvider.GetUnitOfWork(); - using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(Moving, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, parentId)))) + { + uow.Commit(); + return Attempt.Fail(new OperationStatus(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid); + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); + try { EntityContainer container = null; @@ -346,10 +338,9 @@ namespace Umbraco.Core.Services new OperationStatus(ex.Operation, evtMsgs)); } uow.Commit(); + uow.Events.Dispatch(Moved, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray())); } - Moved.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - return Attempt.Succeed( new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); } @@ -361,20 +352,29 @@ namespace Umbraco.Core.Services /// Id of the user issueing the save public void Save(IDataTypeDefinition dataTypeDefinition, int userId = 0) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(dataTypeDefinition), this)) - return; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinition))) + { + uow.Commit(); + return; + } + + if (string.IsNullOrWhiteSpace(dataTypeDefinition.Name)) + { + throw new ArgumentException("Cannot save datatype with empty name."); + } + + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); + dataTypeDefinition.CreatorId = userId; repository.AddOrUpdate(dataTypeDefinition); + + uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinition, false)); + + Audit(uow, AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); uow.Commit(); - - Saved.RaiseEvent(new SaveEventArgs(dataTypeDefinition, false), this); } - - Audit(AuditType.Save, string.Format("Save DataTypeDefinition performed by user"), userId, dataTypeDefinition.Id); } /// @@ -395,27 +395,31 @@ namespace Umbraco.Core.Services /// Boolean indicating whether or not to raise events public void Save(IEnumerable dataTypeDefinitions, int userId, bool raiseEvents) { - if (raiseEvents) + using (var uow = UowProvider.GetUnitOfWork()) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(dataTypeDefinitions), this)) - return; - } + if (raiseEvents) + { + if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinitions))) + { + uow.Commit(); + return; + } + } + + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow)) - { foreach (var dataTypeDefinition in dataTypeDefinitions) { dataTypeDefinition.CreatorId = userId; repository.AddOrUpdate(dataTypeDefinition); } - uow.Commit(); if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(dataTypeDefinitions, false), this); - } + uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinitions, false)); - Audit(AuditType.Save, string.Format("Save DataTypeDefinition performed by user"), userId, -1); + Audit(uow, AuditType.Save, string.Format("Save DataTypeDefinition performed by user"), userId, -1); + uow.Commit(); + } } /// @@ -430,26 +434,23 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork()) { - using (var transaction = uow.Database.GetTransaction()) + var sortOrderObj = + uow.Database.ExecuteScalar( + "SELECT max(sortorder) FROM cmsDataTypePreValues WHERE datatypeNodeId = @DataTypeId", new { DataTypeId = dataTypeId }); + int sortOrder; + if (sortOrderObj == null || int.TryParse(sortOrderObj.ToString(), out sortOrder) == false) { - var sortOrderObj = - uow.Database.ExecuteScalar( - "SELECT max(sortorder) FROM cmsDataTypePreValues WHERE datatypeNodeId = @DataTypeId", new { DataTypeId = dataTypeId }); - int sortOrder; - if (sortOrderObj == null || int.TryParse(sortOrderObj.ToString(), out sortOrder) == false) - { - sortOrder = 1; - } - - foreach (var value in values) - { - var dto = new DataTypePreValueDto { DataTypeNodeId = dataTypeId, Value = value, SortOrder = sortOrder }; - uow.Database.Insert(dto); - sortOrder++; - } - - transaction.Complete(); + sortOrder = 1; } + + foreach (var value in values) + { + var dto = new DataTypePreValueDto { DataTypeNodeId = dataTypeId, Value = value, SortOrder = sortOrder }; + uow.Database.Insert(dto); + sortOrder++; + } + + uow.Commit(); } } @@ -464,7 +465,7 @@ namespace Umbraco.Core.Services /// public void SavePreValues(int dataTypeId, IDictionary values) { - var dtd = this.GetDataTypeDefinitionById(dataTypeId); + var dtd = GetDataTypeDefinitionById(dataTypeId); if (dtd == null) { throw new InvalidOperationException("No data type found for id " + dataTypeId); @@ -485,9 +486,9 @@ namespace Umbraco.Core.Services { //TODO: Should we raise an event here since we are really saving values for the data type? - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); repository.AddOrUpdatePreValues(dataTypeDefinition, values); uow.Commit(); } @@ -501,16 +502,20 @@ namespace Umbraco.Core.Services /// public void SaveDataTypeAndPreValues(IDataTypeDefinition dataTypeDefinition, IDictionary values, int userId = 0) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(dataTypeDefinition), this)) - return; - - // if preValues contain the data type, override the data type definition accordingly - if (values != null && values.ContainsKey(Constants.PropertyEditors.PreValueKeys.DataValueType)) - dataTypeDefinition.DatabaseType = PropertyValueEditor.GetDatabaseType(values[Constants.PropertyEditors.PreValueKeys.DataValueType].Value); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinition))) + { + uow.Commit(); + return; + } + + // if preValues contain the data type, override the data type definition accordingly + if (values != null && values.ContainsKey(Constants.PropertyEditors.PreValueKeys.DataValueType)) + dataTypeDefinition.DatabaseType = PropertyValueEditor.GetDatabaseType(values[Constants.PropertyEditors.PreValueKeys.DataValueType].Value); + + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); + dataTypeDefinition.CreatorId = userId; //add/update the dtd @@ -519,12 +524,11 @@ namespace Umbraco.Core.Services //add/update the prevalues repository.AddOrUpdatePreValues(dataTypeDefinition, values); + Audit(uow, AuditType.Save, string.Format("Save DataTypeDefinition performed by user"), userId, dataTypeDefinition.Id); uow.Commit(); - Saved.RaiseEvent(new SaveEventArgs(dataTypeDefinition, false), this); + uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinition, false)); } - - Audit(AuditType.Save, string.Format("Save DataTypeDefinition performed by user"), userId, dataTypeDefinition.Id); } /// @@ -537,21 +541,24 @@ namespace Umbraco.Core.Services /// to delete /// Optional Id of the user issueing the deletion public void Delete(IDataTypeDefinition dataTypeDefinition, int userId = 0) - { - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(dataTypeDefinition), this)) - return; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow)) - { - repository.Delete(dataTypeDefinition); + { + using (var uow = UowProvider.GetUnitOfWork()) + { + if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(dataTypeDefinition))) + { + uow.Commit(); + return; + } - uow.Commit(); + var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow); - Deleted.RaiseEvent(new DeleteEventArgs(dataTypeDefinition, false), this); - } + repository.Delete(dataTypeDefinition); - Audit(AuditType.Delete, string.Format("Delete DataTypeDefinition performed by user"), userId, dataTypeDefinition.Id); + Audit(uow, AuditType.Delete, "Delete DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); + uow.Commit(); + + uow.Events.Dispatch(Deleted, this, new DeleteEventArgs(dataTypeDefinition, false)); + } } /// @@ -575,14 +582,10 @@ namespace Umbraco.Core.Services return DataTypesResolver.Current.DataTypes; } - private void Audit(AuditType type, string message, int userId, int objectId) + private void Audit(IScopeUnitOfWork uow, AuditType type, string message, int userId, int objectId) { - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Commit(); - } + var auditRepo = RepositoryFactory.CreateAuditRepository(uow); + auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); } #region Event Handlers @@ -622,7 +625,5 @@ namespace Umbraco.Core.Services /// public static event TypedEventHandler> Moved; #endregion - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs index 3ffcb92778..d5637d50be 100644 --- a/src/Umbraco.Core/Services/DomainService.cs +++ b/src/Umbraco.Core/Services/DomainService.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { - public class DomainService : RepositoryService, IDomainService + public class DomainService : ScopeRepositoryService, IDomainService { public DomainService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, repositoryFactory, logger, eventMessagesFactory) @@ -19,9 +19,9 @@ namespace Umbraco.Core.Services public bool Exists(string domainName) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateDomainRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repo = RepositoryFactory.CreateDomainRepository(uow); return repo.Exists(domainName); } } @@ -29,57 +29,59 @@ namespace Umbraco.Core.Services public Attempt Delete(IDomain domain) { var evtMsgs = EventMessagesFactory.Get(); - if (Deleting.IsRaisedEventCancelled( - new DeleteEventArgs(domain, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateDomainRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(domain, evtMsgs))) + { + uow.Commit(); + return OperationStatus.Cancelled(evtMsgs); + } + + var repository = RepositoryFactory.CreateDomainRepository(uow); repository.Delete(domain); - uow.Commit(); + uow.Commit(); + + var args = new DeleteEventArgs(domain, false, evtMsgs); + uow.Events.Dispatch(Deleted, this, args); + return OperationStatus.Success(evtMsgs); } - var args = new DeleteEventArgs(domain, false, evtMsgs); - Deleted.RaiseEvent(args, this); - return OperationStatus.Success(evtMsgs); + } public IDomain GetByName(string name) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateDomainRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateDomainRepository(uow); return repository.GetByName(name); } } public IDomain GetById(int id) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateDomainRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { - return repo.Get(id); + var repo = RepositoryFactory.CreateDomainRepository(uow); + return repo.Get(id); } } public IEnumerable GetAll(bool includeWildcards) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateDomainRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repo = RepositoryFactory.CreateDomainRepository(uow); return repo.GetAll(includeWildcards); } } public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateDomainRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repo = RepositoryFactory.CreateDomainRepository(uow); return repo.GetAssignedDomains(contentId, includeWildcards); } } @@ -87,22 +89,23 @@ namespace Umbraco.Core.Services public Attempt Save(IDomain domainEntity) { var evtMsgs = EventMessagesFactory.Get(); - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(domainEntity, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateDomainRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(domainEntity, evtMsgs))) + { + uow.Commit(); + return OperationStatus.Cancelled(evtMsgs); + } + + var repository = RepositoryFactory.CreateDomainRepository(uow); repository.AddOrUpdate(domainEntity); uow.Commit(); + uow.Events.Dispatch(Saved, this, new SaveEventArgs(domainEntity, false, evtMsgs)); + return OperationStatus.Success(evtMsgs); } - Saved.RaiseEvent(new SaveEventArgs(domainEntity, false, evtMsgs), this); - return OperationStatus.Success(evtMsgs); + } #region Event Handlers diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index b56c1fbed2..ea926db5b9 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Umbraco.Core.Cache; using Umbraco.Core.CodeAnnotations; using Umbraco.Core.Events; @@ -9,12 +8,13 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { - public class EntityService : RepositoryService, IEntityService + public class EntityService : ScopeRepositoryService, IEntityService { private readonly IRuntimeCacheProvider _runtimeCache; private readonly Dictionary>> _supportedObjectTypes; @@ -78,30 +78,18 @@ namespace Umbraco.Core.Services { var result = _runtimeCache.GetCacheItem(CacheKeys.IdToKeyCacheKey + key, () => { - using (var uow = UowProvider.GetUnitOfWork()) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { - switch (umbracoObjectType) - { - case UmbracoObjectTypes.Document: - case UmbracoObjectTypes.MemberType: - case UmbracoObjectTypes.Media: - case UmbracoObjectTypes.Template: - case UmbracoObjectTypes.MediaType: - case UmbracoObjectTypes.DocumentType: - case UmbracoObjectTypes.Member: - case UmbracoObjectTypes.DataType: - case UmbracoObjectTypes.DocumentTypeContainer: - return uow.Database.ExecuteScalar(new Sql().Select("id").From().Where(dto => dto.UniqueId == key)); - case UmbracoObjectTypes.RecycleBin: - case UmbracoObjectTypes.Stylesheet: - case UmbracoObjectTypes.MemberGroup: - case UmbracoObjectTypes.ContentItem: - case UmbracoObjectTypes.ContentItemType: - case UmbracoObjectTypes.ROOT: - case UmbracoObjectTypes.Unknown: - default: - throw new NotSupportedException(); - } + var nodeObjectType = GetNodeObjectTypeGuid(umbracoObjectType); + + var sql = new Sql() + .Select("id") + .From() + .Where( + dto => + dto.UniqueId == key && + dto.NodeObjectType == nodeObjectType); + return uow.Database.ExecuteScalar(sql); } }); return result.HasValue ? Attempt.Succeed(result.Value) : Attempt.Fail(); @@ -117,41 +105,42 @@ namespace Umbraco.Core.Services { var result = _runtimeCache.GetCacheItem(CacheKeys.KeyToIdCacheKey + id, () => { - using (var uow = UowProvider.GetUnitOfWork()) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { - switch (umbracoObjectType) - { - case UmbracoObjectTypes.Document: - case UmbracoObjectTypes.MemberType: - case UmbracoObjectTypes.Media: - case UmbracoObjectTypes.Template: - case UmbracoObjectTypes.MediaType: - case UmbracoObjectTypes.DocumentType: - case UmbracoObjectTypes.Member: - case UmbracoObjectTypes.DataType: - return uow.Database.ExecuteScalar(new Sql().Select("uniqueID").From().Where(dto => dto.NodeId == id)); - case UmbracoObjectTypes.RecycleBin: - case UmbracoObjectTypes.Stylesheet: - case UmbracoObjectTypes.MemberGroup: - case UmbracoObjectTypes.ContentItem: - case UmbracoObjectTypes.ContentItemType: - case UmbracoObjectTypes.ROOT: - case UmbracoObjectTypes.Unknown: - default: - throw new NotSupportedException(); - } + var nodeObjectType = GetNodeObjectTypeGuid(umbracoObjectType); + + var sql = new Sql() + .Select("uniqueID") + .From() + .Where( + dto => + dto.NodeId == id && + dto.NodeObjectType == nodeObjectType); + return uow.Database.ExecuteScalar(sql); } }); return result.HasValue ? Attempt.Succeed(result.Value) : Attempt.Fail(); } + private static Guid GetNodeObjectTypeGuid(UmbracoObjectTypes umbracoObjectType) + { + var guid = umbracoObjectType.GetGuid(); + if (guid == Guid.Empty) + throw new NotSupportedException("Unsupported object type (" + umbracoObjectType + ")."); + + return guid; + } + public IUmbracoEntity GetByKey(Guid key, bool loadBaseType = true) { if (loadBaseType) { - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork()) { - return repository.GetByKey(key); + var repository = RepositoryFactory.CreateEntityRepository(uow); + var ret = repository.GetByKey(key); + uow.Commit(); + return ret; } } @@ -179,9 +168,11 @@ namespace Umbraco.Core.Services { if (loadBaseType) { - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { - return repository.Get(id); + var repository = RepositoryFactory.CreateEntityRepository(uow); + var ret = repository.Get(id); + return ret; } } @@ -198,9 +189,11 @@ namespace Umbraco.Core.Services if (loadBaseType) { var objectTypeId = umbracoObjectType.GetGuid(); - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { - return repository.GetByKey(key, objectTypeId); + var repository = RepositoryFactory.CreateEntityRepository(uow); + var ret = repository.GetByKey(key, objectTypeId); + return ret; } } @@ -229,9 +222,11 @@ namespace Umbraco.Core.Services if (loadBaseType) { var objectTypeId = umbracoObjectType.GetGuid(); - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { - return repository.Get(id, objectTypeId); + var repository = RepositoryFactory.CreateEntityRepository(uow); + var ret = repository.Get(id, objectTypeId); + return ret; } } @@ -261,9 +256,11 @@ namespace Umbraco.Core.Services { if (loadBaseType) { - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { - return repository.Get(id); + var repository = RepositoryFactory.CreateEntityRepository(uow); + var ret = repository.Get(id); + return ret; } } @@ -285,13 +282,15 @@ namespace Umbraco.Core.Services /// An public virtual IUmbracoEntity GetParent(int id) { - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { + var repository = RepositoryFactory.CreateEntityRepository(uow); var entity = repository.Get(id); + if (entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21) return null; - - return repository.Get(entity.ParentId); + var ret = repository.Get(entity.ParentId); + return ret; } } @@ -303,14 +302,17 @@ namespace Umbraco.Core.Services /// An public virtual IUmbracoEntity GetParent(int id, UmbracoObjectTypes umbracoObjectType) { - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { + var repository = RepositoryFactory.CreateEntityRepository(uow); var entity = repository.Get(id); + if (entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21) return null; - var objectTypeId = umbracoObjectType.GetGuid(); - return repository.Get(entity.ParentId, objectTypeId); + + var ret = repository.Get(entity.ParentId, objectTypeId); + return ret; } } @@ -321,11 +323,12 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public virtual IEnumerable GetChildren(int parentId) { - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { + var repository = RepositoryFactory.CreateEntityRepository(uow); var query = Query.Builder.Where(x => x.ParentId == parentId); - var contents = repository.GetByQuery(query); + var contents = repository.GetByQuery(query); return contents; } } @@ -339,11 +342,118 @@ namespace Umbraco.Core.Services public virtual IEnumerable GetChildren(int parentId, UmbracoObjectTypes umbracoObjectType) { var objectTypeId = umbracoObjectType.GetGuid(); - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { + var repository = RepositoryFactory.CreateEntityRepository(uow); var query = Query.Builder.Where(x => x.ParentId == parentId); - var contents = repository.GetByQuery(query, objectTypeId); + var contents = repository.GetByQuery(query, objectTypeId); + return contents; + } + } + + /// + /// Returns a paged collection of children + /// + /// The parent id to return children for + /// + /// + /// + /// + /// + /// + /// + /// + public IEnumerable GetPagedChildren(int parentId, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "") + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) + { + var repository = RepositoryFactory.CreateEntityRepository(uow); + var query = Query.Builder.Where(x => x.ParentId == parentId && x.Trashed == false); + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); + } + + var contents = repository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return contents; + } + } + + /// + /// Returns a paged collection of descendants + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) + { + var repository = RepositoryFactory.CreateEntityRepository(uow); + + var query = Query.Builder; + //if the id is System Root, then just get all + if (id != Constants.System.Root) + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); + } + + var contents = repository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return contents; + } + } + + /// + /// Returns a paged collection of descendants from the root + /// + /// + /// + /// + /// + /// + /// + /// + /// true/false to include trashed objects + /// + public IEnumerable GetPagedDescendantsFromRoot(UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "", bool includeTrashed = true) + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) + { + var repository = RepositoryFactory.CreateEntityRepository(uow); + + var query = Query.Builder; + //don't include trashed if specfied + if (includeTrashed == false) + { + query.Where(x => x.Trashed == false); + } + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); + } + + var contents = repository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); return contents; } } @@ -355,13 +465,14 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public virtual IEnumerable GetDescendents(int id) { - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { + var repository = RepositoryFactory.CreateEntityRepository(uow); var entity = repository.Get(id); var pathMatch = entity.Path + ","; var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != id); - var entities = repository.GetByQuery(query); + var entities = repository.GetByQuery(query); return entities; } } @@ -375,12 +486,13 @@ namespace Umbraco.Core.Services public virtual IEnumerable GetDescendents(int id, UmbracoObjectTypes umbracoObjectType) { var objectTypeId = umbracoObjectType.GetGuid(); - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { + var repository = RepositoryFactory.CreateEntityRepository(uow); var entity = repository.Get(id); var query = Query.Builder.Where(x => x.Path.StartsWith(entity.Path) && x.Id != id); - var entities = repository.GetByQuery(query, objectTypeId); + var entities = repository.GetByQuery(query, objectTypeId); return entities; } } @@ -399,10 +511,10 @@ namespace Umbraco.Core.Services } var objectTypeId = umbracoObjectType.GetGuid(); - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { + var repository = RepositoryFactory.CreateEntityRepository(uow); var entities = repository.GetByQuery(_rootEntityQuery, objectTypeId); - return entities; } } @@ -442,9 +554,11 @@ namespace Umbraco.Core.Services }); var objectTypeId = umbracoObjectType.GetGuid(); - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { - return repository.GetAll(objectTypeId, ids); + var repository = RepositoryFactory.CreateEntityRepository(uow); + var ret = repository.GetAll(objectTypeId, ids); + return ret; } } @@ -459,9 +573,11 @@ namespace Umbraco.Core.Services }); var objectTypeId = umbracoObjectType.GetGuid(); - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { - return repository.GetAll(objectTypeId, keys); + var repository = RepositoryFactory.CreateEntityRepository(uow); + var ret = repository.GetAll(objectTypeId, keys); + return ret; } } @@ -482,9 +598,11 @@ namespace Umbraco.Core.Services ("The passed in type is not supported"); }); - using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { - return repository.GetAll(objectTypeId, ids); + var repository = RepositoryFactory.CreateEntityRepository(uow); + var ret = repository.GetAll(objectTypeId, ids); + return ret; } } @@ -495,7 +613,7 @@ namespace Umbraco.Core.Services /// public virtual UmbracoObjectTypes GetObjectType(int id) { - using (var uow = UowProvider.GetUnitOfWork()) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var sql = new Sql().Select("nodeObjectType").From().Where(x => x.NodeId == id); var nodeObjectTypeId = uow.Database.ExecuteScalar(sql); @@ -511,7 +629,7 @@ namespace Umbraco.Core.Services /// public virtual UmbracoObjectTypes GetObjectType(Guid key) { - using (var uow = UowProvider.GetUnitOfWork()) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var sql = new Sql().Select("nodeObjectType").From().Where(x => x.UniqueId == key); var nodeObjectTypeId = uow.Database.ExecuteScalar(sql); @@ -566,5 +684,25 @@ namespace Umbraco.Core.Services return attribute.ModelType; } + + public bool Exists(Guid key) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) + { + var repository = RepositoryFactory.CreateEntityRepository(uow); + var exists = repository.Exists(key); + return exists; + } + } + + public bool Exists(int id) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) + { + var repository = RepositoryFactory.CreateEntityRepository(uow); + var exists = repository.Exists(id); + return exists; + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs index 91ca77872d..71ad0fcce9 100644 --- a/src/Umbraco.Core/Services/ExternalLoginService.cs +++ b/src/Umbraco.Core/Services/ExternalLoginService.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { - public class ExternalLoginService : RepositoryService, IExternalLoginService + public class ExternalLoginService : ScopeRepositoryService, IExternalLoginService { public ExternalLoginService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, repositoryFactory, logger, eventMessagesFactory) @@ -24,9 +24,12 @@ namespace Umbraco.Core.Services /// public IEnumerable GetAll(int userId) { - using (var repo = RepositoryFactory.CreateExternalLoginRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork()) { - return repo.GetByQuery(new Query().Where(x => x.UserId == userId)); + var repo = RepositoryFactory.CreateExternalLoginRepository(uow); + var ret = repo.GetByQuery(new Query().Where(x => x.UserId == userId)); + uow.Commit(); + return ret; } } @@ -38,10 +41,12 @@ namespace Umbraco.Core.Services /// public IEnumerable Find(UserLoginInfo login) { - using (var repo = RepositoryFactory.CreateExternalLoginRepository(UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork()) { - return repo.GetByQuery(new Query() - .Where(x => x.ProviderKey == login.ProviderKey && x.LoginProvider == login.LoginProvider)); + var repo = RepositoryFactory.CreateExternalLoginRepository(uow); + var ret = repo.GetByQuery(new Query().Where(x => x.ProviderKey == login.ProviderKey && x.LoginProvider == login.LoginProvider)); + uow.Commit(); + return ret; } } @@ -52,9 +57,9 @@ namespace Umbraco.Core.Services /// public void SaveUserLogins(int userId, IEnumerable logins) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateExternalLoginRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repo = RepositoryFactory.CreateExternalLoginRepository(uow); repo.SaveUserLogins(userId, logins); uow.Commit(); } @@ -66,9 +71,9 @@ namespace Umbraco.Core.Services /// public void DeleteUserLogins(int userId) { - var uow = UowProvider.GetUnitOfWork(); - using (var repo = RepositoryFactory.CreateExternalLoginRepository(uow)) + using (var uow = UowProvider.GetUnitOfWork()) { + var repo = RepositoryFactory.CreateExternalLoginRepository(uow); repo.DeleteUserLogins(userId); uow.Commit(); } diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index e4cbb0b410..28fb07fe9e 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -3,12 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; -using System.Runtime.Remoting.Messaging; using System.Text.RegularExpressions; -using System.Web; -using Umbraco.Core.Auditing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -23,23 +18,18 @@ namespace Umbraco.Core.Services /// /// Represents the File Service, which is an easy access to operations involving objects like Scripts, Stylesheets and Templates /// - public class FileService : RepositoryService, IFileService + public class FileService : ScopeRepositoryService, IFileService { - private readonly IUnitOfWorkProvider _fileUowProvider; - private const string PartialViewHeader = "@inherits Umbraco.Web.Mvc.UmbracoTemplatePage"; private const string PartialViewMacroHeader = "@inherits Umbraco.Web.Macros.PartialViewMacroPage"; public FileService( - IUnitOfWorkProvider fileProvider, - IDatabaseUnitOfWorkProvider dataProvider, + IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory) - : base(dataProvider, repositoryFactory, logger, eventMessagesFactory) - { - _fileUowProvider = fileProvider; - } + : base(provider, repositoryFactory, logger, eventMessagesFactory) + { } #region Stylesheets @@ -50,8 +40,9 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public IEnumerable GetStylesheets(params string[] names) { - using (var repository = RepositoryFactory.CreateStylesheetRepository(_fileUowProvider.GetUnitOfWork(), UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateStylesheetRepository(uow); return repository.GetAll(names); } } @@ -63,8 +54,9 @@ namespace Umbraco.Core.Services /// A object public Stylesheet GetStylesheetByName(string name) { - using (var repository = RepositoryFactory.CreateStylesheetRepository(_fileUowProvider.GetUnitOfWork(), UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateStylesheetRepository(uow); return repository.Get(name); } } @@ -76,19 +68,22 @@ namespace Umbraco.Core.Services /// public void SaveStylesheet(Stylesheet stylesheet, int userId = 0) { - if (SavingStylesheet.IsRaisedEventCancelled(new SaveEventArgs(stylesheet), this)) - return; - - var uow = _fileUowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateStylesheetRepository(uow, UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork()) { + if (uow.Events.DispatchCancelable(SavingStylesheet, this, new SaveEventArgs(stylesheet))) + { + uow.Commit(); + return; + } + + var repository = RepositoryFactory.CreateStylesheetRepository(uow); repository.AddOrUpdate(stylesheet); + + uow.Events.Dispatch(SavedStylesheet, this, new SaveEventArgs(stylesheet, false)); + + Audit(uow, AuditType.Save, "Save Stylesheet performed by user", userId, -1); uow.Commit(); - - SavedStylesheet.RaiseEvent(new SaveEventArgs(stylesheet, false), this); } - - Audit(AuditType.Save, string.Format("Save Stylesheet performed by user"), userId, -1); } /// @@ -98,21 +93,28 @@ namespace Umbraco.Core.Services /// public void DeleteStylesheet(string path, int userId = 0) { - var uow = _fileUowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateStylesheetRepository(uow, UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork()) { + var repository = RepositoryFactory.CreateStylesheetRepository(uow); var stylesheet = repository.Get(path); - if (stylesheet == null) return; - - if (DeletingStylesheet.IsRaisedEventCancelled(new DeleteEventArgs(stylesheet), this)) + if (stylesheet == null) + { + uow.Commit(); return; + } + + if (uow.Events.DispatchCancelable(DeletingStylesheet, this, new DeleteEventArgs(stylesheet))) + { + uow.Commit(); + return; + } repository.Delete(stylesheet); + + uow.Events.Dispatch(DeletedStylesheet, this, new DeleteEventArgs(stylesheet, false)); + + Audit(uow, AuditType.Delete, string.Format("Delete Stylesheet performed by user"), userId, -1); uow.Commit(); - - DeletedStylesheet.RaiseEvent(new DeleteEventArgs(stylesheet, false), this); - - Audit(AuditType.Delete, string.Format("Delete Stylesheet performed by user"), userId, -1); } } @@ -123,10 +125,9 @@ namespace Umbraco.Core.Services /// True if Stylesheet is valid, otherwise false public bool ValidateStylesheet(Stylesheet stylesheet) { - - var uow = _fileUowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateStylesheetRepository(uow, UowProvider.GetUnitOfWork())) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + var repository = RepositoryFactory.CreateStylesheetRepository(uow); return repository.ValidateStylesheet(stylesheet); } } @@ -140,8 +141,9 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public IEnumerable - <% } %> - - - -
    - - -
  • "> -
    - <%# ((Package)Container.DataItem).Text %> - -

    <%# ((Package)Container.DataItem).Text %>

    - <%# ((Package)Container.DataItem).Description %> - - - Install - -
    -
  • -
    - -
- <%-- - No thanks, do not install a starterkit! - --%> - -
- - - -
"> - -
-

Oops...the installer can't connect to the repository

- Starter Kits could not be fetched from the repository as there was no connection - which can occur if you are using a proxy server or firewall with certain configurations, - or if you are not currently connected to the internet. -
- Click Continue to complete the installation then navigate to the Developer section of your Umbraco installation - where you will find the Starter Kits listed in the Packages tree. -
- - -
-
 
- Continue -
- -
- - \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index b0452fb631..281dbacdbd 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -150,7 +150,7 @@ Udgivelsesdato Dato for Fortryd udgivelse Fjern dato - Sorteringrækkefølgen er opdateret + Sorteringsrækkefølgen er opdateret For at sortere, træk siderne eller klik på en af kolonnehovederne. Du kan vælge flere sider ved at holde "shift" eller "control" nede mens du vælger. Statistik Titel (valgfri) @@ -184,7 +184,7 @@ Vælg en type og skriv en titel "dokument typer".]]> "media typer".]]> - Dokument type uden skabelon + Dokumenttype uden skabelon Ny mappe Ny datatype @@ -291,6 +291,7 @@ Vælg medlem Vælg medlemsgruppe Der er ingen parametre for denne makro + Der er ikke tilføjet nogen makroer Link dit Fjern link fra dit konto @@ -320,6 +321,7 @@ Søg... Filtrer... Indtast nøgleord (tryk på Enter efter hvert nøgleord)... + Dit brugernavn er typisk din e-mail adresse Tillad på rodniveau @@ -525,6 +527,16 @@ Brug listevisning Tillad på rodniveau + + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + + General + Editor @@ -568,13 +580,13 @@ Berørte filer og foldere Flere informationer om at opsætte rettigheder for Umbraco her Du er nødt til at give ASP.NET 'modify' rettigheder på følgende filer/foldere - Dine rettighedsinstillinger er næsten perfekte!

Du kan køre Umbraco uden problemer, men du vil ikke være i stand til at installere pakker, som er anbefalet for at få fuldt udbytte af Umbraco.]]>
+ Dine rettighedsindstillinger er næsten perfekte!

Du kan køre Umbraco uden problemer, men du vil ikke være i stand til at installere pakker, som er anbefalet for at få fuldt udbytte af Umbraco.]]>
Hvorledes besluttes Klik her for at læse tekstversionen video tutorials om at opsætte folderrettigheder for Umbraco eller læs tekstversionen.]]> - Dine rettighedsinstillinger kan være et problem!

Du kan afvikle Umbraco uden problemer, men du vil ikke være i stand til at oprette foldere eller installere pakker, hvilket er anbefalet for at få fuldt udbytte af Umbraco.]]>
- Dine rettighedsinstillinger er ikke klar til Umbraco!

For at afvikle Umbraco er du nødt til at opdatere dine rettighedsinstillinger.]]>
- Dine rettighedsinstillinger er perfekte!

Du er nu parat til at afvikle Umbraco og installere pakker!]]>
+ Dine rettighedsindstillinger kan være et problem!

Du kan afvikle Umbraco uden problemer, men du vil ikke være i stand til at oprette foldere eller installere pakker, hvilket er anbefalet for at få fuldt udbytte af Umbraco.]]>
+ Dine rettighedsindstillinger er ikke klar til Umbraco!

For at afvikle Umbraco er du nødt til at opdatere dine rettighedsindstillinger.]]>
+ Dine rettighedsindstillinger er perfekte!

Du er nu parat til at afvikle Umbraco og installere pakker!]]>
Løser folder problem Følg dette link for mere information om udfordringer med ASP.NET og oprettelse af foldere Sætter folderrettigheder op @@ -586,7 +598,7 @@ Dette er vores liste over anbefalede moduler. Kryds dem af du ønsker at installere eller se den fulde liste af moduler ]]> Kun anbefalet for erfarne brugere Jeg ønsker at begynder med et simpelt website - "Runway" er et simpelt website som stiller nogle basale dokumenttyper og skabeloner til rådighed. Instaleringsprogrammet kan automatisk opsætte Runway for dig, men du kan nemt redigere, udvide eller fjerne det. Det er ikke nødvendigt og du kan sagtens bruge Umbraco uden. Men Runway tilbyder et fundament, som er baseret på 'Best Practices', som får dig igang hurtigere end nogensinde før. Hvis du vælger at installere Runway, kan du efter eget valg vælge de grundlæggende byggesten kaldet 'Runway Modules' til at forbedre dine Runway-sider.

Inkluderet med Runway:Home Page, Getting Started page, Installing Modules page.
Valgfri Moduler: Top Navigation, Sitemap, Contact, Gallery.

]]>
+ "Runway" er et simpelt website som stiller nogle basale dokumenttyper og skabeloner til rådighed. Installeringsprogrammet kan automatisk opsætte Runway for dig, men du kan nemt redigere, udvide eller fjerne det. Det er ikke nødvendigt og du kan sagtens bruge Umbraco uden. Men Runway tilbyder et fundament, som er baseret på 'Best Practices', som får dig igang hurtigere end nogensinde før. Hvis du vælger at installere Runway, kan du efter eget valg vælge de grundlæggende byggesten kaldet 'Runway Modules' til at forbedre dine Runway-sider.

Inkluderet med Runway:Home Page, Getting Started page, Installing Modules page.
Valgfri Moduler: Top Navigation, Sitemap, Contact, Gallery.

]]>
Hvad er Runway Skridt 1/5: Acceptér licens Skridt 2/5: Database-konfiguration @@ -703,7 +715,7 @@ Mange hilsner fra Umbraco robotten Pakke opbevaringsbase Bekræft af-installering Pakken blev fjernet - Pakken er på succefuld vis blevet fjernet + Pakken er på succesfuld vis blevet fjernet Afinstallér pakke @@ -725,7 +737,7 @@ Mange hilsner fra Umbraco robotten Rollebaseret beskyttelse Hvis du ønsker at kontrollere adgang til siden ved hjælp af rollebaseret godkendelse via Umbracos medlemsgrupper. - rollebaseret godkendelse]]> + Du skal oprette en medlemsgruppe før du kan bruge rollebaseret godkendelse Fejlside Brugt når folk er logget ind, men ingen adgang Vælg hvordan siden skal beskyttes @@ -784,7 +796,7 @@ Mange hilsner fra Umbraco robotten Tilføj række Tilføj indhold Slip indhold - Instillinger tilføjet + Indstillinger tilføjet Indholdet er ikke tilladt her Indholdet er tilladt her @@ -929,7 +941,7 @@ Mange hilsner fra Umbraco robotten Faneblad Titel på faneblad Faneblade - Master Dokument Type + Master Dokumenttype Opret matchende skabelon @@ -1004,17 +1016,117 @@ Mange hilsner fra Umbraco robotten Vis prøve Styles + Rediger skabelon + + Sektioner Indsæt indholdsområde - Indsæt indholdsområdemarkering - Indsæt ordbogselement - Indsæt makro - Indsæt Umbraco sidefelt + Indsæt pladsholder for indholdsområde + + Indsæt + Hvad vil du indsætte? + + Oversættelse + Indsætter en oversætbar tekst, som skifter efter det sprog, som websitet vises i. + + Makro + + En makro er et element, som kan have forskellige indstillinger, når det indsættes. + Brug det som en genbrugelig del af dit design såsom gallerier, formularer og lister. + + + Sideværdi + + Viser værdien af et felt fra den nuværende side. Kan indstilles til at bruge rekursive værdier eller + vise en standardværdi i tilfælde af, at feltet er tomt. + + + Partial view + + Et Partial View er et skabelonelement, som kan indsættes i andre skabeloner og derved + genbruges og deles på tværs af sideskabelonerne. + + Master skabelon - Lynguide til Umbracos skabelontags + Ingen masterskabelon + Ingen master + + Indsæt en underliggende skabelon + + @RenderBody() element. + ]]> + + + + Definer en sektion + + @section { ... }. Herefter kan denne sektion flettes ind i + overliggende skabelon ved at indsætte et @RenderSection element. + ]]> + + + Indsæt en sektion + + @RenderSection(name) element. Den underliggende skabelon skal have + defineret en sektion via et @section [name]{ ... } element. + ]]> + + + Sektionsnavn + Sektionen er obligatorisk + + + Hvis obligatorisk, skal underskabelonen indeholde en @section -definition. + + + + Query builder + sider returneret, på + + Returner + alt indhold + indhold af typen "%0%" + + fra + mit website + hvor + og + + er + ikke er + er før + er før (inkl. valgte dato) + er efter + er efter (inkl. valgte dato) + er + ikke er + indeholder + ikke indeholder + er større end + er større end eller det samme som + er mindre end + er mindre end eller det samme som + + Id + Navn + Oprettelsesdato + Sidste opdatering + + Sortér efter + stigende rækkefølge + faldende rækkefølge + Skabelon + + Alternativt felt Alternativ tekst diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml index ed7f1e07a2..776a23183b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml @@ -278,6 +278,7 @@ Durchsuchen ... Filtern ... Tippen, um Tags hinzuzufügen (nach jedem Tag die Eingabetaste drücken) ... + Der Benutzername ist normalerweise Ihre E-Mail-Adresse Auf oberster Ebene erlauben diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index f7ac08d837..ecea653650 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -100,6 +100,8 @@ Show styles Insert table Generate models + Undo + Redo To change the document type for the selected content, first select from the list of valid types for this location. @@ -197,6 +199,13 @@ Document Type without a template New folder New data type + New javascript file + New empty partial view + New partial view macro + New partial view from snippet + New empty partial view macro + New partial view macro from snippet + New partial view macro (without macro) Browse your website @@ -305,6 +314,7 @@ Select member group No icons were found There are no parameters for this macro + There are no macros available to insert External login providers Exception Details Stacktrace @@ -338,6 +348,7 @@ Type to filter... Type to add tags (press enter after each tag)... Enter your email + Your username is usually your email Allow at root @@ -490,6 +501,8 @@ Retry Permissions Search + Sorry, we can not find what you are looking for + No items have been added Server Show Show page on Send @@ -550,6 +563,16 @@ Toggle list view Toggle allow as root + + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + + General + Editor @@ -828,7 +851,7 @@ To manage your website, simply open the Umbraco back office and start adding con Role based protection using Umbraco's member groups.]]> - role-based authentication.]]> + You need to create a membergroup before you can use role-based authentication Error Page Used when people are logged on, but do not have access Choose how to restrict access to this page @@ -972,7 +995,7 @@ To manage your website, simply open the Umbraco back office and start adding con Dictionary item saved Publishing failed because the parent page isn't published Content published - and visible at the website + and visible on the website Content saved Remember to publish to make changes visible Sent For Approval @@ -1025,17 +1048,112 @@ To manage your website, simply open the Umbraco back office and start adding con Preview Styles + Edit template + + Sections Insert content area Insert content area placeholder - Insert dictionary item - Insert Macro - Insert Umbraco page field + + Insert + Choose what to insert into your template + + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + Master template - Quick Guide to Umbraco template tags + No master template + No master + + Render child template + + @RenderBody() placeholder. + ]]> + + + + Define a named section + + @section { ... }. This can be rendered in a + specific area of the parent of this template, by using @RenderSection. + ]]> + + + Render a named section + + @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. + ]]> + + + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + + + Query builder + items returned, in + + I want + all content + content of type "%0%" + from + my website + where + and + + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + + Id + Name + Created Date + Last Updated Date + + order by + ascending + descending + Template + + Choose type of content Choose a layout @@ -1432,4 +1550,7 @@ To manage your website, simply open the Umbraco back office and start adding con URL tracker has now been enabled. Error enabling the URL tracker, more information can be found in your log file. + + No Dictionary items to choose from + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 173da6cc9b..63fc12101f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -199,6 +199,13 @@ Document Type without a template New folder New data type + New javascript file + New empty partial view + New partial view macro + New partial view from snippet + New empty partial view macro + New partial view macro from snippet + New partial view macro (without macro) Browse your website @@ -553,6 +560,16 @@ Toggle list view Toggle allow as root + + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + + General + Editor Background color @@ -828,7 +845,7 @@ To manage your website, simply open the Umbraco back office and start adding con Role based protection using Umbraco's member groups.]]> - role-based authentication.]]> + You need to create a membergroup before you can use role-based authentication Error Page Used when people are logged on, but do not have access Choose how to restrict access to this page @@ -972,7 +989,7 @@ To manage your website, simply open the Umbraco back office and start adding con Dictionary item saved Publishing failed because the parent page isn't published Content published - and visible at the website + and visible on the website Content saved Remember to publish to make changes visible Sent For Approval @@ -1027,13 +1044,105 @@ To manage your website, simply open the Umbraco back office and start adding con Edit template + + Sections Insert content area Insert content area placeholder - Insert dictionary item - Insert Macro - Insert Umbraco page field + + Insert + Choose what to insert into your template + + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + Master template - Quick Guide to Umbraco template tags + No master template + No master + + Render child template + + @RenderBody() placeholder. + ]]> + + + + Define a named section + + @section { ... }. This can be rendered in a + specific area of the parent of this template, by using @RenderSection. + ]]> + + + Render a named section + + @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. + ]]> + + + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + + + Query builder + items returned, in + + I want + all content + content of type "%0%" + from + my website + where + and + + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + + Id + Name + Created Date + Last Updated Date + + order by + ascending + descending + Template @@ -1142,7 +1251,7 @@ To manage your website, simply open the Umbraco back office and start adding con Building models - this can take abit of time, don't worry + this can take a bit of time, don't worry Models generated Models could not be generated Models generation has failed, see exception in U log @@ -1438,4 +1547,7 @@ To manage your website, simply open the Umbraco back office and start adding con URL tracker has now been enabled. Error enabling the URL tracker, more information can be found in your log file. + + No Dictionary items to choose from + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml index 09af60593f..3635ec6f23 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml @@ -277,6 +277,7 @@ Escribe un nombre... Escribe tu búsqueda... Escribe para filtrar resultados... + Tu nombre de usuario normalmente es tu e-mail Permitir en nodo raíz @@ -596,7 +597,7 @@ Proteccion basada en roles usando los grupos de miembros de Umbraco.]]> - autenticación basada en roles.]]> + Necesita crear un grupo de miembros antes de poder usar autenticación basada en roles Página de error Usada cuando alguien hace login, pero no tiene acceso Elija cómo restringir el acceso a esta página diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml index 39cafeef7f..0064697d95 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml @@ -329,6 +329,7 @@ Filtrer... Ajouter des tags (appuyer sur enter entre chaque tag)... Entrez votre email + Votre nom d'utilisateur est généralement votre adresse email @@ -817,7 +818,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Protection basée sur les rôles via les groupes de membres Umbraco.]]> - l'authentification basée sur les rôles.]]> + Vous devez créer un groupe avant de pouvoir utiliser l'authentification basée sur les rôles Page d'erreur Utilisé pour les personnes connectées, mais qui n'ont pas accès Choisissez comment restreindre l'accès à cette page diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/umbraco/config/lang/it.xml index 8be7261a8d..171dcd4902 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/it.xml @@ -572,7 +572,7 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i usando i gruppi di membri di Umbraco.]]> - l'autenticazione basata sui ruoli.]]> + Devi creare un gruppo di membri prima di utilizzare l'autenticazione basata sui ruoli diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml index 2955ce716a..bab3bd5d63 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml @@ -27,6 +27,10 @@ Depubliceren Nodes opnieuw inladen Herpubliceer de site + Stel rechten voor pagina %0% in + Waarheen verplaatsen/ + Naar de onderstaande boomstructuur + Herstellen Rechten Vorige versies Klaar voor publicatie @@ -64,10 +68,10 @@ Tonen voor + Selectie ongedaan maken Selecteren Selecteer huidige map Doe iets anders - Vet Paragraaf uitspringen Voeg formulierveld in @@ -89,11 +93,14 @@ Opslaan Opslaan en publiceren Opslaan en verzenden voor goedkeuring + Sla list view op voorbeeld bekijken Voorbeeld bekijken is uitgeschakeld omdat er geen template is geselecteerd Stijl kiezen Stijlen tonen Tabel invoegen + Genereer models + Opslaan en models genereren Om het documenttype voor de geselecteerde inhoud te wijzigen, selecteert u eerst uit de lijst van geldige types voor deze locatie. @@ -133,7 +140,8 @@ Dit item is gewijzigd na publicatie Dit item is niet gepubliceerd Laatst gepubliceerd op - Nog geen items om weer te geven. + Er zijn geen items om weer te geven + Er zijn geen items om weer te geven. Mediatype Link naar media item(s) Ledengroep @@ -143,7 +151,9 @@ Pagina Titel Eigenschappen Dit document is gepubliceerd maar niet zichtbaar omdat de bovenliggende node '%0%' niet gepubliceerd is - Oeps: dit document is gepubliceerd, maar het is niet in de cache (interne serverfout) + Dit document is gepubliceerd, maar het is niet in de cache (interne serverfout) + Kan de Url niet ophalen + Dit document is gepubliceerd, maar de Url conflicteert met %0% Publiceren Publicatiestatus Publiceren op @@ -161,23 +171,34 @@ Bestand(en) verwijderen Link naar het document Lid van groep(en) - Geen lid van groep(en) - + Geen lid van groep(en) Kinderen Doel + Dit betekend de volgende tijd op de server: + Wat houd dit in?]]> Klik om te uploaden Plaats je bestanden hier... + Link naar media + Of klik hier om bestanden te kiezen + De toegestane bestandtypen zijn + Dit bestand heeft niet het juiste file-type. Dit bestand kan niet geupload worden. + Max file size is + + + Maak nieuwe member aan + Alle Members Waar wil je de nieuwe %0% aanmaken? Aanmaken onder Kies een type en een titel - "Documenttypes".]]> - "Mediatypes".]]> + Document Type zonder template + Nieuwe folder + Nieuw data type Open je website @@ -189,38 +210,38 @@ Welkom - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes + Blijf op deze pagina + Negeer wijzigingen + Wijzigingen niet opgeslagen + Weet je zeker dat deze pagina wilt verlaten? - er zijn onopgeslagen wijzigingen Done - - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items + + %0% item verwijderd + %0% items verwijderd + Item %0% van de %1% verwijderd + Items %0% van de %1% verwijderd + + %0% item gepubliceerd + %0% items gepubliceerd + Item %0% van de %1% gepubliceerd + Items %0% van de %1% gepubliceerd + + %0% item gedepubliceerd + %0% items gedepubliceerd + Item %0% van de %1% gedepubliceerd + Items %0% van de %1% gedepubliceerd + + %0% item verplaatst + %0% items verplaatst + item %0% van de %1% verplaatst + items %0% van de %1% verplaatst + + %0% item gekopieerd + %0% items gekopieerd + item %0% van de %1% gekopieerd + item %0% van de %1% gekopieerd Naam @@ -269,21 +290,56 @@ Klik op de afbeelding voor volledige grootte Kies een item Toon cache item + Maak folder aan... + Relateer aan origineel + Descendants meenemen + De vriendelijkste community + Link naar pagina + Opent het gelinkte document in een nieuw venster of tab + Link naar media + Selecteer media + Selecteer icoon + Selecteer item + Selecteer link + Selecteer macro + Selecteer content + Selecteer member + Selecteer member group + Er zijn geen parameters voor deze macro + Externe login providers + Error details + Stacktrace + Inner Exception + Link je + De-Link je + account + Selecteer editor Cultuurnaam + Verander de key van het dictionary item. + + + - Typ je gebruikersnaam - Typ je wachtwoord + Typ jouw gebruikersnaam + Typ jouw wachtwoord + Bevestig jouw wachtwoord Benoem de %0%... Typ een naam... + Label... + Voer een omschrijving in... Typ om te zoeken... Typ om te filteren... Typ om tags toe te voegen (druk op enter na elke tag)... + Voer jouw email in + Jouw gebruikersnaam is meestal jouw email @@ -331,10 +387,17 @@ %0% is niet in het correcte formaat + Een error ontvangen van de server Het opgegeven bestandstype is niet toegestaan ​​door de beheerder OPMERKING! Ondanks dat CodeMiror is ingeschakeld, is het uitgeschakeld in Internet Explorer omdat het niet stabiel genoeg is. Zowel de alias als de naam van het nieuwe eigenschappen type moeten worden ingevuld! Er is een probleem met de lees/schrijf rechten op een bestand of map + Error bij het laden van Partial View script (file: %0%) + Error bij het laden van userControl '%0%' + Error bij het laden van customControl (Assembly: %0%, Type: '%1%') + Error bij het laden van MacroEngine script (file: %0%) + "Error bij het parsen van XSLT file: %0% + "Error bij het laden van XSLT file: %0% Vul een titel in Selecteer een type U wilt een afbeelding groter maken dan de originele afmetingen. Weet je zeker dat je wilt doorgaan? @@ -355,102 +418,142 @@ Acties Toevoegen Alias + Alles Weet je het zeker? - Rand - of - Annuleren + Terug + Border + bij + Cancel Cel marge - Kiezen - Sluiten - Venster sluiten - Opmerking - Bevestigen - Verhouding behouden - Doorgaan - Kopiëren + Kies + Sluit + Sluit venster + Comment + Bevestig + Verhoudingen behouden + Ga verder + Copy Aanmaken - Databank + Database Datum Standaard - Verwijderen + Verwijder Verwijderd - Verwijderen... + Aan het verwijderen... Ontwerp Afmetingen - Beneden + Omlaag Download - Aanpassen - Aangepast + Bewerk + Bewerkt Elementen - E-mail + Email Fout - Zoeken - Hoogte + Vind + Hogte Help Icoon - Importeren - Binnenmarge + Import + Binnenste marge Invoegen Installeren - Uitvullen + Ongeldig + Justify + Label Taal - Lay-out - Bezig met laden - Geblokkeerd + Layout + Aan het laden + Gesloten Inloggen - Afmelden - Afmelden + Uitloggen + Uitloggen Macro - Verplaatsen - Meer + Verplicht + Verplaats + meer Naam Nieuw Volgende Nee - van - Ok - Openen + of + OK + Open of Wachtwoord Pad Placeholder ID - Een ogenblik geduld a.u.b. + Een ogenblik geduld aub... Vorige Eigenschappen - E-mail om formulier te ontvangen + Email om formulier resultaten te ontvangen Prullenbak - Overgebleven - Hernoemen + De prullenbak is leeg + Overblijvend + Hernoem Vernieuw Verplicht Opnieuw proberen Rechten - Zoek + Zoeken + We konden helaas niet vinden wat je zocht Server - Tonen - Toon pagina bij versturen - Formaat - Sorteren - Submit - Type - Type om te zoeken... + Toon + Toon pagina na verzenden + Grootte + Sorteer + Verstuur + Typen + Typ om te zoeken... Omhoog - Bijwerken + Update Upgrade Upload Url Gebruiker Gebruikersnaam Waarde - Toon + Bekijk Welkom... Breedte Ja Map - Reorder - I am done reordering - Zoekresultaten + Herschik + Ik ben klaar met herschikken + Voorvertoning + Wachtwoord veranderen + naar + Lijstweergave + Aan het opslaan... + huidig + Embed + geselecteerd + + + Zwart + Groen + Geel + Oranje + Blauw + Rood + + + Tab toevoegen + Property toevoegen + Editor toevoegen + Template toevoegen + Child node toevoegen + Child toevoegen + + Data type bewerken + + Secties navigeren + + Shortcuts + Toon shortcuts + + Toggle lijstweergave + Toggle toestaan op root-niveau Achtergrondkleur @@ -528,6 +631,57 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Bekijken Umbraco %0% voor een nieuwe installatie of een upgrade van versie 3.0.

Druk op "volgende" om de wizard te starten.]]>
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Cultuurcode Cultuurnaam @@ -543,12 +697,22 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Fijne woensdag Fijne donderdag Fijne vrijdag - Fijne zaterdag - + Fijne zaterdag log hieronder in + Inloggen met Sessie is verlopen - © 2001 - %0%
umbraco.com

]]>
+ Wachtwoord vergeten? + Er zal een email worden gestuurd naar het emailadres van jouw account. Hierin staat een link om je wachtwoord te resetten + Een email met daarin de wachtwoord reset uitleg zal worden gestuurd als het emailadres in onze database voorkomt. + Terug naar loginformulier + Geeft alsjeblieft een nieuw wachtwoord op + Je wachtwoord is aangepast + De link die je hebt aangeklikt is niet (meer) geldig. + Umbraco: Wachtwoord Reset + + De gebruikersnaam om in te loggen bij jouw Umbraco omgeving is: %0%

Klik hier om je wachtwoord te resetten of knip/plak deze URL in je browser:

%1%

]]> +
Dashboard @@ -646,6 +810,15 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Package versie Package versiehistorie Bekijk de package website + Package reeds geinstalleerd + Deze package kan niet worden geinstalleerd omdat minimaal Umbraco versie %0% benodigd is. + Aan het deinstalleren... + Aan het downloaden... + Aan het importeren... + Aan het installeren... + Aan het herstarten, een ongenblik geduld aub... + Geinstalleerd! Je browser zal nu automatisch ververst worden... + Kruk op "finish" om de installate te voltooien en de pagina te verversen. Plakken met alle opmaak (Niet aanbevolen) @@ -656,7 +829,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Geavanceerd: Beveilig door de Member Groups te seecteren die toegang hebben op de pagina gebruik makend van Umbraco's member groups.]]> - role-based authentication.]]> + Je moet eerst een membergroup maken voordat je kunt werken met role-based authentication. Error Pagina Gebruikt om te tonen als een gebruiker is ingelogd, maar geen rechten heeft om de pagina te bekijken Hoe wil je de pagina beveiligen? @@ -677,13 +850,18 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je %0% kan niet worden gepubliceerd omdat het item is gepland voor release. ]]>
+ + Inclusief ongepubliceerde kinderen Publicatie in uitvoering - even geduld... %0% van %1% pagina’s zijn gepubliceerd... @@ -698,16 +876,13 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Je hebt geen goedgekeurde kleuren geconfigureerd - Externe link toevoegen - Interne link toevoegen - Toevoegen - Bijschrift - Interne pagina - URL - Verplaats omlaag - Verplaats omhoog - Open in nieuw venster - Verwijder link + Externe link toevoegen + Interne link toevoegen + Bijschrift + Link + In een nieuw venster openen + Voer het bijschrijft in + Voer de link in Reset @@ -736,13 +911,17 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Instellingen Statistieken Vertaling - Gebruikers - Umbraco Contour - + Gebruikers Help Formulieren Analytics + + ga naar + Help onderwerpen voor + Video's voor + De beste Umbraco video tutorials + Standaard template Woordenboek sleutel @@ -762,6 +941,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Geen eigenschappen gedefinieerd op dit tabblad. Klik op de link "voeg een nieuwe eigenschap" aan de bovenkant om een ​​nieuwe eigenschap te creëren. Master Document Type Maak een bijbehorend template + Icon toevoegen Sort order @@ -771,7 +951,13 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je
Sluit dit venster niet tijdens het sorteren]]>
- Publicatie werd geannuleerd door een 3rd party plug-in + Validatie + Validatiefouten moeten worden opgelost voor dit item kan worden opgeslagen + Mislukt + Wegens onvoldoende rechten kon deze handeling kon niet worden uitegevoerd + Geannuleerd + Uitvoering is g eannuleerd door de plugin van een 3e partij + Publicatie werd geannuleerd doordeeen plugin van een 3e partij Eigenschappen type bestaat al Eigenschappen type aangemaakt Data type: %1%]]> @@ -806,6 +992,8 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Bestand opgeslagen Bestand opgeslagen zonder fouten Taal opgeslagen + Media Type opgeslagen + Member Type opgeslagen Python script niet opgeslagen Python script kon niet worden opgeslagen door een fout Python script opeslagen! @@ -824,6 +1012,11 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Partial view opgeslagen zonder fouten! Partial view niet opgeslagen Er is een fout opgetreden bij het opslaan van het bestand. + Script view opgeslagen + Script view opgeslagen zonder fouten! + Script view niet opgeslagen + Er is een fout opgetreden bij het opslaan van dit bestand. + Er is een fout opgetreden bij het opslaan van dit bestand. Gebruik CSS syntax bijv: h1, .redHeader, .blueTex @@ -846,15 +1039,15 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Item toevoegen - Choose a layout - Een rij aan de lay-out toevoegen - teken onderaan en voeg je eerste item toe]]> + Kies de indeling + Kies een indeling voor deze pagina om content toe te kunnen voegen + Plaats een (extra) content blok]]> Drop content - Settings applied - - This content is not allowed here - This content is allowed here + Instellingen toegepast + Deze content is hier niet toegestaan + Deze content is hier toegestaan + Klik om een item te embedden Klik om een afbeelding in te voegen Afbeelding ondertitel... @@ -883,7 +1076,79 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Alle editors toelaten Alle rijconfiguraties toelaten + Instellen als standaard + Kies extra + Kies standaard + zijn toegevoegd + + + Composities + Er zijn nog geen tabs toegevoegd + Voeg een nieuwe tab toe + Voeg nog een tab toe + Inherited van + Voeg property toe + Verplicht label + + Zet list view aan + Laat de child nodes van het content item zien als een sorteer- en doorzoekbare lijstweergave zien. Deze child nodes worden dan niet in de boomstructuur getoond. + + Toegestane Templates + Kies welke templates toegestaan zijn om door de editors op dit content-type gebruikt te worden + Sta toe op root-niveau + Sta editors toe om content van dit type aan te maken op root-niveau + Ja - sta content van dit type toe op root-niveau + + Toegestane child node types + Sta contetn van een bepaalde type toe om onder dit type aangemaakt te kunnen worden + + Kies child node + Overerfde tabs en properties van een bestaand document-type. Nieuwe tabs worden toegevoegd aan het huidige document-type of samengevoegd als een tab met de identieke naam al bestaat. + Dit content-type wordt gebruikt in een compositie en kan daarom niet zelf een compositie worden. + Er zijn geen content-typen beschikbaar om als compositie te gebruiken. + + Beschikbare editors + Herbruik + Editor instellingen + + Configuratie + + Ja, verwijder + + is naar onder geplaatst + is naar onder gecopierd + Selecteer de map om te verplaatsen + Selecteer de map om te kopieren + naar de boomstructuur onder + + Alle Document types + Alle documenten + Alle media items + + die gebruik maken van dit document type zullen permanent verwijderd worden. Bevestig aub dat je deze ook wilt verwijderen. + die gebruik maken van dit media type zullen permanent verwijderd worden. Bevestig aub dat je deze ook wilt verwijderen. + die gebruik maken van dit member type zullen permanent verwijderd worden. Bevestig aub dat je deze ook wilt verwijderen. + + en alle documenten van dit type + en alle media items van dit type + en alle leden van dit type + + die gebruik maken van deze editor zullen geupdate worden met deze nieuwe instellingen + + Lid kan bewerken + Toon in het profiel van leden + tab heeft geen sorteervolgorde + + + + Models aan het gereneren + dit kan enige tijd duren, geduld aub + Models gegenereerd + Models konden niet gegenereerd worden + Models generatie is mislukt, kijk in de Umbraco log voor details + + Alternatief veld Alternatieve tekst @@ -916,7 +1181,8 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Taken aan jou toegewezen - Sluit taak @@ -932,18 +1198,24 @@ Om een vertalingstaak te sluiten, ga aub naar het detailoverzicht en klik op de Dit is een geautomatiseerde mail om u op de hoogte te brengen dat document '%1%' is aangevraagd voor vertaling naar '%5%' door %2%. - Ga naar http://%3%/Umbraco/translation/default.aspx?id=%4% om te bewerken. + Ga naar http://%3%/translation/details.aspx?id=%4% om te bewerken. + Of log in bij Umbraco om een overzicht te krijgen van al jouw vertalingen. - Een prettige dag! + + Met vriendelijke groet! - Dit is een bericht van uw Content Management Systeem. - + + De Umbraco Robot ]]> [%0%] Vertaalopdracht voor %1% Geen vertaal-gebruikers gevonden. Maak eerst een vertaal-gebruiker aan voordat je pagina's voor vertaling verstuurd Taken aangemaakt door jou - die je aanmaakte. Om een detailweergave met opmerkingen te zien, klik op "Detail" of op de paginanaam. Je kan ook de pagina in XML-formaat downloaden door op de "Download XML"-link te klikken. Om een vertalingstaak te sluiten, klik je op de "Sluiten"-knop in detailweergave.]]> + die je aanmaakte. + Om een detailweergave met opmerkingen te zien, klik op "Detail" of op de paginanaam. + Je kan ook de pagina in XML-formaat downloaden door op de "Download XML"-link te klikken. + Om een vertalingstaak te sluiten, klik je op de "Sluiten"-knop in detailweergave.]]> De pagina '%0%' is verstuurd voor vertaling + Kies de taal waarin deze contetn vertaald moet worden Stuur voor vertaling Toegewezen door Taak geopend @@ -973,7 +1245,8 @@ Om een vertalingstaak te sluiten, ga aub naar het detailoverzicht en klik op de Ledengroepen Rollen Ledentypes - Documenttypes + Documenttypen + RelatieTypen Packages Packages Python-bestanden @@ -985,6 +1258,7 @@ Om een vertalingstaak te sluiten, ga aub naar het detailoverzicht en klik op de Stylesheets Sjablonen XSLT Bestanden + Analytics Nieuwe update beschikbaar @@ -1010,6 +1284,7 @@ Om een vertalingstaak te sluiten, ga aub naar het detailoverzicht en klik op de Startnode in Mediabibliotheek Secties Blokkeer Umbraco toegang + Oude wachtwoord Wachtwoord Reset wachtwoord Je wachtwoord is veranderd! @@ -1030,10 +1305,138 @@ Om een vertalingstaak te sluiten, ga aub naar het detailoverzicht en klik op de Gebruikerstype Gebruikerstypes Auteur + Vertaler Wijzig - Je profiel Je recente historie Sessie verloopt over + + + Validatie + Valideer als email + Valideer als nummer + Valideer als Url + ...of gebruik custom validatie + Veld is verplicht + + + + Waarde is insteld naar the aanbevolen waarde: '%0%'. + Waarde was '%1%' voor XPath '%2%' in configuratie bestand '%3%'. + De verwachtte waarde voor '%2%' is '%1%' in configuratie bestand '%3%', maar is '%0%'. + Onverwachte waarde '%0%' gevonden voor '%2%' in configuratie bestand '%3%'. + + + Custom foutmeldingen zijn ingesteld op '%0%'. + Custom foutmeldingen zijn momenteel '%0%'. Wij raden aan deze aan te passen naar '%1%' voor livegang. + Custom foutmeldingen aangepast naar '%0%'. + + Macro foutmeldingen zijn ingesteld op'%0%'. + Macro foutmeldingen zijn ingesteld op '%0%'. Dit zal er voor zorgen dat bepaalde, of alle, pagina's van de website niet geladen kunnen worden als er errors in een Macro zitten. Corrigeren zal deze waarde aanpassen naar '%1%'. + Macro foutmeldingen zijn aangepast naar '%0%'. + + + Try Skip IIS Custom foutmeldingen is ingesteld op '%0%'. IIS versie '%1%' wordt gebruikt. + Try Skip IIS Custom foutmeldingen is ingesteld op '%0%'. Het wordt voor de gebruikte IIS versie (%2%) aangeraden deze in te stellen op '%1%'. + Try Skip IIS Custom foutmeldingen ingesteld op '%0%'. + + + Het volgende bestand bestaat niet: '%0%'. + '%0%' kon niet gevonden worden in configuratie bestand '%1%'.]]> + Er is een fout opgetreden. Bekijk de log file voor de volledige fout: %0%. + + Members - Totaal XML: %0%, Totaal: %1%, Total incorrect: %2% + Media - Totaal XML: %0%, Totaal: %1%, Total incorrect: %2% + Content - Totaal XML: %0%, Totaal gepubliceerd: %1%, Total incorrect: %2% + + Het cerficaat van de website is ongeldig. + Cerficaat validatie foutmelding: '%0%' + Fout bij pingen van URL %0% - '%1%' + De site wordt momenteel %0% bekeken via HTTPS. + De appSetting 'umbracoUseSSL' in web.config staat op 'false'. Indien HTTPS gebruikt wordt moet deze op 'true' staan. + De appSetting 'umbracoUseSSL' in web.config is ingesteld op '%0%'. Cookies zijn %1% ingesteld als secure. + De 'umbracoUseSSL' waarde in web.config kon niet aangepast worden. Foutmelding: %0% + + + HTTPS inschakelen + Zet in de appSettings van de web.config de umbracoSSL instelling op 'true'. + De appSetting 'umbracoUseSSL' is nu ingesteld op 'true', cookies zullen als 'secure' worden aangemerkt. + + Fix + Cannot fix a check with a value comparison type of 'ShouldNotEqual'. + Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. + Value to fix check not provided. + + Debug compiliate mode staat uit. + Debug compiliate mode staat momenteel aan. Wij raden aan deze instelling uit te zetten voor livegang. + Debug compiliate mode uitgezet. + + Trace mode staat uit. + Trace mode staat momenteel aan. Wij raden aan deze instelling uit te zetten voor livegang. + Trace mode uitgezet. + + Alle mappen hebben de juiste permissie-instellingen!. + + %0%.]]> + %0%. Als deze niet in gebruik zijn voor deze omgeving hoeft er geen actie te worden ondernomen.]]> + + All files have the correct permissions set. + + %0%.]]> + %0%. Als deze niet in gebruik zijn voor deze omgeving hoeft er geen actie te worden ondernomen.]]> + + X-Frame-Options header of meta-tag om IFRAMEing door andere websites te voorkomen is aanwezig!]]> + X-Frame-Options header of meta-tag om IFRAMEing door andere websites te voorkomen is NIET aanwezig.]]> + Voorkom IFRAMEing via web.config + Voegt de instelling toe aan de httpProtocol/customHeaders section in web.config om IFRAMEing door andere websites te voorkomen. + De instelling om IFRAMEing door andere websites te voorkomen is toegevoegd aan de web.config! + Web.config kon niet aangepast worden door error: %0% + + + %0%.]]> + Er zijn geen headeres gevonden welke informatie over de gebruikte website technologie prijsgeven! + + In de Web.config werd system.net/mailsettings niet gevonden + In de Web.config sectie system.net/mailsettings is de host niet geconfigureerd. + SMTP instellingen zijn correct ingesteld en werken zoals verwacht. + De SMTP server geconfigureerd met host '%0%' en poort '%1%' kon niet gevonden worden. Controleer of de SMTP instellingen in Web.config file system.net/mailsettings correct zijn. + + %0%.]]> + %0%.]]> + + + URL tracker uitzetten + URL tracker aanzetten + Originele URL + Doorgestuurd naar + Er zijn geen redirects + Er wordt automatisch een redirect aangemaakt als een gepubliceerde pagina hernoemd of verplaatst wordt. + Verwijder + Weet je zeker dat je de redirect van '%0%' naar '%1%' wilt verwijderen? + Redirect URL verwijderd. + Fout bij verwijderen redirect URL. + Weet je zeker dat je de URL tracker wilt uitzetten? + URL tracker staat nu uit. + Fout bij het uitzetten van de URL Tracker. Meer informatie kan gevonden worden in de log file. + URL tracker staat nu aan. + Fout bij het aanzetten van de URL tracker. Meer informatie kan gevonden worden in de log file. diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml b/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml index e67a4c4b15..adc26ca46a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml @@ -576,7 +576,7 @@ Você pode remover com segurança do seu sistema clicando em "desinstalar pacote Proteção baseada em função usando grupos de membros do Umbraco.]]> - autenticação baseada em função.]]> + Você precisa criar um grupo de membros antes que possa usar autenticação baseada em função. Página de Erro Usado quando as pessoas estão logadas, mas não para ter acesso Escolha como restringir o acesso à esta página diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml index 255bf62d5d..11ce7a3252 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -27,6 +27,9 @@ Опубликовать Обновить узлы Опубликовать весь сайт + Установить разрешения для страницы '%0%' + Выберите, куда переместить + В структуре документов ниже Восстановить Разрешения Откатить @@ -120,7 +123,6 @@ Сохранить список Выбрать Выбрать текущую папку - выбранные Предварительный просмотр Предварительный просмотр запрещен, так как документу не сопоставлен шаблон Другие действия @@ -189,7 +191,7 @@ Роль участника Тип участника Дата не указана - Заголовок страницы + Заголовок ссылки Не является членом групп(ы) Свойства Этот документ опубликован, но скрыт, потому что его родительский документ '%0%' не опубликован @@ -282,6 +284,13 @@ "Типы медиа-материалов".]]> Выберите тип и заголовок Тип документа без сопоставленного шаблона + Новый файл javascript + Новое пустое частичное представление + Новый макрос-представление + Новое частичное представление по образцу + Новый пустой макрос-представление + Новый макрос-представление по образцу + Новый макрос-представление (без регистрации макроса) Обзор сайта @@ -341,9 +350,10 @@ Просмотр элемента кэша Создать папку... Связать с оригиналом + Включая все дочерние Самое дружелюбное сообщество Ссылка на страницу - Открывает документ по ссылке в новом окне или вкладке браузера + Открывать ссылку в новом окне или вкладке браузера Ссылка на медиа-файл Выбрать медиа Выбрать значок @@ -406,6 +416,9 @@ Показать метку Ширина и высота + + Нет доступных элементов словаря + Ваши данные сохранены, но для того, чтобы опубликовать этот документ, Вы должны сначала исправить следующие ошибки: Текущий провайдер ролей пользователей не поддерживает изменение пароля (необходимо свойству EnablePasswordRetrieval в файле web.config присвоить значение true) @@ -989,6 +1002,7 @@ Установка... Перезапуск, подождите, пожалуйста... Все готово, сейчас браузер перезагрузит страницу, подождите, пожалуйста... + Пожалуйста, нажмите кнопку 'finish' для завершения установки и перезагрузки страницы. Вставить, полностью сохранив форматирование (не рекомендуется) @@ -1008,6 +1022,7 @@ Укажите пароль Что искать... Укажите имя пользователя + Имя пользователя (часто это Ваш email-адрес) Остаться @@ -1018,7 +1033,7 @@ Расширенный: Защита на основе ролей (групп) с использованием групп участников Umbraco.]]> - для применения ролевой модели безопасности.]]> + Вам необходимо создать хотя бы одну группу участников для применения ролевой модели безопасности. Страница сообщения об ошибке Используется в случае, когда пользователь авторизован в системе, но не имеет доступа к документу. Выберите способ ограничения доступа к документу @@ -1115,9 +1130,9 @@ Статистика Перевод Пользователи - Добавить значок + Добавить значок в качестве родительского типа. Вкладки родительского типа не показаны и могут быть изменены непосредственно в родительском типе Родительский тип контента разрешен Данный тип контента использует @@ -1154,6 +1169,16 @@ В формате списка Разрешить в качестве корневого + + Закомментировать/раскомментировать строки + Удалить строку + Копировать строки вверх + Копировать строки вниз + Переместить строки вверх + Переместить строки вниз + + Общее + Редактор Порядок сортировки @@ -1174,6 +1199,7 @@ Вкладка с идентификатором (id): %0% удалена Документ скрыт (публикация отменена) Стиль CSS не сохранен + При сохранении файла произошла ошибка. Стиль CSS сохранен Стиль CSS сохранен без ошибок Тип данных сохранен @@ -1198,9 +1224,13 @@ Файл не может быть сохранен. Пожалуйста, проверьте установки файловых разрешений Файл сохранен Файл сохранен без ошибок + У текущего пользователя недостаточно прав, невозможно завершить операцию Язык сохранен Тип медиа сохранен Тип участника сохранен + Отменено + Операция отменена установленным сторонним расширением или блоком кода + Ошибка Представление не сохранено Произошла ошибка при сохранении файла Представление сохранено @@ -1209,10 +1239,16 @@ Cкрипт Python не может быть сохранен в связи с ошибками Cкрипт Python сохранен Cкрипт Python сохранен без ошибок + Скрипт не сохранен + При сохранении файла скрипта произошла ошибка + Скрипт сохранен + Файл скрипта сохранен без ошибок Шаблон не сохранен Пожалуйста, проверьте, что нет двух шаблонов с одним и тем же алиасом (названием) Шаблон сохранен Шаблон сохранен без ошибок + Проверка значений + Ошибки, найденные при проверке значений, должны быть исправлены, чтобы было возможно сохранить документ XSLT-документ не сохранен XSLT-документ содержит одну или несколько ошибок XSLT-документ не может быть сохранен, проверьте установки файловых разрешений @@ -1228,14 +1264,106 @@ Стили - Править шаблон + Изменить шаблон + + Секции Вставить контент-область Вставить контейнер (placeholder) - Вставить статью словаря - Вставить макрос - Вставить поле документа + + Вставить + Выберите, что хотите вставить в шаблон + + Статью словаря + Статья словаря - это контейнер для части текста, переводимой на разные языки, это позволяет упростить создание многоязычных сайтов. + + Макрос + + Макросы - это настраиваемые компоненты, которые хорошо подходят для + реализации переиспользуемых блоков, (особенно, если необходимо менять их внешний вид и/или поведение при помощи параметров) + таких как галереи, формы, списки и т.п. + + + Значение поля + Отображает значение указанного поля данных текущей страницы, + с возможностью указать альтернативные поля и/или подстановку константы. + + + Частичное представление + + Частичное представление - это шаблон в отдельном файле, который может быть вызван для отображения внутри + другого шаблона, хорошо подходит для реализации переиспользуемых фрагментов разметки или для разбиения сложных шаблонов на составные части. + + Мастер-шаблон - Краткая справка по тэгам шаблонов Umbraco + Без мастер-шаблона + Не выбран + + Вставить дочерний шаблон + + @RenderBody() в выбранном месте. + ]]> + + + Определить именованную секцию + + @section { ... }. Такая секци может быть отображена в нужном месте родительского шаблона + при помощи конструкции @RenderSection. + ]]> + + + Вставить именованную секцию + + @RenderSection(name). + Таким образом из дочернего шаблона отображается содержимое внутри конструкции @section [name]{ ... }. + ]]> + + + Название секции + Секция обязательна + + Если секция помечена как обязательная, то дочерний шаблон должен обязательно содержать ее определение @section, в противном случае генерируется ошибка. + + + Генератор запросов + элементов в результате, за + + Мне нужны + все документы + документы типа "%0%" + из + всего сайта + где + и + + равна + не равна + до + до (включая выбранную дату) + после + после (включая выбранную дату) + равно + не равно + содержит + не содержит + больше, чем + больше или равно + меньше, чем + меньше или равно + + Id + Название + Создан + Обновлен + + сортировать + по возрастанию + по убыванию + Шаблон @@ -1339,6 +1467,7 @@ Пакеты дополнений Пакеты дополнений Скрипты Python + Типы связей Установить из репозитория Установить Runway Модули Runway @@ -1357,6 +1486,7 @@ Администратор Поле категории + Изменить Изменить пароль Вы можете сменить свой пароль для доступа к административной панели Umbraco, заполнив нижеследующие поля и нажав на кнопку 'Изменить пароль' Подтверждение нового пароля diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml index 259b914a72..75bb41e35c 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml @@ -87,7 +87,7 @@ Välj aktuell mapp Förhandsgranska Förhandsgranskning är avstängt på grund av att det inte finns någon mall tilldelad - Ångra + Annat Välj stil Visa stil Infoga tabell @@ -171,6 +171,7 @@ "dokumenttyper".]]> "mediatyper".]]> Välj typ och rubrik + Dokumenttyp utan sidmall Surfa på din webbplats @@ -593,11 +594,12 @@ Skriv för att söka... Fyll i ditt lösenord Skriv för att lägga till taggar (och tryck enter efter varje tagg)... + Ditt användarnamn är vanligtvis din e-postadress Rollbaserat lösenordsskydd Då används Umbracos medlemsgrupper.]]> - rollbaserat lösenordsskydd.]]> + Du måste skapa en medlemsgrupp innan du kan använda rollbaserat lösenordsskydd. Sida med felmeddelande Används när en användare är inloggad, men saknar rättigheter att se sidan Välj hur du vill lösenordsskydda sidan @@ -936,4 +938,4 @@ Översättare Din profil - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml index 359609633b..2d32cb6d0b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml @@ -734,7 +734,7 @@ To manage your website, simply open the Umbraco back office and start adding con Role based protection using Umbraco's member groups.]]> - role-based authentication.]]> + You need to create a membergroup before you can use role-based authentication. Error Page Used when people are logged on, but do not have access Choose how to restrict access to this page diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml b/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml index b8c933c101..1f538e7fa0 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml @@ -1,7 +1,7 @@ - 孙柱梁 + 黄仁祥(wanddy@163.com) http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files @@ -24,8 +24,13 @@ 提醒 公众访问权限 发布 + 取消发布 重新加载节点 重新发布整站 + 恢复 + 为 %0%设置权限 + 选择移动目的地 + 到下列的树结构中 权限 回滚 提交至发布者 @@ -33,39 +38,40 @@ 排序 提交至发布者 翻译 - 取消发布 更新 + 默认值 禁止访问 添加域名 移除 错误的节点 + 域名错误 域名重复 - 域名 语言 + 域名 新域名 '%0%' 已创建 域名 '%0%' 已删除 域名 '%0%' 已使用 - - - https://www.example.com/、example.com/en、……使用 * 代表任意域名,
- 只需要设置语言部分即可。]]> -
域名 '%0%' 已更新 - 域名错误 编辑当前域名 + + https://www.example.com/、example.com/en、……使用 * 代表任意域名,
+ 只需要设置语言部分即可。]]>
继承 语言 - - 也可以从父节点继承。]]> - + + 也可以从父节点继承。]]> 域名 查看 + 清除选择 + 选择 + 选择当前目录 + 其它功能 粗体 取消段落缩进 插入表单字段 @@ -83,14 +89,17 @@ 插入宏 插入图片 编辑关联 + 返回列表 保存 保存并发布 保存并提交审核 + 保存列表视图 预览 因未设置模板无法预览 选择样式 显示样式 插入表格 + 生成模型 要更改所选节点的文档类型,先在列表中选择合适的文档类型。 @@ -113,21 +122,27 @@ 仅显示可作为替代的文档类型。 + 已发布 关于本页 别名 (图片的替代文本) 替代链接 点击编辑 创建者 + 原作者 + 更新者 创建时间 + 创建此文档的日期/时间 文档类型 编辑 过期于 该项发布之后有更改 该项没有发布 最近发布 - 媒体链接地址 + 没有要显示的项目 + 列表中没有要显示的项目。 媒体类型 + 媒体链接地址 会员组 角色 会员类型 @@ -136,26 +151,52 @@ 属性 该文档不可见,因为其上级 '%0%' 未发布。 该文档已发布,但是没有更新至缓存(内部错误) + Could not get the url + This document is published but its url would collide with content %0% 发布 发布状态 发布于 + 取消发布于 清空时间 排序完成 拖拽项目或单击列头即可排序,可以按住Shift多选。 统计 标题(可选) + Alternative text (optional) 类型 取消发布 最近编辑 + 编辑此文档的日期/时间 移除文件 链接到文档 会员组成员 非会员组成员 + 子项 + 目标 + 这将转换到服务器上的以下时间: + 这是什么意思?]]> + + + 点击上传 + 将文件放在此处.. + 链接到媒体 + 或单击此处选择文件 + 仅允许的文件类型为 + 最大文件大小为 + + + 创建新成员 + 所有成员 您想在哪里创建 %0% 创建在 选择类型和标题 + "文档类型" 下的 "设置" 部分中启用这些内容。]]> + "媒体类型" 下的 "设置" 部分中启用这些内容。]]> + 没有模板的文档类型 + 新建文件夹 + 新数据类型 浏览您的网站 @@ -167,38 +208,38 @@ 欢迎 - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes + 保持 + 丢弃更改 + 您有未保存的更改 + 确实要离开此页吗?-您有未保存的更改 - Done + 完成 - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items + 已删除 %0% 项 + 已删除 %0% 项 + 已删除 %0% 项,共 %1% 项 + 已删除 %0% 项,共 %1% 项 - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items + 已发布 %0% 项 + 已发布 %0% 项 + 已发布 %0% 项,共 %1% 项 + 已发布 %0% 项,共 %1% 项 - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items + 已取消发布 %0% 项 + 已取消发布 %0% 项 + 已取消发布 %0% 项,共 %1% 项 + 已取消发布 %0% 项,共 %1% 项 - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items + 已移动 %0% 项 + 已移动 %0% 项 + 已移动 %0% 项,共 %1% 项 + 已移动 %0% 项,共 %1% 项 - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items + 已复制 %0% 项 + 已复制 %0% 项 + 已复制 %0% 项,共 %1% 项 + 已复制 %0% 项,共 %1% 项 锚点名称 @@ -240,24 +281,81 @@ 网站缓存将会刷新,所有已发布的内容将会更新。 表格列数 表格行数 - 设置一个占位符id 您可以在子模板中通过该ID来插入内容,引用格式: <asp:content />。]]> - 选择一个占位符id。]]> + 设置一个占位符id 您可以在子模板中通过该ID来插入内容, + 引用格式: <asp:content />。]]> + 选择一个 + 占位符id。]]> 点击图片查看完整大小 拾取项 查看缓存项 + 创建文件夹... + 与原始连接 + 包括后代 + 最友好的社区 + 链接到页面 + 在新窗口或选项卡中打开链接的文档 + 链接到媒体 + 选择媒体 + 选择图标 + 选择项 + 选择链接 + 选择宏 + 选择内容 + 选择成员 + 选择成员组 + 未找到图标 + 此宏没有参数 + 外部登录提供程序 + 异常详细信息 + 堆栈跟踪 + 内部异常 + 链接您的 + 取消链接您的 + 帐户 + 选择编辑器 - %0%’
您可以在左侧的“语言”中添加一种语言]]>
+ %0%’
您可以在左侧的“语言”中添加一种语言 + ]]>
语言名称 + 编辑字典项的键。 + + + + + + 输入您的用户名 + 输入您的密码 + 确认密码 + 命名 %0%... + 输入用户名... + 标签... + 输入说明... + 输入搜索关键字... + 输入过滤词... + 键入添加tags (在每个tag之后按 enter)... + 输入您的电子邮件 + 允许在根目录 + 只能在内容和媒体树的根级别创建具有选中的内容类型 允许子项节点类型 + 文档类型组合 创建 删除选项卡 描述 新建选项卡 选项卡 缩略图 + 启用列表视图 + 配置内容项以显示可排序和搜索的子项列表, 这些子项将不会显示在树中 + 当前列表视图 + 活动列表视图数据类型 + 创建自定义列表视图 + 删除自定义列表视图 添加预设值 @@ -286,10 +384,17 @@ %0% 格式不正确 + 从服务器收到错误 该文件类型已被管理员禁用 注意,尽管配置中允许CodeMirror,但是它在IE上不够稳定,所以无法在IE运行。 请为新的属性类型填写名称和别名! 权限有问题,访问指定文件或文件夹失败! + 加载Partial视图脚本时出错(文件: %0%) + 加载 userControl 时出错 '%0%' + 加载 customControl 时出错(程序集: %0%, 类型: '%1%') + 加载 MacroEngine 脚本时出错 (文件: %0%) + "解析 xslt 文件时出错: %0% + "读取 xslt 文件时出错: %0% 请输入标题 请选择类型 图片尺寸大于原始尺寸不会提高图片质量,您确定要把图片尺寸变大吗? @@ -302,13 +407,17 @@ 非合并单元格不能分离。 XSLT源码出错 XSLT未保存,因为包含错误。 + 此属性使用的数据类型存在配置错误, 请检查数据类型 关于 操作 + 操作 添加 别名 + 所有 您确定吗? + 返回 边框 取消 @@ -338,7 +447,6 @@ 邮箱 错误 查找文档 - 文件夹 帮助 图标 @@ -346,6 +454,7 @@ 内边距 插入 安装 + 无效 对齐 语言 布局 @@ -355,7 +464,9 @@ 退出 注销 + 必填项 移动 + 更多 名称 新的 下一步 @@ -375,6 +486,7 @@ 保持状态中 重命名 更新 + 必填 重试 权限 搜索 @@ -383,7 +495,7 @@ 在发送时预览 大小 排序 - Submit + 提交 类型 输入内容开始查找… @@ -398,9 +510,48 @@ 欢迎… - Reorder - I am done reordering + 文件夹 + 搜索结果 + 重新排序 + 我已结束排序 + 预览 + 更改密码 + + 列表视图 + 保存中... + 当前 + 嵌入 + 已选择 + + + 黑色 + 绿色 + 黄色 + 橙色 + 蓝色 + 红色 + + + + 添加选项卡 + 添加属性 + 添加编辑器 + 添加模板 + 添加子节点 + 添加子项 + + 编辑数据类型 + + 导航节 + + 快捷方式 + 显示快捷方式 + + 切换列表视图 + 切换允许作为根 + + 背景色 粗体 @@ -408,6 +559,7 @@ 字体 文本 + 页面 @@ -416,7 +568,9 @@ 无法保存web.config文件,请手工修改。 发现数据库 数据库配置 - 安装进行 %0% 数据库配置]]> + 安装进行 %0% 数据库配置 + ]]> 下一步继续。]]> 数据库未找到!请检查数据库连接串设置。

您可以自行编辑“web.config”文件,键名为 “UmbracoDbDSN”

@@ -426,8 +580,7 @@ ]]>
如有必要,请联系您的系统管理员。 - 如果您是本机安装,请使用管理员账号。 - ]]> + 如果您是本机安装,请使用管理员账号。]]> 点击更新来更新系统到 %0%

@@ -436,6 +589,7 @@

]]>
点击下一步继续。]]> + 下一步继续]]> 需要修改默认密码!]]> 默认账户已禁用或无权访问系统!

点击下一步继续。]]> @@ -443,7 +597,8 @@ 密码已更改 - 系统创建了一个默认用户(‘admin’)和默认密码(‘default’)。现在密码是随机的。 + 系统创建了一个默认用户(‘admin’)和默认密码(‘default’)。 + 现在密码是随机的。

该步骤建议您修改默认密码。 @@ -456,27 +611,24 @@ 此处查看更多信息 您需要对以下文件和文件夹授于ASP.NET用户修改权限 您当前的安全设置满足要求!

- 您可以毫无问题的运行系统,但您不能安装系统所推荐的扩展包的完整功能。 - ]]>
+ 您可以毫无问题的运行系统,但您不能安装系统所推荐的扩展包的完整功能。]]> 如何解决 点击阅读文字版 视频教程 ]]> 您当前的安全设置有问题!

- 您可以毫无问题的运行系统,但您不能新建文件夹、也不能安装系统所推荐的包的完整功能。 - ]]>
+ 您可以毫无问题的运行系统,但您不能新建文件夹、也不能安装系统所推荐的包的完整功能。 ]]> 您当前的安全设置不适合于系统!

- 您需要修改系统访问权限。 - ]]>
+ 您需要修改系统访问权限。]]> 您当前的权限设置正确!

- 您可以运行系统并安装其它扩展包! - ]]>
+ 您可以运行系统并安装其它扩展包!]]> 解决文件夹问题 点此查看ASP.NET和创建文件夹的问题解决方案 设置文件夹权限 我要从头开始 - Runway: Home page, Getting Started page, Installing Modules page.
- 可选模块: Top Navigation, Sitemap, Contact, Gallery. + Runway: 主页, 开始页, 安装模块页.
+ 可选模块: 顶部导航, 站点地图, 联系我们, 图库.
]]>
“Runway”是什么? @@ -515,11 +668,12 @@ 更多的帮助信息 从社区获取帮助]]> 系统 %0% 安装完毕 - /web.config file 的 AppSetting 键 UmbracoConfigurationStatus'%0%'。]]> + /web.config file 的 AppSetting 键 + UmbracoConfigurationStatus'%0%'。]]> 立即开始请点“运行系统”
如果您是新手, 您可以得到相当丰富的学习资源。]]>
运行系统 -管理您的网站, 运行后台添加内容,也可以添加模板和功能。 - ]]> +管理您的网站, 运行后台添加内容, +也可以添加模板和功能。]]> 无法连接到数据库。 系统版本 3 系统版本 4 @@ -537,8 +691,28 @@ 已更新,继续工作。 - © 2001 - %0%

]]>
- 欢迎使用Umbraco,在下方输入用户名和密码 + 星期一快乐 + 星期二快乐 + 星期三快乐 + 星期四快乐 + 星期五快乐 + 星期六快乐 + 星期天快乐 + 在下方登录 + 登录 + 会话超时 + © 2001 - %0%
Umbraco.com

]]>
+ 忘记密码? + 电子邮件将发送到地址指定的链接, 以重置您的密码 + 如果电子邮件与我们的记录相符, 则将发送带有密码重置指令的邮件 + 返回登录表单 + 请提供新密码 + 您的密码已更新 + 您单击的链接无效或已过期 + Umbraco: 重置密码 + + 您的用户名登录到 Umbraco 后台是: %0%

点击 这里 重置密码,或复制链接粘贴到您的浏览器访问:

%1%

]]> +
仪表板 @@ -564,37 +738,43 @@ %0%:

-

您好!这是一封自动发送的邮件,告诉您任务'%1%'已在'%2%'被用户'%3%'执行

- -

+

您好!这是一封自动发送的邮件,告诉您任务'%1%' + 已在'%2%' + 被用户'%3%'执行 +

+ +

更新概况:

- - %6% -
-

+ + %6% +
+

-
-
+

祝您愉快!

该信息由系统自动发送 -

- ]]> +

]]> 在 %2%,[%0%] 关于 %1% 的通告已执行。 通知 @@ -610,8 +790,7 @@ 名称 扩展包不含任何项
- 点击下面的“卸载”,您可以安全的删除。 - ]]>
+ 点击下面的“卸载”,您可以安全的删除。]]> 无可用更新 选项 说明 @@ -621,15 +800,24 @@ 扩展包卸载成功 卸载 - 注意:卸载包将导致所有依赖该包的东西失效,请确认。 - ]]> + 注意: + 卸载包将导致所有依赖该包的东西失效,请确认。 ]]> 从程序库下载更新 更新扩展包 更新说明 - 扩展包有可用的更新,您可以从程序库网站更新。 + 此软件包有一个可用的升级。您可以直接从 Umbraco 软件包存储库下载。 版本 版本历史 访问扩展包网站 + 已安装软件包 + 此软件包无法安装, 它需要一个最小的 Umbraco 版本的%0% + 卸载中... + 下载中... + 导入中... + 安装中... + 重启中, 请稍候... + 所有完成后, 您的浏览器将立即刷新, 请稍候... + 请单击 "完成" 以完成安装和重新加载页面。 带格式粘贴(不推荐) @@ -656,16 +844,23 @@ 如果您只希望提供一个用户名和密码就能访问 - - - - - + - + + + + + 包含未发布的子项 正在发布,请稍候… %0% 中的 %1% 页面已发布… @@ -673,20 +868,23 @@ %0% 及其子项已发布 发布 %0% 及其子项 确定 发布 %0%

-要发布当前页和所有子页,请选中 全部发布 发布所有子页。 - ]]>
+ 要发布当前页和所有子页,请选中 全部发布 发布所有子页。 + ]]> + + + 您没有配置任何认可的颜色 - - - - - - - - - - + 输入外部链接 + 选择内部页面 + 标题 + 链接 + 新窗口 + 输入新标题 + 输入链接 + + + Reset 当前版本 @@ -701,9 +899,9 @@ 编辑脚本 - Concierge + 礼宾 内容 - Courier + 导游 开发 Umbraco配置向导 媒体 @@ -713,11 +911,17 @@ 统计 翻译 用户 + 帮助 + 窗体 + 统计 + + + 转到 + 帮助主题 + 视频章节 + 最佳 Umbraco 视频教程 - 作为主控文档类型. 主控文档类型的标签只能在主控文档类型里修改。 - 主控文档类型激活 - 该文档类型使用 默认模板 字典键 要导入文档类型,请点击“浏览”按钮,再点击“导入”,然后在您电脑上查找 ".udt"文件导入(下一页中需要您再次确认) @@ -725,20 +929,33 @@ 节点类型 类型 样式表 + 脚本 样式表属性 选项卡 选项卡标题 选项卡 + 主控文档类型激活 + 该文档类型使用 + 作为主控文档类型. 主控文档类型的标签只能在主控文档类型里修改。 没有字段设置在该标签页 + 主控文档类型 + 创建匹配模板 + 添加图标 - Sort order - Creation date + 排序次序 + 创建日期 排序完成。 上下拖拽项目或单击列头进行排序
请不要关闭窗口]]>
+ 验证 + 在保存项之前必须修复验证错误 + 失败 + 用户权限不足, 无法完成操作 + 取消 + 操作被第三方插件取消。 发布因为第三方插件取消 属性类型已存在 属性类型已创建 @@ -748,7 +965,6 @@ 选项卡已创建 选项卡已删除 id为%0%的选项卡已删除 - 内容已取消发布 样式表未保存 样式表已保存 样式表保存,无错误。 @@ -775,6 +991,8 @@ 文件保存 文件保存,无错误。 语言已保存 + 已保存媒体类型 + 已保存成员类型 Python脚本未保存 Python脚本因为错误未能保存 Python已保存 @@ -788,10 +1006,16 @@ XSLT无法保存,请检查权限。 XSLT已保存 XSLT无错误 + 未发布内容 片段视图已保存 片段视图保存,无错误。 片段视图未保存 片段视图因为错误未能保存 + 已保存脚本视图 + 脚本视图保存时没有发生任何错误! + 未保存脚本视图 + 保存文件时出错。 + 保存文件时出错。 使用CSS语法,如:h1、.redHeader、.blueTex。 @@ -813,44 +1037,111 @@ 模板 - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied + 选择内容类别 + 选择一项布局 + 添加一行 + 添加内容 + 丢弃内容 + 设置已应用 - This content is not allowed here - This content is allowed here + 此处不允许有该内容 + 此处允许有该内容 - Click to embed - Click to insert image - Image caption... - Write here... + 点击嵌入 + 点击添加图片 + 图片说明... + 在这里输入... - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells + 网格布局 + 布局是网格编辑器的整体工作区域, 通常只需要一个或两个不同的布局 + 添加网络布局 + 通过设置列宽并添加其他节来调整版式 + 行配置 + 行是水平排列的预定义单元格 + 添加行配置 + 通过设置单元格宽度和添加其他单元格来调整行 - Columns - Total combined number of columns in the grid layout + + 网格布局中的总和列数 - Settings - Configure what settings editors can change + 设置 + 配置编辑器可以更改的设置 - Styles - Configure what styling editors can change + 样式 + 配置编辑器可以更改的样式 - Settings will only save if the entered json configuration is valid + 输入的 json 配置有效, 设置才可保存 - Allow all editors - Allow all row configurations + 允许所有的编辑器 + 允许所有行配置 + 设置为默认值 + 选择附加 + 选择默认值 + 已增加 + + + + 组合 + 您没有添加任何选项卡 + 添加新选项卡 + 添加其他选项卡 + 继承自 + 添加属性 + 必需的标签 + + 启用列表视图 + 配置内容项以显示其子项的可排序和搜索列表, 这些子项将不会显示在树中 + + 允许的模板 + 选择允许在该类型的内容上使用哪些模板编辑器 + + 允许作为根 + 允许编辑器在内容树的根目录中创建此类型的内容 + 是 - 允许根中的此类型的内容 + + 允许的子节点类型 + 允许在该类型的内容下方创建指定类型的内容 + + 选择子节点 + + 从现有文档类型继承选项卡和属性。如果存在同名的选项卡, 则新选项卡将添加到当前文档类型或合并。 + 此内容类型在组合中使用, 因此不能自行组成。 + 没有可供组合使用的内容类型。 + + 可用编辑器 + 重用 + 编辑器设置 + + 配置 + + 是,删除 + + 被移动到下方 + 被复制到下面 + 选择要移动的文件夹 + 选择要复制的文件夹 + 在下面的树结构中 + + 所有文档类型 + 所有文档 + 所有媒体项目 + + 使用此文档类型将被永久删除, 请确认您还要删除这些文件。 + 使用此媒体类型将被永久删除, 请确认您也要删除这些。 + 使用此成员类型将被永久删除, 请确认您想要删除这些 + + 和所有使用此类型的文档 + 和所有使用此类型的媒体项目 + 和使用此类型的所有成员 + + 使用此编辑器将用新设置更新 + + 成员可编辑 + 显示成员配置文件 + + + 替代字段 替代文本 @@ -902,9 +1193,13 @@ 转到 http://%3%/translation/details.aspx?id=%4% 进行编辑 - 或登录http://%3%查看任务 + 或登录下列网址查看翻译任务 + http://%3% - 祝好运!]]> + Have a nice day! + + 来自Umbraco 机器人的祝福 + ]]> [%0%]翻译任务:%1% 没有翻译员,请创建翻译员角色的用户。 您创建的任务 @@ -913,6 +1208,7 @@ 关闭翻译任务,请返回详细视图点击“关闭”按钮。 ]]> 页面'%0%'已经发送给翻译 + 请选择内容应翻译成的语言 发送页面'%0%'以便翻译 分配者 任务开启 @@ -943,6 +1239,7 @@ 角色 会员类型 文档类型 + 关系类型 扩展包 扩展包 Python文件 @@ -954,6 +1251,7 @@ 样式表 模板 XSLT文件 + 分析 有可用更新 @@ -965,8 +1263,9 @@ 管理员 分类字段 更改密码 - 要改变密码,请在框中输入新密码,然后单击“更改密码”。 + 更改密码 确认新密码 + 要改变密码,请在框中输入新密码,然后单击“更改密码”。 内容频道 描述字段 禁用用户 @@ -977,16 +1276,16 @@ 登录 默认打开媒体项 区域 - 更改密码 禁用后台管理界面 + 旧密码 密码 重设密码 您的密码已更改! 重输密码 - 当前密码 输入新密码 - 密码错误 新密码不能为空! + 当前密码 + 密码错误 新密码和重输入的密码不一致,请重试! 重输的密码和原密码不一致! 替换子项权限设置 @@ -999,5 +1298,138 @@ 用户类型 用户类型 撰稿人 + 翻译人 + 更改 + 你的资料 + 你最近的历史信息 + 会话过期于 + + + 验证 + 验证为电子邮件 + 验证为数字 + 验证为 url + ...或输入自定义验证 + 字段是强制性的 + + + + Value is set to the recommended value: '%0%'. + Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. + Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. + Found unexpected value '%0%' for '%2%' in configuration file '%3%'. + + + Custom errors are set to '%0%'. + Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. + Custom errors successfully set to '%0%'. + + MacroErrors are set to '%0%'. + MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. + MacroErrors are now set to '%0%'. + + + Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. + Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). + Try Skip IIS Custom Errors successfully set to '%0%'. + + + File does not exist: '%0%'. + '%0%' in config file '%1%'.]]> + There was an error, check log for full error: %0%. + + Members - Total XML: %0%, Total: %1%, Total invalid: %2% + Media - Total XML: %0%, Total: %1%, Total invalid: %2% + Content - Total XML: %0%, Total published: %1%, Total invalid: %2% + + Your site certificate was marked as valid. + Certificate validation error: '%0%' + Error pinging the URL %0% - '%1%' + You are currently %0% viewing the site using the HTTPS scheme. + The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. + The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. + Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0% + + + Enable HTTPS + Sets umbracoSSL setting to true in the appSettings of the web.config file. + The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure. + + Fix + Cannot fix a check with a value comparison type of 'ShouldNotEqual'. + Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. + Value to fix check not provided. + + Debug compilation mode is disabled. + Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Debug compilation mode successfully disabled. + + Trace mode is disabled. + Trace mode is currently enabled. It is recommended to disable this setting before go live. + Trace mode successfully disabled. + + All folders have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + + All files have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + Set Header in Config + Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. + A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. + Could not update web.config file. Error: %0% + + + %0%.]]> + No headers revealing information about the website technology were found. + + In the Web.config file, system.net/mailsettings could not be found. + In the Web.config file system.net/mailsettings section, the host is not configured. + SMTP settings are configured correctly and the service is operating as expected. + The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. + + %0%.]]> + %0%.]]> + + + 禁用 url 跟踪程序 + 启用 url 跟踪程序 + 原始网址 + 已重定向至 + 未进行重定向 + 当已发布的页重命名或移动时, 将自动对新页进行重定向。 + 删除 + 确实要删除 "%0%" 到 "%1%" 的重定向吗? + 重定向URL已删除。 + 删除重定向 url 时出错. + 是否确实要禁用 url 跟踪程序? + url 跟踪器现在已被禁用。 + 禁用 url 跟踪程序时出错, 可以在日志文件中找到更多信息。 + 现在已启用 url 跟踪程序。 + 启用 url 跟踪程序时出错, 可以在日志文件中找到更多信息。 diff --git a/src/Umbraco.Web.UI/umbraco/controls/Images/ImageViewer.ascx.cs b/src/Umbraco.Web.UI/umbraco/controls/Images/ImageViewer.ascx.cs index d0cfd28d01..e031663e8a 100644 --- a/src/Umbraco.Web.UI/umbraco/controls/Images/ImageViewer.ascx.cs +++ b/src/Umbraco.Web.UI/umbraco/controls/Images/ImageViewer.ascx.cs @@ -2,6 +2,7 @@ namespace Umbraco.Web.UI.Umbraco.Controls.Images { + [Obsolete("This is no longer used and will be removed in future versions")] public partial class ImageViewer : global::umbraco.controls.Images.ImageViewer { } diff --git a/src/Umbraco.Web.UI/umbraco/controls/PasswordChanger.ascx.cs b/src/Umbraco.Web.UI/umbraco/controls/PasswordChanger.ascx.cs index fbf30a776d..75540408b8 100644 --- a/src/Umbraco.Web.UI/umbraco/controls/PasswordChanger.ascx.cs +++ b/src/Umbraco.Web.UI/umbraco/controls/PasswordChanger.ascx.cs @@ -4,6 +4,9 @@ using System.Configuration.Provider; using System.Linq; using System.Web; using System.Web.Security; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Security; namespace Umbraco.Web.UI.Umbraco.Controls { @@ -20,9 +23,13 @@ namespace Umbraco.Web.UI.Umbraco.Controls umbPasswordChanger_passwordNewConfirm.Text = null; //reset the flag always IsChangingPasswordField.Value = "false"; - this.DataBind(); - } - + var canReset = Provider.CanResetPassword(ApplicationContext.Current.Services.UserService); + + ResetPlaceHolder.Visible = canReset; + + this.DataBind(); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx b/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx index 7059fd0803..2990c55d55 100644 --- a/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx +++ b/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx @@ -67,7 +67,7 @@ - +
diff --git a/src/Umbraco.Web.UI/umbraco/developer/Packages/BrowseRepository.aspx b/src/Umbraco.Web.UI/umbraco/developer/Packages/BrowseRepository.aspx deleted file mode 100644 index 076c767b01..0000000000 --- a/src/Umbraco.Web.UI/umbraco/developer/Packages/BrowseRepository.aspx +++ /dev/null @@ -1,20 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="true" MasterPageFile="../../masterpages/umbracoPage.Master" Title="Browse Repository" CodeBehind="BrowseRepository.aspx.cs" Inherits="umbraco.presentation.developer.packages.BrowseRepository" %> -<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> - - - - - - - - - - - diff --git a/src/Umbraco.Web.UI/umbraco/developer/Packages/LoadNitros.ascx b/src/Umbraco.Web.UI/umbraco/developer/Packages/LoadNitros.ascx deleted file mode 100644 index d79cc6e881..0000000000 --- a/src/Umbraco.Web.UI/umbraco/developer/Packages/LoadNitros.ascx +++ /dev/null @@ -1,32 +0,0 @@ -<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="LoadNitros.ascx.cs" Inherits="umbraco.presentation.developer.packages.LoadNitros" %> - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/developer/Packages/StarterKits.aspx b/src/Umbraco.Web.UI/umbraco/developer/Packages/StarterKits.aspx deleted file mode 100644 index 059e0f727a..0000000000 --- a/src/Umbraco.Web.UI/umbraco/developer/Packages/StarterKits.aspx +++ /dev/null @@ -1,86 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="True" MasterPageFile="../../masterpages/umbracoPage.Master" Title="Install starter kit" CodeBehind="StarterKits.aspx.cs" Inherits="Umbraco.Web.UI.Umbraco.Developer.Packages.StarterKits" %> -<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> -<%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %> - - - - - - - - - - - - - - -

Available starter kits

-

You can choose from the following starter kits, each having specific functionality.

-
Please wait...
-
-
-
- -
-
- - -

Installation completed succesfully

-
- - -

We can not install starterkits when the install directory or package repository is not present.

-
- - -
- - -
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/developer/Packages/StarterKits.aspx.cs b/src/Umbraco.Web.UI/umbraco/developer/Packages/StarterKits.aspx.cs deleted file mode 100644 index f673156fbf..0000000000 --- a/src/Umbraco.Web.UI/umbraco/developer/Packages/StarterKits.aspx.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Web.UI.Install.Steps.Skinning; -using Umbraco.Web.UI.Pages; -using System.IO; -using umbraco.cms.businesslogic.packager; - -namespace Umbraco.Web.UI.Umbraco.Developer.Packages -{ - - - public partial class StarterKits : UmbracoEnsuredPage - { - private const string RepoGuid = "65194810-1f85-11dd-bd0b-0800200c9a66"; - - protected void Page_Load(object sender, EventArgs e) - { - //check if a starter kit is already isntalled - - var installed = InstalledPackage.GetAllInstalledPackages(); - - if (installed.Count == 0) - { - ShowStarterKits(); - return; - } - - var repo = global::umbraco.cms.businesslogic.packager.repositories.Repository.getByGuid(RepoGuid); - if (repo.HasConnection()) - { - try - { - var kits = repo.Webservice.StarterKits(); - var kitIds = kits.Select(x => x.RepoGuid).ToArray(); - - //if a starter kit is already installed show finish - if (installed.Any(x => kitIds.Contains(Guid.Parse(x.Data.PackageGuid)))) - { - StarterKitNotInstalled.Visible = false; - installationCompleted.Visible = true; - } - else - { - ShowStarterKits(); - } - } - catch (Exception ex) - { - LogHelper.Error("Cannot connect to package repository", ex); - InstallationDirectoryNotAvailable.Visible = true; - StarterKitNotInstalled.Visible = false; - } - } - else - { - InstallationDirectoryNotAvailable.Visible = true; - StarterKitNotInstalled.Visible = false; - } - } - - private void ShowStarterKits() - { - if (Directory.Exists(Server.MapPath(GlobalSettings.Path.EnsureEndsWith('/') + "install/Legacy")) == false) - { - InstallationDirectoryNotAvailable.Visible = true; - StarterKitNotInstalled.Visible = false; - - return; - } - - - var starterkitsctrl = (LoadStarterKits)LoadControl(GlobalSettings.Path.EnsureEndsWith('/') + "install/Legacy/loadStarterKits.ascx"); - - ph_starterkits.Controls.Add(starterkitsctrl); - - StarterKitNotInstalled.Visible = true; - - } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/developer/Packages/StarterKits.aspx.designer.cs b/src/Umbraco.Web.UI/umbraco/developer/Packages/StarterKits.aspx.designer.cs deleted file mode 100644 index 105a8ff5df..0000000000 --- a/src/Umbraco.Web.UI/umbraco/developer/Packages/StarterKits.aspx.designer.cs +++ /dev/null @@ -1,78 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Umbraco.Web.UI.Umbraco.Developer.Packages { - - - public partial class StarterKits { - - /// - /// JsInclude1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.JsInclude JsInclude1; - - /// - /// Panel1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.UmbracoPanel Panel1; - - /// - /// fb control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Feedback fb; - - /// - /// StarterKitNotInstalled control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane StarterKitNotInstalled; - - /// - /// ph_starterkits control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.PlaceHolder ph_starterkits; - - /// - /// installationCompleted control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane installationCompleted; - - /// - /// InstallationDirectoryNotAvailable control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane InstallationDirectoryNotAvailable; - } -} diff --git a/src/Umbraco.Web.UI/umbraco/developer/Packages/SubmitPackage.aspx b/src/Umbraco.Web.UI/umbraco/developer/Packages/SubmitPackage.aspx deleted file mode 100644 index a882fedef2..0000000000 --- a/src/Umbraco.Web.UI/umbraco/developer/Packages/SubmitPackage.aspx +++ /dev/null @@ -1,113 +0,0 @@ -<%@ Page Language="C#" Title="Submit package" MasterPageFile="../../masterpages/umbracoPage.Master" AutoEventWireup="true" CodeBehind="SubmitPackage.aspx.cs" Inherits="umbraco.presentation.developer.packages.SubmitPackage" %> -<%@ Register TagPrefix="cc2" Namespace="umbraco.uicontrols" Assembly="controls" %> - - - - - - - - - - - -
- - -
-

- -

-
- - - - - -

Choose the repository you want to submit the package to

-
- - - -
- - - -
- - - -

Upload additional documentation for your package to help new users getting started with your package

-
- - - - - -
- - -
- -
-

By clicking "submit package" below you understand that your package will be submitted to a package repository and will in some cases be publicly available to download.

-

Please notice: only packages with complete read-me, author information and install information gets considered for inclusion.

-

The package administrators group reservers the right to decline packages based on lack of documentation, poorly written readme and missing author information

-
- -

-  <%= umbraco.ui.Text("or") %>  "><%= umbraco.ui.Text("cancel") %> -

-
- -
-
diff --git a/src/Umbraco.Web.UI/umbraco/developer/Packages/editPackage.aspx b/src/Umbraco.Web.UI/umbraco/developer/Packages/editPackage.aspx index 87faed4e43..edd484c82e 100644 --- a/src/Umbraco.Web.UI/umbraco/developer/Packages/editPackage.aspx +++ b/src/Umbraco.Web.UI/umbraco/developer/Packages/editPackage.aspx @@ -39,7 +39,6 @@ - diff --git a/src/Umbraco.Web.UI/umbraco/developer/Packages/installedPackage.aspx b/src/Umbraco.Web.UI/umbraco/developer/Packages/installedPackage.aspx deleted file mode 100644 index 44b2991ada..0000000000 --- a/src/Umbraco.Web.UI/umbraco/developer/Packages/installedPackage.aspx +++ /dev/null @@ -1,184 +0,0 @@ -<%@ Page Language="C#" MasterPageFile="../../masterpages/umbracoPage.Master" AutoEventWireup="true" CodeBehind="installedPackage.aspx.cs" Inherits="umbraco.presentation.developer.packages.installedPackage" %> -<%@ Register TagPrefix="cc2" Namespace="umbraco.uicontrols" Assembly="controls" %> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - -
- - - - - - - - - - - - -

- <%= umbraco.ui.Text("packager", "packageUpgradeText") %> -

-
- - -

- -

- -

- -

-
-
- - -
- - <%= umbraco.ui.Text("packager", "packageNoItemsText") %> - -

- -

-
- -
- - - -

- <%= umbraco.ui.Text("packager", "packageUninstallText") %> -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- Package uninstall in progress, please wait while the browser is reloaded... -
- - - - -
-
-
diff --git a/src/Umbraco.Web.UI/umbraco/developer/Packages/proxy.htm b/src/Umbraco.Web.UI/umbraco/developer/Packages/proxy.htm deleted file mode 100644 index 43aef0ab96..0000000000 --- a/src/Umbraco.Web.UI/umbraco/developer/Packages/proxy.htm +++ /dev/null @@ -1,67 +0,0 @@ - - - - Repo proxy - - - - - - - diff --git a/src/Umbraco.Web.UI/umbraco/developer/Xslt/editXslt.aspx b/src/Umbraco.Web.UI/umbraco/developer/Xslt/editXslt.aspx index 8a58fd6589..f1ad582627 100644 --- a/src/Umbraco.Web.UI/umbraco/developer/Xslt/editXslt.aspx +++ b/src/Umbraco.Web.UI/umbraco/developer/Xslt/editXslt.aspx @@ -9,7 +9,7 @@ - + @@ -21,8 +21,7 @@ nameTxtBox: $('#<%= xsltFileName.ClientID %>'), originalFileName: '<%= xsltFileName.Text %>', saveButton: $("#<%= ((Control)SaveButton).ClientID %>"), - editorSourceElement: $('#<%= editorSource.ClientID %>'), - skipTestingCheckBox: $("#<%= SkipTesting.ClientID %>"), + editorSourceElement: $('#<%= editorSource.ClientID %>') }); editor.init(); @@ -32,7 +31,7 @@ })(jQuery); //TODO: Move these to EditXslt.js one day - var xsltSnippet = ""; + var xsltSnippet = ""; function xsltVisualize() { xsltSnippet = UmbEditor.IsSimpleEditor @@ -46,7 +45,7 @@ } UmbClientMgr.openModalWindow('<%= Umbraco.Core.IO.IOHelper.ResolveUrl(Umbraco.Core.IO.SystemDirectories.Umbraco) %>/developer/xslt/xsltVisualize.aspx', 'Visualize XSLT', true, 550, 650); - } + } @@ -56,14 +55,11 @@ - + - - - diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/protectPage.aspx b/src/Umbraco.Web.UI/umbraco/dialogs/protectPage.aspx index be515f693c..49e1a43ccf 100644 --- a/src/Umbraco.Web.UI/umbraco/dialogs/protectPage.aspx +++ b/src/Umbraco.Web.UI/umbraco/dialogs/protectPage.aspx @@ -103,11 +103,10 @@
@@ -137,7 +136,7 @@
- +

Member name already exists, click Change to use a different name or Update to continue

diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/publish.aspx b/src/Umbraco.Web.UI/umbraco/dialogs/publish.aspx index e86108616d..d6b6936144 100644 --- a/src/Umbraco.Web.UI/umbraco/dialogs/publish.aspx +++ b/src/Umbraco.Web.UI/umbraco/dialogs/publish.aspx @@ -80,7 +80,7 @@
-
+
  • diff --git a/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx b/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx deleted file mode 100644 index 6f5518706c..0000000000 --- a/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx +++ /dev/null @@ -1,88 +0,0 @@ -<%@ Page Language="C#" MasterPageFile="../../masterpages/umbracoPage.Master" AutoEventWireup="True" - CodeBehind="EditView.aspx.cs" Inherits="Umbraco.Web.UI.Umbraco.Settings.Views.EditView" - ValidateRequest="False" %> - -<%@ OutputCache Location="None" %> - -<%@ Import Namespace="Umbraco.Core" %> -<%@ Import Namespace="Umbraco.Core.IO" %> -<%@ Import Namespace="Umbraco.Web" %> -<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> -<%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx.cs b/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx.cs deleted file mode 100644 index c08e4ba48c..0000000000 --- a/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx.cs +++ /dev/null @@ -1,280 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Web; -using System.Web.UI; -using System.Web.UI.WebControls; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Web.Trees; -using Umbraco.Web.UI.Controls; -using umbraco; -using umbraco.BasePages; -using umbraco.cms.businesslogic.template; -using umbraco.cms.helpers; -using umbraco.cms.presentation.Trees; -using Umbraco.Core; -using umbraco.uicontrols; - -namespace Umbraco.Web.UI.Umbraco.Settings.Views -{ - public partial class EditView : global::umbraco.BasePages.UmbracoEnsuredPage - { - private Template _template; - public MenuButton SaveButton; - - public EditView() - { - CurrentApp = global::umbraco.BusinessLogic.DefaultApps.settings.ToString(); - } - - /// - /// The type of MVC/Umbraco view the editor is editing - /// - public enum ViewEditorType - { - Template, - PartialView, - PartialViewMacro - } - - /// - /// Returns the type of view being edited - /// - protected ViewEditorType EditorType - { - get - { - if (_template != null) return ViewEditorType.Template; - if (Request.QueryString["treeType"].IsNullOrWhiteSpace() == false && Request.QueryString["treeType"].InvariantEquals("partialViewMacros")) return ViewEditorType.PartialViewMacro; - return ViewEditorType.PartialView; - } - } - - protected string TemplateTreeSyncPath { get; private set; } - - /// - /// This view is shared between different trees so we'll look for the query string - /// - protected string CurrentTreeType - { - get - { - if (Request.QueryString["treeType"].IsNullOrWhiteSpace()) - { - return TreeDefinitionCollection.Instance.FindTree().Tree.Alias; - } - return Request.CleanForXss("treeType"); - } - } - - /// - /// Returns the original file name that the editor was loaded with - /// - /// - /// this is used for editing a partial view - /// - protected string OriginalFileName { get; private set; } - - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - - if (!IsPostBack) - { - - //configure screen for editing a template - if (_template != null) - { - MasterTemplate.Items.Add(new ListItem(ui.Text("none"), "0")); - var selectedTemplate = string.Empty; - - foreach (var t in Template.GetAllAsList()) - { - if (t.Id == _template.Id) continue; - - var li = new ListItem(t.Text, t.Id.ToString(CultureInfo.InvariantCulture)); - li.Attributes.Add("id", t.Alias.Replace(" ", "") + ".cshtml"); - MasterTemplate.Items.Add(li); - } - - try - { - if (_template.MasterTemplate > 0) - MasterTemplate.SelectedValue = _template.MasterTemplate.ToString(CultureInfo.InvariantCulture); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred setting a master template id", ex); - } - - MasterTemplate.SelectedValue = selectedTemplate; - NameTxt.Text = _template.GetRawText(); - AliasTxt.Text = _template.Alias; - editorSource.Text = _template.Design; - PathPrefix.Visible = false; - } - else - { - //configure editor for editing a file.... - - NameTxt.Text = OriginalFileName; - var svce = ApplicationContext.Current.Services.FileService; - var file = EditorType == ViewEditorType.PartialView - ? svce.GetPartialView(OriginalFileName) - : svce.GetPartialViewMacro(OriginalFileName); - editorSource.Text = file.Content; - - const string prefixFormat = "{0}"; - PathPrefix.Text = string.Format(prefixFormat, EditorType == ViewEditorType.PartialView - ? "Partials/" - : "MacroPartials/"); - } - } - - ClientTools - .SetActiveTreeType(CurrentTreeType) - .SyncTree(TemplateTreeSyncPath, false); - } - - - protected override void OnInit(EventArgs e) - { - base.OnInit(e); - - //check if a templateId is assigned, meaning we are editing a template - if (!Request.QueryString["templateID"].IsNullOrWhiteSpace()) - { - _template = new Template(int.Parse(Request.QueryString["templateID"])); - TemplateTreeSyncPath = "-1,init," + _template.Path.Replace("-1,", ""); - } - else if (!Request.QueryString["file"].IsNullOrWhiteSpace()) - { - //we are editing a view (i.e. partial view) - OriginalFileName = HttpUtility.UrlDecode(Request.QueryString["file"]); - - //TemplateTreeSyncPath = "-1,init," + Path.GetFileName(OriginalFileName); - - TemplateTreeSyncPath = DeepLink.GetTreePathFromFilePath(OriginalFileName.TrimStart("MacroPartials/").TrimStart("Partials/")); - } - else - { - throw new InvalidOperationException("Cannot render the editor without a supplied templateId or a file"); - } - - Panel1.hasMenu = true; - var editor = Panel1.NewTabPage(ui.Text("template")); - editor.Controls.Add(Pane8); - - var props = Panel1.NewTabPage(ui.Text("properties")); - props.Controls.Add(Pane7); - - - SaveButton = Panel1.Menu.NewButton(); - SaveButton.Text = ui.Text("save"); - SaveButton.ButtonType = MenuButtonType.Primary; - SaveButton.ID = "save"; - SaveButton.CssClass = "client-side"; - - Panel1.Text = ui.Text("edittemplate"); - pp_name.Text = ui.Text("name", base.getUser()); - pp_alias.Text = ui.Text("alias", base.getUser()); - pp_masterTemplate.Text = ui.Text("mastertemplate", base.getUser()); - - // Editing buttons - MenuIconI umbField = editorSource.Menu.NewIcon(); - umbField.ImageURL = UmbracoPath + "/images/editor/insField.gif"; - umbField.OnClickCommand = - ClientTools.Scripts.OpenModalWindow( - IOHelper.ResolveUrl(SystemDirectories.Umbraco) + "/dialogs/umbracoField.aspx?objectId=" + - editorSource.ClientID + "&tagName=UMBRACOGETDATA&mvcView=true", ui.Text("template", "insertPageField"), 640, 550); - umbField.AltText = ui.Text("template", "insertPageField"); - - - // TODO: Update icon - MenuIconI umbDictionary = editorSource.Menu.NewIcon(); - umbDictionary.ImageURL = GlobalSettings.Path + "/images/editor/dictionaryItem.gif"; - umbDictionary.OnClickCommand = - ClientTools.Scripts.OpenModalWindow( - IOHelper.ResolveUrl(SystemDirectories.Umbraco) + "/dialogs/umbracoField.aspx?objectId=" + - editorSource.ClientID + "&tagName=UMBRACOGETDICTIONARY&mvcView=true", ui.Text("template", "insertDictionaryItem"), - 640, 550); - umbDictionary.AltText = "Insert umbraco dictionary item"; - - var macroSplitButton = new InsertMacroSplitButton - { - ClientCallbackInsertMacroMarkup = "function(alias) {editViewEditor.insertMacroMarkup(alias);}", - ClientCallbackOpenMacroModel = "function(alias) {editViewEditor.openMacroModal(alias);}" - }; - editorSource.Menu.InsertNewControl(macroSplitButton, 40); - - MenuIconI umbTemplateQueryBuilder = editorSource.Menu.NewIcon(); - umbTemplateQueryBuilder.ImageURL = UmbracoPath + "/images/editor/inshtml.gif"; - umbTemplateQueryBuilder.OnClickCommand = "editViewEditor.openQueryModal()"; - umbTemplateQueryBuilder.AltText = "Open query builder"; - - if (_template == null) - { - InitializeEditorForPartialView(); - } - else - { - InitializeEditorForTemplate(); - } - - } - - protected override void OnPreRender(EventArgs e) - { - base.OnPreRender(e); - ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/codeEditorSave.asmx")); - ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/legacyAjaxCalls.asmx")); - } - - /// - /// Configure the editor for partial view editing - /// - private void InitializeEditorForPartialView() - { - pp_masterTemplate.Visible = false; - pp_alias.Visible = false; - pp_name.Text = "Filename"; - } - - /// - /// Configure the editor for editing a template - /// - private void InitializeEditorForTemplate() - { - - //TODO: implement content placeholders, etc... just like we had in v5 - - editorSource.Menu.InsertSplitter(); - - MenuIconI umbRenderBody = editorSource.Menu.NewIcon(); - umbRenderBody.ImageURL = UmbracoPath + "/images/editor/renderbody.gif"; - //umbContainer.AltText = ui.Text("template", "insertContentAreaPlaceHolder"); - umbRenderBody.AltText = "Insert @RenderBody()"; - - umbRenderBody.OnClickCommand = "editViewEditor.insertRenderBody()"; - - MenuIconI umbSection = editorSource.Menu.NewIcon(); - umbSection.ImageURL = UmbracoPath + "/images/editor/masterpagePlaceHolder.gif"; - //umbContainer.AltText = ui.Text("template", "insertContentAreaPlaceHolder"); - umbSection.AltText = "Insert Section"; - - umbSection.OnClickCommand = "editViewEditor.openSnippetModal('section')"; - - MenuIconI umbRenderSection = editorSource.Menu.NewIcon(); - umbRenderSection.ImageURL = UmbracoPath + "/images/editor/masterpageContent.gif"; - //umbContainer.AltText = ui.Text("template", "insertContentAreaPlaceHolder"); - umbRenderSection.AltText = "Insert @RenderSection"; - - umbRenderSection.OnClickCommand = "editViewEditor.openSnippetModal('rendersection')"; - - } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx.designer.cs b/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx.designer.cs deleted file mode 100644 index dd81da023a..0000000000 --- a/src/Umbraco.Web.UI/umbraco/settings/views/EditView.aspx.designer.cs +++ /dev/null @@ -1,132 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Umbraco.Web.UI.Umbraco.Settings.Views { - - - public partial class EditView { - - /// - /// JsInclude1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.JsInclude JsInclude1; - - /// - /// Panel1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.TabView Panel1; - - /// - /// Pane8 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane Pane8; - - /// - /// pp_source control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_source; - - /// - /// editorSource control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.CodeArea editorSource; - - /// - /// Pane7 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane Pane7; - - /// - /// pp_name control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_name; - - /// - /// PathPrefix control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Literal PathPrefix; - - /// - /// NameTxt control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox NameTxt; - - /// - /// pp_alias control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_alias; - - /// - /// AliasTxt control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox AliasTxt; - - /// - /// pp_masterTemplate control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_masterTemplate; - - /// - /// MasterTemplate control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.DropDownList MasterTemplate; - } -} diff --git a/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx b/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx index 815a420f88..d80a60c92c 100644 --- a/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx +++ b/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx @@ -27,7 +27,7 @@ - diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/BrowseRepository.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/BrowseRepository.aspx.cs deleted file mode 100644 index 647a84232a..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/BrowseRepository.aspx.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Web; -using System.Web.SessionState; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Web.UI.HtmlControls; -using System.Xml; -using System.Xml.XPath; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Web; - -namespace umbraco.presentation.developer.packages { - public partial class BrowseRepository : BasePages.UmbracoEnsuredPage { - - public BrowseRepository() - { - CurrentApp = BusinessLogic.DefaultApps.developer.ToString(); - - } - - protected void Page_Load(object sender, System.EventArgs e) { - - Exception ex = new Exception(); - if (!cms.businesslogic.packager.Settings.HasFileAccess(ref ex)) { - fb.Style.Add("margin-top", "7px"); - fb.type = global::umbraco.uicontrols.Feedback.feedbacktype.error; - fb.Text = "" + ui.Text("errors", "filePermissionsError") + ":
    " + ex.Message; - } - - string category = Request.CleanForXss("category"); - string repoGuid = Request.CleanForXss("repoGuid"); - - var repo = cms.businesslogic.packager.repositories.Repository.getByGuid(repoGuid); - if (repo == null) - { - throw new InvalidOperationException("Could not find repository with id " + repoGuid); - } - - string url = repo.RepositoryUrl; - - Panel1.Text = "Browse repository: " + repo.Name; - - if (!string.IsNullOrEmpty(category)) - category = "&category=" + category; - - iframeGen.Text = - string.Format( - "", - url, repoGuid, category, Request.ServerVariables["SERVER_NAME"], - Request.ServerVariables["SERVER_PORT"], IOHelper.ResolveUrl(SystemDirectories.Umbraco), - IOHelper.ResolveUrl(SystemDirectories.Umbraco).Trim('/'), repoGuid, - UmbracoVersion.Current.Major, - UmbracoVersion.Current.Minor, - UmbracoVersion.Current.Build, - UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema.ToString(), Environment.Version, - Umbraco.Core.SystemUtilities.GetCurrentTrustLevel()); - } - - - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/BrowseRepository.aspx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/BrowseRepository.aspx.designer.cs deleted file mode 100644 index 1612a7daa0..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/BrowseRepository.aspx.designer.cs +++ /dev/null @@ -1,43 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:2.0.50727.3053 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace umbraco.presentation.developer.packages { - - - public partial class BrowseRepository { - - /// - /// Panel1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.UmbracoPanel Panel1; - - /// - /// fb control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Feedback fb; - - /// - /// iframeGen control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Literal iframeGen; - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx deleted file mode 100644 index d79cc6e881..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx +++ /dev/null @@ -1,32 +0,0 @@ -<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="LoadNitros.ascx.cs" Inherits="umbraco.presentation.developer.packages.LoadNitros" %> - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx.cs deleted file mode 100644 index a9701c9ded..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System.IO; -using System; -using System.Data; -using System.Drawing; -using System.Web; -using System.Web.UI.WebControls; -using System.Web.UI.HtmlControls; -using System.Collections; -using System.Web.UI; -using System.Web.UI.WebControls.WebParts; -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using umbraco.BusinessLogic; - -namespace umbraco.presentation.developer.packages { - public partial class LoadNitros : System.Web.UI.UserControl { - - private List _nitroList = new List(); - private List _selectedNitros = new List(); - - protected void Page_Load(object sender, EventArgs e) { } - - public void installNitros(object sender, EventArgs e) { - - string repoGuid = "65194810-1f85-11dd-bd0b-0800200c9a66"; //Hardcoded official package repo key. - - var p = new cms.businesslogic.packager.Installer(Umbraco.Web.UmbracoContext.Current.Security.CurrentUser.Id); - var repo = cms.businesslogic.packager.repositories.Repository.getByGuid(repoGuid); - - if (repo == null) - { - throw new InvalidOperationException("Could not find repository with id " + repoGuid); - } - - foreach (CheckBox cb in _nitroList) { - if (cb.Checked) { - if (!_selectedNitros.Contains(cb.ID)) - _selectedNitros.Add(cb.ID); - } - } - - foreach (string guid in _selectedNitros) { - - string tempFile = p.Import(repo.fetch(guid)); - p.LoadConfig(tempFile); - - int pId = p.CreateManifest(tempFile, guid, repoGuid); - - //and then copy over the files. This will take some time if it contains .dlls that will reboot the system.. - p.InstallFiles(pId, tempFile); - - //finally install the businesslogic - p.InstallBusinessLogic(pId, tempFile); - - //cleanup.. - p.InstallCleanUp(pId, tempFile); - - } - - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearAllCache(); - ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.ClearAllCaches(); - library.RefreshContent(); - - loadNitros.Visible = false; - - RaiseBubbleEvent(new object(), new EventArgs()); - } - - protected void onCategoryDataBound(object sender, RepeaterItemEventArgs e) { - - if (e.Item.ItemType == ListItemType.AlternatingItem || e.Item.ItemType == ListItemType.Item) { - - cms.businesslogic.packager.repositories.Category cat = (cms.businesslogic.packager.repositories.Category)e.Item.DataItem; - Literal _name = (Literal)e.Item.FindControl("lit_name"); - Literal _desc = (Literal)e.Item.FindControl("lit_desc"); - PlaceHolder _nitros = (PlaceHolder)e.Item.FindControl("ph_nitroHolder"); - - _name.Text = cat.Text; - _desc.Text = cat.Description; - - e.Item.Visible = false; - - foreach (cms.businesslogic.packager.repositories.Package nitro in cat.Packages) { - bool installed = cms.businesslogic.packager.InstalledPackage.isPackageInstalled(nitro.RepoGuid.ToString()); - int localPackageID = 0; - - if (installed) - localPackageID = cms.businesslogic.packager.InstalledPackage.GetByGuid(nitro.RepoGuid.ToString()).Data.Id; - - CheckBox cb_nitro = new CheckBox(); - cb_nitro.ID = nitro.RepoGuid.ToString(); - cb_nitro.Enabled = !installed; - - cb_nitro.CssClass = "nitroCB"; - - cb_nitro.Text = "

    " + nitro.Text; - - if (installed) { - cb_nitro.CssClass = "nitroCB installed"; - cb_nitro.Text += "Already installed"; - } - - cb_nitro.Text += "

    " + nitro.Description + "
    "; - - if (!string.IsNullOrEmpty(nitro.Demo)) { - cb_nitro.Text += "Demonstration  "; - } - - if (!string.IsNullOrEmpty(nitro.Documentation)) { - cb_nitro.Text += "Documentation  "; - } - - cb_nitro.Text += "

    "; - - _nitros.Controls.Add(cb_nitro); - _nitroList.Add(cb_nitro); - - e.Item.Visible = true; - - if (nitro.EditorsPick) { - - CheckBox cb_Recnitro = new CheckBox(); - cb_Recnitro.ID = nitro.RepoGuid.ToString(); - cb_Recnitro.CssClass = "nitroCB"; - cb_Recnitro.Enabled = !installed; - - cb_Recnitro.Text = "

    " + nitro.Text; - - if (installed) { - cb_Recnitro.CssClass = "nitroCB installed"; - cb_Recnitro.Text += "Already installed"; - } - - cb_Recnitro.Text += "

    " + nitro.Description + "
    "; - - if (!string.IsNullOrEmpty(nitro.Demo)) { - cb_Recnitro.Text += "Demonstration  "; - } - - if (!string.IsNullOrEmpty(nitro.Documentation)) { - cb_Recnitro.Text += "Documentation  "; - } - - cb_Recnitro.Text += "

    "; - - - _nitroList.Add(cb_Recnitro); - ph_recommendedHolder.Controls.Add(cb_Recnitro); - - } - } - - } - } - - - protected override void OnInit(EventArgs e) { - - base.OnInit(e); - - string repoGuid = "65194810-1f85-11dd-bd0b-0800200c9a66"; - var repo = cms.businesslogic.packager.repositories.Repository.getByGuid(repoGuid); - - if (repo == null) - { - throw new InvalidOperationException("Could not find repository with id " + repoGuid); - } - - var fb = new global::umbraco.uicontrols.Feedback(); - fb.type = global::umbraco.uicontrols.Feedback.feedbacktype.error; - fb.Text = "No connection to repository. Modules could not be fetched from the repository as there was no connection to: '" + repo.RepositoryUrl + "'"; - - - if (repo.HasConnection()) - { - try - { - - if (UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema) - rep_nitros.DataSource = repo.Webservice.NitrosCategorizedByVersion(cms.businesslogic.packager.repositories.Version.Version4); - else - rep_nitros.DataSource = repo.Webservice.NitrosCategorizedByVersion(cms.businesslogic.packager.repositories.Version.Version41); - - rep_nitros.DataBind(); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred", ex); - - loadNitros.Controls.Clear(); - loadNitros.Controls.Add(fb); - //nitroList.Visible = false; - //lt_status.Text = "

    Nitros could not be fetched from the repository. Please check your internet connection

    You can always install Nitros later in the packages section

    " + ex.ToString() + "

    "; - } - } - else - { - loadNitros.Controls.Clear(); - loadNitros.Controls.Add(fb); - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx.designer.cs deleted file mode 100644 index 50aca0d082..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/LoadNitros.ascx.designer.cs +++ /dev/null @@ -1,52 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:2.0.50727.3053 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace umbraco.presentation.developer.packages { - - - public partial class LoadNitros { - - /// - /// loadNitros control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel loadNitros; - - /// - /// ph_recommendedHolder control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.PlaceHolder ph_recommendedHolder; - - /// - /// rep_nitros control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Repeater rep_nitros; - - /// - /// bt_install control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button bt_install; - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/SubmitPackage.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/SubmitPackage.aspx deleted file mode 100644 index a882fedef2..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/SubmitPackage.aspx +++ /dev/null @@ -1,113 +0,0 @@ -<%@ Page Language="C#" Title="Submit package" MasterPageFile="../../masterpages/umbracoPage.Master" AutoEventWireup="true" CodeBehind="SubmitPackage.aspx.cs" Inherits="umbraco.presentation.developer.packages.SubmitPackage" %> -<%@ Register TagPrefix="cc2" Namespace="umbraco.uicontrols" Assembly="controls" %> - - - - - - - - - - - -
    - - -
    -

    - -

    -
    - - - - - -

    Choose the repository you want to submit the package to

    -
    - - - -
    - - - -
    - - - -

    Upload additional documentation for your package to help new users getting started with your package

    -
    - - - - - -
    - - -
    - -
    -

    By clicking "submit package" below you understand that your package will be submitted to a package repository and will in some cases be publicly available to download.

    -

    Please notice: only packages with complete read-me, author information and install information gets considered for inclusion.

    -

    The package administrators group reservers the right to decline packages based on lack of documentation, poorly written readme and missing author information

    -
    - -

    -  <%= umbraco.ui.Text("or") %>  "><%= umbraco.ui.Text("cancel") %> -

    -
    - -
    -
    diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/SubmitPackage.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/SubmitPackage.aspx.cs deleted file mode 100644 index 67e69a4b99..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/SubmitPackage.aspx.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Data; -using System.Configuration; -using System.Collections; -using System.Collections.Generic; - -using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Web.UI.WebControls.WebParts; -using System.Web.UI.HtmlControls; - -namespace umbraco.presentation.developer.packages { - public partial class SubmitPackage : BasePages.UmbracoEnsuredPage { - - public SubmitPackage() - { - CurrentApp = BusinessLogic.DefaultApps.developer.ToString(); - - } - private cms.businesslogic.packager.PackageInstance pack; - private cms.businesslogic.packager.CreatedPackage createdPackage; - - protected void Page_Load(object sender, EventArgs e) { - - if(!String.IsNullOrEmpty(helper.Request("id"))){ - - if (!IsPostBack) { - dd_repositories.Items.Clear(); - - dd_repositories.Items.Add(new ListItem("Choose a repository...", "")); - - List repos = cms.businesslogic.packager.repositories.Repository.getAll(); - - if (repos.Count == 1) { - ListItem li = new ListItem(repos[0].Name, repos[0].Guid); - li.Selected = true; - - dd_repositories.Items.Add(li); - - pl_repoChoose.Visible = false; - pl_repoLogin.Style.Clear(); - - privateRepoHelp.Visible = false; - publicRepoHelp.Style.Clear(); - - } else if (repos.Count == 0) { - Response.Redirect("editpackage.aspx?id=" + helper.Request("id")); - } else { - - foreach (cms.businesslogic.packager.repositories.Repository repo in repos) { - dd_repositories.Items.Add(new ListItem(repo.Name, repo.Guid)); - } - - dd_repositories.Items[0].Selected = true; - - dd_repositories.Attributes.Add("onChange", "onRepoChange()"); - - } - } - - createdPackage = cms.businesslogic.packager.CreatedPackage.GetById(int.Parse(helper.Request("id"))); - pack = createdPackage.Data; - - if (pack.Url != "") { - Panel1.Text = "Submit '" + pack.Name + "' to a repository"; - } - } - } - - protected void submitPackage(object sender, EventArgs e) { - - Page.Validate(); - string feedback = ""; - - if (Page.IsValid) { - - try { - var repo = cms.businesslogic.packager.repositories.Repository.getByGuid(dd_repositories.SelectedValue); - - if (repo == null) - { - throw new InvalidOperationException("Could not find repository with id " + dd_repositories.SelectedValue); - } - - var memberKey = repo.Webservice.authenticate(tb_email.Text, library.md5(tb_password.Text)); - - byte[] doc = new byte[0]; - - if (fu_doc.HasFile) - doc = fu_doc.FileBytes; - - - - if (memberKey != "") { - - string result = repo.SubmitPackage(memberKey, pack, doc).ToString().ToLower(); - - switch (result) { - case "complete": - feedback = "Your package has been submitted successfully. It will be reviewed by the package repository administrator before it's publicly available"; - fb_feedback.type = global::umbraco.uicontrols.Feedback.feedbacktype.success; - break; - case "error": - feedback = "There was a general error submitting your package to the repository. This can be due to general communitations error or too much traffic. Please try again later"; - fb_feedback.type = global::umbraco.uicontrols.Feedback.feedbacktype.error; - break; - case "exists": - feedback = "This package has already been submitted to the repository. You cannot submit it again. If you have updates for a package, you should contact the repositor administrator to submit an update"; - fb_feedback.type = global::umbraco.uicontrols.Feedback.feedbacktype.error; - break; - case "noaccess": - feedback = "Authentication failed, You do not have access to this repository. Contact your package repository administrator"; - fb_feedback.type = global::umbraco.uicontrols.Feedback.feedbacktype.error; - break; - default: - break; - } - - if (result == "complete") { - Pane1.Visible = false; - Pane2.Visible = false; - submitControls.Visible = false; - feedbackControls.Visible = true; - } else { - Pane1.Visible = true; - Pane2.Visible = true; - submitControls.Visible = true; - feedbackControls.Visible = false; - } - - } else { - feedback = "Authentication failed, You do not have access to this repository. Contact your package repository administrator"; - fb_feedback.type = global::umbraco.uicontrols.Feedback.feedbacktype.error; - } - } catch { - feedback = "Authentication failed, or the repository is currently off-line. Contact your package repository administrator"; - fb_feedback.type = global::umbraco.uicontrols.Feedback.feedbacktype.error; - } - - fb_feedback.Text = feedback; - } - - - - } - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/SubmitPackage.aspx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/SubmitPackage.aspx.designer.cs deleted file mode 100644 index 7eb884632a..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/SubmitPackage.aspx.designer.cs +++ /dev/null @@ -1,214 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:2.0.50727.3053 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace umbraco.presentation.developer.packages { - - - public partial class SubmitPackage { - - /// - /// Panel1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.UmbracoPanel Panel1; - - /// - /// fb_feedback control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Feedback fb_feedback; - - /// - /// feedbackControls control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.PlaceHolder feedbackControls; - - /// - /// Pane2 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane Pane2; - - /// - /// pl_repoChoose control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel pl_repoChoose; - - /// - /// dd_repositories control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.DropDownList dd_repositories; - - /// - /// pl_repoLogin control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel pl_repoLogin; - - /// - /// PropertyPanel1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel PropertyPanel1; - - /// - /// publicRepoHelp control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.HtmlControls.HtmlGenericControl publicRepoHelp; - - /// - /// privateRepoHelp control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.HtmlControls.HtmlGenericControl privateRepoHelp; - - /// - /// PropertyPanel2 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel PropertyPanel2; - - /// - /// tb_email control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox tb_email; - - /// - /// RequiredFieldValidator1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.RequiredFieldValidator RequiredFieldValidator1; - - /// - /// PropertyPanel3 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel PropertyPanel3; - - /// - /// tb_password control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox tb_password; - - /// - /// Pane1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane Pane1; - - /// - /// PropertyPanel4 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel PropertyPanel4; - - /// - /// PropertyPanel5 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel PropertyPanel5; - - /// - /// fu_doc control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.FileUpload fu_doc; - - /// - /// doc_regex control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.RegularExpressionValidator doc_regex; - - /// - /// submitControls control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.PlaceHolder submitControls; - - /// - /// bt_submit control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button bt_submit; - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx index bd34c32478..270e58fd19 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx @@ -36,7 +36,6 @@ ControlToValidate="packageVersion">* - diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs index 091996f7f0..1bd3918ec2 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs @@ -48,16 +48,10 @@ namespace umbraco.presentation.developer.packages cp = new ContentPicker(); content.Controls.Add(cp); - - bt_submitButton.Attributes.Add("onClick", "window.location = 'submitpackage.aspx?id=" + pack.Id.ToString() + "'; return false;"); - + if (string.IsNullOrEmpty(pack.PackagePath) == false) { - packageUmbFile.Text = "   Download"; - - if (cms.businesslogic.packager.repositories.Repository.getAll().Count > 0) - bt_submitButton.Visible = true; - + packageUmbFile.Text = "   Download"; } else { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.designer.cs index a4c21695a7..dfd3eaa5d7 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.designer.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.designer.cs @@ -47,6 +47,7 @@ namespace umbraco.presentation.developer.packages { protected global::System.Web.UI.WebControls.RegularExpressionValidator VersionValidator; protected global::System.Web.UI.WebControls.RequiredFieldValidator RequiredFieldValidator7; + /// /// packageName control. /// @@ -128,15 +129,6 @@ namespace umbraco.presentation.developer.packages { /// protected global::umbraco.uicontrols.PropertyPanel pp_file; - /// - /// bt_submitButton control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button bt_submitButton; - /// /// packageUmbFile control. /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx deleted file mode 100644 index aa565cde1d..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx +++ /dev/null @@ -1,180 +0,0 @@ -<%@ Page Language="C#" MasterPageFile="../../masterpages/umbracoPage.Master" AutoEventWireup="true" CodeBehind="installedPackage.aspx.cs" Inherits="umbraco.presentation.developer.packages.installedPackage" %> -<%@ Register TagPrefix="cc2" Namespace="umbraco.uicontrols" Assembly="controls" %> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    - - -
    - - - - - - - - - - - - -

    - <%= umbraco.ui.Text("packager", "packageUpgradeText") %> -

    -
    - - -

    - -

    - -

    - -

    -
    -
    - - -
    - - <%= umbraco.ui.Text("packager", "packageNoItemsText") %> - -

    - -

    -
    - -
    - - - -

    - <%= umbraco.ui.Text("packager", "packageUninstallText") %> -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    - - -

    -
    -
    - - - -
    - Please wait while the browser is reloaded... -
    - - - - -
    - -
    -
    diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.cs deleted file mode 100644 index 2473a4cb5b..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.cs +++ /dev/null @@ -1,635 +0,0 @@ -using System; -using System.Data; -using System.Configuration; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Web.UI.WebControls.WebParts; -using System.Web.UI.HtmlControls; -using Umbraco.Core.IO; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using umbraco.BusinessLogic; -using umbraco.cms.businesslogic.web; -using runtimeMacro = umbraco.macro; -using System.Xml; -using umbraco.cms.presentation.Trees; -using BizLogicAction = umbraco.BusinessLogic.Actions.Action; -using Macro = umbraco.cms.businesslogic.macro.Macro; -using Template = umbraco.cms.businesslogic.template.Template; - -namespace umbraco.presentation.developer.packages -{ - public partial class installedPackage : BasePages.UmbracoEnsuredPage - { - public installedPackage() - { - CurrentApp = DefaultApps.developer.ToString(); - } - - private cms.businesslogic.packager.InstalledPackage _pack; - private cms.businesslogic.packager.repositories.Repository _repo = new cms.businesslogic.packager.repositories.Repository(); - - protected void Page_Load(object sender, EventArgs e) - { - - if (Request.QueryString["id"] != null) - { - _pack = cms.businesslogic.packager.InstalledPackage.GetById(int.Parse(Request.QueryString["id"])); - - lt_packagename.Text = _pack.Data.Name; - lt_packageVersion.Text = _pack.Data.Version; - lt_packageAuthor.Text = _pack.Data.Author; - lt_readme.Text = library.ReplaceLineBreaks( _pack.Data.Readme ); - - bt_confirmUninstall.Attributes.Add("onClick", "jQuery('#buttons').hide(); jQuery('#loadingbar').show();; return true;"); - - - if (!Page.IsPostBack) - { - //temp list to contain failing items... - var tempList = new List(); - - foreach (var str in _pack.Data.Documenttypes) - { - var tId = 0; - if (int.TryParse(str, out tId)) - { - try - { - var dc = new DocumentType(tId); - var li = new ListItem(dc.Text, dc.Id.ToString()); - li.Selected = true; - documentTypes.Items.Add(li); - } - catch - { - tempList.Add(str); - } - } - } - //removing failing documentTypes items from the uninstall manifest - SyncLists(_pack.Data.Documenttypes, tempList); - - - foreach (var str in _pack.Data.Templates) - { - var tId = 0; - if (int.TryParse(str, out tId)) - { - try - { - var t = new Template(tId); - var li = new ListItem(t.Text, t.Id.ToString()); - li.Selected = true; - templates.Items.Add(li); - } - catch - { - tempList.Add(str); - } - } - } - //removing failing template items from the uninstall manifest - SyncLists(_pack.Data.Templates, tempList); - - foreach (string str in _pack.Data.Stylesheets) - { - int tId = 0; - if (int.TryParse(str, out tId)) - { - try - { - var s = new StyleSheet(tId); - ListItem li = new ListItem(s.Text, s.Id.ToString()); - li.Selected = true; - stylesheets.Items.Add(li); - } - catch - { - tempList.Add(str); - } - } - } - //removing failing stylesheet items from the uninstall manifest - SyncLists(_pack.Data.Stylesheets, tempList); - - foreach (var str in _pack.Data.Macros) - { - var tId = 0; - if (int.TryParse(str, out tId)) - { - try - { - var m = new Macro(tId); - if (!string.IsNullOrEmpty(m.Name)) - { - //Macros need an extra check to see if they actually exists. For some reason the macro does not return null, if the id is not found... - var li = new ListItem(m.Name, m.Id.ToString()); - li.Selected = true; - macros.Items.Add(li); - } - else - { - tempList.Add(str); - } - } - catch - { - tempList.Add(str); - } - } - } - //removing failing macros items from the uninstall manifest - SyncLists(_pack.Data.Macros, tempList); - - foreach (var str in _pack.Data.Files) - { - try - { - if (!string.IsNullOrEmpty(str) && System.IO.File.Exists(IOHelper.MapPath(str) )) - { - var li = new ListItem(str, str); - li.Selected = true; - files.Items.Add(li); - } - else - { - tempList.Add(str); - } - - } - catch - { - tempList.Add(str); - } - } - - //removing failing files from the uninstall manifest - SyncLists(_pack.Data.Files, tempList); - - foreach (string str in _pack.Data.DictionaryItems) - { - var tId = 0; - - if (int.TryParse(str, out tId)) - { - try - { - var di = new cms.businesslogic.Dictionary.DictionaryItem(tId); - - var li = new ListItem(di.key, di.id.ToString()); - li.Selected = true; - - dictionaryItems.Items.Add(li); - } - catch - { - tempList.Add(str); - } - } - } - - //removing failing files from the uninstall manifest - SyncLists(_pack.Data.DictionaryItems, tempList); - - - foreach (var str in _pack.Data.DataTypes) - { - var tId = 0; - - if (int.TryParse(str, out tId)) - { - try - { - var dtd = new cms.businesslogic.datatype.DataTypeDefinition(tId); - - if (dtd != null) - { - var li = new ListItem(dtd.Text, dtd.Id.ToString()); - li.Selected = true; - - dataTypes.Items.Add(li); - } - else - { - tempList.Add(str); - } - } - catch - { - tempList.Add(str); - } - } - } - - //removing failing files from the uninstall manifest - SyncLists(_pack.Data.DataTypes, tempList); - - //save the install manifest, so even tho the user doesn't uninstall, it stays uptodate. - _pack.Save(); - - - //Look for updates on packages. - if (!string.IsNullOrEmpty(_pack.Data.RepositoryGuid) && !string.IsNullOrEmpty(_pack.Data.PackageGuid)) - { - try - { - - _repo = cms.businesslogic.packager.repositories.Repository.getByGuid(_pack.Data.RepositoryGuid); - - if (_repo != null) - { - hl_packageRepo.Text = _repo.Name; - hl_packageRepo.NavigateUrl = "BrowseRepository.aspx?repoGuid=" + _repo.Guid; - pp_repository.Visible = true; - } - - var repoPackage = _repo.Webservice.PackageByGuid(_pack.Data.PackageGuid); - - if (repoPackage != null) - { - if (repoPackage.HasUpgrade && repoPackage.UpgradeVersion != _pack.Data.Version) - { - pane_upgrade.Visible = true; - lt_upgradeReadme.Text = repoPackage.UpgradeReadMe; - bt_gotoUpgrade.OnClientClick = "window.location.href = 'browseRepository.aspx?url=" + repoPackage.Url + "'; return true;"; - } - - if (!string.IsNullOrEmpty(repoPackage.Demo)) - { - lb_demoLink.OnClientClick = "openDemo(this, '" + _pack.Data.PackageGuid + "'); return false;"; - pp_documentation.Visible = true; - } - - if (!string.IsNullOrEmpty(repoPackage.Documentation)) - { - hl_docLink.NavigateUrl = repoPackage.Documentation; - hl_docLink.Target = "_blank"; - pp_documentation.Visible = true; - } - } - } - catch - { - pane_upgrade.Visible = false; - } - } - - - var deletePackage = true; - //sync the UI to match what is in the package - if (macros.Items.Count == 0) - pp_macros.Visible = false; - else - deletePackage = false; - - if (documentTypes.Items.Count == 0) - pp_docTypes.Visible = false; - else - deletePackage = false; - - if (files.Items.Count == 0) - pp_files.Visible = false; - else - deletePackage = false; - - if (templates.Items.Count == 0) - pp_templates.Visible = false; - else - deletePackage = false; - - if (stylesheets.Items.Count == 0) - pp_css.Visible = false; - else - deletePackage = false; - - if (dictionaryItems.Items.Count == 0) - pp_di.Visible = false; - else - deletePackage = false; - - if (dataTypes.Items.Count == 0) - pp_dt.Visible = false; - else - deletePackage = false; - - - if (deletePackage) - { - pane_noItems.Visible = true; - pane_uninstall.Visible = false; - } - - // List the package version history [LK 2013-067-10] - Version v; - var packageVersionHistory = cms.businesslogic.packager.InstalledPackage.GetAllInstalledPackages() - .Where(x => x.Data.Id != _pack.Data.Id && string.Equals(x.Data.Name, _pack.Data.Name, StringComparison.OrdinalIgnoreCase)) - .OrderBy(x => Version.TryParse(x.Data.Version, out v) ? v : new Version()); - - if (packageVersionHistory != null && packageVersionHistory.Count() > 0) - { - rptr_versions.DataSource = packageVersionHistory; - rptr_versions.DataBind(); - - pane_versions.Visible = true; - } - } - } - } - - private static void SyncLists(List list, List removed) - { - foreach (var str in removed) - { - list.Remove(str); - } - - for (var i = 0; i < list.Count; i++) - { - if (String.IsNullOrEmpty(list[i].Trim())) - list.RemoveAt(i); - } - - removed.Clear(); - } - - protected void delPack(object sender, EventArgs e) - { - _pack.Delete(UmbracoUser.Id); - pane_uninstalled.Visible = true; - pane_uninstall.Visible = false; - } - - - protected void confirmUnInstall(object sender, EventArgs e) - { - var refreshCache = false; - - //Uninstall Stylesheets - foreach (ListItem li in stylesheets.Items) - { - if (li.Selected) - { - int nId; - - if (int.TryParse(li.Value, out nId)) - { - var s = new StyleSheet(nId); - s.delete(); - _pack.Data.Stylesheets.Remove(nId.ToString()); - } - } - } - - //Uninstall templates - foreach (ListItem li in templates.Items) - { - if (li.Selected) - { - int nId; - - if (int.TryParse(li.Value, out nId)) - { - var found = ApplicationContext.Services.FileService.GetTemplate(nId); - if (found != null) - { - ApplicationContext.Services.FileService.DeleteTemplate(found.Alias, UmbracoUser.Id); - } - _pack.Data.Templates.Remove(nId.ToString()); - } - } - } - - //Uninstall macros - foreach (ListItem li in macros.Items) - { - if (li.Selected) - { - int nId; - - if (int.TryParse(li.Value, out nId)) - { - var s = new Macro(nId); - if (!string.IsNullOrEmpty(s.Name)) - { - s.Delete(); - } - - _pack.Data.Macros.Remove(nId.ToString()); - } - } - } - - //Remove Document Types - var contentTypes = new List(); - var contentTypeService = ApplicationContext.Current.Services.ContentTypeService; - foreach (ListItem li in documentTypes.Items) - { - if (li.Selected) - { - int nId; - - if (int.TryParse(li.Value, out nId)) - { - var contentType = contentTypeService.GetContentType(nId); - if (contentType != null) - { - contentTypes.Add(contentType); - _pack.Data.Documenttypes.Remove(nId.ToString(CultureInfo.InvariantCulture)); - // refresh content cache when document types are removed - refreshCache = true; - } - } - } - } - //Order the DocumentTypes before removing them - if (contentTypes.Any()) - { - var orderedTypes = (from contentType in contentTypes - orderby contentType.ParentId descending, contentType.Id descending - select contentType); - - foreach (var contentType in orderedTypes) - { - contentTypeService.Delete(contentType); - } - } - - //Remove Dictionary items - foreach (ListItem li in dictionaryItems.Items) - { - if (li.Selected) - { - int nId; - - if (int.TryParse(li.Value, out nId)) - { - var di = new cms.businesslogic.Dictionary.DictionaryItem(nId); - di.delete(); - _pack.Data.DictionaryItems.Remove(nId.ToString()); - } - } - } - - //Remove Data types - foreach (ListItem li in dataTypes.Items) - { - if (li.Selected) - { - int nId; - - if (int.TryParse(li.Value, out nId)) - { - var dtd = new cms.businesslogic.datatype.DataTypeDefinition(nId); - dtd.delete(); - _pack.Data.DataTypes.Remove(nId.ToString()); - } - } - } - - _pack.Save(); - - if (!IsManifestEmpty()) - { - Response.Redirect(Request.RawUrl); - } - else - { - - // uninstall actions - try - { - var actionsXml = new XmlDocument(); - actionsXml.LoadXml("" + _pack.Data.Actions + ""); - - LogHelper.Debug("executing undo actions: {0}", () => actionsXml.OuterXml); - - foreach (XmlNode n in actionsXml.DocumentElement.SelectNodes("//Action")) - { - try - { - cms.businesslogic.packager.PackageAction.UndoPackageAction(_pack.Data.Name, n.Attributes["alias"].Value, n); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred running undo actions", ex); - } - } - } - catch (Exception ex) - { - LogHelper.Error("An error occurred running undo actions", ex); - } - - //moved remove of files here so custom package actions can still undo - //Remove files - foreach (ListItem li in files.Items) - { - if (li.Selected) - { - //here we need to try to find the file in question as most packages does not support the tilde char - - var file = IOHelper.FindFile(li.Value); - - var filePath = IOHelper.MapPath(file); - if (System.IO.File.Exists(filePath)) - { - System.IO.File.Delete(filePath); - _pack.Data.Files.Remove(li.Value); - } - } - } - _pack.Save(); - _pack.Delete(UmbracoUser.Id); - - pane_uninstalled.Visible = true; - pane_uninstall.Visible = false; - - } - - // refresh cache - if (refreshCache) - { - library.RefreshContent(); - } - - //ensure that all tree's are refreshed after uninstall - ClientTools.ClearClientTreeCache() - .RefreshTree(); - - TreeDefinitionCollection.Instance.ReRegisterTrees(); - - BizLogicAction.ReRegisterActionsAndHandlers(); - - } - - private bool IsManifestEmpty() - { - - _pack.Data.Documenttypes.TrimExcess(); - _pack.Data.Files.TrimExcess(); - _pack.Data.Macros.TrimExcess(); - _pack.Data.Stylesheets.TrimExcess(); - _pack.Data.Templates.TrimExcess(); - _pack.Data.DataTypes.TrimExcess(); - _pack.Data.DictionaryItems.TrimExcess(); - - var lists = new List> - { - _pack.Data.Documenttypes, - _pack.Data.Macros, - _pack.Data.Stylesheets, - _pack.Data.Templates, - _pack.Data.DictionaryItems, - _pack.Data.DataTypes - }; - - //Not including files, since there might be assemblies that contain package actions - //lists.Add(pack.Data.Files); - - return lists.SelectMany(list => list).All(str => string.IsNullOrEmpty(str.Trim())); - } - - protected override void OnInit(EventArgs e) - { - base.OnInit(e); - - Panel1.Text = ui.Text("treeHeaders", "installedPackages"); - pane_meta.Text = ui.Text("packager", "packageMetaData"); - pp_name.Text = ui.Text("packager", "packageName"); - pp_version.Text = ui.Text("packager", "packageVersion"); - pp_author.Text = ui.Text("packager", "packageAuthor"); - pp_repository.Text = ui.Text("packager", "packageRepository"); - pp_documentation.Text = ui.Text("packager", "packageDocumentation"); - pp_readme.Text = ui.Text("packager", "packageReadme"); - hl_docLink.Text = ui.Text("packager", "packageDocumentation"); - lb_demoLink.Text = ui.Text("packager", "packageDemonstration"); - - pane_versions.Text = ui.Text("packager", "packageVersionHistory"); - pane_noItems.Text = ui.Text("packager", "packageNoItemsHeader"); - - pane_uninstall.Text = ui.Text("packager", "packageUninstallHeader"); - bt_deletePackage.Text = ui.Text("packager", "packageUninstallHeader"); - bt_confirmUninstall.Text = ui.Text("packager", "packageUninstallConfirm"); - - pane_uninstalled.Text = ui.Text("packager", "packageUninstalledHeader"); - - var general = Panel1.NewTabPage(ui.Text("packager", "packageName")); - general.Controls.Add(pane_meta); - general.Controls.Add(pane_versions); - - - var uninstall = Panel1.NewTabPage(ui.Text("packager", "packageUninstallHeader")); - uninstall.Controls.Add(pane_noItems); - uninstall.Controls.Add(pane_uninstall); - uninstall.Controls.Add(pane_uninstalled); - } - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.designer.cs deleted file mode 100644 index 10c933e861..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.designer.cs +++ /dev/null @@ -1,402 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace umbraco.presentation.developer.packages { - - - public partial class installedPackage { - - /// - /// Panel1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.TabView Panel1; - - /// - /// pane_meta control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pane_meta; - - /// - /// pp_name control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_name; - - /// - /// lt_packagename control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Literal lt_packagename; - - /// - /// pp_version control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_version; - - /// - /// lt_packageVersion control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Literal lt_packageVersion; - - /// - /// pp_author control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_author; - - /// - /// lt_packageAuthor control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Literal lt_packageAuthor; - - /// - /// pp_documentation control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_documentation; - - /// - /// hl_docLink control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.HyperLink hl_docLink; - - /// - /// lb_demoLink control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.LinkButton lb_demoLink; - - /// - /// pp_repository control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_repository; - - /// - /// hl_packageRepo control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.HyperLink hl_packageRepo; - - /// - /// pp_readme control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_readme; - - /// - /// lt_readme control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Literal lt_readme; - - /// - /// pane_versions control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pane_versions; - - /// - /// pp_versions control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_versions; - - /// - /// rptr_versions control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Repeater rptr_versions; - - /// - /// pane_upgrade control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pane_upgrade; - - /// - /// pp_upgradeInstruction control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_upgradeInstruction; - - /// - /// lt_upgradeReadme control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Literal lt_upgradeReadme; - - /// - /// bt_gotoUpgrade control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button bt_gotoUpgrade; - - /// - /// pane_noItems control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pane_noItems; - - /// - /// bt_deletePackage control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button bt_deletePackage; - - /// - /// pane_uninstall control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pane_uninstall; - - /// - /// pp_docTypes control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_docTypes; - - /// - /// documentTypes control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBoxList documentTypes; - - /// - /// pp_templates control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_templates; - - /// - /// templates control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBoxList templates; - - /// - /// pp_css control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_css; - - /// - /// stylesheets control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBoxList stylesheets; - - /// - /// pp_macros control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_macros; - - /// - /// macros control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBoxList macros; - - /// - /// pp_files control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_files; - - /// - /// files control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBoxList files; - - /// - /// pp_di control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_di; - - /// - /// dictionaryItems control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBoxList dictionaryItems; - - /// - /// pp_dt control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_dt; - - /// - /// dataTypes control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBoxList dataTypes; - - /// - /// pp_confirm control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_confirm; - - /// - /// bt_confirmUninstall control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button bt_confirmUninstall; - - /// - /// progbar control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.ProgressBar progbar; - - /// - /// pane_uninstalled control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pane_uninstalled; - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs index b88b0f1d4f..03ce088941 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs @@ -177,24 +177,6 @@ namespace umbraco.cms.presentation.developer /// protected global::System.Web.UI.WebControls.TextBox xsltFileName; - /// - /// pp_testing control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_testing; - - /// - /// SkipTesting control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBox SkipTesting; - /// /// pp_errorMsg control. /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/mediaPicker.aspx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/mediaPicker.aspx.designer.cs index 23024b07b7..e1342377a4 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/mediaPicker.aspx.designer.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/mediaPicker.aspx.designer.cs @@ -6,10 +6,11 @@ // the code is regenerated. // //------------------------------------------------------------------------------ +using System; namespace umbraco.presentation.umbraco.dialogs { - - + + [Obsolete("This is no longer used and will be removed in future versions")] public partial class mediaPicker { /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs index 2499c1c817..9ba5f91bd3 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs @@ -99,7 +99,7 @@ namespace umbraco.presentation.preview //Inject preview xml parentId = document.Level == 1 ? -1 : document.ParentId; var previewXml = document.ToPreviewXml(XmlContent); - if (document.ContentEntity.Published == false + if (document.ContentEntity.Published == false && ApplicationContext.Current.Services.ContentService.HasPublishedVersion(document.Id)) previewXml.Attributes.Append(XmlContent.CreateAttribute("isDraft")); XmlContent = content.GetAddOrUpdateXmlNode(XmlContent, document.Id, document.Level, parentId, previewXml); @@ -187,13 +187,9 @@ namespace umbraco.presentation.preview private static void CleanPreviewDirectory(int userId, DirectoryInfo dir) { - foreach (FileInfo file in dir.GetFiles(userId + "_*.config")) - { - DeletePreviewFile(userId, file); - } // also delete any files accessed more than 10 minutes ago var now = DateTime.Now; - foreach (FileInfo file in dir.GetFiles("*.config")) + foreach (var file in dir.GetFiles("*.config")) { if ((now - file.LastAccessTime).TotalMinutes > 10) DeletePreviewFile(userId, file); @@ -206,6 +202,12 @@ namespace umbraco.presentation.preview { file.Delete(); } + catch (IOException) + { + // for *some* reason deleting the file can fail, + // and it will work later on (long-lasting locks, etc), + // so just ignore the exception + } catch (Exception ex) { LogHelper.Error(string.Format("Couldn't delete preview set: {0} - User {1}", file.Name, userId), ex); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Image.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Image.cs index c920d43c7a..dd0c0e0554 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Image.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Image.cs @@ -1,10 +1,12 @@ -using System.Web.UI; +using System; +using System.Web.UI; using System.Web.UI.HtmlControls; using Umbraco.Core.Media; using Umbraco.Web.Media; namespace umbraco.presentation.templateControls { + [Obsolete("This is no longer used and will be removed in future versions")] public class Image : HtmlImage { public string NodeId { get; set; } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Resources.Designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Resources.Designer.cs index 9aa180110b..3adaf973ca 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Resources.Designer.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Resources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18034 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs index 411734ae99..42626d406f 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs @@ -7,10 +7,11 @@ using Umbraco.Core; namespace umbraco { - /// - /// Extension methods for umbraco.cms.businesslogic.media.Media - /// - public static class MediaExtensions + /// + /// Extension methods for umbraco.cms.businesslogic.media.Media + /// + [Obsolete("Obsolete, Use Umbraco.Core.Models.Media", false)] + public static class MediaExtensions { /// /// Functionally similar to the XPath axis 'ancestor' @@ -203,12 +204,13 @@ namespace umbraco return string.Empty; } - /// - /// Gets the image thumbnail URL. - /// - /// an umbraco.cms.businesslogic.media.Media object - /// - public static string GetImageThumbnailUrl(this Media media) + /// + /// Gets the image thumbnail URL. + /// + /// an umbraco.cms.businesslogic.media.Media object + /// + [Obsolete("This should no longer be used, thumbnail generation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] + public static string GetImageThumbnailUrl(this Media media) { if (media.ContentType.Alias.Equals(Constants.Conventions.MediaTypes.Image)) { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index 9734401d95..91a8677c81 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Configuration.Provider; using System.Globalization; using System.IO; +using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; @@ -320,13 +321,14 @@ namespace umbraco.cms.presentation.user } // Populate dropdowns - foreach (DocumentType dt in DocumentType.GetAllAsList()) - cDocumentType.Items.Add( - new ListItem(dt.Text, dt.Alias) - ); + var allContentTypes = Services.ContentTypeService.GetAllContentTypes().ToList(); + foreach (var dt in allContentTypes) + { + cDocumentType.Items.Add(new ListItem(dt.Name, dt.Alias)); + } // populate fields - ArrayList fields = new ArrayList(); + var fields = new ArrayList(); cDescription.ID = "cDescription"; cCategories.ID = "cCategories"; cExcerpt.ID = "cExcerpt"; @@ -334,9 +336,9 @@ namespace umbraco.cms.presentation.user cCategories.Items.Add(new ListItem(ui.Text("choose"), "")); cExcerpt.Items.Add(new ListItem(ui.Text("choose"), "")); - foreach (PropertyType pt in PropertyType.GetAll()) + foreach (var pt in allContentTypes.SelectMany(x => x.PropertyTypes).OrderBy(x => x.Name)) { - if (!fields.Contains(pt.Alias)) + if (fields.Contains(pt.Alias) == false) { cDescription.Items.Add(new ListItem(string.Format("{0} ({1})", pt.Name, pt.Alias), pt.Alias)); cCategories.Items.Add(new ListItem(string.Format("{0} ({1})", pt.Name, pt.Alias), pt.Alias)); diff --git a/src/UmbracoExamine/BaseUmbracoIndexer.cs b/src/UmbracoExamine/BaseUmbracoIndexer.cs index ebb718898b..1d87b0dfff 100644 --- a/src/UmbracoExamine/BaseUmbracoIndexer.cs +++ b/src/UmbracoExamine/BaseUmbracoIndexer.cs @@ -32,6 +32,12 @@ namespace UmbracoExamine /// public abstract class BaseUmbracoIndexer : LuceneIndexer { + // note + // wrapping all operations that end up calling base.SafelyProcessQueueItems in a safe call + // context because they will fork a thread/task/whatever which should *not* capture our + // call context (and the database it can contain)! ideally we should be able to override + // SafelyProcessQueueItems but that's not possible in the current version of Examine. + #region Constructors /// @@ -49,6 +55,7 @@ namespace UmbracoExamine /// /// /// + /// protected BaseUmbracoIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) : base(indexerData, indexPath, analyzer, async) { @@ -61,6 +68,19 @@ namespace UmbracoExamine DataService = dataService; } + /// + /// Creates an NRT indexer + /// + /// + /// + /// + /// + protected BaseUmbracoIndexer(IIndexCriteria indexerData, IndexWriter writer, IDataService dataService, bool async) + : base(indexerData, writer, async) + { + DataService = dataService; + } + #endregion /// @@ -95,7 +115,7 @@ namespace UmbracoExamine /// Determines if the manager will call the indexing methods when content is saved or deleted as /// opposed to cache being updated. /// - public bool SupportUnpublishedContent { get; protected set; } + public bool SupportUnpublishedContent { get; protected internal set; } /// /// The data service used for retreiving and submitting data to the cms @@ -268,7 +288,12 @@ namespace UmbracoExamine { if (CanInitialize()) { - base.RebuildIndex(); + // remove the db from lcc + using (new SafeCallContext()) + //using (ApplicationContext.Current.DatabaseContext.UseSafeDatabase()) + { + base.RebuildIndex(); + } // will try to re-instate the original DB *but* if a DB has been created in the meantime what shall we do? } } @@ -282,7 +307,10 @@ namespace UmbracoExamine { if (CanInitialize()) { - base.IndexAll(type); + using (new SafeCallContext()) + { + base.IndexAll(type); + } } } @@ -293,7 +321,10 @@ namespace UmbracoExamine if (!SupportedTypes.Contains(type)) return; - base.ReIndexNode(node, type); + using (new SafeCallContext()) + { + base.ReIndexNode(node, type); + } } } @@ -307,7 +338,10 @@ namespace UmbracoExamine { if (CanInitialize()) { - base.DeleteFromIndex(nodeId); + using (new SafeCallContext()) + { + base.DeleteFromIndex(nodeId); + } } } @@ -449,31 +483,32 @@ namespace UmbracoExamine } #endregion - [Obsolete("This method is not be used, it will be removed in future versions")] [EditorBrowsable(EditorBrowsableState.Never)] private void AddNodesToIndex(string xPath, string type) { - // Get all the nodes of nodeTypeAlias == nodeTypeAlias - XDocument xDoc = GetXDocument(xPath, type); - if (xDoc != null) + using (new SafeCallContext()) { - var rootNode = xDoc.Root; - if (rootNode != null) + // Get all the nodes of nodeTypeAlias == nodeTypeAlias + XDocument xDoc = GetXDocument(xPath, type); + if (xDoc != null) { - //the result will either be a single doc with an id as the root, or it will - // be multiple docs with a wrapper, we need to check for this - if (rootNode.HasAttributes) + var rootNode = xDoc.Root; + if (rootNode != null) { - AddNodesToIndex(new[] {rootNode}, type); - } - else - { - AddNodesToIndex(rootNode.Elements(), type); + //the result will either be a single doc with an id as the root, or it will + // be multiple docs with a wrapper, we need to check for this + if (rootNode.HasAttributes) + { + AddNodesToIndex(new[] { rootNode }, type); + } + else + { + AddNodesToIndex(rootNode.Elements(), type); + } } } } - } } } diff --git a/src/UmbracoExamine/Config/IndexSetExtensions.cs b/src/UmbracoExamine/Config/IndexSetExtensions.cs index 1255f50a3c..f4bd2e24b2 100644 --- a/src/UmbracoExamine/Config/IndexSetExtensions.cs +++ b/src/UmbracoExamine/Config/IndexSetExtensions.cs @@ -14,7 +14,7 @@ namespace UmbracoExamine.Config public static class IndexSetExtensions { internal static IIndexCriteria ToIndexCriteria(this IndexSet set, IDataService svc, - IEnumerable indexFieldPolicies) + StaticFieldCollection indexFieldPolicies) { return new LazyIndexCriteria(set, svc, indexFieldPolicies); } @@ -29,7 +29,7 @@ namespace UmbracoExamine.Config /// public static IIndexCriteria ToIndexCriteria(this IndexSet set, IDataService svc) { - return set.ToIndexCriteria(svc, Enumerable.Empty()); + return set.ToIndexCriteria(svc, new StaticFieldCollection()); } } diff --git a/src/UmbracoExamine/Config/LazyIndexCriteria.cs b/src/UmbracoExamine/Config/LazyIndexCriteria.cs index 72ab3f31ba..ee58431930 100644 --- a/src/UmbracoExamine/Config/LazyIndexCriteria.cs +++ b/src/UmbracoExamine/Config/LazyIndexCriteria.cs @@ -12,7 +12,7 @@ namespace UmbracoExamine.Config public LazyIndexCriteria( IndexSet set, IDataService svc, - IEnumerable indexFieldPolicies) + StaticFieldCollection indexFieldPolicies) { if (set == null) throw new ArgumentNullException("set"); if (indexFieldPolicies == null) throw new ArgumentNullException("indexFieldPolicies"); @@ -35,8 +35,9 @@ namespace UmbracoExamine.Config foreach (var u in userProps) { var field = new IndexField() { Name = u }; - var policy = indexFieldPolicies.FirstOrDefault(x => x.Name == u); - if (policy != null) + + StaticField policy; + if (indexFieldPolicies.TryGetValue(u, out policy)) { field.Type = policy.Type; field.EnableSorting = policy.EnableSorting; @@ -55,8 +56,9 @@ namespace UmbracoExamine.Config foreach (var s in sysProps) { var field = new IndexField() { Name = s }; - var policy = indexFieldPolicies.FirstOrDefault(x => x.Name == s); - if (policy != null) + + StaticField policy; + if (indexFieldPolicies.TryGetValue(s, out policy)) { field.Type = policy.Type; field.EnableSorting = policy.EnableSorting; diff --git a/src/UmbracoExamine/DataServices/UmbracoContentService.cs b/src/UmbracoExamine/DataServices/UmbracoContentService.cs index 3cd94751ae..b52f60a2f2 100644 --- a/src/UmbracoExamine/DataServices/UmbracoContentService.cs +++ b/src/UmbracoExamine/DataServices/UmbracoContentService.cs @@ -1,39 +1,23 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Security; -using System.Text; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services; -using umbraco; using System.Xml.Linq; -using System.Xml; -using umbraco.cms.businesslogic.web; using System.Collections; using System.Xml.XPath; -using umbraco.DataLayer; -using umbraco.BusinessLogic; -using UmbracoExamine.Config; using Examine.LuceneEngine; -using System.Data.SqlClient; -using System.Diagnostics; namespace UmbracoExamine.DataServices { public class UmbracoContentService : IContentService { - private readonly ApplicationContext _applicationContext; public UmbracoContentService() : this(ApplicationContext.Current) - { - - } + { } public UmbracoContentService(ApplicationContext applicationContext) { @@ -73,13 +57,19 @@ namespace UmbracoExamine.DataServices [Obsolete("This should no longer be used, latest content will be indexed by using the IContentService directly")] public XDocument GetLatestContentByXPath(string xpath) { - var xmlContent = XDocument.Parse(""); - foreach (var c in _applicationContext.Services.ContentService.GetRootContent()) + using (var scope = ApplicationContext.Current.ScopeProvider.CreateScope()) { - xmlContent.Root.Add(c.ToDeepXml(_applicationContext.Services.PackagingService)); + var xmlContent = XDocument.Parse(""); + var rootContent = _applicationContext.Services.ContentService.GetRootContent(); + foreach (var c in rootContent) + { + // not sure this uses the database, but better be save + xmlContent.Root.Add(c.ToDeepXml(_applicationContext.Services.PackagingService)); + } + var result = ((IEnumerable)xmlContent.XPathEvaluate(xpath)).Cast(); + scope.Complete(); + return result.ToXDocument(); } - var result = ((IEnumerable)xmlContent.XPathEvaluate(xpath)).Cast(); - return result.ToXDocument(); } /// @@ -90,7 +80,12 @@ namespace UmbracoExamine.DataServices /// public bool IsProtected(int nodeId, string path) { - return _applicationContext.Services.PublicAccessService.IsProtected(path.EnsureEndsWith("," + nodeId)); + using (var scope = ApplicationContext.Current.ScopeProvider.CreateScope()) + { + var ret = _applicationContext.Services.PublicAccessService.IsProtected(path.EnsureEndsWith("," + nodeId)); + scope.Complete(); + return ret; + } } /// @@ -100,17 +95,20 @@ namespace UmbracoExamine.DataServices public IEnumerable GetAllUserPropertyNames() { - try + using (var scope = ApplicationContext.Current.ScopeProvider.CreateScope()) { - - var result = _applicationContext.DatabaseContext.Database.Fetch("select distinct alias from cmsPropertyType order by alias"); - return result; - } - catch (Exception ex) - { - LogHelper.Error("EXCEPTION OCCURRED reading GetAllUserPropertyNames", ex); - return Enumerable.Empty(); - } + try + { + var result = _applicationContext.DatabaseContext.Database.Fetch("select distinct alias from cmsPropertyType order by alias"); + scope.Complete(); + return result; + } + catch (Exception ex) + { + LogHelper.Error("EXCEPTION OCCURRED reading GetAllUserPropertyNames", ex); + return Enumerable.Empty(); + } + } } /// diff --git a/src/UmbracoExamine/StaticFieldCollection.cs b/src/UmbracoExamine/StaticFieldCollection.cs new file mode 100644 index 0000000000..909271e0b5 --- /dev/null +++ b/src/UmbracoExamine/StaticFieldCollection.cs @@ -0,0 +1,28 @@ +using System.Collections.ObjectModel; + +namespace UmbracoExamine +{ + internal class StaticFieldCollection : KeyedCollection + { + protected override string GetKeyForItem(StaticField item) + { + return item.Name; + } + + /// + /// Implements TryGetValue using the underlying dictionary + /// + /// + /// + /// + public bool TryGetValue(string key, out StaticField field) + { + if (Dictionary == null) + { + field = null; + return false; + } + return Dictionary.TryGetValue(key, out field); + } + } +} \ No newline at end of file diff --git a/src/UmbracoExamine/UmbracoContentIndexer.cs b/src/UmbracoExamine/UmbracoContentIndexer.cs index efc3e4a214..0a3eca1f1e 100644 --- a/src/UmbracoExamine/UmbracoContentIndexer.cs +++ b/src/UmbracoExamine/UmbracoContentIndexer.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Xml; using System.Xml.Linq; using Examine; using Lucene.Net.Documents; @@ -15,6 +17,7 @@ using Examine.LuceneEngine; using Examine.LuceneEngine.Config; using UmbracoExamine.Config; using Lucene.Net.Analysis; +using Lucene.Net.Index; using Umbraco.Core.Persistence.Querying; using IContentService = Umbraco.Core.Services.IContentService; using IMediaService = Umbraco.Core.Services.IMediaService; @@ -33,6 +36,7 @@ namespace UmbracoExamine private readonly IUserService _userService; private readonly IContentTypeService _contentTypeService; private readonly EntityXmlSerializer _serializer = new EntityXmlSerializer(); + private const int PageSize = 10000; #region Constructors @@ -144,6 +148,34 @@ namespace UmbracoExamine _contentTypeService = contentTypeService; } + /// + /// Creates an NRT indexer + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public UmbracoContentIndexer(IIndexCriteria indexerData, IndexWriter writer, IDataService dataService, + IContentService contentService, + IMediaService mediaService, + IDataTypeService dataTypeService, + IUserService userService, + IContentTypeService contentTypeService, + bool async) + : base(indexerData, writer, dataService, async) + { + _contentService = contentService; + _mediaService = mediaService; + _dataTypeService = dataTypeService; + _userService = userService; + _contentTypeService = contentTypeService; + } + #endregion #region Constants & Fields @@ -152,6 +184,7 @@ namespace UmbracoExamine /// Used to store the path of a content object /// public const string IndexPathFieldName = "__Path"; + public const string NodeKeyFieldName = "__Key"; public const string NodeTypeAliasFieldName = "__NodeTypeAlias"; public const string IconFieldName = "__Icon"; @@ -165,27 +198,27 @@ namespace UmbracoExamine /// Alot of standard umbraco fields shouldn't be tokenized or even indexed, just stored into lucene /// for retreival after searching. /// - internal static readonly List IndexFieldPolicies - = new List + internal static readonly StaticFieldCollection IndexFieldPolicies + = new StaticFieldCollection { new StaticField("id", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), new StaticField("key", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), - new StaticField( "version", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), - new StaticField( "parentID", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), - new StaticField( "level", FieldIndexTypes.NOT_ANALYZED, true, "NUMBER"), - new StaticField( "writerID", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), - new StaticField( "creatorID", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), - new StaticField( "nodeType", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), - new StaticField( "template", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), - new StaticField( "sortOrder", FieldIndexTypes.NOT_ANALYZED, true, "NUMBER"), - new StaticField( "createDate", FieldIndexTypes.NOT_ANALYZED, false, "DATETIME"), - new StaticField( "updateDate", FieldIndexTypes.NOT_ANALYZED, false, "DATETIME"), - new StaticField( "nodeName", FieldIndexTypes.ANALYZED, false, string.Empty), - new StaticField( "urlName", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), - new StaticField( "writerName", FieldIndexTypes.ANALYZED, false, string.Empty), - new StaticField( "creatorName", FieldIndexTypes.ANALYZED, false, string.Empty), - new StaticField( "nodeTypeAlias", FieldIndexTypes.ANALYZED, false, string.Empty), - new StaticField( "path", FieldIndexTypes.NOT_ANALYZED, false, string.Empty) + new StaticField("version", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), + new StaticField("parentID", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), + new StaticField("level", FieldIndexTypes.NOT_ANALYZED, true, "NUMBER"), + new StaticField("writerID", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), + new StaticField("creatorID", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), + new StaticField("nodeType", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), + new StaticField("template", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), + new StaticField("sortOrder", FieldIndexTypes.NOT_ANALYZED, true, "NUMBER"), + new StaticField("createDate", FieldIndexTypes.NOT_ANALYZED, false, "DATETIME"), + new StaticField("updateDate", FieldIndexTypes.NOT_ANALYZED, false, "DATETIME"), + new StaticField("nodeName", FieldIndexTypes.ANALYZED, false, string.Empty), + new StaticField("urlName", FieldIndexTypes.NOT_ANALYZED, false, string.Empty), + new StaticField("writerName", FieldIndexTypes.ANALYZED, false, string.Empty), + new StaticField("creatorName", FieldIndexTypes.ANALYZED, false, string.Empty), + new StaticField("nodeTypeAlias", FieldIndexTypes.ANALYZED, false, string.Empty), + new StaticField("path", FieldIndexTypes.NOT_ANALYZED, false, string.Empty) }; #endregion @@ -229,7 +262,13 @@ namespace UmbracoExamine SupportProtectedContent = supportProtected; else SupportProtectedContent = false; - + + bool disableXmlDocLookup; + if (config["disableXmlDocLookup"] != null && bool.TryParse(config["disableXmlDocLookup"], out disableXmlDocLookup)) + DisableXmlDocumentLookup = disableXmlDocLookup; + else + DisableXmlDocumentLookup = false; + base.Initialize(name, config); } @@ -237,6 +276,11 @@ namespace UmbracoExamine #region Properties + /// + /// Whether to use the cmsContentXml data to re-index when possible (i.e. for published content, media and members) + /// + public bool DisableXmlDocumentLookup { get; private set; } + /// /// By default this is false, if set to true then the indexer will include indexing content that is flagged as publicly protected. /// This property is ignored if SupportUnpublishedContent is set to true. @@ -359,131 +403,249 @@ namespace UmbracoExamine } #endregion - #region Protected - - /// - /// This is a static query, it's parameters don't change so store statically - /// - private IQuery _publishedQuery; + #region Protected protected override void PerformIndexAll(string type) { - const int pageSize = 10000; + if (SupportedTypes.Contains(type) == false) + return; + var pageIndex = 0; - switch (type) + DataService.LogService.AddInfoLog(-1, string.Format("PerformIndexAll - Start data queries - {0}", type)); + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + try { - case IndexTypes.Content: - var contentParentId = -1; - if (IndexerData.ParentNodeId.HasValue && IndexerData.ParentNodeId.Value > 0) - { - contentParentId = IndexerData.ParentNodeId.Value; - } - IContent[] content; - - //used to track non-published entities so we can determine what items are implicitly not published - var notPublished = new HashSet(); - - do - { - long total; - - IEnumerable descendants; - if (SupportUnpublishedContent) + switch (type) + { + case IndexTypes.Content: + var contentParentId = -1; + if (IndexerData.ParentNodeId.HasValue && IndexerData.ParentNodeId.Value > 0) { - descendants = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out total); - } - else - { - if (_publishedQuery == null) - { - _publishedQuery = Query.Builder.Where(x => x.Published == true); - } - - //get all paged records but order by level ascending, we need to do this because we need to track which nodes are not published so that we can determine - // which descendent nodes are implicitly not published - descendants = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out total, "level", Direction.Ascending, true, (string)null); - } - - //if specific types are declared we need to post filter them - //TODO: Update the service layer to join the cmsContentType table so we can query by content type too - if (IndexerData.IncludeNodeTypes.Any()) - { - content = descendants.Where(x => IndexerData.IncludeNodeTypes.Contains(x.ContentType.Alias)).ToArray(); - } - else - { - content = descendants.ToArray(); - } - AddNodesToIndex(GetSerializedContent(content, notPublished).WhereNotNull(), type); - pageIndex++; - } while (content.Length == pageSize); - - notPublished.Clear(); - - break; - case IndexTypes.Media: - var mediaParentId = -1; - - if (IndexerData.ParentNodeId.HasValue && IndexerData.ParentNodeId.Value > 0) - { - mediaParentId = IndexerData.ParentNodeId.Value; - } - - XElement[] mediaXElements; - - var mediaTypes = _contentTypeService.GetAllMediaTypes().ToArray(); - var icons = mediaTypes.ToDictionary(x => x.Id, y => y.Icon); - - do - { - long total; - if (mediaParentId == -1) - { - mediaXElements = _mediaService.GetPagedXmlEntries("-1", pageIndex, pageSize, out total).ToArray(); - } - else - { - //Get the parent - var parent = _mediaService.GetById(mediaParentId); - if (parent == null) - mediaXElements = new XElement[0]; - else - mediaXElements = _mediaService.GetPagedXmlEntries(parent.Path, pageIndex, pageSize, out total).ToArray(); - } - - //if specific types are declared we need to post filter them - //TODO: Update the service layer to join the cmsContentType table so we can query by content type too - if (IndexerData.IncludeNodeTypes.Any()) - { - var includeNodeTypeIds = mediaTypes.Where(x => IndexerData.IncludeNodeTypes.Contains(x.Alias)).Select(x => x.Id); - mediaXElements = mediaXElements.Where(elm => includeNodeTypeIds.Contains(elm.AttributeValue("nodeType"))).ToArray(); + contentParentId = IndexerData.ParentNodeId.Value; } - foreach (var element in mediaXElements) + if (SupportUnpublishedContent == false && DisableXmlDocumentLookup == false) { - element.Add(new XAttribute("icon", icons[element.AttributeValue("nodeType")])); + //get all node Ids that have a published version - this is a fail safe check, in theory + // only document nodes that have a published version would exist in the cmsContentXml table + var allNodesWithPublishedVersions = ApplicationContext.Current.DatabaseContext.Database.Fetch( + "select DISTINCT cmsDocument.nodeId from cmsDocument where cmsDocument.published = 1"); + + XElement last = null; + var trackedIds = new HashSet(); + + ReindexWithXmlEntries(type, contentParentId, + () => _contentTypeService.GetAllContentTypes().ToArray(), + (path, pIndex, pSize) => + { + long totalContent; + + //sorted by: umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder + var result = _contentService.GetPagedXmlEntries(path, pIndex, pSize, out totalContent).ToArray(); + var more = result.Length == pSize; + + //then like we do in the ContentRepository.BuildXmlCache we need to track what Parents have been processed + // already so that we can then exclude implicitly unpublished content items + var filtered = new List(); + + foreach (var xml in result) + { + var id = xml.AttributeValue("id"); + + //don't include this if it doesn't have a published version + if (allNodesWithPublishedVersions.Contains(id) == false) + continue; + + var parentId = xml.AttributeValue("parentID"); + + if (parentId == null) continue; //this shouldn't happen + + //if the parentid is changing + if (last != null && last.AttributeValue("parentID") != parentId) + { + var found = trackedIds.Contains(parentId); + if (found == false) + { + //Need to short circuit here, if the parent is not there it means that the parent is unpublished + // and therefore the child is not published either so cannot be included in the xml cache + continue; + } + } + + last = xml; + trackedIds.Add(xml.AttributeValue("id")); + + filtered.Add(xml); + } + + return Tuple.Create(filtered.ToArray(), more); + }, + i => _contentService.GetById(i)); + } + else + { + //used to track non-published entities so we can determine what items are implicitly not published + //currently this is not in use apart form in tests + var notPublished = new HashSet(); + + int currentPageSize; + do + { + long total; + + IContent[] descendants; + if (SupportUnpublishedContent) + { + descendants = _contentService.GetPagedDescendants(contentParentId, pageIndex, PageSize, out total, "umbracoNode.id").ToArray(); + } + else + { + //get all paged records but order by level ascending, we need to do this because we need to track which nodes are not published so that we can determine + // which descendent nodes are implicitly not published + descendants = _contentService.GetPagedDescendants(contentParentId, pageIndex, PageSize, out total, "level", Direction.Ascending, true, (string)null).ToArray(); + } + + // need to store decendants count before filtering, in order for loop to work correctly + currentPageSize = descendants.Length; + + //if specific types are declared we need to post filter them + //TODO: Update the service layer to join the cmsContentType table so we can query by content type too + IEnumerable content; + if (IndexerData.IncludeNodeTypes.Any()) + { + content = descendants.Where(x => IndexerData.IncludeNodeTypes.Contains(x.ContentType.Alias)); + } + else + { + content = descendants; + } + + AddNodesToIndex(GetSerializedContent( + SupportUnpublishedContent, + c => _serializer.Serialize(_contentService, _dataTypeService, _userService, c), + content, notPublished).WhereNotNull(), type); + + pageIndex++; + } while (currentPageSize == PageSize); } - AddNodesToIndex(mediaXElements, type); - pageIndex++; - } while (mediaXElements.Length == pageSize); + break; + case IndexTypes.Media: + var mediaParentId = -1; - break; + if (IndexerData.ParentNodeId.HasValue && IndexerData.ParentNodeId.Value > 0) + { + mediaParentId = IndexerData.ParentNodeId.Value; + } + + ReindexWithXmlEntries(type, mediaParentId, + () => _contentTypeService.GetAllMediaTypes().ToArray(), + (path, pIndex, pSize) => + { + long totalMedia; + var result = _mediaService.GetPagedXmlEntries(path, pIndex, pSize, out totalMedia).ToArray(); + var more = result.Length == pSize; + return Tuple.Create(result, more); + }, + i => _mediaService.GetById(i)); + + break; + } } + finally + { + stopwatch.Stop(); + } + + DataService.LogService.AddInfoLog(-1, string.Format("PerformIndexAll - End data queries - {0}, took {1}ms", type, stopwatch.ElapsedMilliseconds)); } - private IEnumerable GetSerializedContent(IEnumerable content, ISet notPublished) + /// + /// Performs a reindex of a type based on looking up entries from the cmsContentXml table - but using callbacks to get this data since + /// we don't have a common underlying service interface for the media/content stuff + /// + /// + /// + /// + /// + /// + internal void ReindexWithXmlEntries( + string type, + int parentId, + Func getContentTypes, + Func> getPagedXmlEntries, + Func getContent) + where TContentType: IContentTypeComposition + { + var pageIndex = 0; + + var contentTypes = getContentTypes(); + var icons = contentTypes.ToDictionary(x => x.Id, y => y.Icon); + var parent = parentId == -1 ? null : getContent(parentId); + bool more; + + do + { + XElement[] xElements; + + if (parentId == -1) + { + var pagedElements = getPagedXmlEntries("-1", pageIndex, PageSize); + xElements = pagedElements.Item1; + more = pagedElements.Item2; + } + else if (parent == null) + { + xElements = new XElement[0]; + more = false; + } + else + { + var pagedElements = getPagedXmlEntries(parent.Path, pageIndex, PageSize); + xElements = pagedElements.Item1; + more = pagedElements.Item2; + } + + //if specific types are declared we need to post filter them + //TODO: Update the service layer to join the cmsContentType table so we can query by content type too + if (IndexerData.IncludeNodeTypes.Any()) + { + var includeNodeTypeIds = contentTypes.Where(x => IndexerData.IncludeNodeTypes.Contains(x.Alias)).Select(x => x.Id); + xElements = xElements.Where(elm => includeNodeTypeIds.Contains(elm.AttributeValue("nodeType"))).ToArray(); + } + + foreach (var element in xElements) + { + if (element.Attribute("icon") == null) + { + element.Add(new XAttribute("icon", icons[element.AttributeValue("nodeType")])); + } + } + + AddNodesToIndex(xElements, type); + pageIndex++; + } while (more); + } + + internal static IEnumerable GetSerializedContent( + bool supportUnpublishdContent, + Func serializer, + IEnumerable content, + ISet notPublished) { foreach (var c in content) { - if (SupportUnpublishedContent == false) + if (supportUnpublishdContent == false) { //if we don't support published content and this is not published then track it and return null if (c.Published == false) { notPublished.Add(c.Path); yield return null; + continue; } //if we don't support published content, check if this content item exists underneath any already tracked @@ -491,14 +653,11 @@ namespace UmbracoExamine if (notPublished.Any(path => c.Path.StartsWith(string.Format("{0},", path)))) { yield return null; + continue; } - } + } - var xml = _serializer.Serialize( - _contentService, - _dataTypeService, - _userService, - c); + var xml = serializer(c); //add a custom 'icon' attribute xml.Add(new XAttribute("icon", c.ContentType.Icon)); @@ -520,7 +679,7 @@ namespace UmbracoExamine public override void RebuildIndex() { - DataService.LogService.AddVerboseLog(-1, "Rebuilding index"); + DataService.LogService.AddInfoLog(-1, "Rebuilding index"); base.RebuildIndex(); } @@ -551,15 +710,16 @@ namespace UmbracoExamine // Get all user data that we want to index and store into a dictionary foreach (var field in IndexerData.UserFields) { - if (e.Fields.ContainsKey(field.Name)) + string fieldVal; + if (e.Fields.TryGetValue(field.Name, out fieldVal)) { //check if the field value has html - if (XmlHelper.CouldItBeXml(e.Fields[field.Name])) + if (XmlHelper.CouldItBeXml(fieldVal)) { //First save the raw value to a raw field, we will change the policy of this field by detecting the prefix later - e.Fields[RawFieldPrefix + field.Name] = e.Fields[field.Name]; + e.Fields[RawFieldPrefix + field.Name] = fieldVal; //now replace the original value with the stripped html - e.Fields[field.Name] = DataService.ContentService.StripHtml(e.Fields[field.Name]); + e.Fields[field.Name] = DataService.ContentService.StripHtml(fieldVal); } } } @@ -568,18 +728,23 @@ namespace UmbracoExamine //ensure the special path and node type alias fields is added to the dictionary to be saved to file var path = e.Node.Attribute("path").Value; - if (!e.Fields.ContainsKey(IndexPathFieldName)) + if (e.Fields.ContainsKey(IndexPathFieldName) == false) e.Fields.Add(IndexPathFieldName, path); //this needs to support both schema's so get the nodeTypeAlias if it exists, otherwise the name var nodeTypeAlias = e.Node.Attribute("nodeTypeAlias") == null ? e.Node.Name.LocalName : e.Node.Attribute("nodeTypeAlias").Value; - if (!e.Fields.ContainsKey(NodeTypeAliasFieldName)) + if (e.Fields.ContainsKey(NodeTypeAliasFieldName) == false) e.Fields.Add(NodeTypeAliasFieldName, nodeTypeAlias); //add icon var icon = (string)e.Node.Attribute("icon"); - if (!e.Fields.ContainsKey(IconFieldName)) - e.Fields.Add(IconFieldName, icon); + if (e.Fields.ContainsKey(IconFieldName) == false) + e.Fields.Add(IconFieldName, icon); + + //add guid + var guid = (string)e.Node.Attribute("key"); + if (e.Fields.ContainsKey(NodeKeyFieldName) == false) + e.Fields.Add(NodeKeyFieldName, guid); } /// @@ -610,10 +775,18 @@ namespace UmbracoExamine //adds the special node type alias property to the index fields.Add(NodeTypeAliasFieldName, allValuesForIndexing[NodeTypeAliasFieldName]); - //icon - if (allValuesForIndexing[IconFieldName].IsNullOrWhiteSpace() == false) + //guid + string guidVal; + if (allValuesForIndexing.TryGetValue(NodeKeyFieldName, out guidVal) && guidVal.IsNullOrWhiteSpace() == false) { - fields.Add(IconFieldName, allValuesForIndexing[IconFieldName]); + fields.Add(NodeKeyFieldName, guidVal); + } + + //icon + string iconVal; + if (allValuesForIndexing.TryGetValue(IconFieldName, out iconVal) && iconVal.IsNullOrWhiteSpace() == false) + { + fields.Add(IconFieldName, iconVal); } return fields; @@ -645,9 +818,13 @@ namespace UmbracoExamine /// /// protected override FieldIndexTypes GetPolicy(string fieldName) - { - var def = IndexFieldPolicies.Where(x => x.Name == fieldName).ToArray(); - return (def.Any() == false ? FieldIndexTypes.ANALYZED : def.Single().IndexType); + { + StaticField def; + if (IndexFieldPolicies.TryGetValue(fieldName, out def)) + { + return def.IndexType; + } + return FieldIndexTypes.ANALYZED; } /// @@ -657,14 +834,18 @@ namespace UmbracoExamine /// protected override bool ValidateDocument(XElement node) { - var nodeId = int.Parse(node.Attribute("id").Value); // Test for access if we're only indexing published content // return nothing if we're not supporting protected content and it is protected, and we're not supporting unpublished content - if (!SupportUnpublishedContent - && (!SupportProtectedContent - && DataService.ContentService.IsProtected(nodeId, node.Attribute("path").Value))) + if (SupportUnpublishedContent == false + && SupportProtectedContent == false) { - return false; + + var nodeId = int.Parse(node.Attribute("id").Value); + + if (DataService.ContentService.IsProtected(nodeId, node.Attribute("path").Value)) + { + return false; + } } return base.ValidateDocument(node); } diff --git a/src/UmbracoExamine/UmbracoExamine.csproj b/src/UmbracoExamine/UmbracoExamine.csproj index 1bc438edd7..19abcf9313 100644 --- a/src/UmbracoExamine/UmbracoExamine.csproj +++ b/src/UmbracoExamine/UmbracoExamine.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -82,14 +82,17 @@ ..\Solution Items\TheFARM-Public.snk - - ..\packages\Examine.0.1.70.0\lib\Examine.dll + + ..\packages\Examine.0.1.82\lib\net45\Examine.dll + True ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll + True ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + True @@ -133,6 +136,7 @@ + diff --git a/src/UmbracoExamine/UmbracoExamineSearcher.cs b/src/UmbracoExamine/UmbracoExamineSearcher.cs index 7a2a80c3fe..26b02c904e 100644 --- a/src/UmbracoExamine/UmbracoExamineSearcher.cs +++ b/src/UmbracoExamine/UmbracoExamineSearcher.cs @@ -51,12 +51,41 @@ namespace UmbracoExamine /// public override string Name { - get - { - return _name; - } + get { return _name; } + } + + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + + public UmbracoExamineSearcher(DirectoryInfo indexPath, Analyzer analyzer) + : base(indexPath, analyzer) + { } + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + public UmbracoExamineSearcher(Lucene.Net.Store.Directory luceneDirectory, Analyzer analyzer) + : base(luceneDirectory, analyzer) + { + } + + /// + /// Creates an NRT searcher + /// + /// + /// + public UmbracoExamineSearcher(IndexWriter writer, Analyzer analyzer) + : base(writer, analyzer) + { + } + + #endregion public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { @@ -115,30 +144,6 @@ namespace UmbracoExamine } } - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - - public UmbracoExamineSearcher(DirectoryInfo indexPath, Analyzer analyzer) - : base(indexPath, analyzer) - { - } - - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - - public UmbracoExamineSearcher(Lucene.Net.Store.Directory luceneDirectory, Analyzer analyzer) - : base(luceneDirectory, analyzer) - { - } - - #endregion - /// /// Used for unit tests /// diff --git a/src/UmbracoExamine/UmbracoMemberIndexer.cs b/src/UmbracoExamine/UmbracoMemberIndexer.cs index 64a574822f..1a3c9befd2 100644 --- a/src/UmbracoExamine/UmbracoMemberIndexer.cs +++ b/src/UmbracoExamine/UmbracoMemberIndexer.cs @@ -9,6 +9,8 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Services; using UmbracoExamine.Config; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; using Examine; using System.IO; using UmbracoExamine.DataServices; @@ -24,6 +26,7 @@ namespace UmbracoExamine { private readonly IMemberService _memberService; + private readonly IMemberTypeService _memberTypeService; private readonly IDataTypeService _dataTypeService; /// @@ -33,6 +36,7 @@ namespace UmbracoExamine { _dataTypeService = ApplicationContext.Current.Services.DataTypeService; _memberService = ApplicationContext.Current.Services.MemberService; + _memberTypeService = ApplicationContext.Current.Services.MemberTypeService; } /// @@ -48,6 +52,7 @@ namespace UmbracoExamine { _dataTypeService = ApplicationContext.Current.Services.DataTypeService; _memberService = ApplicationContext.Current.Services.MemberService; + _memberTypeService = ApplicationContext.Current.Services.MemberTypeService; } /// @@ -60,6 +65,8 @@ namespace UmbracoExamine /// /// /// + [Obsolete("Use the ctor specifying all dependencies instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, IDataTypeService dataTypeService, IMemberService memberService, @@ -68,9 +75,31 @@ namespace UmbracoExamine { _dataTypeService = dataTypeService; _memberService = memberService; + _memberTypeService = ApplicationContext.Current.Services.MemberTypeService; } - + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + /// + /// + /// + /// + public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, + IDataTypeService dataTypeService, + IMemberService memberService, + IMemberTypeService memberTypeService, + Analyzer analyzer, bool async) + : base(indexerData, indexPath, dataService, analyzer, async) + { + _dataTypeService = dataTypeService; + _memberService = memberService; + _memberTypeService = memberTypeService; + } /// /// Ensures that the'_searchEmail' is added to the user fields so that it is indexed - without having to modify the config @@ -88,8 +117,9 @@ namespace UmbracoExamine if (indexerData.UserFields.Any(x => x.Name == "_searchEmail") == false) { var field = new IndexField { Name = "_searchEmail" }; - var policy = IndexFieldPolicies.FirstOrDefault(x => x.Name == "_searchEmail"); - if (policy != null) + + StaticField policy; + if (IndexFieldPolicies.TryGetValue("_searchEmail", out policy)) { field.Type = policy.Type; field.EnableSorting = policy.EnableSorting; @@ -129,40 +159,69 @@ namespace UmbracoExamine if (SupportedTypes.Contains(type) == false) return; - const int pageSize = 1000; - var pageIndex = 0; + DataService.LogService.AddInfoLog(-1, string.Format("PerformIndexAll - Start data queries - {0}", type)); + var stopwatch = new Stopwatch(); + stopwatch.Start(); - IMember[] members; - - if (IndexerData.IncludeNodeTypes.Any()) + try { - //if there are specific node types then just index those - foreach (var nodeType in IndexerData.IncludeNodeTypes) + if (DisableXmlDocumentLookup == false) { - do + ReindexWithXmlEntries(type, -1, + () => _memberTypeService.GetAll().ToArray(), + (path, pIndex, pSize) => + { + long totalContent; + var result = _memberService.GetPagedXmlEntries(pIndex, pSize, out totalContent).ToArray(); + var more = result.Length == pSize; + return Tuple.Create(result, more); + }, + i => _memberService.GetById(i)); + } + else + { + const int pageSize = 1000; + var pageIndex = 0; + + IMember[] members; + + if (IndexerData.IncludeNodeTypes.Any()) { - long total; - members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, true, null, nodeType).ToArray(); + //if there are specific node types then just index those + foreach (var nodeType in IndexerData.IncludeNodeTypes) + { + do + { + long total; + members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, true, null, nodeType).ToArray(); - AddNodesToIndex(GetSerializedMembers(members), type); + AddNodesToIndex(GetSerializedMembers(members), type); - pageIndex++; - } while (members.Length == pageSize); + pageIndex++; + } while (members.Length == pageSize); + } + } + else + { + //no node types specified, do all members + do + { + int total; + members = _memberService.GetAll(pageIndex, pageSize, out total).ToArray(); + + AddNodesToIndex(GetSerializedMembers(members), type); + + pageIndex++; + } while (members.Length == pageSize); + } } } - else + finally { - //no node types specified, do all members - do - { - int total; - members = _memberService.GetAll(pageIndex, pageSize, out total).ToArray(); - - AddNodesToIndex(GetSerializedMembers(members), type); - - pageIndex++; - } while (members.Length == pageSize); + stopwatch.Stop(); } + + DataService.LogService.AddInfoLog(-1, string.Format("PerformIndexAll - End data queries - {0}, took {1}ms", type, stopwatch.ElapsedMilliseconds)); } private IEnumerable GetSerializedMembers(IEnumerable members) @@ -174,19 +233,8 @@ namespace UmbracoExamine protected override XDocument GetXDocument(string xPath, string type) { throw new NotSupportedException(); - } - - protected override Dictionary GetSpecialFieldsToIndex(Dictionary allValuesForIndexing) - { - var fields = base.GetSpecialFieldsToIndex(allValuesForIndexing); - - //adds the special path property to the index - fields.Add("__key", allValuesForIndexing["__key"]); - - return fields; - - } - + } + /// /// Add the special __key and _searchEmail fields /// @@ -197,8 +245,8 @@ namespace UmbracoExamine if (e.Node.Attribute("key") != null) { - if (e.Fields.ContainsKey("__key") == false) - e.Fields.Add("__key", e.Node.Attribute("key").Value); + if (e.Fields.ContainsKey(NodeKeyFieldName) == false) + e.Fields.Add(NodeKeyFieldName, e.Node.Attribute("key").Value); } if (e.Node.Attribute("email") != null) diff --git a/src/UmbracoExamine/app.config b/src/UmbracoExamine/app.config index a0794caa99..2928c2785f 100644 --- a/src/UmbracoExamine/app.config +++ b/src/UmbracoExamine/app.config @@ -12,7 +12,7 @@ - + @@ -30,6 +30,10 @@ + + + + - \ No newline at end of file + diff --git a/src/UmbracoExamine/packages.config b/src/UmbracoExamine/packages.config index 04734b9fb8..17c77f7284 100644 --- a/src/UmbracoExamine/packages.config +++ b/src/UmbracoExamine/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/packages/repositories.config b/src/packages/repositories.config deleted file mode 100644 index f2902f1e46..0000000000 --- a/src/packages/repositories.config +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/umbraco.MacroEngines/Resources/Strings.Designer.cs b/src/umbraco.MacroEngines/Resources/Strings.Designer.cs index 236db870a0..1acc353daf 100644 --- a/src/umbraco.MacroEngines/Resources/Strings.Designer.cs +++ b/src/umbraco.MacroEngines/Resources/Strings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34014 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. diff --git a/src/umbraco.MacroEngines/app.config b/src/umbraco.MacroEngines/app.config index cc98223bd1..457c804e51 100644 --- a/src/umbraco.MacroEngines/app.config +++ b/src/umbraco.MacroEngines/app.config @@ -4,7 +4,7 @@ - + @@ -42,6 +42,10 @@ + + + + - \ No newline at end of file + diff --git a/src/umbraco.MacroEngines/packages.config b/src/umbraco.MacroEngines/packages.config index 9a785e35c3..e125024a28 100644 --- a/src/umbraco.MacroEngines/packages.config +++ b/src/umbraco.MacroEngines/packages.config @@ -1,7 +1,7 @@  - - + + @@ -11,6 +11,6 @@ - + \ No newline at end of file diff --git a/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj b/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj index c9ee70e52c..33cfdacd2f 100644 --- a/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj +++ b/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -45,25 +45,29 @@ false - - ..\packages\Examine.0.1.70.0\lib\Examine.dll + + ..\packages\Examine.0.1.82\lib\net45\Examine.dll + True - - ..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll + + ..\packages\HtmlAgilityPack.1.4.9.5\lib\Net45\HtmlAgilityPack.dll + True ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll + True ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - - ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll True + + ..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll + @@ -71,32 +75,41 @@ ..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll + True ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll + True ..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll + True ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll + True ..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll + True ..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll + True ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll + True ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll + True ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll + True diff --git a/src/umbraco.businesslogic/Application.cs b/src/umbraco.businesslogic/Application.cs index 2fd7b3cc02..ad6822019b 100644 --- a/src/umbraco.businesslogic/Application.cs +++ b/src/umbraco.businesslogic/Application.cs @@ -44,7 +44,7 @@ namespace umbraco.BusinessLogic try { - const string umbracoDsn = Umbraco.Core.Configuration.GlobalSettings.UmbracoConnectionName; + const string umbracoDsn = Constants.System.UmbracoConnectionName; var databaseSettings = ConfigurationManager.ConnectionStrings[umbracoDsn]; if (databaseSettings != null) diff --git a/src/umbraco.businesslogic/BasePages/BasePage.cs b/src/umbraco.businesslogic/BasePages/BasePage.cs index 6af70e21a9..b1f58d0745 100644 --- a/src/umbraco.businesslogic/BasePages/BasePage.cs +++ b/src/umbraco.businesslogic/BasePages/BasePage.cs @@ -113,7 +113,7 @@ namespace umbraco.BasePages /// public UrlHelper Url { - get { return _url ?? (_url = new UrlHelper(new RequestContext(new HttpContextWrapper(Context), new RouteData()))); } + get { return _url ?? (_url = new UrlHelper(Context.Request.RequestContext)); } } /// diff --git a/src/umbraco.businesslogic/GlobalSettings.cs b/src/umbraco.businesslogic/GlobalSettings.cs index 3d3721f783..a0fdc3ea49 100644 --- a/src/umbraco.businesslogic/GlobalSettings.cs +++ b/src/umbraco.businesslogic/GlobalSettings.cs @@ -18,8 +18,15 @@ namespace umbraco /// public class GlobalSettings { - - /// + /// + /// This returns the string of the MVC Area route. + /// + public static string UmbracoMvcArea + { + get { return Umbraco.Core.Configuration.GlobalSettings.UmbracoMvcArea; } + } + + /// /// Gets the reserved urls from web.config. /// /// The reserved urls. @@ -79,7 +86,7 @@ namespace umbraco /// Gets the database connection string /// /// The database connection string. - [Obsolete("Use System.ConfigurationManager.ConnectionStrings to get the connection with the key Umbraco.Core.Configuration.GlobalSettings.UmbracoConnectionName instead")] + [Obsolete("Use System.ConfigurationManager.ConnectionStrings to get the connection with the key Constants.System.UmbracoConnectionName instead")] public static string DbDSN { get { return Umbraco.Core.Configuration.GlobalSettings.DbDsn; } @@ -359,7 +366,7 @@ namespace umbraco { get { - var databaseSettings = ConfigurationManager.ConnectionStrings[Umbraco.Core.Configuration.GlobalSettings.UmbracoConnectionName]; + var databaseSettings = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName]; var dataHelper = DataLayerHelper.CreateSqlHelper(databaseSettings.ConnectionString, false); if (HttpContext.Current != null) diff --git a/src/umbraco.businesslogic/IO/SystemFiles.cs b/src/umbraco.businesslogic/IO/SystemFiles.cs index 991ec0b29f..3c5841a31b 100644 --- a/src/umbraco.businesslogic/IO/SystemFiles.cs +++ b/src/umbraco.businesslogic/IO/SystemFiles.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Configuration; using System.IO; using System.Linq; @@ -54,7 +55,9 @@ namespace umbraco.IO get { return Umbraco.Core.IO.SystemFiles.ContentCacheXml; } } - public static bool ContentCacheXmlIsEphemeral + [Obsolete("This is not used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static bool ContentCacheXmlIsEphemeral { get { return Umbraco.Core.IO.SystemFiles.ContentCacheXmlStoredInCodeGen; } } diff --git a/src/umbraco.businesslogic/PluginManagerExtensions.cs b/src/umbraco.businesslogic/PluginManagerExtensions.cs index 7a61414315..0df66b3b0c 100644 --- a/src/umbraco.businesslogic/PluginManagerExtensions.cs +++ b/src/umbraco.businesslogic/PluginManagerExtensions.cs @@ -16,9 +16,8 @@ namespace umbraco.businesslogic /// /// internal static IEnumerable ResolveApplications(this PluginManager resolver) - { - //don't cache the result of this because it is only used once during app startup, caching will just add a bit more mem overhead for no reason - return resolver.ResolveTypesWithAttribute(cacheResult:false); + { + return resolver.ResolveTypesWithAttribute(); } /// @@ -28,8 +27,7 @@ namespace umbraco.businesslogic /// internal static IEnumerable ResolveAttributedTrees(this PluginManager resolver) { - //don't cache the result of this because it is only used once during app startup, caching will just add a bit more mem overhead for no reason - return resolver.ResolveTypesWithAttribute(cacheResult:false); + return resolver.ResolveTypesWithAttribute(); } } diff --git a/src/umbraco.businesslogic/app.config b/src/umbraco.businesslogic/app.config index a0794caa99..2928c2785f 100644 --- a/src/umbraco.businesslogic/app.config +++ b/src/umbraco.businesslogic/app.config @@ -12,7 +12,7 @@ - + @@ -30,6 +30,10 @@ + + + + - \ No newline at end of file + diff --git a/src/umbraco.businesslogic/ui.cs b/src/umbraco.businesslogic/ui.cs index b289b6d6c5..18c6412bf2 100644 --- a/src/umbraco.businesslogic/ui.cs +++ b/src/umbraco.businesslogic/ui.cs @@ -56,8 +56,8 @@ namespace umbraco private static string GetLanguage() { - var user = UmbracoEnsuredPage.CurrentUser; - return GetLanguage(user); + //Return the current user's language which is based on the current thread culture + return GetLanguage(""); } private static string GetLanguage(User u) @@ -84,7 +84,7 @@ namespace umbraco { return userLanguage; } - var language = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName; + var language = Thread.CurrentThread.CurrentCulture.Name; if (string.IsNullOrEmpty(language)) language = UmbracoDefaultUiLanguage; return language; diff --git a/src/umbraco.businesslogic/umbraco.businesslogic.csproj b/src/umbraco.businesslogic/umbraco.businesslogic.csproj index 851a6a3c8c..86996480f2 100644 --- a/src/umbraco.businesslogic/umbraco.businesslogic.csproj +++ b/src/umbraco.businesslogic/umbraco.businesslogic.csproj @@ -1,5 +1,5 @@  - + Local 9.0.30729 @@ -108,18 +108,23 @@ ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.dll + True ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll + True ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + True ..\packages\Owin.1.0\lib\net40\Owin.dll + True @@ -138,21 +143,27 @@ ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll + True ..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll + True ..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll + True ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll + True ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll + True ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll + True System.XML diff --git a/src/umbraco.cms/Actions/Action.cs b/src/umbraco.cms/Actions/Action.cs index df6c257396..3a5503d714 100644 --- a/src/umbraco.cms/Actions/Action.cs +++ b/src/umbraco.cms/Actions/Action.cs @@ -35,40 +35,10 @@ namespace umbraco.BusinessLogic.Actions public class Action { private static readonly Dictionary ActionJs = new Dictionary(); - - private static readonly object Lock = new object(); - - static Action() - { - ReRegisterActionsAndHandlers(); - } - - /// - /// This is used when an IAction or IActionHandler is installed into the system - /// and needs to be loaded into memory. - /// - /// - /// TODO: this shouldn't be needed... we should restart the app pool when a package is installed! - /// + + [Obsolete("This no longer performs any action there is never a reason to rescan because the app domain will be restarted if new IActions are added because they are included in assemblies")] public static void ReRegisterActionsAndHandlers() - { - lock (Lock) - { - // NOTE use the DirtyBackdoor to change the resolution configuration EXCLUSIVELY - // ie do NOT do ANYTHING else while holding the backdoor, because while it is open - // the whole resolution system is locked => nothing can work properly => deadlocks - - var newResolver = new ActionsResolver( - new ActivatorServiceProvider(), LoggerResolver.Current.Logger, - () => TypeFinder.FindClassesOfType(PluginManager.Current.AssembliesToScan)); - - using (Umbraco.Core.ObjectResolution.Resolution.DirtyBackdoorToConfiguration) - { - ActionsResolver.Reset(false); // and do NOT reset the whole resolution! - ActionsResolver.Current = newResolver; - } - - } + { } /// diff --git a/src/umbraco.cms/app.config b/src/umbraco.cms/app.config index a0794caa99..2928c2785f 100644 --- a/src/umbraco.cms/app.config +++ b/src/umbraco.cms/app.config @@ -12,7 +12,7 @@ - + @@ -30,6 +30,10 @@ + + + + - \ No newline at end of file + diff --git a/src/umbraco.cms/businesslogic/Content.cs b/src/umbraco.cms/businesslogic/Content.cs index abdaa1102e..b64e4cf678 100644 --- a/src/umbraco.cms/businesslogic/Content.cs +++ b/src/umbraco.cms/businesslogic/Content.cs @@ -14,6 +14,7 @@ using System.Runtime.CompilerServices; using umbraco.BusinessLogic; using umbraco.cms.helpers; using umbraco.cms.businesslogic.datatype.controls; +using Umbraco.Core.Media; using File = System.IO.File; using Property = umbraco.cms.businesslogic.property.Property; using PropertyType = umbraco.cms.businesslogic.propertytype.PropertyType; @@ -148,7 +149,7 @@ namespace umbraco.cms.businesslogic { _contentType = new ContentType(contentTypeId); } - catch + catch (Exception e) { return null; } diff --git a/src/umbraco.cms/businesslogic/Files/UmbracoFile.cs b/src/umbraco.cms/businesslogic/Files/UmbracoFile.cs index 5822e8c72c..9ac5547af3 100644 --- a/src/umbraco.cms/businesslogic/Files/UmbracoFile.cs +++ b/src/umbraco.cms/businesslogic/Files/UmbracoFile.cs @@ -35,35 +35,38 @@ namespace umbraco.cms.businesslogic.Files #endregion #region Static Methods - - //MB: Do we really need all these overloads? looking through the code, only one of them is actually used - + + [Obsolete("This is no longer used and will be removed in future versions")] public static UmbracoFile Save(HttpPostedFile file, string path) { return new UmbracoFile(UmbracoMediaFile.Save(file.InputStream, path)); } + [Obsolete("This is no longer used and will be removed in future versions")] public static UmbracoFile Save(HttpPostedFileBase file, string path) { return new UmbracoFile(UmbracoMediaFile.Save(file.InputStream, path)); } + [Obsolete("This is no longer used and will be removed in future versions")] public static UmbracoFile Save(Stream inputStream, string path) { return new UmbracoFile(UmbracoMediaFile.Save(inputStream, path)); } + [Obsolete("This is no longer used and will be removed in future versions")] public static UmbracoFile Save(byte[] file, string relativePath) { return new UmbracoFile(UmbracoMediaFile.Save(new MemoryStream(file), relativePath)); } + [Obsolete("This is no longer used and will be removed in future versions")] public static UmbracoFile Save(HttpPostedFile file) { return new UmbracoFile(UmbracoMediaFile.Save(file)); } - //filebase overload... + [Obsolete("This is no longer used and will be removed in future versions")] public static UmbracoFile Save(HttpPostedFileBase file) { return new UmbracoFile(UmbracoMediaFile.Save(file)); @@ -118,11 +121,13 @@ namespace umbraco.cms.businesslogic.Files return new System.Tuple(size.Width, size.Height); } + [Obsolete("This is no longer used and will be removed in future versions")] public string Resize(int width, int height) { return _mediaFile.Resize(width, height); } + [Obsolete("This is no longer used and will be removed in future versions")] public string Resize(int maxWidthHeight, string fileNameAddition) { return _mediaFile.Resize(maxWidthHeight, fileNameAddition); diff --git a/src/umbraco.cms/businesslogic/Packager/FileResources/PackageFiles.Designer.cs b/src/umbraco.cms/businesslogic/Packager/FileResources/PackageFiles.Designer.cs index 81909bb6a5..0d0b9c03e2 100644 --- a/src/umbraco.cms/businesslogic/Packager/FileResources/PackageFiles.Designer.cs +++ b/src/umbraco.cms/businesslogic/Packager/FileResources/PackageFiles.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18034 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. diff --git a/src/umbraco.cms/businesslogic/Packager/Installer.cs b/src/umbraco.cms/businesslogic/Packager/Installer.cs index 1f17da3922..e97b9317d8 100644 --- a/src/umbraco.cms/businesslogic/Packager/Installer.cs +++ b/src/umbraco.cms/businesslogic/Packager/Installer.cs @@ -14,16 +14,18 @@ using Umbraco.Core.Packaging; using umbraco.cms.businesslogic.web; using umbraco.BusinessLogic; using System.Diagnostics; -using umbraco.cms.businesslogic.macro; using umbraco.cms.businesslogic.template; using umbraco.interfaces; +using Umbraco.Core.Events; +using Umbraco.Core.Packaging.Models; +using Umbraco.Core.Services; namespace umbraco.cms.businesslogic.packager { /// /// The packager is a component which enables sharing of both data and functionality components between different umbraco installations. /// - /// The output is a .umb (a zip compressed file) which contains the exported documents/medias/macroes/documenttypes (etc.) + /// The output is a .umb (a zip compressed file) which contains the exported documents/medias/macros/documenttypes (etc.) /// in a Xml document, along with the physical files used (images/usercontrols/xsl documents etc.) /// /// Partly implemented, import of packages is done, the export is *under construction*. @@ -507,6 +509,7 @@ namespace umbraco.cms.businesslogic.packager } OnPackageBusinessLogicInstalled(insPack); + OnPackageInstalled(insPack); } } @@ -517,6 +520,7 @@ namespace umbraco.cms.businesslogic.packager /// public void InstallCleanUp(int packageId, string tempDir) { + if (Directory.Exists(tempDir)) { Directory.Delete(tempDir, true); @@ -814,5 +818,21 @@ namespace umbraco.cms.businesslogic.packager EventHandler handler = PackageBusinessLogicInstalled; if (handler != null) handler(null, e); } + + private void OnPackageInstalled(InstalledPackage insPack) + { + // getting an InstallationSummary for sending to the PackagingService.ImportedPackage event + var fileService = ApplicationContext.Current.Services.FileService; + var macroService = ApplicationContext.Current.Services.MacroService; + var contentTypeService = ApplicationContext.Current.Services.ContentTypeService; + var dataTypeService = ApplicationContext.Current.Services.DataTypeService; + var localizationService = ApplicationContext.Current.Services.LocalizationService; + + var installationSummary = insPack.GetInstallationSummary(contentTypeService, dataTypeService, fileService, localizationService, macroService); + installationSummary.PackageInstalled = true; + + var args = new ImportPackageEventArgs(installationSummary, false); + PackagingService.OnImportedPackage(args); + } } } diff --git a/src/umbraco.cms/businesslogic/Packager/PackageActions/addDashboardSection.cs b/src/umbraco.cms/businesslogic/Packager/PackageActions/addDashboardSection.cs index 6025123bc4..ceca7c21ff 100644 --- a/src/umbraco.cms/businesslogic/Packager/PackageActions/addDashboardSection.cs +++ b/src/umbraco.cms/businesslogic/Packager/PackageActions/addDashboardSection.cs @@ -43,23 +43,28 @@ namespace umbraco.cms.businesslogic.packager.standardPackageActions if (xmlData.HasChildNodes) { - string sectionAlias = xmlData.Attributes["dashboardAlias"].Value; - string dbConfig = SystemFiles.DashboardConfig; + string sectionAlias = xmlData.Attributes["dashboardAlias"].Value; + string dbConfig = SystemFiles.DashboardConfig; - XmlNode section = xmlData.SelectSingleNode("./section"); - XmlDocument dashboardFile = XmlHelper.OpenAsXmlDocument(dbConfig); + XmlNode section = xmlData.SelectSingleNode("./section"); + XmlDocument dashboardFile = XmlHelper.OpenAsXmlDocument(dbConfig); - XmlNode importedSection = dashboardFile.ImportNode(section, true); + //don't continue if it already exists + var found = dashboardFile.SelectNodes("//section[@alias='" + sectionAlias + "']"); + if (found == null || found.Count <= 0) + { + XmlNode importedSection = dashboardFile.ImportNode(section, true); - XmlAttribute alias = XmlHelper.AddAttribute(dashboardFile, "alias", sectionAlias); - importedSection.Attributes.Append(alias); + XmlAttribute alias = XmlHelper.AddAttribute(dashboardFile, "alias", sectionAlias); + importedSection.Attributes.Append(alias); - dashboardFile.DocumentElement.AppendChild(importedSection); + dashboardFile.DocumentElement.AppendChild(importedSection); - dashboardFile.Save(IOHelper.MapPath(dbConfig)); + dashboardFile.Save(IOHelper.MapPath(dbConfig)); + } - return true; - } + return true; + } return false; } diff --git a/src/umbraco.cms/businesslogic/Packager/PackageInstance/InstalledPackage.cs b/src/umbraco.cms/businesslogic/Packager/PackageInstance/InstalledPackage.cs index 974f398ca7..172a9d6443 100644 --- a/src/umbraco.cms/businesslogic/Packager/PackageInstance/InstalledPackage.cs +++ b/src/umbraco.cms/businesslogic/Packager/PackageInstance/InstalledPackage.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Auditing; using Umbraco.Core.Logging; using Umbraco.Core.IO; +using Umbraco.Core.Packaging.Models; +using Umbraco.Core.Services; namespace umbraco.cms.businesslogic.packager { public class InstalledPackage @@ -124,5 +127,70 @@ namespace umbraco.cms.businesslogic.packager { if (AfterDelete != null) AfterDelete(this, e); } + + /// + /// Used internally for creating an InstallationSummary (used in new PackagingService) representation of this InstalledPackage object. + /// + /// + /// + /// + /// + /// + /// + internal InstallationSummary GetInstallationSummary(IContentTypeService contentTypeService, IDataTypeService dataTypeService, IFileService fileService, ILocalizationService localizationService, IMacroService macroService) + { + var macros = TryGetIntegerIds(Data.Macros).Select(macroService.GetById).ToList(); + var templates = TryGetIntegerIds(Data.Templates).Select(fileService.GetTemplate).ToList(); + var contentTypes = TryGetIntegerIds(Data.Documenttypes).Select(contentTypeService.GetContentType).ToList(); + var dataTypes = TryGetIntegerIds(Data.DataTypes).Select(dataTypeService.GetDataTypeDefinitionById).ToList(); + var dictionaryItems = TryGetIntegerIds(Data.DictionaryItems).Select(localizationService.GetDictionaryItemById).ToList(); + var languages = TryGetIntegerIds(Data.Languages).Select(localizationService.GetLanguageById).ToList(); + + for (var i = 0; i < Data.Files.Count; i++) + { + var filePath = Data.Files[i]; + Data.Files[i] = filePath.GetRelativePath(); + } + + return new InstallationSummary + { + ContentTypesInstalled = contentTypes, + DataTypesInstalled = dataTypes, + DictionaryItemsInstalled = dictionaryItems, + FilesInstalled = Data.Files, + LanguagesInstalled = languages, + MacrosInstalled = macros, + MetaData = GetMetaData(), + TemplatesInstalled = templates, + }; + } + + internal MetaData GetMetaData() + { + return new MetaData() + { + AuthorName = Data.Author, + AuthorUrl = Data.AuthorUrl, + Control = Data.LoadControl, + License = Data.License, + LicenseUrl = Data.LicenseUrl, + Name = Data.Name, + Readme = Data.Readme, + Url = Data.Url, + Version = Data.Version + }; + } + + private static IEnumerable TryGetIntegerIds(IEnumerable ids) + { + var intIds = new List(); + foreach (var id in ids) + { + int parsed; + if (int.TryParse(id, out parsed)) + intIds.Add(parsed); + } + return intIds; + } } } diff --git a/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs b/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs index 3ffa0e9054..28cc814a02 100644 --- a/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs +++ b/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs @@ -12,7 +12,8 @@ using Umbraco.Core.Logging; using Umbraco.Core.IO; namespace umbraco.cms.businesslogic.packager.repositories -{ +{ + [Obsolete("This should not be used and will be removed in future Umbraco versions")] public class Repository : DisposableObject { public string Guid { get; private set; } @@ -141,12 +142,12 @@ namespace umbraco.cms.businesslogic.packager.repositories return repository; } + + //shortcut method to download pack from repo and place it on the server... public string fetch(string packageGuid) { - return fetch(packageGuid, string.Empty); - } public string fetch(string packageGuid, int userId) @@ -158,6 +159,40 @@ namespace umbraco.cms.businesslogic.packager.repositories return fetch(packageGuid); } + /// + /// Used to get the correct package file from the repo for the current umbraco version + /// + /// + /// + /// + /// + public string GetPackageFile(string packageGuid, int userId, System.Version currentUmbracoVersion) + { + // log + Audit.Add(AuditTypes.PackagerInstall, + string.Format("Package {0} fetched from {1}", packageGuid, this.Guid), + userId, -1); + + var fileByteArray = Webservice.GetPackageFile(packageGuid, currentUmbracoVersion.ToString(3)); + + //successfull + if (fileByteArray.Length > 0) + { + // Check for package directory + if (Directory.Exists(IOHelper.MapPath(Settings.PackagerRoot)) == false) + Directory.CreateDirectory(IOHelper.MapPath(Settings.PackagerRoot)); + + using (var fs1 = new FileStream(IOHelper.MapPath(Settings.PackagerRoot + Path.DirectorySeparatorChar + packageGuid + ".umb"), FileMode.Create)) + { + fs1.Write(fileByteArray, 0, fileByteArray.Length); + fs1.Close(); + return "packages\\" + packageGuid + ".umb"; + } + } + + return ""; + } + public bool HasConnection() { @@ -199,22 +234,37 @@ namespace umbraco.cms.businesslogic.packager.repositories } } - + + + /// + /// This goes and fetches the Byte array for the package from OUR, but it's pretty strange + /// + /// + /// The package ID for the package file to be returned + /// + /// + /// This is a strange Umbraco version parameter - but it's not really an umbraco version, it's a special/odd version format like Version41 + /// but it's actually not used for the 7.5+ package installs so it's obsolete/unused. + /// + /// public string fetch(string packageGuid, string key) { byte[] fileByteArray = new byte[0]; if (key == string.Empty) - { + { + //SD: this is odd, not sure why it returns a different package depending on the legacy xml schema but I'll leave it for now if (UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema) - fileByteArray = this.Webservice.fetchPackage(packageGuid); + fileByteArray = Webservice.fetchPackage(packageGuid); else - fileByteArray = this.Webservice.fetchPackageByVersion(packageGuid, Version.Version41); + { + fileByteArray = Webservice.fetchPackageByVersion(packageGuid, Version.Version41); + } } else { - fileByteArray = this.Webservice.fetchProtectedPackage(packageGuid, key); + fileByteArray = Webservice.fetchProtectedPackage(packageGuid, key); } //successfull diff --git a/src/umbraco.cms/businesslogic/Packager/Repositories/RepositoryWebservice.cs b/src/umbraco.cms/businesslogic/Packager/Repositories/RepositoryWebservice.cs index ccf8566687..a2da17dd76 100644 --- a/src/umbraco.cms/businesslogic/Packager/Repositories/RepositoryWebservice.cs +++ b/src/umbraco.cms/businesslogic/Packager/Repositories/RepositoryWebservice.cs @@ -36,6 +36,8 @@ namespace umbraco.cms.businesslogic.packager.repositories private System.Threading.SendOrPostCallback authenticateOperationCompleted; + private System.Threading.SendOrPostCallback GetPackageFileOperationCompleted; + private System.Threading.SendOrPostCallback fetchPackageByVersionOperationCompleted; private System.Threading.SendOrPostCallback fetchPackageOperationCompleted; @@ -83,6 +85,9 @@ namespace umbraco.cms.businesslogic.packager.repositories /// public event authenticateCompletedEventHandler authenticateCompleted; + /// + public event GetPackageFileCompletedEventHandler GetPackageFileCompleted; + /// public event fetchPackageByVersionCompletedEventHandler fetchPackageByVersionCompleted; @@ -533,6 +538,76 @@ namespace umbraco.cms.businesslogic.packager.repositories } } + + + + + + /// + [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/GetPackageFile", RequestNamespace = "http://packages.umbraco.org/webservices/", ResponseNamespace = "http://packages.umbraco.org/webservices/", Use = System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle = System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] + [return: System.Xml.Serialization.XmlElementAttribute(DataType = "base64Binary")] + public byte[] GetPackageFile(string packageGuid, string umbracoVersion) + { + object[] results = this.Invoke("GetPackageFile", new object[] { + packageGuid, + umbracoVersion}); + return ((byte[])(results[0])); + } + + /// + public System.IAsyncResult BeginfetchPackageByVersion(string packageGuid, string umbracoVersion, System.AsyncCallback callback, object asyncState) + { + return this.BeginInvoke("GetPackageFile", new object[] { + packageGuid, + umbracoVersion}, callback, asyncState); + } + + /// + public byte[] EndGetPackageFile(System.IAsyncResult asyncResult) + { + object[] results = this.EndInvoke(asyncResult); + return ((byte[])(results[0])); + } + + /// + public void GetPackageFileAsync(string packageGuid, string umbracoVersion) + { + this.GetPackageFileAsync(packageGuid, umbracoVersion, null); + } + + /// + public void GetPackageFileAsync(string packageGuid, string umbracoVersion, object userState) + { + if ((this.GetPackageFileOperationCompleted == null)) + { + this.GetPackageFileOperationCompleted = new System.Threading.SendOrPostCallback(this.OnGetPackageFileOperationCompleted); + } + this.InvokeAsync("GetPackageFile", new object[] { + packageGuid, + umbracoVersion}, this.GetPackageFileOperationCompleted, userState); + } + + private void OnGetPackageFileOperationCompleted(object arg) + { + if ((this.GetPackageFileCompleted != null)) + { + System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); + this.GetPackageFileCompleted(this, new GetPackageFileCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); + } + } + + + + + + + + + + + + + /// [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchPackageByVersion", RequestNamespace = "http://packages.umbraco.org/webservices/", ResponseNamespace = "http://packages.umbraco.org/webservices/", Use = System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle = System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] [return: System.Xml.Serialization.XmlElementAttribute(DataType = "base64Binary")] @@ -1382,7 +1457,9 @@ namespace umbraco.cms.businesslogic.packager.repositories /// Version4, - /// + /// + /// This is apparently the version number we pass in for all installs + /// Version41, /// @@ -1682,9 +1759,40 @@ namespace umbraco.cms.businesslogic.packager.repositories } } + /// [System.CodeDom.Compiler.GeneratedCodeAttribute("wsdl", "4.0.30319.1")] - public delegate void fetchPackageByVersionCompletedEventHandler(object sender, fetchPackageByVersionCompletedEventArgs e); + public delegate void GetPackageFileCompletedEventHandler(object sender, GetPackageFileCompletedEventArgs e); + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("wsdl", "4.0.30319.1")] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + public partial class GetPackageFileCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs + { + + private object[] results; + + internal GetPackageFileCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : + base(exception, cancelled, userState) + { + this.results = results; + } + + /// + public byte[] Result + { + get + { + this.RaiseExceptionIfNecessary(); + return ((byte[])(this.results[0])); + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("wsdl", "4.0.30319.1")] + public delegate void fetchPackageByVersionCompletedEventHandler(object sender, fetchPackageByVersionCompletedEventArgs e); /// [System.CodeDom.Compiler.GeneratedCodeAttribute("wsdl", "4.0.30319.1")] diff --git a/src/umbraco.cms/businesslogic/Packager/data.cs b/src/umbraco.cms/businesslogic/Packager/data.cs index 7781f1790f..536be6d435 100644 --- a/src/umbraco.cms/businesslogic/Packager/data.cs +++ b/src/umbraco.cms/businesslogic/Packager/data.cs @@ -302,7 +302,11 @@ namespace umbraco.cms.businesslogic.packager XmlHelper.SetAttribute(Source, xmlDef, "enableSkins", package.EnableSkins.ToString()); XmlHelper.SetAttribute(Source, xmlDef, "skinRepoGuid", package.SkinRepoGuid.ToString()); XmlHelper.SetAttribute(Source, xmlDef, "iconUrl", package.IconUrl); - XmlHelper.SetAttribute(Source, xmlDef, "umbVersion", package.UmbracoVersion.ToString(3)); + if (package.UmbracoVersion != null) + { + XmlHelper.SetAttribute(Source, xmlDef, "umbVersion", package.UmbracoVersion.ToString(3)); + } + var licenseNode = xmlDef.SelectSingleNode("license"); if (licenseNode == null) diff --git a/src/umbraco.cms/businesslogic/Tags/Tag.cs b/src/umbraco.cms/businesslogic/Tags/Tag.cs index 8534d02d26..d4e0c90d00 100644 --- a/src/umbraco.cms/businesslogic/Tags/Tag.cs +++ b/src/umbraco.cms/businesslogic/Tags/Tag.cs @@ -351,7 +351,7 @@ namespace umbraco.cms.businesslogic.Tags { Document cnode = new Document(rr.GetInt("nodeid")); - if (cnode != null && cnode.Published) + if (cnode.Published) docs.Add(cnode); } } diff --git a/src/umbraco.cms/businesslogic/datatype/DataEditorSettingsStorage.cs b/src/umbraco.cms/businesslogic/datatype/DataEditorSettingsStorage.cs index 5d2434164f..ddebb2ee2d 100644 --- a/src/umbraco.cms/businesslogic/datatype/DataEditorSettingsStorage.cs +++ b/src/umbraco.cms/businesslogic/datatype/DataEditorSettingsStorage.cs @@ -4,6 +4,7 @@ using System.Configuration; using System.Linq; using System.Text; using umbraco.DataLayer; +using Umbraco.Core; namespace umbraco.cms.businesslogic.datatype { @@ -14,7 +15,7 @@ namespace umbraco.cms.businesslogic.datatype public DataEditorSettingsStorage() { - var databaseSettings = ConfigurationManager.ConnectionStrings[Umbraco.Core.Configuration.GlobalSettings.UmbracoConnectionName]; + var databaseSettings = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName]; var dataHelper = DataLayerHelper.CreateSqlHelper(databaseSettings.ConnectionString, false); init(DataLayerHelper.CreateSqlHelper(dataHelper.ConnectionString, false)); diff --git a/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs b/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs index 12f151876d..5eecd607b5 100644 --- a/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs +++ b/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs @@ -73,7 +73,7 @@ namespace umbraco.cms.businesslogic.datatype int subfolderId; var numberedFolder = int.TryParse(subfolder, out subfolderId) ? subfolderId.ToString(CultureInfo.InvariantCulture) - : MediaSubfolderCounter.Current.Increment().ToString(CultureInfo.InvariantCulture); + : fs.GetNextFolder(); var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? Path.Combine(numberedFolder, name) diff --git a/src/umbraco.cms/businesslogic/macro/IMacroEngine.cs b/src/umbraco.cms/businesslogic/macro/IMacroEngine.cs index 952ee11158..9655a9b0c7 100644 --- a/src/umbraco.cms/businesslogic/macro/IMacroEngine.cs +++ b/src/umbraco.cms/businesslogic/macro/IMacroEngine.cs @@ -6,7 +6,8 @@ using umbraco.interfaces; namespace umbraco.cms.businesslogic.macro { - public interface IMacroEngine { + public interface IMacroEngine : IDiscoverable + { string Name { get; } IEnumerable SupportedExtensions { get; } IEnumerable SupportedUIExtensions { get; } diff --git a/src/umbraco.cms/businesslogic/macro/Macro.cs b/src/umbraco.cms/businesslogic/macro/Macro.cs index 0b043a90a3..0ce2c45c6b 100644 --- a/src/umbraco.cms/businesslogic/macro/Macro.cs +++ b/src/umbraco.cms/businesslogic/macro/Macro.cs @@ -48,6 +48,14 @@ namespace umbraco.cms.businesslogic.macro { get { return MacroEntity.Id; } } + + /// + /// key + /// + public Guid Key + { + get { return MacroEntity.Key; } + } /// /// If set to true, the macro can be inserted on documents using the richtexteditor. @@ -178,6 +186,7 @@ namespace umbraco.cms.businesslogic.macro { return MacroEntity.Properties.Select(x => new MacroProperty { + Key = x.Key, Alias = x.Alias, Name = x.Name, SortOrder = x.SortOrder, diff --git a/src/umbraco.cms/businesslogic/macro/MacroProperty.cs b/src/umbraco.cms/businesslogic/macro/MacroProperty.cs index ff3a58d46a..8d440857e8 100644 --- a/src/umbraco.cms/businesslogic/macro/MacroProperty.cs +++ b/src/umbraco.cms/businesslogic/macro/MacroProperty.cs @@ -36,6 +36,7 @@ namespace umbraco.cms.businesslogic.macro /// public MacroProperty() { + Key = Guid.NewGuid(); } /// @@ -75,6 +76,11 @@ namespace umbraco.cms.businesslogic.macro /// The id. public int Id { get; private set; } + /// + /// Gets the key. + /// + public Guid Key { get; set; } + /// /// Gets or sets the macro. /// @@ -94,8 +100,9 @@ namespace umbraco.cms.businesslogic.macro { if (_type == null) { - //we'll try to create one based on the resolved new parameter editors - var found = ParameterEditorResolver.Current.GetByAlias(ParameterEditorAlias); + //we'll try to create one based on the resolved new parameter editors + // we need to show the depracated ones for backwards compatibility + var found = ParameterEditorResolver.Current.GetByAlias(ParameterEditorAlias, true); if (found == null) { return null; @@ -147,10 +154,11 @@ namespace umbraco.cms.businesslogic.macro private void Setup() { using (var sqlHelper = Application.SqlHelper) - using (var dr = sqlHelper.ExecuteReader("select macro, editorAlias, macroPropertySortOrder, macroPropertyAlias, macroPropertyName from cmsMacroProperty where id = @id", sqlHelper.CreateParameter("@id", Id))) + using (var dr = sqlHelper.ExecuteReader("select uniqueId, macro, editorAlias, macroPropertySortOrder, macroPropertyAlias, macroPropertyName from cmsMacroProperty where id = @id", sqlHelper.CreateParameter("@id", Id))) { if (dr.Read()) { + Key = dr.GetGuid("uniqueId"); Macro = new Macro(dr.GetInt("macro")); SortOrder = (int)dr.GetByte("macroPropertySortOrder"); Alias = dr.GetString("macroPropertyAlias"); diff --git a/src/umbraco.cms/businesslogic/media/IMediaFactory.cs b/src/umbraco.cms/businesslogic/media/IMediaFactory.cs index 8f8f87556e..0106f131b4 100644 --- a/src/umbraco.cms/businesslogic/media/IMediaFactory.cs +++ b/src/umbraco.cms/businesslogic/media/IMediaFactory.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Text; using umbraco.BusinessLogic; +using umbraco.interfaces; namespace umbraco.cms.businesslogic.media { [Obsolete("This interface is no longer used and will be removed from the codebase in future versions")] - public interface IMediaFactory + public interface IMediaFactory : IDiscoverable { List Extensions { get; } int Priority { get; } diff --git a/src/umbraco.cms/businesslogic/media/UmbracoFileMediaFactory.cs b/src/umbraco.cms/businesslogic/media/UmbracoFileMediaFactory.cs index 0c265d8b07..dc13f83ea6 100644 --- a/src/umbraco.cms/businesslogic/media/UmbracoFileMediaFactory.cs +++ b/src/umbraco.cms/businesslogic/media/UmbracoFileMediaFactory.cs @@ -29,7 +29,7 @@ namespace umbraco.cms.businesslogic.media var propertyId = media.getProperty(Constants.Conventions.Media.File).Id; // Get paths - var destFilePath = FileSystem.GetRelativePath(propertyId, uploadedFile.FileName); + var destFilePath = GetRelativePath(propertyId, uploadedFile.FileName); var ext = Path.GetExtension(destFilePath).Substring(1); //var absoluteDestPath = HttpContext.Current.Server.MapPath(destPath); diff --git a/src/umbraco.cms/businesslogic/media/UmbracoMediaFactory.cs b/src/umbraco.cms/businesslogic/media/UmbracoMediaFactory.cs index 54d8dfa770..8cdf519a09 100644 --- a/src/umbraco.cms/businesslogic/media/UmbracoMediaFactory.cs +++ b/src/umbraco.cms/businesslogic/media/UmbracoMediaFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; @@ -99,7 +100,7 @@ namespace umbraco.cms.businesslogic.media if (int.TryParse(subfolder, out subfolderId)) { - var destFilePath = FileSystem.GetRelativePath(subfolderId, fileName); + var destFilePath = GetRelativePath(subfolderId, fileName); var destFileUrl = FileSystem.GetUrl(destFilePath); if (prop.Value.ToString() == destFileUrl) @@ -154,6 +155,17 @@ namespace umbraco.cms.businesslogic.media return friendlyName; } + public static string GetRelativePath(int propertyId, string fileName) + { + var contentConfig = UmbracoConfig.For.UmbracoSettings().Content; + + var sep = contentConfig.UploadAllowDirectories + ? Path.DirectorySeparatorChar + : '-'; + + return propertyId.ToString(CultureInfo.InvariantCulture) + sep + fileName; + } + #endregion } } diff --git a/src/umbraco.cms/businesslogic/propertytype/propertytype.cs b/src/umbraco.cms/businesslogic/propertytype/propertytype.cs index b9622d7c91..63a4d80e91 100644 --- a/src/umbraco.cms/businesslogic/propertytype/propertytype.cs +++ b/src/umbraco.cms/businesslogic/propertytype/propertytype.cs @@ -54,32 +54,31 @@ namespace umbraco.cms.businesslogic.propertytype public PropertyType(int id) { - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader dr = sqlHelper.ExecuteReader( - "Select mandatory, DataTypeId, propertyTypeGroupId, ContentTypeId, sortOrder, alias, name, validationRegExp, description from cmsPropertyType where id=@id", - sqlHelper.CreateParameter("@id", id))) + var found = ApplicationContext.Current.DatabaseContext.Database + .SingleOrDefault( + "Select mandatory as mandatory, dataTypeId as dataTypeId, propertyTypeGroupId as propertyTypeGroupId, contentTypeId as contentTypeId, sortOrder as sortOrder, alias as alias, name as name, validationRegExp as validationRegExp, description as description from cmsPropertyType where id=@id", + new {id = id}); + + if (found == null) + throw new ArgumentException("Propertytype with id: " + id + " doesnt exist!"); + + _mandatory = found.mandatory; + _id = id; + + if (found.propertyTypeGroupId != null) { - if (!dr.Read()) - throw new ArgumentException("Propertytype with id: " + id + " doesnt exist!"); - - _mandatory = dr.GetBoolean("mandatory"); - _id = id; - - if (!dr.IsNull("propertyTypeGroupId")) - { - _propertyTypeGroup = dr.GetInt("propertyTypeGroupId"); - //TODO: Remove after refactoring! - _tabId = _propertyTypeGroup; - } - - _sortOrder = dr.GetInt("sortOrder"); - _alias = dr.GetString("alias"); - _name = dr.GetString("Name"); - _validationRegExp = dr.GetString("validationRegExp"); - _DataTypeId = dr.GetInt("DataTypeId"); - _contenttypeid = dr.GetInt("contentTypeId"); - _description = dr.GetString("description"); + _propertyTypeGroup = found.propertyTypeGroupId; + //TODO: Remove after refactoring! + _tabId = _propertyTypeGroup; } + + _sortOrder = found.sortOrder; + _alias = found.alias; + _name = found.name; + _validationRegExp = found.validationRegExp; + _DataTypeId = found.dataTypeId; + _contenttypeid = found.contentTypeId; + _description = found.description; } #endregion @@ -92,7 +91,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _DataTypeId = value.Id; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery( "Update cmsPropertyType set DataTypeId = " + value.Id + " where id=" + Id); @@ -119,7 +117,6 @@ namespace umbraco.cms.businesslogic.propertytype { _tabId = value; PropertyTypeGroup = value; - InvalidateCache(); } } @@ -148,7 +145,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _mandatory = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery("Update cmsPropertyType set mandatory = @mandatory where id = @id", sqlHelper.CreateParameter("@mandatory", value), @@ -162,7 +158,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _validationRegExp = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery("Update cmsPropertyType set validationRegExp = @validationRegExp where id = @id", sqlHelper.CreateParameter("@validationRegExp", value), sqlHelper.CreateParameter("@id", Id)); @@ -199,7 +194,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _description = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery("Update cmsPropertyType set description = @description where id = @id", sqlHelper.CreateParameter("@description", value), @@ -213,7 +207,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _sortOrder = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery("Update cmsPropertyType set sortOrder = @sortOrder where id = @id", sqlHelper.CreateParameter("@sortOrder", value), @@ -227,7 +220,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _alias = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery("Update cmsPropertyType set alias = @alias where id= @id", sqlHelper.CreateParameter("@alias", Casing.SafeAliasWithForcingCheck(_alias)), @@ -264,7 +256,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _name = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery( "UPDATE cmsPropertyType SET name=@name WHERE id=@id", @@ -331,17 +322,17 @@ namespace umbraco.cms.businesslogic.propertytype public static IEnumerable GetPropertyTypes() { var result = new List(); - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader dr = - sqlHelper.ExecuteReader("select id from cmsPropertyType order by Name")) + + var propertyTypeIds = ApplicationContext.Current.DatabaseContext.Database.Fetch( + "select id from cmsPropertyType order by Name"); + + foreach (var propertyTypeId in propertyTypeIds) { - while (dr.Read()) - { - PropertyType pt = GetPropertyType(dr.GetInt("id")); - if (pt != null) - result.Add(pt); - } + PropertyType pt = GetPropertyType(propertyTypeId); + if (pt != null) + result.Add(pt); } + return result; } @@ -353,18 +344,17 @@ namespace umbraco.cms.businesslogic.propertytype public static IEnumerable GetPropertyTypesByGroup(int groupId) { var result = new List(); - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader dr = - sqlHelper.ExecuteReader("SELECT id FROM cmsPropertyType WHERE propertyTypeGroupId = @groupId order by SortOrder", - sqlHelper.CreateParameter("@groupId", groupId))) + + var propertyTypeIds = ApplicationContext.Current.DatabaseContext.Database.Fetch( + "SELECT id FROM cmsPropertyType WHERE propertyTypeGroupId = @groupId order by SortOrder", new {groupId = groupId}); + + foreach (var propertyTypeId in propertyTypeIds) { - while (dr.Read()) - { - PropertyType pt = GetPropertyType(dr.GetInt("id")); - if (pt != null) - result.Add(pt); - } + PropertyType pt = GetPropertyType(propertyTypeId); + if (pt != null) + result.Add(pt); } + return result; } @@ -376,20 +366,18 @@ namespace umbraco.cms.businesslogic.propertytype public static IEnumerable GetByDataTypeDefinition(int dataTypeDefId) { var result = new List(); - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader dr = - sqlHelper.ExecuteReader( - "select id, Name from cmsPropertyType where dataTypeId=@dataTypeId order by Name", - sqlHelper.CreateParameter("@dataTypeId", dataTypeDefId))) + + var propertyTypeIds = ApplicationContext.Current.DatabaseContext.Database.Fetch( + "select id from cmsPropertyType where dataTypeId=@dataTypeId order by Name", new {dataTypeId = dataTypeDefId}); + + foreach (var propertyTypeId in propertyTypeIds) { - while (dr.Read()) - { - PropertyType pt = GetPropertyType(dr.GetInt("id")); - if (pt != null) - result.Add(pt); - } + PropertyType pt = GetPropertyType(propertyTypeId); + if (pt != null) + result.Add(pt); } - return result.ToList(); + + return result; } public void delete() @@ -411,7 +399,6 @@ namespace umbraco.cms.businesslogic.propertytype // delete cache from either master (via tabid) or current contentype FlushCacheBasedOnTab(); - InvalidateCache(); } public void FlushCacheBasedOnTab() @@ -478,8 +465,6 @@ namespace umbraco.cms.businesslogic.propertytype protected virtual void FlushCache() { - // clear local cache - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(GetCacheKey(Id)); // clear cache in contentype ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.ContentTypePropertiesCacheKey + _contenttypeid); @@ -496,31 +481,9 @@ namespace umbraco.cms.businesslogic.propertytype public static PropertyType GetPropertyType(int id) { - return ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( - GetCacheKey(id), - timeout: TimeSpan.FromMinutes(30), - getCacheItem: () => - { - try - { - return new PropertyType(id); - } - catch - { - return null; - } - }); - } - - private void InvalidateCache() - { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(GetCacheKey(Id)); - } - - private static string GetCacheKey(int id) - { - return CacheKeys.PropertyTypeCacheKey + id; + return new PropertyType(id); } + #endregion } diff --git a/src/umbraco.cms/businesslogic/template/Template.cs b/src/umbraco.cms/businesslogic/template/Template.cs index 8d4b72da63..8d507792f2 100644 --- a/src/umbraco.cms/businesslogic/template/Template.cs +++ b/src/umbraco.cms/businesslogic/template/Template.cs @@ -29,8 +29,8 @@ namespace umbraco.cms.businesslogic.template #region Private members - private readonly ViewHelper _viewHelper = new ViewHelper(new PhysicalFileSystem(SystemDirectories.MvcViews)); - private readonly MasterPageHelper _masterPageHelper = new MasterPageHelper(new PhysicalFileSystem(SystemDirectories.Masterpages)); + private readonly ViewHelper _viewHelper = new ViewHelper(FileSystemProviderManager.Current.MvcViewsFileSystem); + private readonly MasterPageHelper _masterPageHelper = new MasterPageHelper(FileSystemProviderManager.Current.MasterPagesFileSystem); internal ITemplate TemplateEntity; private int? _mastertemplate; diff --git a/src/umbraco.cms/businesslogic/web/Document.cs b/src/umbraco.cms/businesslogic/web/Document.cs index 70775f5c71..3ed457d776 100644 --- a/src/umbraco.cms/businesslogic/web/Document.cs +++ b/src/umbraco.cms/businesslogic/web/Document.cs @@ -433,21 +433,20 @@ namespace umbraco.cms.businesslogic.web { XmlDocument xd = new XmlDocument(); - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader dr = sqlHelper.ExecuteReader("select nodeId from cmsDocument")) + var nodeIds = ApplicationContext.Current.DatabaseContext.Database.Fetch( + "select nodeId from cmsDocument"); + + foreach (var nodeId in nodeIds) { - while (dr.Read()) + try { - try - { - new Document(dr.GetInt("nodeId")).SaveXmlPreview(xd); - } - catch (Exception ee) - { - LogHelper.Error("Error generating preview xml", ee); - } + new Document(nodeId).SaveXmlPreview(xd); } - } + catch (Exception ee) + { + LogHelper.Error("Error generating preview xml", ee); + } + } } /// @@ -875,18 +874,15 @@ namespace umbraco.cms.businesslogic.web [Obsolete("Don't use! Only used internally to support the legacy events", false)] internal IEnumerable> PublishWithSubs(int userId, bool includeUnpublished) { - PublishEventArgs e = new PublishEventArgs(); + var e = new PublishEventArgs(); FireBeforePublish(e); - IEnumerable> publishedResults = Enumerable.Empty>(); + if (e.Cancel) return Enumerable.Empty>(); - if (!e.Cancel) - { - publishedResults = ApplicationContext.Current.Services.ContentService - .PublishWithChildrenWithStatus(ContentEntity, userId, includeUnpublished); + var publishedResults = ApplicationContext.Current.Services.ContentService + .PublishWithChildrenWithStatus(ContentEntity, userId, includeUnpublished); - FireAfterPublish(e); - } + FireAfterPublish(e); return publishedResults; } @@ -894,7 +890,6 @@ namespace umbraco.cms.businesslogic.web [Obsolete("Don't use! Only used internally to support the legacy events", false)] internal Attempt SaveAndPublish(int userId) { - var result = Attempt.Fail(new PublishStatus(ContentEntity, PublishStatusType.FailedCancelledByEvent, new EventMessages())); foreach (var property in GenericProperties) { ContentEntity.SetValue(property.PropertyType.Alias, property.Value); @@ -902,32 +897,28 @@ namespace umbraco.cms.businesslogic.web var saveArgs = new SaveEventArgs(); FireBeforeSave(saveArgs); + if (saveArgs.Cancel) return Attempt.Fail(new PublishStatus(ContentEntity, PublishStatusType.FailedCancelledByEvent, new EventMessages())); - if (!saveArgs.Cancel) - { - var publishArgs = new PublishEventArgs(); - FireBeforePublish(publishArgs); + var publishArgs = new PublishEventArgs(); + FireBeforePublish(publishArgs); + if (publishArgs.Cancel) return Attempt.Fail(new PublishStatus(ContentEntity, PublishStatusType.FailedCancelledByEvent, new EventMessages())); - if (!publishArgs.Cancel) - { - //NOTE: The 'false' parameter will cause the PublishingStrategy events to fire which will ensure that the cache is refreshed. - result = ApplicationContext.Current.Services.ContentService - .SaveAndPublishWithStatus(ContentEntity, userId); - base.VersionDate = ContentEntity.UpdateDate; - this.UpdateDate = ContentEntity.UpdateDate; + //NOTE: The 'false' parameter will cause the PublishingStrategy events to fire which will ensure that the cache is refreshed. + var result = ApplicationContext.Current.Services.ContentService + .SaveAndPublishWithStatus(ContentEntity, userId); + VersionDate = ContentEntity.UpdateDate; + UpdateDate = ContentEntity.UpdateDate; - //NOTE: This is just going to call the CMSNode Save which will launch into the CMSNode.BeforeSave and CMSNode.AfterSave evenths - // which actually do dick all and there's no point in even having them there but just in case for some insane reason someone - // has bound to those events, I suppose we'll need to keep this here. - base.Save(); + //NOTE: This is just going to call the CMSNode Save which will launch into the CMSNode.BeforeSave and CMSNode.AfterSave evenths + // which actually do dick all and there's no point in even having them there but just in case for some insane reason someone + // has bound to those events, I suppose we'll need to keep this here. + base.Save(); - //Launch the After Save event since we're doing 2 things in one operation: Saving and publishing. - FireAfterSave(saveArgs); + //Launch the After Save event since we're doing 2 things in one operation: Saving and publishing. + FireAfterSave(saveArgs); - //Now we need to fire the After publish event - FireAfterPublish(publishArgs); - } - } + //Now we need to fire the After publish event + FireAfterPublish(publishArgs); return result; } diff --git a/src/umbraco.cms/packages.config b/src/umbraco.cms/packages.config index 46e6d733a1..4242d98950 100644 --- a/src/umbraco.cms/packages.config +++ b/src/umbraco.cms/packages.config @@ -1,8 +1,8 @@  - - + + \ No newline at end of file diff --git a/src/umbraco.cms/umbraco.cms.csproj b/src/umbraco.cms/umbraco.cms.csproj index 89c2db4032..921466498c 100644 --- a/src/umbraco.cms/umbraco.cms.csproj +++ b/src/umbraco.cms/umbraco.cms.csproj @@ -1,5 +1,5 @@  - + Local 9.0.30729 @@ -108,17 +108,19 @@ ..\packages\ClientDependency.1.9.2\lib\net45\ClientDependency.Core.dll + True - - ..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll + + ..\packages\HtmlAgilityPack.1.4.9.5\lib\Net45\HtmlAgilityPack.dll + True ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll + True - - ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - True + + ..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll System @@ -166,6 +168,7 @@ ..\packages\Tidy.Net.1.0.0\lib\TidyNet.dll + True diff --git a/src/umbraco.controls/TreePicker/BaseTreePickerScripts.Designer.cs b/src/umbraco.controls/TreePicker/BaseTreePickerScripts.Designer.cs index 75268145d4..cf53229e77 100644 --- a/src/umbraco.controls/TreePicker/BaseTreePickerScripts.Designer.cs +++ b/src/umbraco.controls/TreePicker/BaseTreePickerScripts.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18034 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -62,16 +62,17 @@ namespace umbraco.uicontrols.TreePicker { /// /// Looks up a localized string similar to /// <reference path="/umbraco_client/Application/NamespaceManager.js" /> - /// - ///Umbraco.Sys.registerNamespace("Umbraco.Controls"); - /// - ///(function($) { - /// Umbraco.Controls.TreePicker = function(clientId, label, itemIdValueClientID, itemTitleClientID, itemPickerUrl, width, height, showHeader, umbracoPath) { - /// var obj = { - /// _itemPickerUrl: itemPickerUrl, - /// _webServiceUrl: umbracoPath + "/webservices/legacyAjaxCalls.asmx/GetNodeBreadcrumbs", - /// _label: label, - /// _wid [rest of string was truncated]";. + ///(function ($) { + /// $(document).ready(function () { + /// // Tooltip only Text + /// $('.umb-tree-picker a.choose').click(function () { + /// var that = this; + /// var s = $(that).data("section"); + /// UmbClientMgr.openAngularModalWindow({ + /// template: 'views/common/dialogs/treepicker.html', + /// section: s, + /// callback: function (data) { + /// //this [rest of string was truncated]";. /// internal static string BaseTreePicker { get { diff --git a/src/umbraco.controls/app.config b/src/umbraco.controls/app.config index a0794caa99..2928c2785f 100644 --- a/src/umbraco.controls/app.config +++ b/src/umbraco.controls/app.config @@ -12,7 +12,7 @@ - + @@ -30,6 +30,10 @@ + + + + - \ No newline at end of file + diff --git a/src/umbraco.controls/umbraco.controls.csproj b/src/umbraco.controls/umbraco.controls.csproj index 9c2d016b93..a9177d9f3f 100644 --- a/src/umbraco.controls/umbraco.controls.csproj +++ b/src/umbraco.controls/umbraco.controls.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -70,6 +70,7 @@ ..\packages\ClientDependency.1.9.2\lib\net45\ClientDependency.Core.dll + True diff --git a/src/umbraco.datalayer/DataLayerHelper.cs b/src/umbraco.datalayer/DataLayerHelper.cs index 7952b737ec..f79a90a0f1 100644 --- a/src/umbraco.datalayer/DataLayerHelper.cs +++ b/src/umbraco.datalayer/DataLayerHelper.cs @@ -10,6 +10,7 @@ using System; using System.Configuration; using System.Data.Common; using System.Reflection; +using Umbraco.Core; namespace umbraco.DataLayer { @@ -72,7 +73,7 @@ namespace umbraco.DataLayer throw new ArgumentException("Bad connection string.", "connectionString", ex); } - var connectionStringSettings = ConfigurationManager.ConnectionStrings[Umbraco.Core.Configuration.GlobalSettings.UmbracoConnectionName]; + var connectionStringSettings = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName]; if (forceLegacyConnection == false && connectionStringSettings != null) SetDataHelperNames(connectionStringSettings); diff --git a/src/umbraco.datalayer/Properties/AssemblyInfo.cs b/src/umbraco.datalayer/Properties/AssemblyInfo.cs index 7f5caaee39..2062f36415 100644 --- a/src/umbraco.datalayer/Properties/AssemblyInfo.cs +++ b/src/umbraco.datalayer/Properties/AssemblyInfo.cs @@ -25,3 +25,5 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("6210756f-436d-4536-bad3-d7b000e1694f")] + +[assembly: InternalsVisibleTo("SqlCE4Umbraco")] \ No newline at end of file diff --git a/src/umbraco.datalayer/SqlHelper.cs b/src/umbraco.datalayer/SqlHelper.cs index db850e6832..8eadd01d35 100644 --- a/src/umbraco.datalayer/SqlHelper.cs +++ b/src/umbraco.datalayer/SqlHelper.cs @@ -10,6 +10,7 @@ using System; using System.Data; using System.Diagnostics; using System.IO; +using System.Reflection; using System.Xml; using umbraco.DataLayer.Utility; using Umbraco.Core.Logging; @@ -343,5 +344,120 @@ namespace umbraco.DataLayer { } #endregion + + #region Evil Reflection + + internal CurrentConnectionUsing UseCurrentConnection + { + get { return new CurrentConnectionUsing(); } + } + + #endregion } + + #region Evil Reflection + + internal class CurrentConnectionUsing : IDisposable + { + private static MethodInfo OpenMethod { get; set; } + //private static MethodInfo CloseMethod { get; set; } + + private static readonly object ScopeProvider; + private static readonly MethodInfo ScopeProviderAmbientOrNoScopeMethod; + private static readonly PropertyInfo ScopeDatabaseProperty; + private static readonly PropertyInfo ConnectionProperty; + private static readonly FieldInfo TransactionField; + private static readonly PropertyInfo InnerConnectionProperty; + private static readonly PropertyInfo InnerTransactionProperty; + private static readonly object[] NoArgs = new object[0]; + + private readonly object _database; + + static CurrentConnectionUsing() + { + var coreAssembly = Assembly.Load("Umbraco.Core"); + + var applicationContextType = coreAssembly.GetType("Umbraco.Core.ApplicationContext"); + var databaseContextType = coreAssembly.GetType("Umbraco.Core.DatabaseContext"); + var umbracoDatabaseType = coreAssembly.GetType("Umbraco.Core.Persistence.UmbracoDatabase"); + var databaseType = coreAssembly.GetType("Umbraco.Core.Persistence.Database"); + var scopeProviderType = coreAssembly.GetType("Umbraco.Core.Scoping.IScopeProviderInternal"); + var scopeType = coreAssembly.GetType("Umbraco.Core.Scoping.IScope"); + + var applicationContextCurrentProperty = applicationContextType.GetProperty("Current", BindingFlags.Static | BindingFlags.Public); + if (applicationContextCurrentProperty == null) throw new Exception("oops: applicationContextCurrentProperty."); + var applicationContext = applicationContextCurrentProperty.GetValue(null, NoArgs); + + var applicationContextDatabaseContextProperty = applicationContextType.GetProperty("DatabaseContext", BindingFlags.Instance | BindingFlags.Public); + if (applicationContextDatabaseContextProperty == null) throw new Exception("oops: applicationContextDatabaseContextProperty."); + var databaseContext = applicationContextDatabaseContextProperty.GetValue(applicationContext, NoArgs); + + var databaseContextScopeProviderField = databaseContextType.GetField("ScopeProvider", BindingFlags.Instance | BindingFlags.NonPublic); + if (databaseContextScopeProviderField == null) throw new Exception("oops: databaseContextScopeProviderField."); + ScopeProvider = databaseContextScopeProviderField.GetValue(databaseContext); + + ScopeProviderAmbientOrNoScopeMethod = scopeProviderType.GetMethod("GetAmbientOrNoScope", BindingFlags.Instance | BindingFlags.Public); + if (ScopeProviderAmbientOrNoScopeMethod == null) throw new Exception("oops: ScopeProviderAmbientOrNoScopeMethod."); + ScopeDatabaseProperty = scopeType.GetProperty("Database", BindingFlags.Instance | BindingFlags.Public); + if (ScopeDatabaseProperty == null) throw new Exception("oops: ScopeDatabaseProperty."); + + OpenMethod = databaseType.GetMethod("OpenSharedConnection", BindingFlags.Instance | BindingFlags.Public); + //CloseMethod = databaseType.GetMethod("CloseSharedConnection", BindingFlags.Instance | BindingFlags.Public); + + ConnectionProperty = umbracoDatabaseType.GetProperty("Connection", BindingFlags.Instance | BindingFlags.Public); + TransactionField = databaseType.GetField("_transaction", BindingFlags.Instance | BindingFlags.NonPublic); + + var profilerAssembly = Assembly.Load("MiniProfiler"); + var profiledDbConnectionType = profilerAssembly.GetType("StackExchange.Profiling.Data.ProfiledDbConnection"); + InnerConnectionProperty = profiledDbConnectionType.GetProperty("InnerConnection", BindingFlags.Instance | BindingFlags.Public); + var profiledDbTransactionType = profilerAssembly.GetType("StackExchange.Profiling.Data.ProfiledDbTransaction"); + InnerTransactionProperty = profiledDbTransactionType.GetProperty("WrappedTransaction", BindingFlags.Instance | BindingFlags.Public); + } + + public CurrentConnectionUsing() + { + var scope = ScopeProviderAmbientOrNoScopeMethod.Invoke(ScopeProvider, NoArgs); + _database = ScopeDatabaseProperty.GetValue(scope); + + var connection = ConnectionProperty.GetValue(_database, NoArgs); + // we have to open to make sure that we *do* have a connection + if (connection == null) + OpenMethod.Invoke(_database, NoArgs); + } + + public IDbConnection Connection + { + get + { + var connection = ConnectionProperty.GetValue(_database, NoArgs); + if (connection == null) return null; + var inner = InnerConnectionProperty.GetValue(connection, NoArgs); + return inner as IDbConnection; + } + } + + public IDbTransaction Transaction + { + get + { + var transaction = TransactionField.GetValue(_database); + if (transaction == null) return null; + var inner = InnerTransactionProperty.GetValue(transaction, NoArgs); + return inner as IDbTransaction; + } + } + + public void Dispose() + { + // this was required when we *always* opened the connection + // but now we open it only if it's not opened already and then, + // we assume that Core will dispose of it somehow - since it + // hasn't been opened twice. + + // we have to close since we have opened + //CloseMethod.Invoke(_database, NoArgs); + } + } + + #endregion } diff --git a/src/umbraco.datalayer/SqlHelpers/MySql/MySqlHelper.cs b/src/umbraco.datalayer/SqlHelpers/MySql/MySqlHelper.cs index 8cd36255cb..c1a530f8f4 100644 --- a/src/umbraco.datalayer/SqlHelpers/MySql/MySqlHelper.cs +++ b/src/umbraco.datalayer/SqlHelpers/MySql/MySqlHelper.cs @@ -56,7 +56,28 @@ namespace umbraco.DataLayer.SqlHelpers.MySql /// The return value of the command. protected override object ExecuteScalar(string commandText, MSC.MySqlParameter[] parameters) { - return MSC.MySqlHelper.ExecuteScalar(ConnectionString, commandText, parameters); + using (var cc = UseCurrentConnection) + { + return ExecuteScalar((MSC.MySqlConnection) cc.Connection, (MSC.MySqlTransaction) cc.Transaction, commandText, parameters); + } + } + + // copied & adapted from MySqlHelper + private static object ExecuteScalar(MSC.MySqlConnection connection, MSC.MySqlTransaction trx, string commandText, params MSC.MySqlParameter[] commandParameters) + { + var mySqlCommand = new MSC.MySqlCommand(); + mySqlCommand.Connection = connection; + if (trx != null) mySqlCommand.Transaction = trx; + mySqlCommand.CommandText = commandText; + mySqlCommand.CommandType = CommandType.Text; + if (commandParameters != null) + { + foreach (var commandParameter in commandParameters) + mySqlCommand.Parameters.Add(commandParameter); + } + var obj = mySqlCommand.ExecuteScalar(); + mySqlCommand.Parameters.Clear(); + return obj; } /// Executes a command and returns the number of rows affected. @@ -67,7 +88,28 @@ namespace umbraco.DataLayer.SqlHelpers.MySql /// protected override int ExecuteNonQuery(string commandText, MSC.MySqlParameter[] parameters) { - return MSC.MySqlHelper.ExecuteNonQuery(ConnectionString, commandText, parameters); + using (var cc = UseCurrentConnection) + { + return ExecuteNonQuery((MSC.MySqlConnection) cc.Connection, (MSC.MySqlTransaction)cc.Transaction, commandText, parameters); + } + } + + // copied & adapted from MySqlHelper + public static int ExecuteNonQuery(MSC.MySqlConnection connection, MSC.MySqlTransaction trx, string commandText, params MSC.MySqlParameter[] commandParameters) + { + var mySqlCommand = new MSC.MySqlCommand(); + mySqlCommand.Connection = connection; + if (trx != null) mySqlCommand.Transaction = trx; + mySqlCommand.CommandText = commandText; + mySqlCommand.CommandType = CommandType.Text; + if (commandParameters != null) + { + foreach (var commandParameter in commandParameters) + mySqlCommand.Parameters.Add(commandParameter); + } + var num = mySqlCommand.ExecuteNonQuery(); + mySqlCommand.Parameters.Clear(); + return num; } /// Executes a command and returns a records reader containing the results. @@ -78,7 +120,28 @@ namespace umbraco.DataLayer.SqlHelpers.MySql /// protected override IRecordsReader ExecuteReader(string commandText, MSC.MySqlParameter[] parameters) { - return new MySqlDataReader(MSC.MySqlHelper.ExecuteReader(ConnectionString, commandText, parameters)); + using (var cc = UseCurrentConnection) + { + return new MySqlDataReader(ExecuteReader((MSC.MySqlConnection) cc.Connection, (MSC.MySqlTransaction) cc.Transaction, commandText, parameters)); + } + } + + // copied & adapted from MySqlHelper + private static MSC.MySqlDataReader ExecuteReader(MSC.MySqlConnection connection, MSC.MySqlTransaction trx, string commandText, MSC.MySqlParameter[] commandParameters) + { + MSC.MySqlCommand mySqlCommand = new MSC.MySqlCommand(); + mySqlCommand.Connection = connection; + if (trx != null) mySqlCommand.Transaction = trx; + mySqlCommand.CommandText = commandText; + mySqlCommand.CommandType = CommandType.Text; + if (commandParameters != null) + { + foreach (var commandParameter in commandParameters) + mySqlCommand.Parameters.Add(commandParameter); + } + MSC.MySqlDataReader mySqlDataReader = mySqlCommand.ExecuteReader(); + mySqlCommand.Parameters.Clear(); + return mySqlDataReader; } /// Converts the scalar value to the given type. diff --git a/src/umbraco.datalayer/SqlHelpers/SqlServer/SqlServerHelper.cs b/src/umbraco.datalayer/SqlHelpers/SqlServer/SqlServerHelper.cs index 3eb298a7f5..23239aea49 100644 --- a/src/umbraco.datalayer/SqlHelpers/SqlServer/SqlServerHelper.cs +++ b/src/umbraco.datalayer/SqlHelpers/SqlServer/SqlServerHelper.cs @@ -52,9 +52,14 @@ namespace umbraco.DataLayer.SqlHelpers.SqlServer #if DEBUG && DebugDataLayer // Log Query Execution Trace.TraceInformation(GetType().Name + " SQL ExecuteScalar: " + commandText); - #endif +#endif - return SH.ExecuteScalar(ConnectionString, CommandType.Text, commandText, parameters); + using (var cc = UseCurrentConnection) + { + return cc.Transaction == null + ? SH.ExecuteScalar((SqlConnection) cc.Connection, CommandType.Text, commandText, parameters) + : SH.ExecuteScalar((SqlTransaction) cc.Transaction, CommandType.Text, commandText, parameters); + } } /// @@ -65,14 +70,19 @@ namespace umbraco.DataLayer.SqlHelpers.SqlServer /// /// The number of rows affected by the command. /// - protected override int ExecuteNonQuery(string commandText, SqlParameter[] parameters) + protected override int ExecuteNonQuery(string commandText, params SqlParameter[] parameters) { #if DEBUG && DebugDataLayer // Log Query Execution Trace.TraceInformation(GetType().Name + " SQL ExecuteNonQuery: " + commandText); - #endif +#endif - return SH.ExecuteNonQuery(ConnectionString, CommandType.Text, commandText, parameters); + using (var cc = UseCurrentConnection) + { + return cc.Transaction == null + ? SH.ExecuteNonQuery((SqlConnection) cc.Connection, CommandType.Text, commandText, parameters) + : SH.ExecuteNonQuery((SqlTransaction) cc.Transaction, CommandType.Text, commandText, parameters); + } } /// @@ -83,15 +93,19 @@ namespace umbraco.DataLayer.SqlHelpers.SqlServer /// /// A data reader containing the results of the command. /// - protected override IRecordsReader ExecuteReader(string commandText, SqlParameter[] parameters) + protected override IRecordsReader ExecuteReader(string commandText, params SqlParameter[] parameters) { #if DEBUG && DebugDataLayer // Log Query Execution Trace.TraceInformation(GetType().Name + " SQL ExecuteReader: " + commandText); - #endif +#endif - return new SqlServerDataReader(SH.ExecuteReader(ConnectionString, CommandType.Text, - commandText, parameters)); + using (var cc = UseCurrentConnection) + { + return cc.Transaction == null + ? new SqlServerDataReader(SH.ExecuteReader((SqlConnection) cc.Connection, CommandType.Text, commandText, parameters)) + : new SqlServerDataReader(SH.ExecuteReader((SqlTransaction) cc.Transaction, CommandType.Text, commandText, parameters)); + } } } } \ No newline at end of file diff --git a/src/umbraco.datalayer/app.config b/src/umbraco.datalayer/app.config index 1f5a6442ad..9bec1bdd87 100644 --- a/src/umbraco.datalayer/app.config +++ b/src/umbraco.datalayer/app.config @@ -4,7 +4,7 @@ - + @@ -30,6 +30,15 @@ + + + + - \ No newline at end of file + + + + + + diff --git a/src/umbraco.datalayer/packages.config b/src/umbraco.datalayer/packages.config index 7cabfc99ad..c77c1fb4a2 100644 --- a/src/umbraco.datalayer/packages.config +++ b/src/umbraco.datalayer/packages.config @@ -1,6 +1,5 @@  - - + \ No newline at end of file diff --git a/src/umbraco.datalayer/umbraco.datalayer.csproj b/src/umbraco.datalayer/umbraco.datalayer.csproj index ab86b3532c..ac978cd1c3 100644 --- a/src/umbraco.datalayer/umbraco.datalayer.csproj +++ b/src/umbraco.datalayer/umbraco.datalayer.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -68,14 +68,12 @@ false - - ..\packages\log4net-mediumtrust.2.0.0\lib\log4net.dll - ..\packages\Microsoft.ApplicationBlocks.Data.1.0.1559.20655\lib\Microsoft.ApplicationBlocks.Data.dll + True - - ..\packages\MySql.Data.6.9.8\lib\net45\MySql.Data.dll + + ..\packages\MySql.Data.6.9.9\lib\net45\MySql.Data.dll diff --git a/src/umbraco.editorControls/PickerRelations/PickerRelationsEventHandler.cs b/src/umbraco.editorControls/PickerRelations/PickerRelationsEventHandler.cs index 13f786359b..844d9920e0 100644 --- a/src/umbraco.editorControls/PickerRelations/PickerRelationsEventHandler.cs +++ b/src/umbraco.editorControls/PickerRelations/PickerRelationsEventHandler.cs @@ -2,8 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; - -using umbraco.BusinessLogic; // ApplicationBase +// ApplicationBase using umbraco.businesslogic; using umbraco.cms.businesslogic; // SaveEventArgs using umbraco.cms.businesslogic.media; // Media @@ -12,6 +11,8 @@ using umbraco.cms.businesslogic.web; // Documentusing umbraco.cms.businesslogic. using umbraco.cms.businesslogic.property; using umbraco.cms.businesslogic.relation; using umbraco.DataLayer; +using Umbraco.Core; +using Application = umbraco.BusinessLogic.Application; namespace umbraco.editorControls.PickerRelations { @@ -212,7 +213,7 @@ namespace umbraco.editorControls.PickerRelations private static void DeleteRelations(RelationType relationType, int contentNodeId, bool reverseIndexing, string instanceIdentifier) { //if relationType is bi-directional or a reverse index then we can't get at the relations via the API, so using SQL - string getRelationsSql = "SELECT id FROM umbracoRelation WHERE relType = " + relationType.Id.ToString() + " AND "; + string getRelationsSql = "SELECT id FROM umbracoRelation WHERE relType = " + relationType.Id + " AND "; if (reverseIndexing || relationType.Dual) { @@ -229,19 +230,16 @@ namespace umbraco.editorControls.PickerRelations getRelationsSql += " AND comment = '" + instanceIdentifier + "'"; - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader relations = sqlHelper.ExecuteReader(getRelationsSql)) - { - //clear data - Relation relation; - while (relations.Read()) - { - relation = new Relation(relations.GetInt("id")); + var relationIds = ApplicationContext.Current.DatabaseContext.Database.Fetch( + getRelationsSql); + foreach (var relationId in relationIds) + { + var relation = new Relation(relationId); - // TODO: [HR] check to see if an instance identifier is used - relation.Delete(); - } - } + // TODO: [HR] check to see if an instance identifier is used + relation.Delete(); + } + } /// diff --git a/src/umbraco.editorControls/app.config b/src/umbraco.editorControls/app.config index 1d7a37c980..9de76a47e4 100644 --- a/src/umbraco.editorControls/app.config +++ b/src/umbraco.editorControls/app.config @@ -16,7 +16,7 @@ - + @@ -51,6 +51,10 @@ + + + + - \ No newline at end of file + diff --git a/src/umbraco.editorControls/mediapicker/MediaChooserScripts.Designer.cs b/src/umbraco.editorControls/mediapicker/MediaChooserScripts.Designer.cs index a7743a55be..30cc94f1de 100644 --- a/src/umbraco.editorControls/mediapicker/MediaChooserScripts.Designer.cs +++ b/src/umbraco.editorControls/mediapicker/MediaChooserScripts.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18034 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. diff --git a/src/umbraco.editorControls/umbraco.editorControls.csproj b/src/umbraco.editorControls/umbraco.editorControls.csproj index b837daf06e..1649d2f462 100644 --- a/src/umbraco.editorControls/umbraco.editorControls.csproj +++ b/src/umbraco.editorControls/umbraco.editorControls.csproj @@ -1,5 +1,5 @@  - + Local 9.0.30729 @@ -116,6 +116,7 @@ ..\packages\ClientDependency.1.9.2\lib\net45\ClientDependency.Core.dll + True System diff --git a/src/umbraco.interfaces/IAction.cs b/src/umbraco.interfaces/IAction.cs index 6d62a0780f..e82c5935c4 100644 --- a/src/umbraco.interfaces/IAction.cs +++ b/src/umbraco.interfaces/IAction.cs @@ -5,8 +5,8 @@ namespace umbraco.interfaces /// /// Summary description for ActionI. /// - public interface IAction - { + public interface IAction : IDiscoverable + { char Letter {get;} bool ShowInNotifier {get;} bool CanBePermissionAssigned {get;} diff --git a/src/umbraco.interfaces/IApplication.cs b/src/umbraco.interfaces/IApplication.cs index 98dc043dcd..3c0528f961 100644 --- a/src/umbraco.interfaces/IApplication.cs +++ b/src/umbraco.interfaces/IApplication.cs @@ -7,6 +7,6 @@ namespace umbraco.interfaces /// /// Interface for created applications in the umbraco backoffice /// - public interface IApplication - {} + public interface IApplication : IDiscoverable + { } } diff --git a/src/umbraco.interfaces/IApplicationStartupHandler.cs b/src/umbraco.interfaces/IApplicationStartupHandler.cs index cff7d25cb9..4e66135db0 100644 --- a/src/umbraco.interfaces/IApplicationStartupHandler.cs +++ b/src/umbraco.interfaces/IApplicationStartupHandler.cs @@ -14,6 +14,6 @@ namespace umbraco.interfaces /// and bind to any custom events in the OnApplicationInitialized method. /// [Obsolete("This interface is obsolete, use IApplicationEventHandler or ApplicationEventHandler instead")] - public interface IApplicationStartupHandler + public interface IApplicationStartupHandler : IDiscoverable { } } diff --git a/src/umbraco.interfaces/ICacheRefresher.cs b/src/umbraco.interfaces/ICacheRefresher.cs index d239e81fe6..642be660d3 100644 --- a/src/umbraco.interfaces/ICacheRefresher.cs +++ b/src/umbraco.interfaces/ICacheRefresher.cs @@ -7,7 +7,7 @@ namespace umbraco.interfaces /// The IcacheRefresher Interface is used for loadbalancing. /// /// - public interface ICacheRefresher + public interface ICacheRefresher : IDiscoverable { Guid UniqueIdentifier { get; } string Name { get; } diff --git a/src/umbraco.interfaces/IDataType.cs b/src/umbraco.interfaces/IDataType.cs index 72d611a843..591057d76b 100644 --- a/src/umbraco.interfaces/IDataType.cs +++ b/src/umbraco.interfaces/IDataType.cs @@ -8,8 +8,8 @@ namespace umbraco.interfaces /// And finally it contains IData which manages the actual data in the Data Type /// [Obsolete("IDataType is obsolete and is no longer used, it will be removed from the codebase in future versions")] - public interface IDataType - { + public interface IDataType : IDiscoverable + { /// /// Gets the id. /// diff --git a/src/umbraco.interfaces/IDiscoverable.cs b/src/umbraco.interfaces/IDiscoverable.cs new file mode 100644 index 0000000000..2c65122104 --- /dev/null +++ b/src/umbraco.interfaces/IDiscoverable.cs @@ -0,0 +1,8 @@ +namespace umbraco.interfaces +{ + /// + /// Marks a class or an interface as discoverable by PluginManager. + /// + public interface IDiscoverable + { } +} \ No newline at end of file diff --git a/src/umbraco.interfaces/IMacroGuiRendering.cs b/src/umbraco.interfaces/IMacroGuiRendering.cs index a9b1bdebe2..717b441889 100644 --- a/src/umbraco.interfaces/IMacroGuiRendering.cs +++ b/src/umbraco.interfaces/IMacroGuiRendering.cs @@ -6,8 +6,8 @@ namespace umbraco.interfaces /// Summary description for IMacroGuiRendering. /// [Obsolete("This interface is no longer used and will be removed from the codebase in future versions")] - public interface IMacroGuiRendering - { + public interface IMacroGuiRendering : IDiscoverable + { /// /// Gets or sets the value. /// diff --git a/src/umbraco.interfaces/IPackageAction.cs b/src/umbraco.interfaces/IPackageAction.cs index aea727221f..b52be3a245 100644 --- a/src/umbraco.interfaces/IPackageAction.cs +++ b/src/umbraco.interfaces/IPackageAction.cs @@ -4,7 +4,8 @@ using System.Text; using System.Xml; namespace umbraco.interfaces { - public interface IPackageAction { + public interface IPackageAction : IDiscoverable + { bool Execute(string packageName, XmlNode xmlData); string Alias(); bool Undo(string packageName, XmlNode xmlData); diff --git a/src/umbraco.interfaces/ITree.cs b/src/umbraco.interfaces/ITree.cs index 43cfe5d322..857c0a49ed 100644 --- a/src/umbraco.interfaces/ITree.cs +++ b/src/umbraco.interfaces/ITree.cs @@ -6,8 +6,8 @@ namespace umbraco.interfaces /// /// Interface for created application trees in the umbraco backoffice /// - public interface ITree - { + public interface ITree : IDiscoverable + { /// /// Sets the tree id. /// diff --git a/src/umbraco.interfaces/umbraco.interfaces.csproj b/src/umbraco.interfaces/umbraco.interfaces.csproj index 28bd27603f..dc246fc9bf 100644 --- a/src/umbraco.interfaces/umbraco.interfaces.csproj +++ b/src/umbraco.interfaces/umbraco.interfaces.csproj @@ -1,5 +1,5 @@  - + Local 9.0.30729 @@ -36,7 +36,7 @@ 3.5 - v4.0 + v4.5 publish\ true Disk @@ -77,6 +77,7 @@ full prompt AllRules.ruleset + false bin\Release\ @@ -100,6 +101,7 @@ pdbonly prompt AllRules.ruleset + false @@ -125,6 +127,7 @@ + diff --git a/src/umbraco.providers/app.config b/src/umbraco.providers/app.config index a0794caa99..2928c2785f 100644 --- a/src/umbraco.providers/app.config +++ b/src/umbraco.providers/app.config @@ -12,7 +12,7 @@ - + @@ -30,6 +30,10 @@ + + + + - \ No newline at end of file + diff --git a/src/umbraco.providers/umbraco.providers.csproj b/src/umbraco.providers/umbraco.providers.csproj index 102e9098da..0406afdc2e 100644 --- a/src/umbraco.providers/umbraco.providers.csproj +++ b/src/umbraco.providers/umbraco.providers.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU diff --git a/src/umbraco.sln.DotSettings b/src/umbraco.sln.DotSettings index 19aedb76e3..dd168f105b 100644 --- a/src/umbraco.sln.DotSettings +++ b/src/umbraco.sln.DotSettings @@ -1,4 +1,4 @@ - + <data><IncludeFilters /><ExcludeFilters /></data> <data /> True @@ -16,14 +16,5 @@ Disposable construction HINT False - - - - - - - - - - - \ No newline at end of file + CSharp50 + \ No newline at end of file