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 fb8092468e..b598939a1a 100644 --- a/.gitignore +++ b/.gitignore @@ -142,5 +142,5 @@ build/ui-docs.zip build/csharp-docs.zip build/msbuild.log .vs/ - +src/packages/ build/tools/ 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 408f37c13d..f26c4877cd 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,7 +111,7 @@ 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% SET toolsFolder=%CD%\tools\ IF NOT EXIST "%toolsFolder%" ( @@ -79,22 +143,33 @@ MOVE "%sevenZipExePath%tools\7za.exe" "%toolsFolder%7za.exe" FOR /f "delims=" %%A in ('dir "%toolsFolder%vswhere.*" /b') DO SET "vswhereExePath=%toolsFolder%%%A\" MOVE "%vswhereExePath%tools\vswhere.exe" "%toolsFolder%vswhere.exe" - + 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" for /f "usebackq tokens=1* delims=: " %%i in (`"%CD%\tools\vswhere.exe" -latest -requires Microsoft.Component.MSBuild`) do ( if /i "%%i"=="installationPath" set InstallDir=%%j ) SET VSWherePath="%InstallDir%\MSBuild" + ECHO. ECHO Visual Studio is installed in: %InstallDir% SET MSBUILDPATH=C:\Program Files (x86)\MSBuild\14.0\Bin SET MSBUILD="%MSBUILDPATH%\MsBuild.exe" +ECHO. +ECHO Reporting NuGet version +"%nuGetExecutable%" help | findstr "^NuGet Version:" + +ECHO. +ECHO Restoring NuGet packages +ECHO Into %nuGetFolder% +"%nuGetExecutable%" restore "%CD%\..\src\umbraco.sln" -Verbosity Quiet -NonInteractive -PackagesDirectory "%nuGetFolder%" +IF ERRORLEVEL 1 GOTO :error + ECHO. ECHO. ECHO Performing MSBuild and producing Umbraco binaries zip files @@ -102,13 +177,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. -%MSBUILD% "Build.proj" /p:BUILD_RELEASE=%release% /p:BUILD_COMMENT=%comment% /p:NugetPackagesDirectory=%nuGetFolder% /p:VSWherePath=%VSWherePath% -IF ERRORLEVEL 1 GOTO :error +%MSBUILD% "Build.proj" /p:BUILD_RELEASE=%RELEASE% /p:BUILD_COMMENT=%COMMENT% /p:NugetPackagesDirectory="%nuGetFolder%" /p:VSWherePath=%VSWherePath% /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 @@ -116,22 +193,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 c413031518..47e10bbcef 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -14,65 +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 b768916289..c9912aeb89 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -16,9 +16,10 @@ umbraco - + - + + 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/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 d6326331ce..f2739c069a 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.14 \ No newline at end of file +7.6.1 \ 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 6be8675e81..ca1dcd758f 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.5.14")] -[assembly: AssemblyInformationalVersion("7.5.14")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.6.1")] +[assembly: AssemblyInformationalVersion("7.6.1")] \ 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/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/CoreDebug.cs b/src/Umbraco.Core/Configuration/CoreDebug.cs index ff55be966b..74a28e7637 100644 --- a/src/Umbraco.Core/Configuration/CoreDebug.cs +++ b/src/Umbraco.Core/Configuration/CoreDebug.cs @@ -17,9 +17,13 @@ namespace Umbraco.Core.Configuration 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 fc2534633f..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. diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index aed7f61a06..51a39e15df 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -159,6 +159,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return GetOptionalTextElement("defaultDocumentTypeProperty", "Textstring"); } } + [ConfigurationProperty("showDeprecatedPropertyEditors")] + internal InnerTextConfigurationElement ShowDeprecatedPropertyEditors + { + get { return GetOptionalTextElement("showDeprecatedPropertyEditors", false); } + } + [ConfigurationProperty("EnableInheritedDocumentTypes")] internal InnerTextConfigurationElement EnableInheritedDocumentTypes { @@ -171,6 +177,18 @@ namespace Umbraco.Core.Configuration.UmbracoSettings 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 { get { return Notifications.NotificationEmailAddress; } @@ -306,6 +324,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return DefaultDocumentTypeProperty; } } + bool IContentSection.ShowDeprecatedPropertyEditors + { + get { return ShowDeprecatedPropertyEditors; } + } + bool IContentSection.EnableInheritedDocumentTypes { get { return EnableInheritedDocumentTypes; } @@ -315,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/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/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 033471d7f0..99f20339c7 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.14"); + private static readonly Version Version = new Version("7.6.1"); /// /// 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 e78d2adc5b..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); diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index d4a5a309af..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; } @@ -246,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; @@ -284,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; @@ -305,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); @@ -345,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 @@ -386,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(); @@ -433,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 { @@ -453,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) @@ -501,7 +603,7 @@ namespace Umbraco.Core } internal Result CreateDatabaseSchemaAndData(ApplicationContext applicationContext) - { + { try { var readyForInstall = CheckReadyForInstall(); @@ -519,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) { @@ -537,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))) { @@ -558,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" }; } @@ -588,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(); @@ -596,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()); @@ -629,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); @@ -756,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); } } @@ -770,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/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 index b0e8778359..3535dd52bf 100644 --- a/src/Umbraco.Core/Exceptions/ConnectionException.cs +++ b/src/Umbraco.Core/Exceptions/ConnectionException.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Exceptions { 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 a5a935ebb5..0d96dd0af1 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -4,9 +4,6 @@ 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? - /// /// Provides methods allowing the manipulation of files within an Umbraco application. /// @@ -147,4 +144,21 @@ namespace Umbraco.Core.IO /// 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 bfa7e4b2d8..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, 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/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/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 d40a192862..0000000000 --- a/src/Umbraco.Core/Media/ImageHelper.cs +++ /dev/null @@ -1,263 +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% - string newFileName; - using (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, SeekOrigin.Begin); - - 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 - newFileName = thumbnailFileName.Replace("UMBRACOSYSTHUMBNAIL", string.Format("{0}x{1}", widthTh, heightTh)); - using (var ms = new MemoryStream()) - { - bp.Save(ms, codec, ep); - ms.Seek(0, SeekOrigin.Begin); - - 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/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 7c47c51112..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, SeekOrigin.Begin); - 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/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/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/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/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/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/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/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/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/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/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 d8543c2ff6..ded7c60676 100644 --- a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs @@ -51,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); 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/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/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 cf7ae44a3e..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; @@ -65,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 47d5ee6bd8..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,8 +538,8 @@ namespace Umbraco.Core.Persistence.Querying case "InvariantContains": case "InvariantEquals": - //special case, if it is 'Contains' and the argument that Contains is being called on is - //Enumerable and the methodArgs is the actual member access, then it's an SQL IN clause + //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 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/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 ed55bbeeb1..a34a77d3b4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -32,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"); @@ -42,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; } @@ -545,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 @@ -1017,7 +1018,7 @@ ORDER BY cmsContentVersion.id DESC // if the cache contains the published version, use it if (withCache) { - var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + 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) { diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index 6bd9c2602c..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); diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index 55af8fc60b..985f9446b7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -19,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) 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 b8b4ac7818..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,6 +270,8 @@ AND umbracoNode.id <> @id", //Delete (base) node data Database.Delete("WHERE uniqueID = @Id", new { Id = entity.Key }); + + entity.DeletedDate = DateTime.Now; } #endregion @@ -287,7 +289,7 @@ AND umbracoNode.id <> @id", /// PreValue as a string public string GetPreValueAsString(int preValueId) { - var collections = RuntimeCache.GetCacheItemsByKeySearch(CacheKeys.DataTypePreValuesCacheKey + "_"); + var collections = IsolatedCache.GetCacheItemsByKeySearch(CacheKeys.DataTypePreValuesCacheKey + "_"); var preValue = collections.SelectMany(x => x.FormatAsDictionary().Values).FirstOrDefault(x => x.Id == preValueId); if (preValue != null) @@ -437,7 +439,7 @@ AND umbracoNode.id <> @id", private PreValueCollection GetCachedPreValueCollection(int datetypeId) { var key = GetPrefixedCacheKey(datetypeId); - return RuntimeCache.GetCacheItem(key, () => + 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(); @@ -493,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) @@ -540,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) 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 b76abf4db6..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) @@ -168,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 aa0e8062d3..4efa8a63ac 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -5,6 +5,7 @@ using System.Linq; using Umbraco.Core.Models; 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; @@ -44,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); @@ -281,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() @@ -294,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) { @@ -317,36 +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) { - if (isContent) + columns.Add("COUNT(*)"); + } + else + { + columns.AddRange(new List { - //only content has/needs this info - columns.Add("published.versionId as publishedVersion"); - columns.Add("document.versionId as newestVersion"); - columns.Add("contentversion.id as versionId"); + "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"); } - - 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 @@ -369,10 +508,13 @@ namespace Umbraco.Core.Persistence.Repositories .On("umbracoNode.id = published.nodeId"); } - entitySql.LeftJoin("cmsContentType contenttype").On("contenttype.nodeId = content.contentType"); + 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) { @@ -508,6 +650,18 @@ namespace Umbraco.Core.Persistence.Repositories UnitOfWork.DisposeIfDisposable(); } + public bool Exists(Guid key) + { + 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] 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 28e4fbf199..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,20 +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/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 9e8890a37b..4c9b1d3561 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -17,27 +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 = ""); - + 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 9528fd3d76..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 /// 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 6b5fcd43d7..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 { @@ -78,5 +81,20 @@ namespace Umbraco.Core.Persistence.Repositories /// 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 4c09fc6a29..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; } @@ -179,7 +179,7 @@ namespace Umbraco.Core.Persistence.Repositories // if the cache contains the item, use it if (withCache) { - var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + 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) @@ -332,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)); @@ -530,26 +535,19 @@ 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); + } } + Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsContentVersion", "contentId"), (sqlFull, pagingSqlQuery) => ProcessQuery(sqlFull, pagingSqlQuery), orderBy, orderDirection, orderBySystemField, 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 9d451a3066..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 @@ -587,42 +587,19 @@ 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()); } + Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsMember", "nodeId"), (sqlFull, sqlIds) => ProcessQuery(sqlFull, sqlIds), orderBy, orderDirection, orderBySystemField, @@ -690,7 +667,7 @@ namespace Umbraco.Core.Persistence.Repositories // if the cache contains the item, use it if (withCache) { - var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + 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) 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 32a9d384af..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) @@ -46,7 +40,10 @@ namespace Umbraco.Core.Persistence.Repositories } 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); } @@ -58,7 +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); } @@ -69,9 +69,8 @@ namespace Umbraco.Core.Persistence.Repositories sql.Select("*") .From(SqlSyntax) .LeftJoin(SqlSyntax) - .On(SqlSyntax, left => left.Id, right => right.AccessId) - //MUST be ordered by this GUID ID for the AccessRulesRelator to work - .OrderBy(dto => dto.Id, SqlSyntax); + .On(SqlSyntax, left => left.Id, right => right.AccessId); + return sql; } @@ -133,6 +132,11 @@ 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) @@ -158,10 +162,6 @@ 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(); diff --git a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs index 6c1d1c2008..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) { } 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 faa197007b..98743a6a63 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -25,6 +25,7 @@ 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 { @@ -38,7 +39,7 @@ namespace Umbraco.Core.Persistence.Repositories /// internal static bool ThrowOnWarning = false; - protected VersionableRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) + protected VersionableRepositoryBase(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) : base(work, cache, logger, sqlSyntax) { _contentSection = contentSection; @@ -663,9 +664,7 @@ ORDER BY contentNodeId, versionId, propertytypeid 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); } @@ -751,7 +750,7 @@ ORDER BY contentNodeId, versionId, propertytypeid var allsuccess = true; - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + var fs = FileSystemProviderManager.Current.MediaFileSystem; Parallel.ForEach(files, file => { try 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 bfb92ccf1a..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 @@ -57,7 +58,7 @@ namespace Umbraco.Core.Persistence }; } - _noCache = CacheHelper.CreateDisabledCacheHelper(); + _nullCache = CacheHelper.NoCache; _logger = logger; _sqlSyntax = sqlSyntax; _settings = settings; @@ -75,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, @@ -144,7 +145,7 @@ namespace Umbraco.Core.Persistence }; } - public virtual IContentTypeRepository CreateContentTypeRepository(IDatabaseUnitOfWork uow) + public virtual IContentTypeRepository CreateContentTypeRepository(IScopeUnitOfWork uow) { return new ContentTypeRepository( uow, @@ -153,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, @@ -162,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, @@ -171,7 +172,7 @@ namespace Umbraco.Core.Persistence _sqlSyntax); } - public virtual ILanguageRepository CreateLanguageRepository(IDatabaseUnitOfWork uow) + public virtual ILanguageRepository CreateLanguageRepository(IScopeUnitOfWork uow) { return new LanguageRepository( uow, @@ -179,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, @@ -190,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, @@ -198,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, @@ -217,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, @@ -261,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, @@ -270,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! @@ -280,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, @@ -299,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, @@ -339,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..15a7a816bd --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs @@ -0,0 +1,32 @@ +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) + { + if(source == null) + { + return string.Empty; + } + + 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/PublishingStrategy.cs b/src/Umbraco.Core/Publishing/PublishingStrategy.cs index 37630f2638..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) + 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,12 +107,17 @@ 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 /// + /// /// /// /// @@ -120,8 +145,8 @@ namespace Umbraco.Core.Publishing /// 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>(); @@ -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( @@ -283,13 +307,13 @@ namespace Umbraco.Core.Publishing // 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); @@ -304,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; + } + } /// @@ -323,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 @@ -348,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)); @@ -386,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; + } + + } /// @@ -407,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(); + } + } /// @@ -419,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(); + } } /// @@ -431,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(); + } } /// @@ -442,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 795453e484..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)); @@ -138,5 +200,36 @@ namespace Umbraco.Core.Security 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; + } } } 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 44e91e2b80..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,23 +567,21 @@ 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); } } @@ -603,7 +605,7 @@ namespace Umbraco.Core.Services /// Field to order by /// Direction to order by /// 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 = "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,22 +1770,20 @@ 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; } @@ -1777,13 +1800,13 @@ namespace Umbraco.Core.Services Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) + 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"}, + new[] { "level", "parentID", "sortOrder" }, out totalRecords); return contents; } @@ -1795,10 +1818,11 @@ namespace Umbraco.Core.Services /// 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; } } @@ -1812,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 @@ -1835,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); } } @@ -1857,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 @@ -1919,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()) { @@ -1932,6 +1951,7 @@ namespace Umbraco.Core.Services int result = exists ? uow.Database.Update(poco) : Convert.ToInt32(uow.Database.Insert(poco)); + uow.Commit(); } } } @@ -1957,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 @@ -2012,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) @@ -2038,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; } } @@ -2072,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 @@ -2085,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)); } @@ -2107,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); @@ -2160,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) @@ -2170,30 +2189,28 @@ namespace Umbraco.Core.Services repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); } - uow.Commit(); - } + if (raiseEvents) + uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false, evtMsgs)); - 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(uow, content); + } - //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)) - { + //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); + var descendants = GetPublishedDescendants(content); + _publishingStrategy.PublishingFinalized(uow, descendants, false); + } - _publishingStrategy.PublishingFinalized(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); } - - Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); - - return Attempt.If(publishStatus.StatusType == PublishStatusType.Success, publishStatus); } } @@ -2208,26 +2225,23 @@ namespace Umbraco.Core.Services { var evtMsgs = EventMessagesFactory.Get(); - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(content, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - } - - if (string.IsNullOrWhiteSpace(content.Name)) - { - throw new ArgumentException("Cannot save content with empty name."); - } - 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; @@ -2243,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); } } @@ -2300,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( @@ -2308,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( @@ -2337,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); @@ -2352,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; } } @@ -2403,7 +2419,7 @@ namespace Umbraco.Core.Services #region Event Handlers /// /// Occurs before Delete - /// + /// public static event TypedEventHandler> Deleting; /// @@ -2413,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 dc7d9dfa1e..1b6735153d 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -41,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) @@ -53,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); } } @@ -78,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) @@ -90,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); } } @@ -116,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( @@ -131,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(); @@ -147,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); @@ -180,28 +184,27 @@ 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); } } @@ -228,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); } } @@ -257,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); } } @@ -277,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 ? @@ -303,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 ? @@ -334,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(); } } @@ -350,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); } } @@ -446,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); } } @@ -459,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); } } @@ -472,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); } } @@ -485,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); } } @@ -498,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()); } } @@ -511,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); } } @@ -526,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); } } @@ -543,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; } } @@ -558,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. @@ -575,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); } } @@ -713,19 +752,23 @@ 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; - - if (string.IsNullOrWhiteSpace(contentType.Name)) - { - throw new ArgumentException("Cannot save content type with empty name."); - } - 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) @@ -733,12 +776,15 @@ namespace Umbraco.Core.Services 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); } /// @@ -750,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 @@ -773,12 +822,15 @@ namespace Umbraco.Core.Services //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); } /// @@ -789,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); } } @@ -833,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); } } @@ -875,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); } } @@ -888,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); } } @@ -901,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); } } @@ -914,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); } } @@ -927,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()); } } @@ -940,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); } } @@ -955,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); } } @@ -972,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; } } @@ -987,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; } } @@ -1001,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; @@ -1028,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)); } @@ -1044,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; @@ -1071,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)); } @@ -1088,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) @@ -1118,6 +1163,7 @@ namespace Umbraco.Core.Services } catch (DataOperationException ex) { + uow.Commit(); return Attempt.Fail(new OperationStatus(null, ex.Operation, evtMsgs)); } uow.Commit(); @@ -1131,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) @@ -1161,6 +1208,7 @@ namespace Umbraco.Core.Services } catch (DataOperationException ex) { + uow.Commit(); return Attempt.Fail(new OperationStatus(null, ex.Operation, evtMsgs)); } uow.Commit(); @@ -1176,14 +1224,18 @@ 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) @@ -1191,13 +1243,14 @@ namespace Umbraco.Core.Services 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); } /// @@ -1209,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 @@ -1232,13 +1288,15 @@ namespace Umbraco.Core.Services //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); } /// @@ -1249,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); } } @@ -1279,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); } } @@ -1348,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)); } } @@ -1365,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 diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs index 2067de847c..c2dfd687dd 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs @@ -10,13 +10,12 @@ 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. @@ -34,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; @@ -51,25 +50,30 @@ 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(); } /// diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index b3fca10798..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,25 +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; - - if (string.IsNullOrWhiteSpace(dataTypeDefinition.Name)) + using (var uow = UowProvider.GetUnitOfWork()) { - throw new ArgumentException("Cannot save datatype with empty name."); - } + 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); - var uow = UowProvider.GetUnitOfWork(); - using (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); } /// @@ -400,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(); + } } /// @@ -435,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(); } } @@ -469,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); @@ -490,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(); } @@ -506,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 @@ -524,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); } /// @@ -542,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)); + } } /// @@ -580,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 @@ -627,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 aca237bc9f..d99cd8f43f 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 @@ -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 @@ -1434,4 +1552,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 0aedd2b7ca..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 @@ -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 @@ -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 84ef529bdb..bab3bd5d63 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml @@ -339,6 +339,7 @@ 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 @@ -828,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? 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 82e5626bac..11ce7a3252 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -284,6 +284,13 @@ "Типы медиа-материалов".]]> Выберите тип и заголовок Тип документа без сопоставленного шаблона + Новый файл javascript + Новое пустое частичное представление + Новый макрос-представление + Новое частичное представление по образцу + Новый пустой макрос-представление + Новый макрос-представление по образцу + Новый макрос-представление (без регистрации макроса) Обзор сайта @@ -409,6 +416,9 @@ Показать метку Ширина и высота + + Нет доступных элементов словаря + Ваши данные сохранены, но для того, чтобы опубликовать этот документ, Вы должны сначала исправить следующие ошибки: Текущий провайдер ролей пользователей не поддерживает изменение пароля (необходимо свойству EnablePasswordRetrieval в файле web.config присвоить значение true) @@ -1012,6 +1022,7 @@ Укажите пароль Что искать... Укажите имя пользователя + Имя пользователя (часто это Ваш email-адрес) Остаться @@ -1022,7 +1033,7 @@ Расширенный: Защита на основе ролей (групп) с использованием групп участников Umbraco.]]> - для применения ролевой модели безопасности.]]> + Вам необходимо создать хотя бы одну группу участников для применения ролевой модели безопасности. Страница сообщения об ошибке Используется в случае, когда пользователь авторизован в системе, но не имеет доступа к документу. Выберите способ ограничения доступа к документу @@ -1158,6 +1169,16 @@ В формате списка Разрешить в качестве корневого + + Закомментировать/раскомментировать строки + Удалить строку + Копировать строки вверх + Копировать строки вниз + Переместить строки вверх + Переместить строки вниз + + Общее + Редактор Порядок сортировки @@ -1243,14 +1264,106 @@ Стили - Править шаблон + Изменить шаблон + + Секции Вставить контент-область Вставить контейнер (placeholder) - Вставить статью словаря - Вставить макрос - Вставить поле документа + + Вставить + Выберите, что хотите вставить в шаблон + + Статью словаря + Статья словаря - это контейнер для части текста, переводимой на разные языки, это позволяет упростить создание многоязычных сайтов. + + Макрос + + Макросы - это настраиваемые компоненты, которые хорошо подходят для + реализации переиспользуемых блоков, (особенно, если необходимо менять их внешний вид и/или поведение при помощи параметров) + таких как галереи, формы, списки и т.п. + + + Значение поля + Отображает значение указанного поля данных текущей страницы, + с возможностью указать альтернативные поля и/или подстановку константы. + + + Частичное представление + + Частичное представление - это шаблон в отдельном файле, который может быть вызван для отображения внутри + другого шаблона, хорошо подходит для реализации переиспользуемых фрагментов разметки или для разбиения сложных шаблонов на составные части. + + Мастер-шаблон - Краткая справка по тэгам шаблонов Umbraco + Без мастер-шаблона + Не выбран + + Вставить дочерний шаблон + + @RenderBody() в выбранном месте. + ]]> + + + Определить именованную секцию + + @section { ... }. Такая секци может быть отображена в нужном месте родительского шаблона + при помощи конструкции @RenderSection. + ]]> + + + Вставить именованную секцию + + @RenderSection(name). + Таким образом из дочернего шаблона отображается содержимое внутри конструкции @section [name]{ ... }. + ]]> + + + Название секции + Секция обязательна + + Если секция помечена как обязательная, то дочерний шаблон должен обязательно содержать ее определение @section, в противном случае генерируется ошибка. + + + Генератор запросов + элементов в результате, за + + Мне нужны + все документы + документы типа "%0%" + из + всего сайта + где + и + + равна + не равна + до + до (включая выбранную дату) + после + после (включая выбранную дату) + равно + не равно + содержит + не содержит + больше, чем + больше или равно + меньше, чем + меньше или равно + + Id + Название + Создан + Обновлен + + сортировать + по возрастанию + по убыванию + Шаблон diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml index aeaf8bd49b..75bb41e35c 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml @@ -594,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 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/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/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 d80a60c92c..75b5d6e66a 100644 --- a/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx +++ b/src/Umbraco.Web.UI/umbraco/users/PermissionEditor.aspx @@ -1,5 +1,5 @@ <%@ Page Language="C#" AutoEventWireup="true" MasterPageFile="../masterpages/umbracoPage.Master" CodeBehind="PermissionEditor.aspx.cs" Inherits="umbraco.cms.presentation.user.PermissionEditor" %> - +<%@ Import Namespace="Umbraco.Web" %> <%@ Register Src="../controls/Tree/TreeControl.ascx" TagName="TreeControl" TagPrefix="umbraco" %> <%@ Register Src="NodePermissions.ascx" TagName="NodePermissions" TagPrefix="user" %> <%@ Register TagPrefix="ui" Namespace="umbraco.uicontrols" Assembly="controls" %> diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index 9ad3174a28..22cdd682fe 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -17,6 +17,8 @@ +
+
@@ -35,6 +37,8 @@ + + @@ -101,11 +105,14 @@ + + + - + @@ -301,15 +308,19 @@ - + - + + + + + @@ -375,14 +386,14 @@ xdt:Locator="Condition(_defaultNamespace:assemblyIdentity[@name='HtmlAgilityPack']])" /> - + - + + + + + + + diff --git a/src/Umbraco.Web.UI/web.Template.Release.config b/src/Umbraco.Web.UI/web.Template.Release.config index 2b1f0d12d9..c483af69c4 100644 --- a/src/Umbraco.Web.UI/web.Template.Release.config +++ b/src/Umbraco.Web.UI/web.Template.Release.config @@ -13,17 +13,6 @@ - - - - - - - diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 10c64797f4..6ee7d3bfba 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -1,7 +1,6 @@ -
@@ -29,7 +28,6 @@ - @@ -109,7 +107,6 @@ - @@ -179,7 +176,7 @@ - + @@ -232,7 +229,7 @@ - + @@ -251,8 +248,8 @@ - - + + @@ -274,9 +271,6 @@ - - - @@ -346,7 +340,17 @@ - + + + + @@ -379,7 +383,7 @@ - + @@ -391,10 +395,10 @@ - + - + @@ -425,7 +429,10 @@ - + + + + @@ -445,4 +452,5 @@ + diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 78b211ee7c..df25d9dfe0 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; using Umbraco.Core; using Umbraco.Core.Events; using Umbraco.Core.Models; @@ -9,10 +11,15 @@ using Umbraco.Core.Services; using umbraco.BusinessLogic; using umbraco.cms.businesslogic; using System.Linq; -using umbraco.cms.businesslogic.web; +using System.Reflection; +using System.Web; +using System.Web.Hosting; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.ObjectResolution; using Umbraco.Core.Publishing; +using Umbraco.Web.Routing; +using Umbraco.Web.Security; using Content = Umbraco.Core.Models.Content; using ApplicationTree = Umbraco.Core.Models.ApplicationTree; using DeleteEventArgs = umbraco.cms.businesslogic.DeleteEventArgs; @@ -25,117 +32,210 @@ namespace Umbraco.Web.Cache [Weight(int.MinValue)] public class CacheRefresherEventHandler : ApplicationEventHandler { + public CacheRefresherEventHandler() + { } + + public CacheRefresherEventHandler(bool supportUnbinding) + { + if (supportUnbinding) + _unbinders = new List(); + } + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { LogHelper.Info("Initializing Umbraco internal event handlers for cache refreshing"); - //bind to application tree events - ApplicationTreeService.Deleted += ApplicationTreeDeleted; - ApplicationTreeService.Updated += ApplicationTreeUpdated; - ApplicationTreeService.New += ApplicationTreeNew; + // bind to application tree events + Bind(() => ApplicationTreeService.Deleted += ApplicationTreeService_Deleted, + () => ApplicationTreeService.Deleted -= ApplicationTreeService_Deleted); + Bind(() => ApplicationTreeService.Updated += ApplicationTreeService_Updated, + () => ApplicationTreeService.Updated -= ApplicationTreeService_Updated); + Bind(() => ApplicationTreeService.New += ApplicationTreeService_New, + () => ApplicationTreeService.New -= ApplicationTreeService_New); - //bind to application events - SectionService.Deleted += ApplicationDeleted; - SectionService.New += ApplicationNew; + // bind to application events + Bind(() => SectionService.Deleted += SectionService_Deleted, + () => SectionService.Deleted -= SectionService_Deleted); + Bind(() => SectionService.New += SectionService_New, + () => SectionService.New -= SectionService_New); - //bind to user / user type events - UserService.SavedUserType += UserServiceSavedUserType; - UserService.DeletedUserType += UserServiceDeletedUserType; - UserService.SavedUser += UserServiceSavedUser; - UserService.DeletedUser += UserServiceDeletedUser; + // bind to user and user type events + Bind(() => UserService.SavedUserType += UserService_SavedUserType, + () => UserService.SavedUserType -= UserService_SavedUserType); + Bind(() => UserService.DeletedUserType += UserService_DeletedUserType, + () => UserService.DeletedUserType -= UserService_DeletedUserType); + Bind(() => UserService.SavedUser += UserService_SavedUser, + () => UserService.SavedUser -= UserService_SavedUser); + Bind(() => UserService.DeletedUser += UserService_DeletedUser, + () => UserService.DeletedUser -= UserService_DeletedUser); - //Bind to dictionary events + // bind to dictionary events + Bind(() => LocalizationService.DeletedDictionaryItem += LocalizationService_DeletedDictionaryItem, + () => LocalizationService.DeletedDictionaryItem -= LocalizationService_DeletedDictionaryItem); + Bind(() => LocalizationService.SavedDictionaryItem += LocalizationService_SavedDictionaryItem, + () => LocalizationService.SavedDictionaryItem -= LocalizationService_SavedDictionaryItem); - LocalizationService.DeletedDictionaryItem += LocalizationServiceDeletedDictionaryItem; - LocalizationService.SavedDictionaryItem += LocalizationServiceSavedDictionaryItem; + // bind to data type events + Bind(() => DataTypeService.Deleted += DataTypeService_Deleted, + () => DataTypeService.Deleted -= DataTypeService_Deleted); + Bind(() => DataTypeService.Saved += DataTypeService_Saved, + () => DataTypeService.Saved -= DataTypeService_Saved); - //Bind to data type events - //NOTE: we need to bind to legacy and new API events currently: http://issues.umbraco.org/issue/U4-1979 + // bind to stylesheet events + Bind(() => FileService.SavedStylesheet += FileService_SavedStylesheet, + () => FileService.SavedStylesheet -= FileService_SavedStylesheet); + Bind(() => FileService.DeletedStylesheet += FileService_DeletedStylesheet, + () => FileService.DeletedStylesheet -= FileService_DeletedStylesheet); - DataTypeService.Deleted += DataTypeServiceDeleted; - DataTypeService.Saved += DataTypeServiceSaved; + // bind to domain events + Bind(() => DomainService.Saved += DomainService_Saved, + () => DomainService.Saved -= DomainService_Saved); + Bind(() => DomainService.Deleted += DomainService_Deleted, + () => DomainService.Deleted -= DomainService_Deleted); - //Bind to stylesheet events + // bind to language events + Bind(() => LocalizationService.SavedLanguage += LocalizationService_SavedLanguage, + () => LocalizationService.SavedLanguage -= LocalizationService_SavedLanguage); + Bind(() => LocalizationService.DeletedLanguage += LocalizationService_DeletedLanguage, + () => LocalizationService.DeletedLanguage -= LocalizationService_DeletedLanguage); - FileService.SavedStylesheet += FileServiceSavedStylesheet; - FileService.DeletedStylesheet += FileServiceDeletedStylesheet; + // bind to content type events + Bind(() => ContentTypeService.SavedContentType += ContentTypeService_SavedContentType, + () => ContentTypeService.SavedContentType -= ContentTypeService_SavedContentType); + Bind(() => ContentTypeService.SavedMediaType += ContentTypeService_SavedMediaType, + () => ContentTypeService.SavedMediaType -= ContentTypeService_SavedMediaType); + Bind(() => ContentTypeService.DeletedContentType += ContentTypeService_DeletedContentType, + () => ContentTypeService.DeletedContentType -= ContentTypeService_DeletedContentType); + Bind(() => ContentTypeService.DeletedMediaType += ContentTypeService_DeletedMediaType, + () => ContentTypeService.DeletedMediaType -= ContentTypeService_DeletedMediaType); + Bind(() => MemberTypeService.Saved += MemberTypeService_Saved, + () => MemberTypeService.Saved -= MemberTypeService_Saved); + Bind(() => MemberTypeService.Deleted += MemberTypeService_Deleted, + () => MemberTypeService.Deleted -= MemberTypeService_Deleted); - //Bind to domain events + // bind to permission events + // we should wrap legacy permissions so we can get rid of this + // fixme - the method names here (PermissionNew...) are not supported + // by the event handling mechanism for scopes and deploy, and not sure + // how to fix with the generic repository + Bind(() => Permission.New += PermissionNew, + () => Permission.New -= PermissionNew); + Bind(() => Permission.Updated += PermissionUpdated, + () => Permission.Updated -= PermissionUpdated); + Bind(() => Permission.Deleted += PermissionDeleted, + () => Permission.Deleted -= PermissionDeleted); + Bind(() => PermissionRepository.AssignedPermissions += CacheRefresherEventHandler_AssignedPermissions, + () => PermissionRepository.AssignedPermissions -= CacheRefresherEventHandler_AssignedPermissions); - DomainService.Saved += DomainService_Saved; - DomainService.Deleted += DomainService_Deleted; + // bind to template events + Bind(() => FileService.SavedTemplate += FileService_SavedTemplate, + () => FileService.SavedTemplate -= FileService_SavedTemplate); + Bind(() => FileService.DeletedTemplate += FileService_DeletedTemplate, + () => FileService.DeletedTemplate -= FileService_DeletedTemplate); - //Bind to language events + // bind to macro events + Bind(() => MacroService.Saved += MacroService_Saved, + () => MacroService.Saved -= MacroService_Saved); + Bind(() => MacroService.Deleted += MacroService_Deleted, + () => MacroService.Deleted -= MacroService_Deleted); - LocalizationService.SavedLanguage += LocalizationServiceSavedLanguage; - LocalizationService.DeletedLanguage += LocalizationServiceDeletedLanguage; + // bind to member events + Bind(() => MemberService.Saved += MemberService_Saved, + () => MemberService.Saved -= MemberService_Saved); + Bind(() => MemberService.Deleted += MemberService_Deleted, + () => MemberService.Deleted -= MemberService_Deleted); + Bind(() => MemberGroupService.Saved += MemberGroupService_Saved, + () => MemberGroupService.Saved -= MemberGroupService_Saved); + Bind(() => MemberGroupService.Deleted += MemberGroupService_Deleted, + () => MemberGroupService.Deleted -= MemberGroupService_Deleted); - //Bind to content type events + // bind to media events + Bind(() => MediaService.Saved += MediaService_Saved, + () => MediaService.Saved -= MediaService_Saved); + Bind(() => MediaService.Deleted += MediaService_Deleted, + () => MediaService.Deleted -= MediaService_Deleted); + Bind(() => MediaService.Moved += MediaService_Moved, + () => MediaService.Moved -= MediaService_Moved); + Bind(() => MediaService.Trashed += MediaService_Trashed, + () => MediaService.Trashed -= MediaService_Trashed); + Bind(() => MediaService.EmptiedRecycleBin += MediaService_EmptiedRecycleBin, + () => MediaService.EmptiedRecycleBin -= MediaService_EmptiedRecycleBin); - ContentTypeService.SavedContentType += ContentTypeServiceSavedContentType; - ContentTypeService.SavedMediaType += ContentTypeServiceSavedMediaType; - ContentTypeService.DeletedContentType += ContentTypeServiceDeletedContentType; - ContentTypeService.DeletedMediaType += ContentTypeServiceDeletedMediaType; - MemberTypeService.Saved += MemberTypeServiceSaved; - MemberTypeService.Deleted += MemberTypeServiceDeleted; + // bind to content events + // this is for unpublished content syncing across servers (primarily for examine) + Bind(() => ContentService.Saved += ContentService_Saved, + () => ContentService.Saved -= ContentService_Saved); + Bind(() => ContentService.Deleted += ContentService_Deleted, + () => ContentService.Deleted -= ContentService_Deleted); + Bind(() => ContentService.Copied += ContentService_Copied, + () => ContentService.Copied -= ContentService_Copied); + // the Move method of the content service fires Saved/Published events during its + // execution so we don't need to listen to moved - this will probably change in due time + //Bind(() => ContentService.Moved += ContentServiceMoved, + // () => ContentService.Moved -= ContentServiceMoved); + Bind(() => ContentService.Trashed += ContentService_Trashed, + () => ContentService.Trashed -= ContentService_Trashed); + Bind(() => ContentService.EmptiedRecycleBin += ContentService_EmptiedRecycleBin, + () => ContentService.EmptiedRecycleBin -= ContentService_EmptiedRecycleBin); + Bind(() => ContentService.Published += ContentService_Published, + () => ContentService.Published -= ContentService_Published); + Bind(() => ContentService.UnPublished += ContentService_UnPublished, + () => ContentService.UnPublished -= ContentService_UnPublished); - //Bind to permission events + // bind to public access events + Bind(() => PublicAccessService.Saved += PublicAccessService_Saved, + () => PublicAccessService.Saved -= PublicAccessService_Saved); + Bind(() => PublicAccessService.Deleted += PublicAccessService_Deleted, + () => PublicAccessService.Deleted -= PublicAccessService_Deleted); - //TODO: Wrap legacy permissions so we can get rid of this - Permission.New += PermissionNew; - Permission.Updated += PermissionUpdated; - Permission.Deleted += PermissionDeleted; - PermissionRepository.AssignedPermissions += CacheRefresherEventHandler_AssignedPermissions; + // bind to relation type events + Bind(() => RelationService.SavedRelationType += RelationService_SavedRelationType, + () => RelationService.SavedRelationType -= RelationService_SavedRelationType); + Bind(() => RelationService.DeletedRelationType += RelationService_DeletedRelationType, + () => RelationService.DeletedRelationType -= RelationService_DeletedRelationType); + } - //Bind to template events + private List _unbinders; - FileService.SavedTemplate += FileServiceSavedTemplate; - FileService.DeletedTemplate += FileServiceDeletedTemplate; + private void Bind(Action binder, Action unbinder) + { + // bind now + binder(); - //Bind to macro events + // abd register unbinder for later, if needed + if (_unbinders == null) return; + _unbinders.Add(unbinder); + } - MacroService.Saved += MacroServiceSaved; - MacroService.Deleted += MacroServiceDeleted; - - //Bind to member events - - MemberService.Saved += MemberServiceSaved; - MemberService.Deleted += MemberServiceDeleted; - MemberGroupService.Saved += MemberGroupService_Saved; - MemberGroupService.Deleted += MemberGroupService_Deleted; - - //Bind to media events - - MediaService.Saved += MediaServiceSaved; - MediaService.Deleted += MediaServiceDeleted; - MediaService.Moved += MediaServiceMoved; - MediaService.Trashed += MediaServiceTrashed; - MediaService.EmptiedRecycleBin += MediaServiceEmptiedRecycleBin; - - //Bind to content events - this is for unpublished content syncing across servers (primarily for examine) - - ContentService.Saved += ContentServiceSaved; - ContentService.Deleted += ContentServiceDeleted; - ContentService.Copied += ContentServiceCopied; - //TODO: The Move method of the content service fires Saved/Published events during its execution so we don't need to listen to moved - //ContentService.Moved += ContentServiceMoved; - ContentService.Trashed += ContentServiceTrashed; - ContentService.EmptiedRecycleBin += ContentServiceEmptiedRecycleBin; - - PublishingStrategy.Published += PublishingStrategy_Published; - PublishingStrategy.UnPublished += PublishingStrategy_UnPublished; - - //public access events - PublicAccessService.Saved += PublicAccessService_Saved; - PublicAccessService.Deleted += PublicAccessService_Deleted; - - RelationService.SavedRelationType += RelationType_Saved; - RelationService.DeletedRelationType += RelationType_Deleted; + // for tests + internal void Unbind() + { + if (_unbinders == null) + throw new NotSupportedException(); + foreach (var unbinder in _unbinders) + unbinder(); + _unbinders = null; } #region Publishing - void PublishingStrategy_UnPublished(IPublishingStrategy sender, PublishEventArgs e) + // IPublishingStrategy (obsolete) events are proxied into ContentService, which works fine when + // events are actually raised, but not when they are handled by HandleEvents, so we have to have + // these proxy methods that are *not* registered against any event *but* used by HandleEvents. + + // ReSharper disable once UnusedMember.Local + static void PublishingStrategy_UnPublished(IPublishingStrategy sender, PublishEventArgs e) + { + ContentService_UnPublished(sender, e); + } + + // ReSharper disable once UnusedMember.Local + static void PublishingStrategy_Published(IPublishingStrategy sender, PublishEventArgs e) + { + ContentService_Published(sender, e); + } + + static void ContentService_UnPublished(IPublishingStrategy sender, PublishEventArgs e) { if (e.PublishedEntities.Any()) { @@ -157,12 +257,12 @@ namespace Umbraco.Web.Cache /// /// Refreshes the xml cache for a single node by removing it /// - private void UnPublishSingle(IContent content) + private static void UnPublishSingle(IContent content) { DistributedCache.Instance.RemovePageCache(content); } - void PublishingStrategy_Published(IPublishingStrategy sender, PublishEventArgs e) + static void ContentService_Published(IPublishingStrategy sender, PublishEventArgs e) { if (e.PublishedEntities.Any()) { @@ -187,7 +287,7 @@ namespace Umbraco.Web.Cache /// /// Refreshes the xml cache for all nodes /// - private void UpdateEntireCache() + private static void UpdateEntireCache() { DistributedCache.Instance.RefreshAllPageCache(); } @@ -195,7 +295,7 @@ namespace Umbraco.Web.Cache /// /// Refreshes the xml cache for nodes in list /// - private void UpdateMultipleContentCache(IEnumerable content) + private static void UpdateMultipleContentCache(IEnumerable content) { DistributedCache.Instance.RefreshPageCache(content.ToArray()); } @@ -203,7 +303,7 @@ namespace Umbraco.Web.Cache /// /// Refreshes the xml cache for a single node /// - private void UpdateSingleContentCache(IContent content) + private static void UpdateSingleContentCache(IContent content) { DistributedCache.Instance.RefreshPageCache(content); } @@ -217,7 +317,7 @@ namespace Umbraco.Web.Cache DistributedCache.Instance.RefreshPublicAccess(); } - private void PublicAccessService_Deleted(IPublicAccessService sender, DeleteEventArgs e) + static void PublicAccessService_Deleted(IPublicAccessService sender, DeleteEventArgs e) { DistributedCache.Instance.RefreshPublicAccess(); } @@ -226,7 +326,7 @@ namespace Umbraco.Web.Cache #region Content service event handlers - static void ContentServiceEmptiedRecycleBin(IContentService sender, RecycleBinEventArgs e) + static void ContentService_EmptiedRecycleBin(IContentService sender, RecycleBinEventArgs e) { if (e.RecycleBinEmptiedSuccessfully && e.IsContentRecycleBin) { @@ -243,7 +343,7 @@ namespace Umbraco.Web.Cache /// This is for the unpublished page refresher - the entity will be unpublished before being moved to the trash /// and the unpublished event will take care of remove it from any published caches /// - static void ContentServiceTrashed(IContentService sender, MoveEventArgs e) + static void ContentService_Trashed(IContentService sender, MoveEventArgs e) { DistributedCache.Instance.RefreshUnpublishedPageCache( e.MoveInfoCollection.Select(x => x.Entity).ToArray()); @@ -258,7 +358,7 @@ namespace Umbraco.Web.Cache /// When an entity is copied new permissions may be assigned to it based on it's parent, if that is the /// case then we need to clear all user permissions cache. /// - static void ContentServiceCopied(IContentService sender, CopyEventArgs e) + static void ContentService_Copied(IContentService sender, CopyEventArgs e) { //check if permissions have changed var permissionsChanged = ((Content)e.Copy).WasPropertyDirty("PermissionsChanged"); @@ -276,7 +376,7 @@ namespace Umbraco.Web.Cache ///
/// /// - static void ContentServiceDeleted(IContentService sender, DeleteEventArgs e) + static void ContentService_Deleted(IContentService sender, DeleteEventArgs e) { DistributedCache.Instance.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray()); } @@ -293,7 +393,7 @@ namespace Umbraco.Web.Cache /// When an entity is created new permissions may be assigned to it based on it's parent, if that is the /// case then we need to clear all user permissions cache. /// - static void ContentServiceSaved(IContentService sender, SaveEventArgs e) + static void ContentService_Saved(IContentService sender, SaveEventArgs e) { var clearUserPermissions = false; e.SavedEntities.ForEach(x => @@ -326,41 +426,41 @@ namespace Umbraco.Web.Cache #endregion #region ApplicationTree event handlers - static void ApplicationTreeNew(ApplicationTree sender, EventArgs e) + static void ApplicationTreeService_New(ApplicationTree sender, EventArgs e) { DistributedCache.Instance.RefreshAllApplicationTreeCache(); } - static void ApplicationTreeUpdated(ApplicationTree sender, EventArgs e) + static void ApplicationTreeService_Updated(ApplicationTree sender, EventArgs e) { DistributedCache.Instance.RefreshAllApplicationTreeCache(); } - static void ApplicationTreeDeleted(ApplicationTree sender, EventArgs e) + static void ApplicationTreeService_Deleted(ApplicationTree sender, EventArgs e) { DistributedCache.Instance.RefreshAllApplicationTreeCache(); } #endregion #region Application event handlers - static void ApplicationNew(Section sender, EventArgs e) + static void SectionService_New(Section sender, EventArgs e) { DistributedCache.Instance.RefreshAllApplicationCache(); } - static void ApplicationDeleted(Section sender, EventArgs e) + static void SectionService_Deleted(Section sender, EventArgs e) { DistributedCache.Instance.RefreshAllApplicationCache(); } #endregion #region UserType event handlers - static void UserServiceDeletedUserType(IUserService sender, DeleteEventArgs e) + static void UserService_DeletedUserType(IUserService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveUserTypeCache(x.Id)); } - static void UserServiceSavedUserType(IUserService sender, SaveEventArgs e) + static void UserService_SavedUserType(IUserService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshUserTypeCache(x.Id)); } @@ -369,12 +469,12 @@ namespace Umbraco.Web.Cache #region Dictionary event handlers - static void LocalizationServiceSavedDictionaryItem(ILocalizationService sender, SaveEventArgs e) + static void LocalizationService_SavedDictionaryItem(ILocalizationService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshDictionaryCache(x.Id)); } - static void LocalizationServiceDeletedDictionaryItem(ILocalizationService sender, DeleteEventArgs e) + static void LocalizationService_DeletedDictionaryItem(ILocalizationService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveDictionaryCache(x.Id)); } @@ -382,12 +482,12 @@ namespace Umbraco.Web.Cache #endregion #region DataType event handlers - static void DataTypeServiceSaved(IDataTypeService sender, SaveEventArgs e) + static void DataTypeService_Saved(IDataTypeService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshDataTypeCache(x)); } - static void DataTypeServiceDeleted(IDataTypeService sender, DeleteEventArgs e) + static void DataTypeService_Deleted(IDataTypeService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveDataTypeCache(x)); } @@ -397,12 +497,12 @@ namespace Umbraco.Web.Cache #region Stylesheet and stylesheet property event handlers - static void FileServiceDeletedStylesheet(IFileService sender, DeleteEventArgs e) + static void FileService_DeletedStylesheet(IFileService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveStylesheetCache(x)); } - static void FileServiceSavedStylesheet(IFileService sender, SaveEventArgs e) + static void FileService_SavedStylesheet(IFileService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshStylesheetCache(x)); } @@ -429,7 +529,7 @@ namespace Umbraco.Web.Cache ///
/// /// - static void LocalizationServiceDeletedLanguage(ILocalizationService sender, DeleteEventArgs e) + static void LocalizationService_DeletedLanguage(ILocalizationService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveLanguageCache(x)); } @@ -439,7 +539,7 @@ namespace Umbraco.Web.Cache /// /// /// - static void LocalizationServiceSavedLanguage(ILocalizationService sender, SaveEventArgs e) + static void LocalizationService_SavedLanguage(ILocalizationService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshLanguageCache(x)); } @@ -452,7 +552,7 @@ namespace Umbraco.Web.Cache /// /// /// - static void ContentTypeServiceDeletedMediaType(IContentTypeService sender, DeleteEventArgs e) + static void ContentTypeService_DeletedMediaType(IContentTypeService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveMediaTypeCache(x)); } @@ -462,7 +562,7 @@ namespace Umbraco.Web.Cache /// /// /// - static void ContentTypeServiceDeletedContentType(IContentTypeService sender, DeleteEventArgs e) + static void ContentTypeService_DeletedContentType(IContentTypeService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(contentType => DistributedCache.Instance.RemoveContentTypeCache(contentType)); } @@ -472,7 +572,7 @@ namespace Umbraco.Web.Cache /// /// /// - static void MemberTypeServiceDeleted(IMemberTypeService sender, DeleteEventArgs e) + static void MemberTypeService_Deleted(IMemberTypeService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(contentType => DistributedCache.Instance.RemoveMemberTypeCache(contentType)); } @@ -482,7 +582,7 @@ namespace Umbraco.Web.Cache /// /// /// - static void ContentTypeServiceSavedMediaType(IContentTypeService sender, SaveEventArgs e) + static void ContentTypeService_SavedMediaType(IContentTypeService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshMediaTypeCache(x)); } @@ -492,7 +592,7 @@ namespace Umbraco.Web.Cache /// /// /// - static void ContentTypeServiceSavedContentType(IContentTypeService sender, SaveEventArgs e) + static void ContentTypeService_SavedContentType(IContentTypeService sender, SaveEventArgs e) { e.SavedEntities.ForEach(contentType => DistributedCache.Instance.RefreshContentTypeCache(contentType)); } @@ -502,7 +602,7 @@ namespace Umbraco.Web.Cache /// /// /// - static void MemberTypeServiceSaved(IMemberTypeService sender, SaveEventArgs e) + static void MemberTypeService_Saved(IMemberTypeService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshMemberTypeCache(x)); } @@ -533,12 +633,12 @@ namespace Umbraco.Web.Cache InvalidateCacheForPermissionsChange(sender); } - static void UserServiceSavedUser(IUserService sender, SaveEventArgs e) + static void UserService_SavedUser(IUserService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshUserCache(x.Id)); } - static void UserServiceDeletedUser(IUserService sender, DeleteEventArgs e) + static void UserService_DeletedUser(IUserService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveUserCache(x.Id)); } @@ -568,7 +668,7 @@ namespace Umbraco.Web.Cache /// /// /// - static void FileServiceDeletedTemplate(IFileService sender, DeleteEventArgs e) + static void FileService_DeletedTemplate(IFileService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveTemplateCache(x.Id)); } @@ -578,7 +678,7 @@ namespace Umbraco.Web.Cache /// /// /// - static void FileServiceSavedTemplate(IFileService sender, SaveEventArgs e) + static void FileService_SavedTemplate(IFileService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshTemplateCache(x.Id)); } @@ -587,7 +687,7 @@ namespace Umbraco.Web.Cache #region Macro event handlers - void MacroServiceDeleted(IMacroService sender, DeleteEventArgs e) + static void MacroService_Deleted(IMacroService sender, DeleteEventArgs e) { foreach (var entity in e.DeletedEntities) { @@ -595,7 +695,7 @@ namespace Umbraco.Web.Cache } } - void MacroServiceSaved(IMacroService sender, SaveEventArgs e) + static void MacroService_Saved(IMacroService sender, SaveEventArgs e) { foreach (var entity in e.SavedEntities) { @@ -607,7 +707,7 @@ namespace Umbraco.Web.Cache #region Media event handlers - static void MediaServiceEmptiedRecycleBin(IMediaService sender, RecycleBinEventArgs e) + static void MediaService_EmptiedRecycleBin(IMediaService sender, RecycleBinEventArgs e) { if (e.RecycleBinEmptiedSuccessfully && e.IsMediaRecycleBin) { @@ -615,22 +715,22 @@ namespace Umbraco.Web.Cache } } - static void MediaServiceTrashed(IMediaService sender, MoveEventArgs e) + static void MediaService_Trashed(IMediaService sender, MoveEventArgs e) { DistributedCache.Instance.RemoveMediaCacheAfterRecycling(e.MoveInfoCollection.ToArray()); } - static void MediaServiceMoved(IMediaService sender, MoveEventArgs e) + static void MediaService_Moved(IMediaService sender, MoveEventArgs e) { DistributedCache.Instance.RefreshMediaCacheAfterMoving(e.MoveInfoCollection.ToArray()); } - static void MediaServiceDeleted(IMediaService sender, DeleteEventArgs e) + static void MediaService_Deleted(IMediaService sender, DeleteEventArgs e) { DistributedCache.Instance.RemoveMediaCachePermanently(e.DeletedEntities.Select(x => x.Id).ToArray()); } - static void MediaServiceSaved(IMediaService sender, SaveEventArgs e) + static void MediaService_Saved(IMediaService sender, SaveEventArgs e) { DistributedCache.Instance.RefreshMediaCache(e.SavedEntities.ToArray()); } @@ -638,12 +738,12 @@ namespace Umbraco.Web.Cache #region Member event handlers - static void MemberServiceDeleted(IMemberService sender, DeleteEventArgs e) + static void MemberService_Deleted(IMemberService sender, DeleteEventArgs e) { DistributedCache.Instance.RemoveMemberCache(e.DeletedEntities.ToArray()); } - static void MemberServiceSaved(IMemberService sender, SaveEventArgs e) + static void MemberService_Saved(IMemberService sender, SaveEventArgs e) { DistributedCache.Instance.RefreshMemberCache(e.SavedEntities.ToArray()); } @@ -671,14 +771,14 @@ namespace Umbraco.Web.Cache #region Relation type event handlers - private static void RelationType_Saved(IRelationService sender, SaveEventArgs args) + static void RelationService_SavedRelationType(IRelationService sender, SaveEventArgs args) { var dc = DistributedCache.Instance; foreach (var e in args.SavedEntities) dc.RefreshRelationTypeCache(e.Id); } - private static void RelationType_Deleted(IRelationService sender, DeleteEventArgs args) + static void RelationService_DeletedRelationType(IRelationService sender, DeleteEventArgs args) { var dc = DistributedCache.Instance; foreach (var e in args.DeletedEntities) @@ -686,5 +786,83 @@ namespace Umbraco.Web.Cache } #endregion + + + /// + /// This will inspect the event metadata and execute it's affiliated handler if one is found + /// + /// + internal static void HandleEvents(IEnumerable events) + { + //TODO: We should remove this in v8, this is a backwards compat hack and is needed because when we are using Deploy, the events will be raised on a background + //thread which means that cache refreshers will also execute on a background thread and in many cases developers may be using UmbracoContext.Current in their + //cache refresher handlers, so before we execute all of the events, we'll ensure a context + UmbracoContext tempContext = null; + if (UmbracoContext.Current == null) + { + var httpContext = new HttpContextWrapper(HttpContext.Current ?? new HttpContext(new SimpleWorkerRequest("temp.aspx", "", new StringWriter()))); + tempContext = UmbracoContext.EnsureContext( + httpContext, + ApplicationContext.Current, + new WebSecurity(httpContext, ApplicationContext.Current), + UmbracoConfig.For.UmbracoSettings(), + UrlProviderResolver.Current.Providers, + true); + } + + try + { + foreach (var e in events) + { + var handler = FindHandler(e); + if (handler == null) continue; + + handler.Invoke(null, new[] { e.Sender, e.Args }); + } + } + finally + { + if (tempContext != null) + tempContext.Dispose(); + } + + } + + /// + /// Used to cache all candidate handlers + /// + private static readonly Lazy CandidateHandlers = new Lazy(() => + { + var underscore = new[] { '_' }; + + return typeof (CacheRefresherEventHandler) + .GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + .Select(x => + { + if (x.Name.Contains("_") == false) return null; + + var parts = x.Name.Split(underscore, StringSplitOptions.RemoveEmptyEntries).Length; + if (parts != 2) return null; + + var parameters = x.GetParameters(); + if (parameters.Length != 2) return null; + if (typeof (EventArgs).IsAssignableFrom(parameters[1].ParameterType) == false) return null; + return x; + }) + .WhereNotNull() + .ToArray(); + }); + + /// + /// Used to cache all found event handlers + /// + private static readonly ConcurrentDictionary FoundHandlers = new ConcurrentDictionary(); + + internal static MethodInfo FindHandler(IEventDefinition eventDefinition) + { + var name = eventDefinition.Sender.GetType().Name + "_" + eventDefinition.EventName; + + return FoundHandlers.GetOrAdd(eventDefinition, _ => CandidateHandlers.Value.FirstOrDefault(x => x.Name == name)); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index 1340545621..955be82783 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Models.Rdbms; - +using Umbraco.Core.Services; using Umbraco.Web.PublishedCache; using Umbraco.Web.PublishedCache.XmlPublishedCache; @@ -47,6 +47,7 @@ namespace Umbraco.Web.Cache /// internal static JsonPayload FromContentType(IContentTypeBase contentType, bool isDeleted = false) { + var contentTypeService = (ContentTypeService) ApplicationContext.Current.Services.ContentTypeService; var payload = new JsonPayload { Alias = contentType.Alias, @@ -58,7 +59,7 @@ namespace Umbraco.Web.Cache : (contentType is IMediaType) ? typeof(IMediaType).Name : typeof(IMemberType).Name, - DescendantPayloads = contentType.Descendants().Select(x => FromContentType(x)).ToArray(), + DescendantPayloads = contentTypeService.GetDescendants(contentType).Select(x => FromContentType(x)).ToArray(), WasDeleted = isDeleted, PropertyRemoved = contentType.WasPropertyDirty("HasPropertyTypeBeenRemoved"), AliasChanged = contentType.WasPropertyDirty("Alias"), diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index 2300082ece..34ca2ceec0 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -5,6 +5,8 @@ using Umbraco.Core.Cache; using System.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.PropertyEditors.ValueConverters; namespace Umbraco.Web.Cache @@ -111,6 +113,12 @@ namespace Umbraco.Web.Cache PublishedContentType.ClearDataType(payload.Id); }); + TagsValueConverter.ClearCaches(); + MultipleMediaPickerPropertyConverter.ClearCaches(); + SliderValueConverter.ClearCaches(); + MediaPickerPropertyConverter.ClearCaches(); + + base.Refresh(jsonPayload); } } diff --git a/src/Umbraco.Web/Cache/PageCacheRefresher.cs b/src/Umbraco.Web/Cache/PageCacheRefresher.cs index fc6e0c8d20..83d38924ed 100644 --- a/src/Umbraco.Web/Cache/PageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/PageCacheRefresher.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.Cache /// public override void RefreshAll() { - content.Instance.RefreshContentFromDatabaseAsync(); + content.Instance.RefreshContentFromDatabase(); base.RefreshAll(); } diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 14dbdac1d4..0489fcbb70 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -139,18 +139,7 @@ namespace Umbraco.Web.Editors //get the user var user = Security.GetBackOfficeUser(loginModel.Username); - var userDetail = Mapper.Map(user); - //update the userDetail and set their remaining seconds - userDetail.SecondsUntilTimeout = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes).TotalSeconds; - - //create a response with the userDetail object - var response = Request.CreateResponse(HttpStatusCode.OK, userDetail); - - //ensure the user is set for the current request - Request.SetPrincipalForRequest(user); - - return response; - + return SetPrincipalAndReturnUserDetail(user); case SignInStatus.RequiresVerification: var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions; @@ -161,7 +150,7 @@ namespace Umbraco.Web.Editors HttpStatusCode.BadRequest, "UserManager does not implement " + typeof(IUmbracoBackOfficeTwoFactorOptions))); } - + var twofactorView = twofactorOptions.GetTwoFactorView( TryGetOwinContext().Result, UmbracoContext, @@ -175,10 +164,13 @@ namespace Umbraco.Web.Editors typeof(IUmbracoBackOfficeTwoFactorOptions) + ".GetTwoFactorView returned an empty string")); } + var attemptedUser = Security.GetBackOfficeUser(loginModel.Username); + //create a with information to display a custom two factor send code view - var verifyResponse = Request.CreateResponse(HttpStatusCode.OK, new + var verifyResponse = Request.CreateResponse(HttpStatusCode.PaymentRequired, new { - twoFactorView = twofactorView + twoFactorView = twofactorView, + userId = attemptedUser.Id }); return verifyResponse; @@ -233,25 +225,74 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - private string ConstructCallbackUrl(int userId, string code) + /// + /// Used to retrived the 2FA providers for code submission + /// + /// + [SetAngularAntiForgeryTokens] + public async Task> Get2FAProviders() { - // Get an mvc helper to get the url - var http = EnsureHttpContext(); - var urlHelper = new UrlHelper(http.Request.RequestContext); - var action = urlHelper.Action("ValidatePasswordResetCode", "BackOffice", - new - { - area = GlobalSettings.UmbracoMvcArea, - u = userId, - r = code - }); + var userId = await SignInManager.GetVerifiedUserIdAsync(); + if (userId < 0) + { + Logger.Warn("Get2FAProviders :: No verified user found, returning 404"); + throw new HttpResponseException(HttpStatusCode.NotFound); + } + var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(userId); + return userFactors; + } + + [SetAngularAntiForgeryTokens] + public async Task PostSend2FACode([FromBody]string provider) + { + if (provider.IsNullOrWhiteSpace()) + throw new HttpResponseException(HttpStatusCode.NotFound); + + var userId = await SignInManager.GetVerifiedUserIdAsync(); + if (userId < 0) + { + Logger.Warn("Get2FAProviders :: No verified user found, returning 404"); + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + // Generate the token and send it + if (await SignInManager.SendTwoFactorCodeAsync(provider) == false) + { + return BadRequest("Invalid code"); + } + return Ok(); + } + + [SetAngularAntiForgeryTokens] + public async Task PostVerify2FACode(Verify2FACodeModel model) + { + if (ModelState.IsValid == false) + { + return Request.CreateValidationErrorResponse(ModelState); + } + + var userName = await SignInManager.GetVerifiedUserNameAsync(); + if (userName == null) + { + Logger.Warn("Get2FAProviders :: No verified user found, returning 404"); + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent: true, rememberBrowser: false); + switch (result) + { + case SignInStatus.Success: + //get the user + var user = Security.GetBackOfficeUser(userName); + return SetPrincipalAndReturnUserDetail(user); + case SignInStatus.LockedOut: + return Request.CreateValidationErrorResponse("User is locked out"); + case SignInStatus.Failure: + default: + return Request.CreateValidationErrorResponse("Invalid code"); + } + } - // Construct full URL using configured application URL (which will fall back to request) - var applicationUri = new Uri(ApplicationContext.UmbracoApplicationUrl); - var callbackUri = new Uri(applicationUri, action); - return callbackUri.ToString(); - } - /// /// Processes a set password request. Validates the request and sets a new password. /// @@ -269,13 +310,6 @@ namespace Umbraco.Web.Editors result.Errors.Any() ? result.Errors.First() : "Set password failed"); } - private HttpContextBase EnsureHttpContext() - { - var attempt = this.TryGetHttpContext(); - if (attempt.Success == false) - throw new InvalidOperationException("This method requires that an HttpContext be active"); - return attempt.Result; - } /// /// Logs the current user out @@ -296,6 +330,59 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } + + /// + /// This is used when the user is auth'd successfully and we need to return an OK with user details along with setting the current Principal in the request + /// + /// + /// + private HttpResponseMessage SetPrincipalAndReturnUserDetail(IUser user) + { + if (user == null) throw new ArgumentNullException("user"); + + var userDetail = Mapper.Map(user); + //update the userDetail and set their remaining seconds + userDetail.SecondsUntilTimeout = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes).TotalSeconds; + + //create a response with the userDetail object + var response = Request.CreateResponse(HttpStatusCode.OK, userDetail); + + //ensure the user is set for the current request + Request.SetPrincipalForRequest(user); + + return response; + } + + private string ConstructCallbackUrl(int userId, string code) + { + // Get an mvc helper to get the url + var http = EnsureHttpContext(); + var urlHelper = new UrlHelper(http.Request.RequestContext); + var action = urlHelper.Action("ValidatePasswordResetCode", "BackOffice", + new + { + area = GlobalSettings.UmbracoMvcArea, + u = userId, + r = code + }); + + // Construct full URL using configured application URL (which will fall back to request) + var applicationUri = new Uri(ApplicationContext.UmbracoApplicationUrl); + var callbackUri = new Uri(applicationUri, action); + return callbackUri.ToString(); + } + + + private HttpContextBase EnsureHttpContext() + { + var attempt = this.TryGetHttpContext(); + if (attempt.Success == false) + throw new InvalidOperationException("This method requires that an HttpContext be active"); + return attempt.Result; + } + + + private void AddModelErrors(IdentityResult result, string prefix = "") { foreach (var error in result.Errors) diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index b93e2a408d..901189e28a 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -258,7 +258,7 @@ namespace Umbraco.Web.Editors }, { "treeApplicationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetApplicationTrees(null, null, null)) + controller => controller.GetApplicationTrees(null, null, null, true)) }, { "contentTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( @@ -336,6 +336,10 @@ namespace Umbraco.Web.Editors "tagApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( controller => controller.GetAllTags(null)) }, + { + "templateApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetById(0)) + }, { "memberTreeBaseUrl", Url.GetUmbracoApiServiceBaseUrl( controller => controller.GetNodes("-1", null)) @@ -363,6 +367,14 @@ namespace Umbraco.Web.Editors { "healthCheckBaseUrl", Url.GetUmbracoApiServiceBaseUrl( controller => controller.GetAllHealthChecks()) + }, + { + "templateQueryApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.PostTemplateQuery(null)) + }, + { + "codeFileApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetByPath("", "")) } } }, @@ -387,6 +399,7 @@ namespace Umbraco.Web.Editors {"keepUserLoggedIn", UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn}, {"cssPath", IOHelper.ResolveUrl(SystemDirectories.Css).TrimEnd('/')}, {"allowPasswordReset", UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset}, + {"loginBackgroundImage", UmbracoConfig.For.UmbracoSettings().Content.LoginBackgroundImage}, } }, { @@ -433,7 +446,7 @@ namespace Umbraco.Web.Editors return JavaScript(result); } - + [HttpPost] public ActionResult ExternalLogin(string provider, string redirectUrl = null) { @@ -466,7 +479,7 @@ namespace Umbraco.Web.Editors if (result) { //Add a flag and redirect for it to be displayed - TempData[TokenPasswordResetCode] = new ValidatePasswordResetCodeModel {UserId = userId, ResetCode = resetCode}; + TempData[TokenPasswordResetCode] = new ValidatePasswordResetCodeModel { UserId = userId, ResetCode = resetCode }; return RedirectToLocal(Url.Action("Default", "BackOffice")); } } @@ -507,7 +520,7 @@ namespace Umbraco.Web.Editors /// /// private async Task RenderDefaultOrProcessExternalLoginAsync( - Func defaultResponse, + Func defaultResponse, Func externalSignInResponse) { if (defaultResponse == null) throw new ArgumentNullException("defaultResponse"); @@ -517,7 +530,7 @@ namespace Umbraco.Web.Editors //check if there is the TempData with the any token name specified, if so, assign to view bag and render the view foreach (var tempDataTokenName in TempDataTokenNames) - { + { if (TempData[tempDataTokenName] != null) { ViewData[tempDataTokenName] = TempData[tempDataTokenName]; @@ -700,7 +713,7 @@ namespace Umbraco.Web.Editors return app; } - + private IEnumerable> GetTreePluginsMetaData() { diff --git a/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs b/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs index f4350bc596..0910ec936e 100644 --- a/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs @@ -1,3 +1,4 @@ +using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Editors @@ -8,6 +9,7 @@ namespace Umbraco.Web.Editors /// currently in the request. /// [AppendCurrentEventMessages] + [PrefixlessBodyModelValidator] public abstract class BackOfficeNotificationsController : UmbracoAuthorizedJsonController { protected BackOfficeNotificationsController() diff --git a/src/Umbraco.Web/Editors/CodeFileController.cs b/src/Umbraco.Web/Editors/CodeFileController.cs new file mode 100644 index 0000000000..5f9f719a69 --- /dev/null +++ b/src/Umbraco.Web/Editors/CodeFileController.cs @@ -0,0 +1,515 @@ +using System; +using AutoMapper; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using ClientDependency.Core; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Web.Trees; + +namespace Umbraco.Web.Editors +{ + //TODO: Put some exception filters in our webapi to return 404 instead of 500 when we throw ArgumentNullException + // ref: https://www.exceptionnotfound.net/the-asp-net-web-api-exception-handling-pipeline-a-guided-tour/ + [PluginController("UmbracoApi")] + [PrefixlessBodyModelValidator] + [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] + public class CodeFileController : BackOfficeNotificationsController + { + + /// + /// Used to create a brand new file + /// + /// This is a string but will be 'scripts' 'partialViews', 'partialViewMacros' + /// + /// Will return a simple 200 if file creation succeeds + [ValidationFilter] + public HttpResponseMessage PostCreate(string type, CodeFileDisplay display) + { + if (display == null) throw new ArgumentNullException("display"); + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + + switch (type) + { + case Core.Constants.Trees.PartialViews: + var view = new PartialView(display.VirtualPath); + view.Content = display.Content; + var result = Services.FileService.CreatePartialView(view, display.Snippet, Security.CurrentUser.Id); + return result.Success == true ? Request.CreateResponse(HttpStatusCode.OK) : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); + + case Core.Constants.Trees.PartialViewMacros: + var viewMacro = new PartialView(display.VirtualPath); + viewMacro.Content = display.Content; + var resultMacro = Services.FileService.CreatePartialViewMacro(viewMacro, display.Snippet, Security.CurrentUser.Id); + return resultMacro.Success == true ? Request.CreateResponse(HttpStatusCode.OK) : Request.CreateNotificationValidationErrorResponse(resultMacro.Exception.Message); + + case Core.Constants.Trees.Scripts: + var script = new Script(display.VirtualPath); + Services.FileService.SaveScript(script, Security.CurrentUser.Id); + return Request.CreateResponse(HttpStatusCode.OK); + + default: + return Request.CreateResponse(HttpStatusCode.NotFound); + } + } + + /// + /// Used to create a container/folder in 'partialViews', 'partialViewMacros' or 'scripts' + /// + /// 'partialViews', 'partialViewMacros' or 'scripts' + /// The virtual path of the parent. + /// The name of the container/folder + /// + [HttpPost] + public CodeFileDisplay PostCreateContainer(string type, string parentId, string name) + { + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + if (string.IsNullOrWhiteSpace(parentId)) throw new ArgumentException("Value cannot be null or whitespace.", "parentId"); + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); + + // if the parentId is root (-1) then we just need an empty string as we are + // creating the path below and we don't wan't -1 in the path + if (parentId == Core.Constants.System.Root.ToInvariantString()) + { + parentId = string.Empty; + } + + name = System.Web.HttpUtility.UrlDecode(name); + + if (parentId.IsNullOrWhiteSpace() == false) + { + parentId = System.Web.HttpUtility.UrlDecode(parentId); + name = parentId.EnsureEndsWith("/") + name; + } + + var virtualPath = string.Empty; + switch (type) + { + case Core.Constants.Trees.PartialViews: + virtualPath = NormalizeVirtualPath(name, SystemDirectories.PartialViews); + Services.FileService.CreatePartialViewFolder(virtualPath); + break; + case Core.Constants.Trees.PartialViewMacros: + virtualPath = NormalizeVirtualPath(name, SystemDirectories.MacroPartials); + Services.FileService.CreatePartialViewMacroFolder(virtualPath); + break; + case Core.Constants.Trees.Scripts: + virtualPath = NormalizeVirtualPath(name, SystemDirectories.Scripts); + Services.FileService.CreateScriptFolder(virtualPath); + break; + + } + + return new CodeFileDisplay + { + VirtualPath = virtualPath, + Path = Url.GetTreePathFromFilePath(virtualPath) + }; + } + + /// + /// Used to get a specific file from disk via the FileService + /// + /// This is a string but will be 'scripts' 'partialViews', 'partialViewMacros' + /// The filename or urlencoded path of the file to open + /// The file and its contents from the virtualPath + public CodeFileDisplay GetByPath(string type, string virtualPath) + { + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + if (string.IsNullOrWhiteSpace(virtualPath)) throw new ArgumentException("Value cannot be null or whitespace.", "virtualPath"); + + virtualPath = System.Web.HttpUtility.UrlDecode(virtualPath); + + switch (type) + { + case Core.Constants.Trees.PartialViews: + var view = Services.FileService.GetPartialView(virtualPath); + if (view != null) + { + var display = Mapper.Map(view); + display.FileType = Core.Constants.Trees.PartialViews; + display.Path = Url.GetTreePathFromFilePath(view.Path); + display.Id = System.Web.HttpUtility.UrlEncode(view.Path); + return display; + } + throw new HttpResponseException(HttpStatusCode.NotFound); + + case Core.Constants.Trees.PartialViewMacros: + var viewMacro = Services.FileService.GetPartialViewMacro(virtualPath); + if (viewMacro != null) + { + var display = Mapper.Map(viewMacro); + display.FileType = Core.Constants.Trees.PartialViewMacros; + display.Path = Url.GetTreePathFromFilePath(viewMacro.Path); + display.Id = System.Web.HttpUtility.UrlEncode(viewMacro.Path); + return display; + } + throw new HttpResponseException(HttpStatusCode.NotFound); + + case Core.Constants.Trees.Scripts: + var script = Services.FileService.GetScriptByName(virtualPath); + if (script != null) + { + var display = Mapper.Map(script); + display.FileType = Core.Constants.Trees.Scripts; + display.Path = Url.GetTreePathFromFilePath(script.Path); + display.Id = System.Web.HttpUtility.UrlEncode(script.Path); + return display; + } + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + /// + /// Used to get a list of available templates/snippets to base a new Partial View og Partial View Macro from + /// + /// This is a string but will be 'partialViews', 'partialViewMacros' + /// Returns a list of if a correct type is sent + public IEnumerable GetSnippets(string type) + { + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + + IEnumerable snippets; + switch (type) + { + case Core.Constants.Trees.PartialViews: + snippets = Services.FileService.GetPartialViewSnippetNames( + //ignore these - (this is taken from the logic in "PartialView.ascx.cs") + "Gallery", + "ListChildPagesFromChangeableSource", + "ListChildPagesOrderedByProperty", + "ListImagesFromMediaFolder"); + break; + case Core.Constants.Trees.PartialViewMacros: + snippets = Services.FileService.GetPartialViewSnippetNames(); + break; + default: + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + return snippets.Select(snippet => new SnippetDisplay() {Name = snippet.SplitPascalCasing().ToFirstUpperInvariant(), FileName = snippet}); + } + + /// + /// Used to scaffold the json object for the editors for 'scripts', 'partialViews', 'partialViewMacros' + /// + /// This is a string but will be 'scripts' 'partialViews', 'partialViewMacros' + /// + /// + /// + public CodeFileDisplay GetScaffold(string type, string id, string snippetName = null) + { + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Value cannot be null or whitespace.", "id"); + + CodeFileDisplay codeFileDisplay; + + switch (type) + { + case Core.Constants.Trees.PartialViews: + codeFileDisplay = Mapper.Map(new PartialView(string.Empty)); + codeFileDisplay.VirtualPath = SystemDirectories.PartialViews; + if (snippetName.IsNullOrWhiteSpace() == false) + codeFileDisplay.Content = Services.FileService.GetPartialViewSnippetContent(snippetName); + break; + case Core.Constants.Trees.PartialViewMacros: + codeFileDisplay = Mapper.Map(new PartialView(string.Empty)); + codeFileDisplay.VirtualPath = SystemDirectories.MacroPartials; + if (snippetName.IsNullOrWhiteSpace() == false) + codeFileDisplay.Content = Services.FileService.GetPartialViewMacroSnippetContent(snippetName); + break; + case Core.Constants.Trees.Scripts: + codeFileDisplay = Mapper.Map(new Script(string.Empty)); + codeFileDisplay.VirtualPath = SystemDirectories.Scripts; + break; + default: + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Unsupported editortype")); + } + + // Make sure that the root virtual path ends with '/' + codeFileDisplay.VirtualPath = codeFileDisplay.VirtualPath.EnsureEndsWith("/"); + + if (id != Core.Constants.System.Root.ToInvariantString()) + { + codeFileDisplay.VirtualPath += id.TrimStart("/").EnsureEndsWith("/"); + //if it's not new then it will have a path, otherwise it won't + codeFileDisplay.Path = Url.GetTreePathFromFilePath(id); + } + + codeFileDisplay.VirtualPath = codeFileDisplay.VirtualPath.TrimStart("~"); + codeFileDisplay.FileType = type; + return codeFileDisplay; + } + + /// + /// Used to delete a specific file from disk via the FileService + /// + /// This is a string but will be 'scripts' 'partialViews', 'partialViewMacros' + /// The filename or urlencoded path of the file to delete + /// Will return a simple 200 if file deletion succeeds + [HttpDelete] + [HttpPost] + public HttpResponseMessage Delete(string type, string virtualPath) + { + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type"); + if (string.IsNullOrWhiteSpace(virtualPath)) throw new ArgumentException("Value cannot be null or whitespace.", "virtualPath"); + + virtualPath = System.Web.HttpUtility.UrlDecode(virtualPath); + + switch (type) + { + case Core.Constants.Trees.PartialViews: + if (IsDirectory(virtualPath, SystemDirectories.PartialViews)) + { + Services.FileService.DeletePartialViewFolder(virtualPath); + return Request.CreateResponse(HttpStatusCode.OK); + } + if (Services.FileService.DeletePartialView(virtualPath, Security.CurrentUser.Id)) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View or folder found with the specified path"); + + case Core.Constants.Trees.PartialViewMacros: + if (IsDirectory(virtualPath, SystemDirectories.MacroPartials)) + { + Services.FileService.DeletePartialViewMacroFolder(virtualPath); + return Request.CreateResponse(HttpStatusCode.OK); + } + if (Services.FileService.DeletePartialViewMacro(virtualPath, Security.CurrentUser.Id)) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View Macro or folder found with the specified path"); + + case Core.Constants.Trees.Scripts: + if (IsDirectory(virtualPath, SystemDirectories.Scripts)) + { + Services.FileService.DeleteScriptFolder(virtualPath); + return Request.CreateResponse(HttpStatusCode.OK); + } + if (Services.FileService.GetScriptByName(virtualPath) != null) + { + Services.FileService.DeleteScript(virtualPath, Security.CurrentUser.Id); + return Request.CreateResponse(HttpStatusCode.OK); + } + return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Script or folder found with the specified path"); + + default: + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + /// + /// Used to create or update a 'partialview', 'partialviewmacro' or 'script' file + /// + /// + /// The updated CodeFileDisplay model + public CodeFileDisplay PostSave(CodeFileDisplay display) + { + if (display == null) throw new ArgumentNullException("display"); + + if (ModelState.IsValid == false) + { + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + switch (display.FileType) + { + case Core.Constants.Trees.PartialViews: + var partialViewResult = CreateOrUpdatePartialView(display); + if (partialViewResult.Success) + { + display = Mapper.Map(partialViewResult.Result, display); + display.Path = Url.GetTreePathFromFilePath(partialViewResult.Result.Path); + display.Id = System.Web.HttpUtility.UrlEncode(partialViewResult.Result.Path); + return display; + } + + display.AddErrorNotification( + Services.TextService.Localize("speechBubbles/partialViewErrorHeader"), + Services.TextService.Localize("speechBubbles/partialViewErrorText")); + break; + + case Core.Constants.Trees.PartialViewMacros: + var partialViewMacroResult = CreateOrUpdatePartialViewMacro(display); + if (partialViewMacroResult.Success) + { + display = Mapper.Map(partialViewMacroResult.Result, display); + display.Path = Url.GetTreePathFromFilePath(partialViewMacroResult.Result.Path); + display.Id = System.Web.HttpUtility.UrlEncode(partialViewMacroResult.Result.Path); + return display; + } + + display.AddErrorNotification( + Services.TextService.Localize("speechBubbles/partialViewErrorHeader"), + Services.TextService.Localize("speechBubbles/partialViewErrorText")); + break; + + case Core.Constants.Trees.Scripts: + + var scriptResult = CreateOrUpdateScript(display); + display = Mapper.Map(scriptResult, display); + display.Path = Url.GetTreePathFromFilePath(scriptResult.Path); + display.Id = System.Web.HttpUtility.UrlEncode(scriptResult.Path); + return display; + + //display.AddErrorNotification( + // Services.TextService.Localize("speechBubbles/partialViewErrorHeader"), + // Services.TextService.Localize("speechBubbles/partialViewErrorText")); + + break; + + + + + default: + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + return display; + } + + /// + /// Create or Update a Script + /// + /// + /// + /// + /// It's important to note that Scripts are DIFFERENT from cshtml files since scripts use IFileSystem and cshtml files + /// use a normal file system because they must exist on a real file system for ASP.NET to work. + /// + private Script CreateOrUpdateScript(CodeFileDisplay display) + { + //must always end with the correct extension + display.Name = EnsureCorrectFileExtension(display.Name, ".js"); + + var virtualPath = display.VirtualPath ?? string.Empty; + // this is all weird, should be using relative paths everywhere! + var relPath = FileSystemProviderManager.Current.ScriptsFileSystem.GetRelativePath(virtualPath); + + if (relPath.EndsWith(".js") == false) + { + //this would typically mean it's new + relPath = relPath.IsNullOrWhiteSpace() + ? relPath + display.Name + : relPath.EnsureEndsWith('/') + display.Name; + } + + var script = Services.FileService.GetScriptByName(relPath); + if (script != null) + { + // might need to find the path + var orgPath = script.OriginalPath.Substring(0, script.OriginalPath.IndexOf(script.Name)); + script.Path = orgPath + display.Name; + + script.Content = display.Content; + //try/catch? since this doesn't return an Attempt? + Services.FileService.SaveScript(script, Security.CurrentUser.Id); + } + else + { + script = new Script(relPath); + script.Content = display.Content; + Services.FileService.SaveScript(script, Security.CurrentUser.Id); + } + + return script; + } + + private Attempt CreateOrUpdatePartialView(CodeFileDisplay display) + { + return CreateOrUpdatePartialView(display, SystemDirectories.PartialViews, + Services.FileService.GetPartialView, Services.FileService.SavePartialView, Services.FileService.CreatePartialView); + } + + private Attempt CreateOrUpdatePartialViewMacro(CodeFileDisplay display) + { + return CreateOrUpdatePartialView(display, SystemDirectories.MacroPartials, + Services.FileService.GetPartialViewMacro, Services.FileService.SavePartialViewMacro, Services.FileService.CreatePartialViewMacro); + } + + /// + /// Helper method to take care of persisting partial views or partial view macros - so we're not duplicating the same logic + /// + /// + /// + /// + /// + /// + /// + private Attempt CreateOrUpdatePartialView( + CodeFileDisplay display, string systemDirectory, + Func getView, + Func> saveView, + Func> createView) + { + //must always end with the correct extension + display.Name = EnsureCorrectFileExtension(display.Name, ".cshtml"); + + Attempt partialViewResult; + var virtualPath = NormalizeVirtualPath(display.VirtualPath, systemDirectory); + var view = getView(virtualPath); + if (view != null) + { + // might need to find the path + var orgPath = view.OriginalPath.Substring(0, view.OriginalPath.IndexOf(view.Name)); + view.Path = orgPath + display.Name; + + view.Content = display.Content; + partialViewResult = saveView(view, Security.CurrentUser.Id); + } + else + { + view = new PartialView(virtualPath + display.Name); + view.Content = display.Content; + partialViewResult = createView(view, display.Snippet, Security.CurrentUser.Id); + } + + return partialViewResult; + } + + private string NormalizeVirtualPath(string virtualPath, string systemDirectory) + { + if (virtualPath.IsNullOrWhiteSpace()) + return string.Empty; + + systemDirectory = systemDirectory.TrimStart("~"); + systemDirectory = systemDirectory.Replace('\\', '/'); + virtualPath = virtualPath.TrimStart("~"); + virtualPath = virtualPath.Replace('\\', '/'); + virtualPath = virtualPath.ReplaceFirst(systemDirectory, string.Empty); + + return virtualPath; + } + + private string EnsureCorrectFileExtension(string value, string extension) + { + if (value.EndsWith(extension) == false) + value += extension; + + return value; + } + + private bool IsDirectory(string virtualPath, string systemDirectory) + { + var path = IOHelper.MapPath(systemDirectory + "/" + virtualPath); + var dirInfo = new DirectoryInfo(path); + return dirInfo.Attributes == FileAttributes.Directory; + } + } +} diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 58494369f5..da5e0c3a3b 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -4,11 +4,10 @@ using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Formatting; using System.Text; using System.Web.Http; +using System.Web.Http.Controllers; using System.Web.Http.ModelBinding; -using System.Web.Http.ModelBinding.Binders; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Logging; @@ -17,24 +16,15 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Publishing; using Umbraco.Core.Services; -using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; -using Umbraco.Web.Security; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; -using umbraco; -using Umbraco.Core.Models; -using Umbraco.Core.Dynamics; -using umbraco.BusinessLogic.Actions; using umbraco.cms.businesslogic.web; using umbraco.presentation.preview; -using Umbraco.Core.PropertyEditors; -using Umbraco.Web.UI; using Constants = Umbraco.Core.Constants; -using Notification = Umbraco.Web.Models.ContentEditing.Notification; namespace Umbraco.Web.Editors { @@ -46,7 +36,8 @@ namespace Umbraco.Web.Editors /// access to ALL of the methods on this controller will need access to the content application. /// [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] + [UmbracoApplicationAuthorize(Constants.Applications.Content)] + [ContentControllerConfiguration] public class ContentController : ContentControllerBase { /// @@ -66,6 +57,18 @@ namespace Umbraco.Web.Editors { } + /// + /// Configures this controller with a custom action selector + /// + private class ContentControllerConfigurationAttribute : Attribute, IControllerConfiguration + { + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)))); + } + } + /// /// Return content for the specified ids /// @@ -173,6 +176,34 @@ namespace Umbraco.Web.Editors return response; } + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Guid id) + { + var url = Umbraco.UrlProvider.GetUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetNiceUrl(guidUdi.Guid); + } + throw new HttpResponseException(HttpStatusCode.NotFound); + } + /// /// Gets the children for the content id passed in /// diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index 0cddb6fcd5..6b8d570e5f 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -97,13 +97,17 @@ namespace Umbraco.Web.Editors var dictionary = new Dictionary(); //add the files if any var files = contentItem.UploadedFiles.Where(x => x.PropertyAlias == property.Alias).ToArray(); - + if (files.Length > 0) + { + dictionary.Add("files", files); + } foreach (var file in files) file.FileName = file.FileName.ToSafeFileName(); - if (files.Any()) - dictionary.Add("files", files); - + // add extra things needed to figure out where to put the files + dictionary.Add("cuid", contentItem.PersistedContent.Key); + dictionary.Add("puid", dboProperty.PropertyType.Key); + var data = new ContentPropertyData(property.Value, property.PreValues, dictionary); //get the deserialized value from the property editor diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 45a2821130..720eca184b 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -102,20 +102,25 @@ namespace Umbraco.Web.Editors return ApplicationContext.Services.ContentTypeService.GetAllPropertyTypeAliases(); } + /// + /// Gets all the standard fields. + /// + /// + [UmbracoTreeAuthorize( + Constants.Trees.DocumentTypes, Constants.Trees.Content, + Constants.Trees.MediaTypes, Constants.Trees.Media, + Constants.Trees.MemberTypes, Constants.Trees.Members)] + public IEnumerable GetAllStandardFields() + { + string[] preValuesSource = { "createDate", "creatorName", "level", "nodeType", "nodeTypeAlias", "pageID", "pageName", "parentID", "path", "template", "updateDate", "writerID", "writerName" }; + return preValuesSource; + } + /// /// Returns the avilable compositions for this content type /// This has been wrapped in a dto instead of simple parameters to support having multiple parameters in post request body /// - /// - /// - /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out - /// along with any content types that have matching property types that are included in the filtered content types - /// - /// - /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. - /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot - /// be looked up via the db, they need to be passed in. - /// + /// /// [HttpPost] public HttpResponseMessage GetAvailableCompositeContentTypes(GetAvailableCompositionsFilter filter) diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index 5c56106063..aacad99fb7 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -37,7 +37,7 @@ namespace Umbraco.Web.Editors public IDictionary GetMembershipProviderConfig() { var provider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider(); - return provider.GetConfiguration(); + return provider.GetConfiguration(Services.UserService); } /// diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 931d00968b..b3e306a48c 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] [UmbracoTreeAuthorize(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)] [EnableOverrideAuthorization] - public class DataTypeController : UmbracoAuthorizedJsonController + public class DataTypeController : BackOfficeNotificationsController { /// /// Gets data type by name diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index cd8e91274f..48da53e263 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Net; using System.Text; @@ -10,11 +11,14 @@ using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using System.Linq; +using System.Net.Http; using Umbraco.Core.Models; using Constants = Umbraco.Core.Constants; using Examine; using Umbraco.Web.Dynamics; using System.Text.RegularExpressions; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using System.Web.Http.Controllers; using Umbraco.Core.Xml; namespace Umbraco.Web.Editors @@ -29,6 +33,25 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class EntityController : UmbracoAuthorizedJsonController { + + /// + /// Configures this controller with a custom action selector + /// + private class EntityControllerConfigurationAttribute : Attribute, IControllerConfiguration + { + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( + + //This is a special case, we'll accept a String here so that we can get page members when the special "all-members" + //id is passed in eventually we'll probably want to support GUID + Udi too + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetPagedChildren", "id", typeof(int), typeof(string)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetPath", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetByIds", "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[])))); + } + } + /// /// Returns an Umbraco alias given a string /// @@ -132,11 +155,75 @@ namespace Umbraco.Web.Editors } /// - /// Gets an entity by it's unique id if the entity supports that + /// Gets the path for a given node ID /// /// /// /// + public IEnumerable GetPath(Guid id, UmbracoEntityTypes type) + { + var foundContent = GetResultForKey(id, type); + + return foundContent.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse); + } + + /// + /// Gets the path for a given node ID + /// + /// + /// + /// + public IEnumerable GetPath(Udi id, UmbracoEntityTypes type) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetPath(guidUdi.Guid, type); + } + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + /// + /// Gets the url of an entity + /// + /// Int id of the entity to fetch URL for + /// The tpye of entity such as Document, Media, Member + /// The URL or path to the item + public HttpResponseMessage GetUrl(int id, UmbracoEntityTypes type) + { + var returnUrl = string.Empty; + + if (type == UmbracoEntityTypes.Document) + { + var foundUrl = Umbraco.Url(id); + if (string.IsNullOrEmpty(foundUrl) == false && foundUrl != "#") + { + returnUrl = foundUrl; + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(returnUrl) + }; + } + } + + var ancestors = GetAncestors(id, type); + + //if content, skip the first node for replicating NiceUrl defaults + if(type == UmbracoEntityTypes.Document) { + ancestors = ancestors.Skip(1); + } + + returnUrl = "/" + string.Join("/", ancestors.Select(x => x.Name)); + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(returnUrl) + }; + } + + [Obsolete("Use GetyById instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public EntityBasic GetByKey(Guid id, UmbracoEntityTypes type) { return GetResultForKey(id, type); @@ -151,7 +238,7 @@ namespace Umbraco.Web.Editors /// public EntityBasic GetByQuery(string query, int nodeContextId, UmbracoEntityTypes type) { - //TODO: Rename this!!! It's a bit misleading, it should be GetByXPath + //TODO: Rename this!!! It's misleading, it should be GetByXPath if (type != UmbracoEntityTypes.Document) @@ -180,13 +267,61 @@ namespace Umbraco.Web.Editors }, publishedContentExists: i => Umbraco.TypedContent(i) != null); } - + + #region GetById + + /// + /// Gets an entity by it's id + /// + /// + /// + /// public EntityBasic GetById(int id, UmbracoEntityTypes type) { return GetResultForId(id, type); } - public IEnumerable GetByIds([FromUri]int[] ids, UmbracoEntityTypes type) + /// + /// Gets an entity by it's key + /// + /// + /// + /// + public EntityBasic GetById(Guid id, UmbracoEntityTypes type) + { + return GetResultForKey(id, type); + } + + /// + /// Gets an entity by it's UDI + /// + /// + /// + /// + public EntityBasic GetById(Udi id, UmbracoEntityTypes type) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetResultForKey(guidUdi.Guid, type); + } + throw new HttpResponseException(HttpStatusCode.NotFound); + } + #endregion + + #region GetByIds + /// + /// Get entities by integer ids + /// + /// + /// + /// + /// + /// We allow for POST because there could be quite a lot of Ids + /// + [HttpGet] + [HttpPost] + public IEnumerable GetByIds([FromJsonPath]int[] ids, UmbracoEntityTypes type) { if (ids == null) { @@ -195,6 +330,66 @@ namespace Umbraco.Web.Editors return GetResultForIds(ids, type); } + /// + /// Get entities by GUID ids + /// + /// + /// + /// + /// + /// We allow for POST because there could be quite a lot of Ids + /// + [HttpGet] + [HttpPost] + public IEnumerable GetByIds([FromJsonPath]Guid[] ids, UmbracoEntityTypes type) + { + if (ids == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + return GetResultForKeys(ids, type); + } + + /// + /// Get entities by UDIs + /// + /// + /// A list of UDIs to lookup items by, all UDIs must be of the same UDI type! + /// + /// + /// + /// + /// We allow for POST because there could be quite a lot of Ids. + /// + [HttpGet] + [HttpPost] + public IEnumerable GetByIds([FromJsonPath]Udi[] ids, [FromUri]UmbracoEntityTypes type) + { + if (ids == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + if (ids.Length == 0) + { + return Enumerable.Empty(); + } + + //all udi types will need to be the same in this list so we'll determine by the first + //currently we only support GuidIdi for this method + + var guidUdi = ids[0] as GuidUdi; + if (guidUdi != null) + { + return GetResultForKeys(ids.Select(x => ((GuidUdi)x).Guid).ToArray(), type); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + #endregion + + [Obsolete("Use GetyByIds instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetByKeys([FromUri]Guid[] ids, UmbracoEntityTypes type) { if (ids == null) @@ -210,7 +405,7 @@ namespace Umbraco.Web.Editors } /// - /// Get paged descendant entities by id + /// Get paged child entities by id /// /// /// @@ -220,6 +415,165 @@ namespace Umbraco.Web.Editors /// /// /// + public PagedResult GetPagedChildren( + string id, + UmbracoEntityTypes type, + int pageNumber, + int pageSize, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + string filter = "") + { + int intId; + + if (int.TryParse(id, out intId)) + { + return GetPagedChildren(intId, type, pageNumber, pageSize, orderBy, orderDirection, filter); + } + + Guid guidId; + if (Guid.TryParse(id, out guidId)) + { + //Not supported currently + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + Udi udiId; + if (Udi.TryParse(id, out udiId)) + { + //Not supported currently + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //so we don't have an INT, GUID or UDI, it's just a string, so now need to check if it's a special id or a member type + if (id == Constants.Conventions.MemberTypes.AllMembersListId) + { + //the EntityService can search paged members from the root + + intId = -1; + return GetPagedChildren(intId, type, pageNumber, pageSize, orderBy, orderDirection, filter); + } + + //the EntityService cannot search members of a certain type, this is currently not supported and would require + //quite a bit of plumbing to do in the Services/Repository, we'll revert to a paged search + + int total; + var searchResult = ExamineSearch(filter ?? "", type, pageSize, pageNumber - 1, out total, id); + + return new PagedResult(total, pageNumber, pageSize) + { + Items = searchResult + }; + } + + /// + /// Get paged child entities by id + /// + /// + /// + /// + /// + /// + /// + /// + /// + public PagedResult GetPagedChildren( + int id, + UmbracoEntityTypes type, + int pageNumber, + int pageSize, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + string filter = "") + { + if (pageNumber <= 0) + throw new HttpResponseException(HttpStatusCode.NotFound); + if (pageSize <= 0) + throw new HttpResponseException(HttpStatusCode.NotFound); + + var objectType = ConvertToObjectType(type); + if (objectType.HasValue) + { + long totalRecords; + var entities = Services.EntityService.GetPagedChildren(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter); + + if (totalRecords == 0) + { + return new PagedResult(0, 0, 0); + } + + var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = entities.Select(Mapper.Map) + }; + + return pagedResult; + } + + //now we need to convert the unknown ones + switch (type) + { + case UmbracoEntityTypes.PropertyType: + case UmbracoEntityTypes.PropertyGroup: + case UmbracoEntityTypes.Domain: + case UmbracoEntityTypes.Language: + case UmbracoEntityTypes.User: + case UmbracoEntityTypes.Macro: + default: + throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + type); + } + } + + public PagedResult GetPagedDescendants( + int id, + UmbracoEntityTypes type, + int pageNumber, + int pageSize, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + string filter = "") + { + if (pageNumber <= 0) + throw new HttpResponseException(HttpStatusCode.NotFound); + if (pageSize <= 0) + throw new HttpResponseException(HttpStatusCode.NotFound); + + var objectType = ConvertToObjectType(type); + if (objectType.HasValue) + { + long totalRecords; + //if it's from root, don't return recycled + var entities = id == Constants.System.Root + ? Services.EntityService.GetPagedDescendantsFromRoot(objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter, includeTrashed:false) + : Services.EntityService.GetPagedDescendants(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter); + + if (totalRecords == 0) + { + return new PagedResult(0, 0, 0); + } + + var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = entities.Select(Mapper.Map) + }; + + return pagedResult; + } + + //now we need to convert the unknown ones + switch (type) + { + case UmbracoEntityTypes.PropertyType: + case UmbracoEntityTypes.PropertyGroup: + case UmbracoEntityTypes.Domain: + case UmbracoEntityTypes.Language: + case UmbracoEntityTypes.User: + case UmbracoEntityTypes.Macro: + default: + throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + type); + } + } + public IEnumerable GetAncestors(int id, UmbracoEntityTypes type) { return GetResultForAncestors(id, type); @@ -235,12 +589,30 @@ namespace Umbraco.Web.Editors /// /// /// - /// - /// A starting point for the search, generally a node id, but for members this is a member type alias - /// + /// /// private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null) { + int total; + return ExamineSearch(query, entityType, 200, 0, out total, searchFrom); + } + + /// + /// Searches for results based on the entity type + /// + /// + /// + /// + /// + /// A starting point for the search, generally a node id, but for members this is a member type alias + /// + /// + /// + /// + private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, int pageSize, int pageIndex, out int totalFound, string searchFrom = null) + { + //TODO: We need to update this to support paging + var sb = new StringBuilder(); string type; @@ -316,91 +688,113 @@ namespace Umbraco.Web.Editors query = Lucene.Net.QueryParsers.QueryParser.Escape(query); - if (query.IsNullOrWhiteSpace()) + //nothing to search + if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace()) { + totalFound = 0; return new List(); } - //add back the surrounding quotes - query = string.Format("{0}{1}{0}", "\"", query); - - //node name exactly boost x 10 - sb.Append("+(__nodeName: ("); - sb.Append(query.ToLower()); - sb.Append(")^10.0 "); - - foreach (var f in fields) + //update the query with the query term + if (query.IsNullOrWhiteSpace() == false) { - //additional fields normally - sb.Append(f); - sb.Append(": ("); - sb.Append(query); + //add back the surrounding quotes + query = string.Format("{0}{1}{0}", "\"", query); + + //node name exactly boost x 10 + sb.Append("+(__nodeName: ("); + sb.Append(query.ToLower()); + sb.Append(")^10.0 "); + + foreach (var f in fields) + { + //additional fields normally + sb.Append(f); + sb.Append(": ("); + sb.Append(query); + sb.Append(") "); + } + sb.Append(") "); } } else { - if (query.Trim(new[] { '\"', '\'' }).IsNullOrWhiteSpace()) + var trimmed = query.Trim(new[] {'\"', '\''}); + + //nothing to search + if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace()) { + totalFound = 0; return new List(); } - - query = Lucene.Net.QueryParsers.QueryParser.Escape(query); - var querywords = query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - - //node name exactly boost x 10 - sb.Append("+(__nodeName:"); - sb.Append("\""); - sb.Append(query.ToLower()); - sb.Append("\""); - sb.Append("^10.0 "); - - //node name normally with wildcards - sb.Append(" __nodeName:"); - sb.Append("("); - foreach (var w in querywords) + //update the query with the query term + if (trimmed.IsNullOrWhiteSpace() == false) { - sb.Append(w.ToLower()); - sb.Append("* "); - } - sb.Append(") "); + query = Lucene.Net.QueryParsers.QueryParser.Escape(query); + var querywords = query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var f in fields) - { - //additional fields normally - sb.Append(f); - sb.Append(":"); + //node name exactly boost x 10 + sb.Append("+(__nodeName:"); + sb.Append("\""); + sb.Append(query.ToLower()); + sb.Append("\""); + sb.Append("^10.0 "); + + //node name normally with wildcards + sb.Append(" __nodeName:"); sb.Append("("); foreach (var w in querywords) { sb.Append(w.ToLower()); sb.Append("* "); } - sb.Append(")"); - sb.Append(" "); + sb.Append(") "); + + + foreach (var f in fields) + { + //additional fields normally + sb.Append(f); + sb.Append(":"); + sb.Append("("); + foreach (var w in querywords) + { + sb.Append(w.ToLower()); + sb.Append("* "); + } + sb.Append(")"); + sb.Append(" "); + } + + sb.Append(") "); } } //must match index type - sb.Append(") +__IndexType:"); + sb.Append("+__IndexType:"); sb.Append(type); - var raw = internalSearcher.CreateSearchCriteria().RawQuery(sb.ToString()); - //limit results to 200 to avoid huge over processing (CPU) - var result = internalSearcher.Search(raw, 200); + var result = internalSearcher + //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested + .Search(raw, pageSize * (pageIndex + 1)); + totalFound = result.TotalItemCount; + + var pagedResult = result.Skip(pageIndex); + switch (entityType) { case UmbracoEntityTypes.Member: - return MemberFromSearchResults(result); + return MemberFromSearchResults(pagedResult.ToArray()); case UmbracoEntityTypes.Media: - return MediaFromSearchResults(result); + return MediaFromSearchResults(pagedResult); case UmbracoEntityTypes.Document: - return ContentFromSearchResults(result); + return ContentFromSearchResults(pagedResult); default: throw new NotSupportedException("The " + typeof(EntityController) + " currently does not support searching against object type " + entityType); } @@ -411,7 +805,7 @@ namespace Umbraco.Web.Editors /// /// /// - private IEnumerable MemberFromSearchResults(ISearchResults results) + private IEnumerable MemberFromSearchResults(SearchResult[] results) { var mapped = Mapper.Map>(results).ToArray(); //add additional data @@ -445,7 +839,7 @@ namespace Umbraco.Web.Editors /// /// /// - private IEnumerable MediaFromSearchResults(ISearchResults results) + private IEnumerable MediaFromSearchResults(IEnumerable results) { var mapped = Mapper.Map>(results).ToArray(); //add additional data @@ -465,9 +859,9 @@ namespace Umbraco.Web.Editors /// /// /// - private IEnumerable ContentFromSearchResults(ISearchResults results) + private IEnumerable ContentFromSearchResults(IEnumerable results) { - var mapped = Mapper.Map>(results).ToArray(); + var mapped = Mapper.Map>(results).ToArray(); //add additional data foreach (var m in mapped) { @@ -599,21 +993,21 @@ namespace Umbraco.Web.Editors } } - private IEnumerable GetResultForKeys(IEnumerable keys, UmbracoEntityTypes entityType) + private IEnumerable GetResultForKeys(Guid[] keys, UmbracoEntityTypes entityType) { - var keysArray = keys.ToArray(); - if (keysArray.Any() == false) return Enumerable.Empty(); + if (keys.Length == 0) + return Enumerable.Empty(); var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { - var entities = Services.EntityService.GetAll(objectType.Value, keysArray) + var entities = Services.EntityService.GetAll(objectType.Value, keys) .WhereNotNull() .Select(Mapper.Map); // entities are in "some" order, put them back in order var xref = entities.ToDictionary(x => x.Key); - var result = keysArray.Select(x => xref.ContainsKey(x) ? xref[x] : null).Where(x => x != null); + var result = keys.Select(x => xref.ContainsKey(x) ? xref[x] : null).Where(x => x != null); return result; } @@ -631,21 +1025,21 @@ namespace Umbraco.Web.Editors } } - private IEnumerable GetResultForIds(IEnumerable ids, UmbracoEntityTypes entityType) + private IEnumerable GetResultForIds(int[] ids, UmbracoEntityTypes entityType) { - var idsArray = ids.ToArray(); - if (idsArray.Any() == false) return Enumerable.Empty(); + if (ids.Length == 0) + return Enumerable.Empty(); var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { - var entities = Services.EntityService.GetAll(objectType.Value, idsArray) + var entities = Services.EntityService.GetAll(objectType.Value, ids) .WhereNotNull() .Select(Mapper.Map); // entities are in "some" order, put them back in order var xref = entities.ToDictionary(x => x.Id); - var result = idsArray.Select(x => xref.ContainsKey(x) ? xref[x] : null).Where(x => x != null); + var result = ids.Select(x => xref.ContainsKey(x) ? xref[x] : null).Where(x => x != null); return result; } diff --git a/src/Umbraco.Web/Editors/EntityControllerActionSelector.cs b/src/Umbraco.Web/Editors/EntityControllerActionSelector.cs deleted file mode 100644 index 9cfab1bdcd..0000000000 --- a/src/Umbraco.Web/Editors/EntityControllerActionSelector.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Web; -using System.Web.Http.Controllers; -using Umbraco.Core; - -namespace Umbraco.Web.Editors -{ - /// - /// This allows for calling GetById/GetByIds with a GUID... so it will automatically route to GetByKey/GetByKeys - /// - internal class EntityControllerActionSelector : ApiControllerActionSelector - { - - public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext) - { - if (controllerContext.Request.RequestUri.GetLeftPart(UriPartial.Path).InvariantEndsWith("GetById")) - { - var id = HttpUtility.ParseQueryString(controllerContext.Request.RequestUri.Query).Get("id"); - - if (id != null) - { - Guid parsed; - if (Guid.TryParse(id, out parsed)) - { - var controllerType = controllerContext.Controller.GetType(); - var method = controllerType.GetMethod("GetByKey"); - if (method != null) - { - return new ReflectedHttpActionDescriptor(controllerContext.ControllerDescriptor, method); - } - } - } - } - - if (controllerContext.Request.RequestUri.GetLeftPart(UriPartial.Path).InvariantEndsWith("GetByIds")) - { - var ids = HttpUtility.ParseQueryString(controllerContext.Request.RequestUri.Query).GetValues("ids"); - - if (ids != null) - { - var allmatched = true; - foreach (var id in ids) - { - Guid parsed; - if (Guid.TryParse(id, out parsed) == false) - { - allmatched = false; - } - } - if (allmatched) - { - var controllerType = controllerContext.Controller.GetType(); - var method = controllerType.GetMethod("GetByKeys"); - if (method != null) - { - return new ReflectedHttpActionDescriptor(controllerContext.ControllerDescriptor, method); - } - } - } - } - - - - return base.SelectAction(controllerContext); - } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/EntityControllerConfigurationAttribute.cs b/src/Umbraco.Web/Editors/EntityControllerConfigurationAttribute.cs index 7ef4ef206a..571b29ed01 100644 --- a/src/Umbraco.Web/Editors/EntityControllerConfigurationAttribute.cs +++ b/src/Umbraco.Web/Editors/EntityControllerConfigurationAttribute.cs @@ -4,18 +4,5 @@ using Umbraco.Web.WebApi; namespace Umbraco.Web.Editors { - /// - /// This get's applied to the EntityController in order to have a custom IHttpActionSelector assigned to it - /// - /// - /// NOTE: It is SOOOO important to remember that you cannot just assign this in the 'initialize' method of a webapi - /// controller as it will assign it GLOBALLY which is what you def do not want to do. - /// - internal class EntityControllerConfigurationAttribute : Attribute, IControllerConfiguration - { - public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) - { - controllerSettings.Services.Replace(typeof(IHttpActionSelector), new EntityControllerActionSelector()); - } - } + } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/FromJsonPathAttribute.cs b/src/Umbraco.Web/Editors/FromJsonPathAttribute.cs new file mode 100644 index 0000000000..bb275f8fa2 --- /dev/null +++ b/src/Umbraco.Web/Editors/FromJsonPathAttribute.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.ModelBinding; +using System.Web.Http.ValueProviders; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core; + +namespace Umbraco.Web.Editors +{ + /// + /// Used to bind a value from an inner json property + /// + /// + /// An example would be if you had json like: + /// { ids: [1,2,3,4] } + /// + /// And you had an action like: GetByIds(int[] ids, UmbracoEntityTypes type) + /// + /// The ids array will not bind because the object being sent up is an object and not an array so the + /// normal json formatter will not figure this out. + /// + /// This would also let you bind sub levels of the JSON being sent up too if you wanted with any jsonpath + /// + internal class FromJsonPathAttribute : ModelBinderAttribute + { + private readonly string _jsonPath; + private readonly FromUriAttribute _fromUriAttribute = new FromUriAttribute(); + + public FromJsonPathAttribute() + { + } + + public FromJsonPathAttribute(string jsonPath) : base(typeof(JsonPathBinder)) + { + _jsonPath = jsonPath; + } + + public override IEnumerable GetValueProviderFactories(HttpConfiguration configuration) + { + return _fromUriAttribute.GetValueProviderFactories(configuration); + } + + public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter) + { + var config = parameter.Configuration; + //get the default binder, we'll use that if it's a GET or if the body is empty + var underlyingBinder = base.GetModelBinder(config, parameter.ParameterType); + var binder = new JsonPathBinder(underlyingBinder, _jsonPath); + var valueProviderFactories = GetValueProviderFactories(config); + + return new ModelBinderParameterBinding(parameter, binder, valueProviderFactories); + } + + private class JsonPathBinder : IModelBinder + { + private readonly IModelBinder _underlyingBinder; + private readonly string _jsonPath; + + public JsonPathBinder(IModelBinder underlyingBinder, string jsonPath) + { + _underlyingBinder = underlyingBinder; + _jsonPath = jsonPath; + } + + public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) + { + if (actionContext.Request.Method == HttpMethod.Get) + { + return _underlyingBinder.BindModel(actionContext, bindingContext); + } + + var requestContent = new HttpMessageContent(actionContext.Request); + var strJson = requestContent.HttpRequestMessage.Content.ReadAsStringAsync().Result; + + if (strJson.IsNullOrWhiteSpace()) + { + return _underlyingBinder.BindModel(actionContext, bindingContext); + } + + var json = JsonConvert.DeserializeObject(strJson); + + //if no explicit json path then use the model name + var match = json.SelectToken(_jsonPath ?? bindingContext.ModelName); + + if (match == null) + { + return false; + } + + bindingContext.Model = match.ToObject(bindingContext.ModelType); + + return true; + } + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/IEditorValidator.cs b/src/Umbraco.Web/Editors/IEditorValidator.cs index e1d4e68ed2..8cbd4e2949 100644 --- a/src/Umbraco.Web/Editors/IEditorValidator.cs +++ b/src/Umbraco.Web/Editors/IEditorValidator.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using umbraco.interfaces; namespace Umbraco.Web.Editors { - internal interface IEditorValidator + internal interface IEditorValidator : IDiscoverable { Type ModelType { get; } IEnumerable Validate(object model); diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index 416ffc2553..fa84ff345f 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -33,17 +33,13 @@ namespace Umbraco.Web.Editors { var media = Services.MediaService.GetById(mediaId); if (media == null) - { return Request.CreateResponse(HttpStatusCode.NotFound); - } + var imageProp = media.Properties[Constants.Conventions.Media.File]; if (imageProp == null) - { return Request.CreateResponse(HttpStatusCode.NotFound); - } var imagePath = imageProp.Value.ToString(); - return GetBigThumbnail(imagePath); } @@ -57,10 +53,9 @@ namespace Umbraco.Web.Editors /// public HttpResponseMessage GetBigThumbnail(string originalImagePath) { - if (string.IsNullOrWhiteSpace(originalImagePath)) - return Request.CreateResponse(HttpStatusCode.OK); - - return GetResized(originalImagePath, 500, "big-thumb"); + return string.IsNullOrWhiteSpace(originalImagePath) + ? Request.CreateResponse(HttpStatusCode.OK) + : GetResized(originalImagePath, 500, "big-thumb"); } /// @@ -76,17 +71,13 @@ namespace Umbraco.Web.Editors { var media = Services.MediaService.GetById(mediaId); if (media == null) - { return new HttpResponseMessage(HttpStatusCode.NotFound); - } + var imageProp = media.Properties[Constants.Conventions.Media.File]; if (imageProp == null) - { return new HttpResponseMessage(HttpStatusCode.NotFound); - } var imagePath = imageProp.Value.ToString(); - return GetResized(imagePath, width); } @@ -109,22 +100,20 @@ namespace Umbraco.Web.Editors /// /// /// - /// + /// /// - private HttpResponseMessage GetResized(string imagePath, int width, string suffix) + private HttpResponseMessage GetResized(string imagePath, int width, string sizeName) { - var mediaFileSystem = FileSystemProviderManager.Current.GetFileSystemProvider(); + var fs = FileSystemProviderManager.Current.MediaFileSystem; var ext = Path.GetExtension(imagePath); - //we need to check if it is an image by extension - if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(ext.TrimStart('.')) == false) - { + // we need to check if it is an image by extension + if (fs.IsImageFile(ext) == false) return Request.CreateResponse(HttpStatusCode.NotFound); - } //redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file - var response = Request.CreateResponse( HttpStatusCode.Found ); - var imageLastModified = mediaFileSystem.GetLastModified( imagePath ); + var response = Request.CreateResponse(HttpStatusCode.Found); + var imageLastModified = fs.GetLastModified(imagePath); response.Headers.Location = new Uri( string.Format( "{0}?rnd={1}&width={2}", imagePath, string.Format( "{0:yyyyMMddHHmmss}", imageLastModified ), width ), UriKind.Relative ); return response; } diff --git a/src/Umbraco.Web/Editors/MacroController.cs b/src/Umbraco.Web/Editors/MacroController.cs index b0b2cb4bf2..129c78cbcd 100644 --- a/src/Umbraco.Web/Editors/MacroController.cs +++ b/src/Umbraco.Web/Editors/MacroController.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -10,6 +11,7 @@ using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using umbraco; using Umbraco.Core; +using Umbraco.Core.Models; namespace Umbraco.Web.Editors { @@ -29,7 +31,7 @@ namespace Umbraco.Web.Editors /// /// /// - /// Note that ALL logged in users have access to this method because editors will need to isnert macros into rte (content/media/members) and it's used for + /// Note that ALL logged in users have access to this method because editors will need to isnert macros into rte (content/media/members) and it's used for /// inserting into templates/views/etc... it doesn't expose any sensitive data. /// public IEnumerable GetMacroParameters(int macroId) @@ -50,9 +52,9 @@ namespace Umbraco.Web.Editors /// /// /// To send a dictionary as a GET parameter the query should be structured like: - /// + /// /// ?macroAlias=Test&pageId=3634¯oParams[0].key=myKey¯oParams[0].value=myVal¯oParams[1].key=anotherKey¯oParams[1].value=anotherVal - /// + /// /// /// [HttpGet] @@ -129,6 +131,31 @@ namespace Umbraco.Web.Editors "text/html"); return result; } - + + [HttpPost] + public HttpResponseMessage CreatePartialViewMacroWithFile(CreatePartialViewMacroWithFileModel model) + { + if (model == null) throw new ArgumentNullException("model"); + if (string.IsNullOrWhiteSpace(model.Filename)) throw new ArgumentException("Filename cannot be null or whitespace", "model.Filename"); + if (string.IsNullOrWhiteSpace(model.VirtualPath)) throw new ArgumentException("VirtualPath cannot be null or whitespace", "model.VirtualPath"); + + var macroName = model.Filename.TrimEnd(".cshtml"); + + var macro = new Macro + { + Alias = macroName.ToSafeAlias(), + Name = macroName, + ScriptPath = model.VirtualPath.EnsureStartsWith("~") + }; + + Services.MacroService.Save(macro); // may throw + return new HttpResponseMessage(HttpStatusCode.OK); + } + + public class CreatePartialViewMacroWithFileModel + { + public string Filename { get; set; } + public string VirtualPath { get; set; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 82ddfe3fa9..4980605374 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.IO; using System.Net; using System.Net.Http; @@ -22,7 +23,9 @@ using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using System.Linq; +using System.Text.RegularExpressions; using System.Web.Http.Controllers; +using Examine; using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; @@ -50,7 +53,8 @@ namespace Umbraco.Web.Editors public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) { controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetChildren", "id", typeof(int), typeof(Guid), typeof(string)))); + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetChildren", "id", typeof(int), typeof(Guid), typeof(Udi), typeof(string)))); } } @@ -119,7 +123,7 @@ namespace Umbraco.Web.Editors } /// - /// Gets the content json for the content id + /// Gets the media item by id /// /// /// @@ -138,6 +142,43 @@ namespace Umbraco.Web.Editors return Mapper.Map(foundContent); } + /// + /// Gets the media item by id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForMedia("id")] + public MediaItemDisplay GetById(Guid id) + { + var foundContent = GetObjectFromRequest(() => Services.MediaService.GetById(id)); + + if (foundContent == null) + { + HandleContentNotFound(id); + //HandleContentNotFound will throw an exception + return null; + } + return Mapper.Map(foundContent); + } + + /// + /// Gets the media item by id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForMedia("id")] + public MediaItemDisplay GetById(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetById(guidUdi.Guid); + } + throw new HttpResponseException(HttpStatusCode.NotFound); + } + /// /// Return media for the specified ids /// @@ -188,7 +229,7 @@ namespace Umbraco.Web.Editors long total; var children = Services.MediaService.GetPagedChildren(id, pageNumber - 1, pageSize, out total, "Name", Direction.Ascending, true, null, folderTypes.ToArray()); - + return new PagedResult>(total, pageNumber, pageSize) { Items = children.Select(Mapper.Map>) @@ -207,6 +248,7 @@ namespace Umbraco.Web.Editors .Select(Mapper.Map>); } + #region GetChildren /// /// Returns the child media objects - using the entity INT id /// @@ -264,7 +306,7 @@ namespace Umbraco.Web.Editors Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") - { + { var entity = Services.EntityService.GetByKey(id); if (entity != null) { @@ -273,6 +315,39 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } + /// + /// Returns the child media objects - using the entity UDI id + /// + /// + /// + /// + /// + /// + /// + /// + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren(Udi id, + int pageNumber = 0, + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + var entity = Services.EntityService.GetByKey(guidUdi.Guid); + if (entity != null) + { + return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + [Obsolete("Do not use this method, use either the overload with INT or GUID instead, this will be removed in future versions")] [EditorBrowsable(EditorBrowsableState.Never)] [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] @@ -296,6 +371,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } + #endregion /// /// Moves an item to the recycle bin, if it is already there then it will permanently delete it @@ -536,8 +612,14 @@ namespace Umbraco.Web.Editors } //get the string json from the request - int parentId; bool entityFound; + int parentId; bool entityFound; GuidUdi parentUdi; string currentFolderId = result.FormData["currentFolder"]; + // test for udi + if (GuidUdi.TryParse(currentFolderId, out parentUdi)) + { + currentFolderId = parentUdi.Guid.ToString(); + } + if (int.TryParse(currentFolderId, out parentId) == false) { // if a guid then try to look up the entity diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index db2a806572..879ffd3d0a 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -40,7 +40,7 @@ namespace Umbraco.Web.Editors public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) { controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetAllowedChildren", "contentId", typeof(int), typeof(Guid), typeof(string)))); + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetAllowedChildren", "contentId", typeof(int), typeof(Guid), typeof(Udi), typeof(string)))); } } @@ -187,6 +187,7 @@ namespace Umbraco.Web.Editors } + #region GetAllowedChildren /// /// Returns the allowed child content type objects for the content item id passed in - based on an INT id /// @@ -247,10 +248,30 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - - [Obsolete("Do not use this method, use either the overload with INT or GUID instead, this will be removed in future versions")] + + /// + /// Returns the allowed child content type objects for the content item id passed in - based on a UDI id + /// + /// + [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] + public IEnumerable GetAllowedChildren(Udi contentId) + { + var guidUdi = contentId as GuidUdi; + if (guidUdi != null) + { + var entity = ApplicationContext.Services.EntityService.GetByKey(guidUdi.Guid); + if (entity != null) + { + return GetAllowedChildren(entity.Id); + } + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + [Obsolete("Do not use this method, use either the overload with INT, GUID or UDI instead, this will be removed in future versions")] [EditorBrowsable(EditorBrowsableState.Never)] - [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] + [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] public IEnumerable GetAllowedChildren(string contentId) { foreach (var type in new[] { typeof(int), typeof(Guid) }) @@ -260,11 +281,12 @@ namespace Umbraco.Web.Editors { //oooh magic! will auto select the right overload return GetAllowedChildren((dynamic)parsed.Result); - } + } } throw new HttpResponseException(HttpStatusCode.NotFound); - } + } + #endregion /// /// Move the media type diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 3f56087926..ec28a9408c 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -92,8 +92,7 @@ namespace Umbraco.Web.Editors { long totalRecords; var members = Services.MemberService - .GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField - , memberTypeAlias, filter).ToArray(); + .GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray(); if (totalRecords == 0) { return new PagedResult(0, 0, 0); diff --git a/src/Umbraco.Web/Editors/PackageInstallController.cs b/src/Umbraco.Web/Editors/PackageInstallController.cs index 48a2808ef3..d3771ce82c 100644 --- a/src/Umbraco.Web/Editors/PackageInstallController.cs +++ b/src/Umbraco.Web/Editors/PackageInstallController.cs @@ -15,9 +15,11 @@ using umbraco.cms.presentation.Trees; using umbraco.presentation.developer.packages; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Packaging.Models; using Umbraco.Core.Services; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; @@ -56,17 +58,25 @@ namespace Umbraco.Web.Editors [HttpPost] public IHttpActionResult Uninstall(int packageId) { - var pack = InstalledPackage.GetById(packageId); - if (pack == null) return NotFound(); - - PerformUninstall(pack); - - //now get all other packages by this name since we'll uninstall all versions - foreach (var installed in InstalledPackage.GetAllInstalledPackages() - .Where(x => x.Data.Name == pack.Data.Name && x.Data.Id != pack.Data.Id)) + try { - //remove from teh xml - installed.Delete(Security.GetUserId()); + var pack = InstalledPackage.GetById(packageId); + if (pack == null) return NotFound(); + + PerformUninstall(pack); + + //now get all other packages by this name since we'll uninstall all versions + foreach (var installed in InstalledPackage.GetAllInstalledPackages() + .Where(x => x.Data.Name == pack.Data.Name && x.Data.Id != pack.Data.Id)) + { + //remove from teh xml + installed.Delete(Security.GetUserId()); + } + } + catch (Exception e) + { + Logger.Error("Failed to uninstall.", e); + throw; } return Ok(); @@ -81,7 +91,14 @@ namespace Umbraco.Web.Editors if (pack == null) throw new ArgumentNullException("pack"); var refreshCache = false; - + + var removedTemplates = new List(); + var removedMacros = new List(); + var removedContentTypes = new List(); + var removedDictionaryItems = new List(); + var removedDataTypes = new List(); + var removedFiles = new List(); + //Uninstall templates foreach (var item in pack.Data.Templates.ToArray()) { @@ -90,6 +107,7 @@ namespace Umbraco.Web.Editors var found = Services.FileService.GetTemplate(nId); if (found != null) { + removedTemplates.Add(found); ApplicationContext.Services.FileService.DeleteTemplate(found.Alias, Security.GetUserId()); } pack.Data.Templates.Remove(nId.ToString()); @@ -103,8 +121,9 @@ namespace Umbraco.Web.Editors var macro = Services.MacroService.GetById(nId); if (macro != null) { + removedMacros.Add(macro); Services.MacroService.Delete(macro); - } + } pack.Data.Macros.Remove(nId.ToString()); } @@ -126,13 +145,12 @@ namespace Umbraco.Web.Editors //Order the DocumentTypes before removing them if (contentTypes.Any()) { + //TODO: I don't think this ordering is necessary var orderedTypes = from contentType in contentTypes - orderby contentType.ParentId descending, contentType.Id descending - select contentType; - foreach (var contentType in orderedTypes) - { - contentTypeService.Delete(contentType); - } + orderby contentType.ParentId descending, contentType.Id descending + select contentType; + removedContentTypes.AddRange(orderedTypes); + contentTypeService.Delete(orderedTypes); } //Remove Dictionary items @@ -143,8 +161,9 @@ namespace Umbraco.Web.Editors var di = Services.LocalizationService.GetDictionaryItemById(nId); if (di != null) { + removedDictionaryItems.Add(di); Services.LocalizationService.Delete(di); - } + } pack.Data.DictionaryItems.Remove(nId.ToString()); } @@ -156,15 +175,16 @@ namespace Umbraco.Web.Editors var dtd = Services.DataTypeService.GetDataTypeDefinitionById(nId); if (dtd != null) { + removedDataTypes.Add(dtd); Services.DataTypeService.Delete(dtd); - } + } pack.Data.DataTypes.Remove(nId.ToString()); } pack.Save(); // uninstall actions - //TODO: We should probably report errors to the UI!! + //TODO: We should probably report errors to the UI!! // This never happened before though, but we should do something now if (pack.Data.Actions.IsNullOrWhiteSpace() == false) { @@ -173,7 +193,7 @@ namespace Umbraco.Web.Editors var actionsXml = new XmlDocument(); actionsXml.LoadXml("" + pack.Data.Actions + ""); - LogHelper.Debug("executing undo actions: {0}", () => actionsXml.OuterXml); + LogHelper.Debug("executing undo actions: {0}", () => actionsXml.OuterXml); foreach (XmlNode n in actionsXml.DocumentElement.SelectNodes("//Action")) { @@ -184,13 +204,13 @@ namespace Umbraco.Web.Editors } catch (Exception ex) { - LogHelper.Error("An error occurred running undo actions", ex); + LogHelper.Error("An error occurred running undo actions", ex); } } } catch (Exception ex) { - LogHelper.Error("An error occurred running undo actions", ex); + LogHelper.Error("An error occurred running undo actions", ex); } } @@ -198,30 +218,45 @@ namespace Umbraco.Web.Editors //Remove files foreach (var item in pack.Data.Files.ToArray()) { + removedFiles.Add(item.GetRelativePath()); + //here we need to try to find the file in question as most packages does not support the tilde char var file = IOHelper.FindFile(item); if (file != null) { if (file.StartsWith("/") == false) file = string.Format("/{0}", file); - var filePath = IOHelper.MapPath(file); + if (File.Exists(filePath)) - { File.Delete(filePath); - - } } pack.Data.Files.Remove(file); } pack.Save(); pack.Delete(Security.GetUserId()); - + + // create a summary of what was actually removed, for PackagingService.UninstalledPackage + var summary = new UninstallationSummary + { + MetaData = pack.GetMetaData(), + TemplatesUninstalled = removedTemplates, + MacrosUninstalled = removedMacros, + ContentTypesUninstalled = removedContentTypes, + DictionaryItemsUninstalled = removedDictionaryItems, + DataTypesUninstalled = removedDataTypes, + FilesUninstalled = removedFiles, + PackageUninstalled = true + }; + + // trigger the UninstalledPackage event + PackagingService.OnUninstalledPackage(new UninstallPackageEventArgs(summary, false)); + //TODO: Legacy - probably not needed if (refreshCache) { library.RefreshContent(); - } + } TreeDefinitionCollection.Instance.ReRegisterTrees(); global::umbraco.BusinessLogic.Actions.Action.ReRegisterActionsAndHandlers(); } @@ -241,8 +276,8 @@ namespace Umbraco.Web.Editors { Version pckVersion; return Version.TryParse(pck.Data.Version, out pckVersion) - ? new {package = pck, version = pckVersion} - : new {package = pck, version = new Version(0, 0, 0)}; + ? new { package = pck, version = pckVersion } + : new { package = pck, version = new Version(0, 0, 0) }; }) .Select(grouping => { @@ -313,7 +348,7 @@ namespace Umbraco.Web.Editors model.UmbracoVersion = ins.RequirementsType == RequirementsType.Strict ? string.Format("{0}.{1}.{2}", ins.RequirementsMajor, ins.RequirementsMinor, ins.RequirementsPatch) : string.Empty; - + //now we need to check for version comparison model.IsCompatible = true; if (ins.RequirementsType == RequirementsType.Strict) @@ -393,7 +428,7 @@ namespace Umbraco.Web.Editors { //TODO: Currently it has to be here, it's not ideal but that's the way it is right now var packageTempDir = IOHelper.MapPath(SystemDirectories.Data); - + //ensure it's there Directory.CreateDirectory(packageTempDir); @@ -409,14 +444,14 @@ namespace Umbraco.Web.Editors PopulateFromPackageData(model); var validate = ValidateInstalledInternal(model.Name, model.Version); - + if (validate == false) { //this package is already installed throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("packager/packageAlreadyInstalled"))); + Services.TextService.Localize("packager/packageAlreadyInstalled"))); } - + } else { @@ -425,7 +460,7 @@ namespace Umbraco.Web.Editors Services.TextService.Localize("media/disallowedFileType"), SpeechBubbleIcon.Warning)); } - + } return model; @@ -444,7 +479,7 @@ namespace Umbraco.Web.Editors string path = Path.Combine("packages", packageGuid + ".umb"); if (File.Exists(IOHelper.MapPath(Path.Combine(SystemDirectories.Data, path))) == false) { - path = Services.PackagingService.FetchPackageFile(Guid.Parse(packageGuid), UmbracoVersion.Current, Security.GetUserId()); + path = Services.PackagingService.FetchPackageFile(Guid.Parse(packageGuid), UmbracoVersion.Current, Security.GetUserId()); } var model = new LocalPackageInstallModel @@ -487,12 +522,12 @@ namespace Umbraco.Web.Editors if (UmbracoVersion.Current < packageMinVersion) { throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("packager/targetVersionMismatch", new[] {packageMinVersion.ToString()}))); + Services.TextService.Localize("packager/targetVersionMismatch", new[] { packageMinVersion.ToString() }))); } } model.TemporaryDirectoryPath = Path.Combine(SystemDirectories.Data, tempPath); - model.Id = ins.CreateManifest( IOHelper.MapPath(model.TemporaryDirectoryPath), model.PackageGuid.ToString(), model.RepositoryGuid.ToString()); + model.Id = ins.CreateManifest(IOHelper.MapPath(model.TemporaryDirectoryPath), model.PackageGuid.ToString(), model.RepositoryGuid.ToString()); return model; } diff --git a/src/Umbraco.Web/Editors/ParameterSwapControllerActionSelector.cs b/src/Umbraco.Web/Editors/ParameterSwapControllerActionSelector.cs index 7f17fb9f8b..53e25c146a 100644 --- a/src/Umbraco.Web/Editors/ParameterSwapControllerActionSelector.cs +++ b/src/Umbraco.Web/Editors/ParameterSwapControllerActionSelector.cs @@ -1,7 +1,15 @@ using System; +using System.Collections; using System.Linq; +using System.Net.Http; +using System.Net.Http.Formatting; using System.Web; +using System.Web.Http; using System.Web.Http.Controllers; +using System.Web.Http.Validation; +using System.Web.Http.ValueProviders; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core; namespace Umbraco.Web.Editors @@ -12,6 +20,8 @@ namespace Umbraco.Web.Editors /// /// As an example, lets say we have 2 methods: GetChildren(int id) and GetChildren(Guid id), by default Web Api won't allow this since /// it won't know what to select, but if this Tuple is passed in new Tuple{string, string}("GetChildren", "id") + /// + /// This supports POST values too however only for JSON values /// internal class ParameterSwapControllerActionSelector : ApiControllerActionSelector { @@ -25,33 +35,102 @@ namespace Umbraco.Web.Editors { _actions = actions; } + public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext) { var found = _actions.FirstOrDefault(x => controllerContext.Request.RequestUri.GetLeftPart(UriPartial.Path).InvariantEndsWith(x.ActionName)); if (found != null) { - var id = HttpUtility.ParseQueryString(controllerContext.Request.RequestUri.Query).Get(found.ParamName); - - if (id != null) + HttpActionDescriptor method; + if (TryBindFromUri(controllerContext, found, out method)) { - var idTypes = found.SupportedTypes; + return method; + } - foreach (var idType in idTypes) + //if it's a post we can try to read from the body and bind from the json value + if (controllerContext.Request.Method == HttpMethod.Post) + { + var requestContent = new HttpMessageContent(controllerContext.Request); + var strJson = requestContent.HttpRequestMessage.Content.ReadAsStringAsync().Result; + var json = JsonConvert.DeserializeObject(strJson); + + if (json == null) { - var converted = id.TryConvertTo(idType); - if (converted) + return base.SelectAction(controllerContext); + } + + var requestParam = json[found.ParamName]; + + if (requestParam != null) + { + var paramTypes = found.SupportedTypes; + + foreach (var paramType in paramTypes) { - var method = MatchByType(idType, controllerContext, found); - if (method != null) - return method; + try + { + var converted = requestParam.ToObject(paramType); + if (converted != null) + { + method = MatchByType(paramType, controllerContext, found); + if (method != null) + return method; + } + } + catch (JsonReaderException) + { + //can't convert + } + catch (JsonSerializationException) + { + //can't convert + } } - } + } } } return base.SelectAction(controllerContext); } + private bool TryBindFromUri(HttpControllerContext controllerContext, ParameterSwapInfo found, out HttpActionDescriptor method) + { + var requestParam = HttpUtility.ParseQueryString(controllerContext.Request.RequestUri.Query).Get(found.ParamName); + + requestParam = (requestParam == null) ? null : requestParam.Trim(); + var paramTypes = found.SupportedTypes; + + if (requestParam == string.Empty && paramTypes.Length > 0) + { + //if it's empty then in theory we can select any of the actions since they'll all need to deal with empty or null parameters + //so we'll try to use the first one available + method = MatchByType(paramTypes[0], controllerContext, found); + if (method != null) + return true; + } + + if (requestParam != null) + { + foreach (var paramType in paramTypes) + { + //check if this is IEnumerable and if so this will get it's type + //we need to know this since the requestParam will always just be a string + var enumType = paramType.GetEnumeratedType(); + + var converted = requestParam.TryConvertTo(enumType ?? paramType); + if (converted) + { + method = MatchByType(paramType, controllerContext, found); + if (method != null) + return true; + } + } + } + + method = null; + return false; + } + private static ReflectedHttpActionDescriptor MatchByType(Type idType, HttpControllerContext controllerContext, ParameterSwapInfo found) { var controllerType = controllerContext.Controller.GetType(); diff --git a/src/Umbraco.Web/Editors/TemplateController.cs b/src/Umbraco.Web/Editors/TemplateController.cs new file mode 100644 index 0000000000..78855c95c2 --- /dev/null +++ b/src/Umbraco.Web/Editors/TemplateController.cs @@ -0,0 +1,197 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using AutoMapper; +using Umbraco.Core.IO; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Editors +{ + [PluginController("UmbracoApi")] + [UmbracoTreeAuthorize(Constants.Trees.Templates)] + public class TemplateController : BackOfficeNotificationsController + { + /// + /// Gets data type by alias + /// + /// + /// + public TemplateDisplay GetByAlias(string alias) + { + var template = Services.FileService.GetTemplate(alias); + return template == null ? null : Mapper.Map(template); + } + + /// + /// Get all templates + /// + /// + public IEnumerable GetAll() + { + return Services.FileService.GetTemplates().Select(Mapper.Map); + } + + /// + /// Gets the content json for the content id + /// + /// + /// + public TemplateDisplay GetById(int id) + { + var template = Services.FileService.GetTemplate(id); + if (template == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + return Mapper.Map(template); + } + + /// + /// Deletes a template wth a given ID + /// + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var template = Services.FileService.GetTemplate(id); + if (template == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + Services.FileService.DeleteTemplate(template.Alias); + return Request.CreateResponse(HttpStatusCode.OK); + } + + public TemplateDisplay GetScaffold(int id) + { + //empty default + var dt = new Template("", ""); + dt.Path = "-1"; + + if (id > 0) + { + var master = Services.FileService.GetTemplate(id); + if(master != null) + { + dt.SetMasterTemplate(master); + } + } + + var content = ViewHelper.GetDefaultFileContent( layoutPageAlias: dt.MasterTemplateAlias ); + var scaffold = Mapper.Map(dt); + + scaffold.Content = content + "\r\n\r\n@* the fun starts here *@\r\n\r\n"; + return scaffold; + } + + /// + /// Saves the data type + /// + /// + /// + public TemplateDisplay PostSave(TemplateDisplay display) + { + + //Checking the submitted is valid with the Required attributes decorated on the ViewModel + if (ModelState.IsValid == false) + { + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + if (display.Id > 0) + { + // update + var template = Services.FileService.GetTemplate(display.Id); + if (template == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + var changeMaster = template.MasterTemplateAlias != display.MasterTemplateAlias; + var changeAlias = template.Alias != display.Alias; + + Mapper.Map(display, template); + + if (changeMaster) + { + if (string.IsNullOrEmpty(display.MasterTemplateAlias) == false) + { + + var master = Services.FileService.GetTemplate(display.MasterTemplateAlias); + if(master == null || master.Id == display.Id) + { + template.SetMasterTemplate(null); + }else + { + template.SetMasterTemplate(master); + + //After updating the master - ensure we update the path property if it has any children already assigned + var templateHasChildren = Services.FileService.GetTemplateDescendants(display.Id); + + foreach (var childTemplate in templateHasChildren) + { + //template ID to find + var templateIdInPath = "," + display.Id + ","; + + if (string.IsNullOrEmpty(childTemplate.Path)) + { + continue; + } + + //Find position in current comma seperate string path (so we get the correct children path) + var positionInPath = childTemplate.Path.IndexOf(templateIdInPath) + templateIdInPath.Length; + + //Get the substring of the child & any children (descendants it may have too) + var childTemplatePath = childTemplate.Path.Substring(positionInPath); + + //As we are updating the template to be a child of a master + //Set the path to the master's path + its current template id + the current child path substring + childTemplate.Path = master.Path + "," + display.Id + "," + childTemplatePath; + + //Save the children with the updated path + Services.FileService.SaveTemplate(childTemplate); + } + } + } + else + { + //remove the master + template.SetMasterTemplate(null); + } + } + + Services.FileService.SaveTemplate(template); + + if (changeAlias) + { + template = Services.FileService.GetTemplate(template.Id); + } + + Mapper.Map(template, display); + } + else + { + //create + ITemplate master = null; + if (string.IsNullOrEmpty(display.MasterTemplateAlias) == false) + { + master = Services.FileService.GetTemplate(display.MasterTemplateAlias); + if (master == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var template = Services.FileService.CreateTemplateWithIdentity(display.Name, display.Content, master); + //template = Services.FileService.GetTemplate(template.Id); + Mapper.Map(template, display); + } + + return display; + } + } +} diff --git a/src/Umbraco.Web/Editors/TemplateQueryController.cs b/src/Umbraco.Web/Editors/TemplateQueryController.cs index 2eaf63f159..d7351b0e97 100644 --- a/src/Umbraco.Web/Editors/TemplateQueryController.cs +++ b/src/Umbraco.Web/Editors/TemplateQueryController.cs @@ -3,17 +3,15 @@ using System.Linq; using System.Text; using Umbraco.Core.Models; using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi.Filters; using Umbraco.Web.WebApi; using System; using System.Diagnostics; using Umbraco.Web.Dynamics; using Umbraco.Web.Models.TemplateQuery; +using Umbraco.Core.Services; namespace Umbraco.Web.Editors { - - /// /// The API controller used for building content queries within the template /// @@ -25,37 +23,47 @@ namespace Umbraco.Web.Editors { } public TemplateQueryController(UmbracoContext umbracoContext) - :base(umbracoContext) + : base(umbracoContext) { } - - private static readonly IEnumerable Terms = new List() + private IEnumerable Terms + { + get { - new OperathorTerm("is", Operathor.Equals, new [] {"string"}), - new OperathorTerm("is not", Operathor.NotEquals, new [] {"string"}), - new OperathorTerm("before", Operathor.LessThan, new [] {"datetime"}), - new OperathorTerm("before (including selected date)", Operathor.LessThanEqualTo, new [] {"datetime"}), - new OperathorTerm("after", Operathor.GreaterThan, new [] {"datetime"}), - new OperathorTerm("after (including selected date)", Operathor.GreaterThanEqualTo, new [] {"datetime"}), - new OperathorTerm("equals", Operathor.Equals, new [] {"int"}), - new OperathorTerm("does not equal", Operathor.NotEquals, new [] {"int"}), - new OperathorTerm("contains", Operathor.Contains, new [] {"string"}), - new OperathorTerm("does not contain", Operathor.NotContains, new [] {"string"}), - new OperathorTerm("greater than", Operathor.GreaterThan, new [] {"int"}), - new OperathorTerm("greater than or equal to", Operathor.GreaterThanEqualTo, new [] {"int"}), - new OperathorTerm("less than", Operathor.LessThan, new [] {"int"}), - new OperathorTerm("less than or equal to", Operathor.LessThanEqualTo, new [] {"int"}) - }; + return new List() + { + new OperathorTerm(Services.TextService.Localize("template/is"), Operathor.Equals, new [] {"string"}), + new OperathorTerm(Services.TextService.Localize("template/isNot"), Operathor.NotEquals, new [] {"string"}), + new OperathorTerm(Services.TextService.Localize("template/before"), Operathor.LessThan, new [] {"datetime"}), + new OperathorTerm(Services.TextService.Localize("template/beforeIncDate"), Operathor.LessThanEqualTo, new [] {"datetime"}), + new OperathorTerm(Services.TextService.Localize("template/after"), Operathor.GreaterThan, new [] {"datetime"}), + new OperathorTerm(Services.TextService.Localize("template/afterIncDate"), Operathor.GreaterThanEqualTo, new [] {"datetime"}), + new OperathorTerm(Services.TextService.Localize("template/equals"), Operathor.Equals, new [] {"int"}), + new OperathorTerm(Services.TextService.Localize("template/doesNotEqual"), Operathor.NotEquals, new [] {"int"}), + new OperathorTerm(Services.TextService.Localize("template/contains"), Operathor.Contains, new [] {"string"}), + new OperathorTerm(Services.TextService.Localize("template/doesNotContain"), Operathor.NotContains, new [] {"string"}), + new OperathorTerm(Services.TextService.Localize("template/greaterThan"), Operathor.GreaterThan, new [] {"int"}), + new OperathorTerm(Services.TextService.Localize("template/greaterThanEqual"), Operathor.GreaterThanEqualTo, new [] {"int"}), + new OperathorTerm(Services.TextService.Localize("template/lessThan"), Operathor.LessThan, new [] {"int"}), + new OperathorTerm(Services.TextService.Localize("template/lessThanEqual"), Operathor.LessThanEqualTo, new [] {"int"}) + }; + } + } - private static readonly IEnumerable Properties = new List() + private IEnumerable Properties + { + get { - new PropertyModel() { Name = "Id", Alias = "Id", Type = "int" }, - new PropertyModel() { Name = "Name", Alias = "Name", Type = "string" }, - //new PropertyModel() { Name = "Url", Alias = "url", Type = "string" }, - new PropertyModel() { Name = "Created Date", Alias = "CreateDate", Type = "datetime" }, - new PropertyModel() { Name = "Last Updated Date", Alias = "UpdateDate", Type = "datetime" } - - }; + return new List() + { + new PropertyModel() {Name = Services.TextService.Localize("template/id"), Alias = "Id", Type = "int"}, + new PropertyModel() {Name = Services.TextService.Localize("template/name"), Alias = "Name", Type = "string"}, + //new PropertyModel() { Name = "Url", Alias = "url", Type = "string" }, + new PropertyModel() {Name = Services.TextService.Localize("template/createdDate"), Alias = "CreateDate", Type = "datetime"}, + new PropertyModel() {Name = Services.TextService.Localize("template/lastUpdatedDate"), Alias = "UpdateDate", Type = "datetime"} + }; + } + } public QueryResultModel PostTemplateQuery(QueryModel model) { @@ -64,21 +72,20 @@ namespace Umbraco.Web.Editors var queryResult = new QueryResultModel(); var sb = new StringBuilder(); - - sb.Append("CurrentPage.Site()"); - + var indention = Environment.NewLine + "\t\t\t\t\t\t"; + + sb.Append("Model.Content.Site()"); var timer = new Stopwatch(); - + timer.Start(); var currentPage = umbraco.TypedContentAtRoot().FirstOrDefault(); timer.Stop(); - var pointerNode = currentPage; // adjust the "FROM" - if (model != null && model.Source.Id > 0) + if (model != null && model.Source != null && model.Source.Id > 0) { var targetNode = umbraco.TypedContent(model.Source.Id); @@ -103,15 +110,15 @@ namespace Umbraco.Web.Editors { // we did not find the path sb.Clear(); - sb.AppendFormat("Umbraco.Content({0})", model.Source.Id); + sb.AppendFormat("Umbraco.TypedContent({0})", model.Source.Id); pointerNode = targetNode; } } } - - // TYPE to return if filtered by type + + // TYPE to return if filtered by type IEnumerable contents; - if (model != null && string.IsNullOrEmpty(model.ContentType.Alias) == false) + if (model != null && model.ContentType != null && string.IsNullOrEmpty(model.ContentType.Alias) == false) { timer.Start(); @@ -126,10 +133,12 @@ namespace Umbraco.Web.Editors timer.Start(); contents = pointerNode.Children; timer.Stop(); - sb.Append(".Children"); + sb.Append(".Children()"); } + //setup 2 clauses, 1 for returning, 1 for testing var clause = string.Empty; + var tokenizedClause = string.Empty; // WHERE var token = 0; @@ -140,35 +149,36 @@ namespace Umbraco.Web.Editors foreach (var condition in model.Filters) { - if(string.IsNullOrEmpty( condition.ConstraintValue)) continue; + if (string.IsNullOrEmpty(condition.ConstraintValue)) continue; - + //x is passed in as the parameter alias for the linq where statement clause + var operation = condition.BuildCondition("x"); + var tokenizedOperation = condition.BuildTokenizedCondition(token); - var operation = condition.BuildCondition(token); - - clause = string.IsNullOrEmpty(clause) ? operation : string.Concat(new[] { clause, " && ", operation }); + clause = string.IsNullOrEmpty(clause) ? operation : string.Concat(new[] { clause, " && ", operation }); + tokenizedClause = string.IsNullOrEmpty(tokenizedClause) ? tokenizedOperation : string.Concat(new[] { tokenizedClause, " && ", tokenizedOperation }); token++; } if (string.IsNullOrEmpty(clause) == false) { - timer.Start(); - //clause = "Visible && " + clause; - - contents = contents.AsQueryable().Where(clause, model.Filters.Select(this.GetConstraintValue).ToArray()); - // contents = contents.Where(clause, values.ToArray()); + //trial-run the tokenized clause to time the execution + //for review - this uses a tonized query rather then the normal linq query. + contents = contents.AsQueryable().Where(tokenizedClause, model.Filters.Select(this.GetConstraintValue).ToArray()); contents = contents.Where(x => x.IsVisible()); timer.Stop(); - clause = string.Format("\"Visible && {0}\",{1}", clause, - string.Join(",", model.Filters.Select(x => x.Property.Type == "string" ? - string.Format("\"{0}\"", x.ConstraintValue) : x.ConstraintValue).ToArray())); - sb.AppendFormat(".Where({0})", clause); + //the query to output to the editor + sb.Append(indention); + sb.Append(".Where(x => x.IsVisible())"); + + sb.Append(indention); + sb.AppendFormat(".Where(x => {0})", clause); } else { @@ -178,8 +188,8 @@ namespace Umbraco.Web.Editors timer.Stop(); - sb.Append(".Where(\"Visible\")"); - + sb.Append(indention); + sb.Append(".Where(x => x.IsVisible())"); } if (model.Sort != null && string.IsNullOrEmpty(model.Sort.Property.Alias) == false) @@ -192,6 +202,7 @@ namespace Umbraco.Web.Editors var direction = model.Sort.Direction == "ascending" ? string.Empty : " desc"; + sb.Append(indention); sb.AppendFormat(".OrderBy(\"{0}{1}\")", model.Sort.Property.Alias, direction); } @@ -203,6 +214,7 @@ namespace Umbraco.Web.Editors timer.Stop(); + sb.Append(indention); sb.AppendFormat(".Take({0})", model.Take); } } @@ -211,11 +223,10 @@ namespace Umbraco.Web.Editors queryResult.ExecutionTime = timer.ElapsedMilliseconds; queryResult.ResultCount = contents.Count(); queryResult.SampleResults = contents.Take(20).Select(x => new TemplateQueryResult() - { - Icon = "icon-file", - Name = x.Name - }); - + { + Icon = "icon-file", + Name = x.Name + }); return queryResult; } @@ -224,7 +235,7 @@ namespace Umbraco.Web.Editors { switch (condition.Property.Type) { - case "int" : + case "int": return int.Parse(condition.ConstraintValue); case "datetime": DateTime dt; @@ -234,42 +245,41 @@ namespace Umbraco.Web.Editors } } - private IEnumerable SortByDefaultPropertyValue(IEnumerable contents, SortExpression sortExpression) + private IEnumerable SortByDefaultPropertyValue(IEnumerable contents, SortExpression sortExpression) { switch (sortExpression.Property.Alias) { - case "id" : + case "id": return sortExpression.Direction == "ascending" - ? contents.OrderBy(x => x.Id) - : contents.OrderByDescending(x => x.Id); - case "createDate" : - + ? contents.OrderBy(x => x.Id) + : contents.OrderByDescending(x => x.Id); + case "createDate": + return sortExpression.Direction == "ascending" - ? contents.OrderBy(x => x.CreateDate) - : contents.OrderByDescending(x => x.CreateDate); + ? contents.OrderBy(x => x.CreateDate) + : contents.OrderByDescending(x => x.CreateDate); case "publishDate": - + return sortExpression.Direction == "ascending" - ? contents.OrderBy(x => x.UpdateDate) - : contents.OrderByDescending(x => x.UpdateDate); + ? contents.OrderBy(x => x.UpdateDate) + : contents.OrderByDescending(x => x.UpdateDate); case "name": return sortExpression.Direction == "ascending" - ? contents.OrderBy(x => x.Name) - : contents.OrderByDescending(x => x.Name); - default : + ? contents.OrderBy(x => x.Name) + : contents.OrderByDescending(x => x.Name); + default: return sortExpression.Direction == "ascending" - ? contents.OrderBy(x => x.Name) - : contents.OrderByDescending(x => x.Name); - + ? contents.OrderBy(x => x.Name) + : contents.OrderByDescending(x => x.Name); } } - + private IEnumerable GetChildContentTypeAliases(IPublishedContent targetNode, IPublishedContent current) { var aliases = new List(); - - if (targetNode.Id == current.Id) return aliases; + + if (targetNode == null || targetNode.Id == current.Id) return aliases; if (targetNode.Id != current.Id) { aliases.Add(targetNode.DocumentTypeAlias); @@ -289,9 +299,10 @@ namespace Umbraco.Web.Editors { var contentTypes = ApplicationContext.Services.ContentTypeService.GetAllContentTypes() - .Select(x => new ContentTypeModel() { Alias = x.Alias, Name = x.Name }) + .Select(x => new ContentTypeModel() { Alias = x.Alias, Name = Services.TextService.Localize("template/contentOfType", tokens: new string[] { x.Name }) }) .OrderBy(x => x.Name).ToList(); - contentTypes.Insert(0, new ContentTypeModel() { Alias = string.Empty, Name = "Everything" }); + + contentTypes.Insert(0, new ContentTypeModel() { Alias = string.Empty, Name = Services.TextService.Localize("template/allContent") }); return contentTypes; } @@ -311,7 +322,5 @@ namespace Umbraco.Web.Editors { return Terms; } - - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Extensions/UdiExtensions.cs b/src/Umbraco.Web/Extensions/UdiExtensions.cs new file mode 100644 index 0000000000..c814d63b8c --- /dev/null +++ b/src/Umbraco.Web/Extensions/UdiExtensions.cs @@ -0,0 +1,43 @@ +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Extensions +{ + public static class UdiExtensions + { + /// + /// An extension method to easily acquire a typed version of content, media or member item for a given Udi + /// + /// + /// An item if the item is a Document, Media or Member + public static IPublishedContent ToPublishedContent(this Udi udi) + { + Udi identifier; + if (Udi.TryParse(udi.ToString(), out identifier) == false) + return null; + + var guidUdi = GuidUdi.Parse(udi.ToString()); + var umbracoType = Constants.UdiEntityType.ToUmbracoObjectType(identifier.EntityType); + + var umbracoHelper = new UmbracoHelper(UmbracoContext.Current); + var entityService = ApplicationContext.Current.Services.EntityService; + switch (umbracoType) + { + case UmbracoObjectTypes.Document: + return umbracoHelper.TypedContent(guidUdi.Guid); + case UmbracoObjectTypes.Media: + var mediaAttempt = entityService.GetIdForKey(guidUdi.Guid, umbracoType); + if (mediaAttempt.Success) + return umbracoHelper.TypedMedia(mediaAttempt.Result); + break; + case UmbracoObjectTypes.Member: + var memberAttempt = entityService.GetIdForKey(guidUdi.Guid, umbracoType); + if (memberAttempt.Success) + return umbracoHelper.TypedMember(memberAttempt.Result); + break; + } + + return null; + } + } +} diff --git a/src/Umbraco.Web/HealthCheck/HealthCheck.cs b/src/Umbraco.Web/HealthCheck/HealthCheck.cs index ec1358947a..34158a66eb 100644 --- a/src/Umbraco.Web/HealthCheck/HealthCheck.cs +++ b/src/Umbraco.Web/HealthCheck/HealthCheck.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.Serialization; +using umbraco.interfaces; using Umbraco.Core; namespace Umbraco.Web.HealthCheck @@ -9,7 +10,7 @@ namespace Umbraco.Web.HealthCheck /// The abstract health check class /// [DataContract(Name = "healtCheck", Namespace = "")] - public abstract class HealthCheck + public abstract class HealthCheck : IDiscoverable { protected HealthCheck(HealthCheckContext healthCheckContext) { diff --git a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs index 71fc3be8f1..1de50f5b71 100644 --- a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs @@ -47,7 +47,8 @@ namespace Umbraco.Web ""externalLoginsUrl"": """ + externalLoginsUrl + @""" }, ""umbracoSettings"": { - ""allowPasswordReset"": " + (UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset ? "true" : "false") + @" + ""allowPasswordReset"": " + (UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset ? "true" : "false") + @", + ""loginBackgroundImage"": """ + UmbracoConfig.For.UmbracoSettings().Content.LoginBackgroundImage + @""" }, ""application"": { ""applicationPath"": """ + html.ViewContext.HttpContext.Request.ApplicationPath + @""", diff --git a/src/Umbraco.Web/HttpUrlHelperExtensions.cs b/src/Umbraco.Web/HttpUrlHelperExtensions.cs index 1b04a7dd8f..d3e87a6bf7 100644 --- a/src/Umbraco.Web/HttpUrlHelperExtensions.cs +++ b/src/Umbraco.Web/HttpUrlHelperExtensions.cs @@ -122,5 +122,29 @@ namespace Umbraco.Web } } } + + /// + /// Return the Base Url (not including the action) for a Web Api service + /// + /// + /// + /// + /// + public static string GetUmbracoApiServiceBaseUrl(this UrlHelper url, string actionName) + where T : UmbracoApiController + { + return url.GetUmbracoApiService(actionName).TrimEnd(actionName); + } + + public static string GetUmbracoApiServiceBaseUrl(this UrlHelper url, Expression> methodSelector) + where T : UmbracoApiController + { + var method = Core.ExpressionHelper.GetMethodInfo(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); + } + return url.GetUmbracoApiService(method.Name).TrimEnd(method.Name); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/IHttpContextAccessor.cs b/src/Umbraco.Web/IHttpContextAccessor.cs index 068783725a..4b5a8ed884 100644 --- a/src/Umbraco.Web/IHttpContextAccessor.cs +++ b/src/Umbraco.Web/IHttpContextAccessor.cs @@ -1,5 +1,3 @@ -using System.Web; - namespace Umbraco.Web { /// @@ -8,8 +6,6 @@ namespace Umbraco.Web /// /// NOTE: This has a singleton lifespan /// - public interface IHttpContextAccessor - { - HttpContextBase Value { get; } - } + public interface IHttpContextAccessor : Core.IHttpContextAccessor + { } } \ No newline at end of file diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index 63157e4fee..ddf8ee0727 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -87,14 +87,19 @@ namespace Umbraco.Web /// Add a serialised date of the last edit of the item to ensure client cache refresh when updated /// /// - /// The further options. + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// /// /// /// Use a dimension as a ratio - /// + /// /// /// If the image should be upscaled to requested dimensions - /// + /// /// /// The . /// @@ -190,14 +195,19 @@ namespace Umbraco.Web /// Add a serialised date of the last edit of the item to ensure client cache refresh when updated /// /// - /// The further options. + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// /// /// /// Use a dimension as a ratio /// /// /// If the image should be upscaled to requested dimensions - /// + /// /// /// The . /// @@ -229,6 +239,57 @@ namespace Umbraco.Web imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); } + /// + /// Gets the ImageProcessor Url from the image path. + /// + /// + /// The image url. + /// + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters + /// + /// + /// Add a serialised date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// The . + /// public static string GetCropUrl( this string imageUrl, ImageCropDataSet cropDataSet, diff --git a/src/Umbraco.Web/Install/Controllers/InstallPackageController.cs b/src/Umbraco.Web/Install/Controllers/InstallPackageController.cs index fc4a8962b8..e73302dc02 100644 --- a/src/Umbraco.Web/Install/Controllers/InstallPackageController.cs +++ b/src/Umbraco.Web/Install/Controllers/InstallPackageController.cs @@ -8,6 +8,7 @@ using System.Web.Http; using Newtonsoft.Json.Linq; using umbraco; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Web.Install.Models; using Umbraco.Web.WebApi; @@ -57,23 +58,15 @@ namespace Umbraco.Web.Install.Controllers /// [HttpPost] public HttpResponseMessage DownloadPackageFiles(InstallPackageModel model) - { - var repo = global::umbraco.cms.businesslogic.packager.repositories.Repository.getByGuid(RepoGuid); - if (repo == null) - { - return Json( - new {success = false, error = "No repository found with id " + RepoGuid}, - HttpStatusCode.OK); - } - if (repo.HasConnection() == false) - { - return Json( - new { success = false, error = "cannot_connect" }, - HttpStatusCode.OK); - } - var installer = new global::umbraco.cms.businesslogic.packager.Installer(UmbracoContext.Current.Security.CurrentUser.Id); + { + var packageFile = _applicationContext.Services.PackagingService.FetchPackageFile( + model.KitGuid, + UmbracoVersion.Current, + UmbracoContext.Current.Security.CurrentUser.Id); - var tempFile = installer.Import(repo.fetch(model.KitGuid.ToString(), UmbracoContext.Current.Security.CurrentUser.Id)); + var installer = new global::umbraco.cms.businesslogic.packager.Installer(UmbracoContext.Current.Security.CurrentUser.Id); + + var tempFile = installer.Import(packageFile); installer.LoadConfig(tempFile); var pId = installer.CreateManifest(tempFile, model.KitGuid.ToString(), RepoGuid); return Json(new diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index d39b16f6ac..8bc21a2334 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.Install new DatabaseConfigureStep(_umbContext.Application), new DatabaseInstallStep(_umbContext.Application), new DatabaseUpgradeStep(_umbContext.Application), - new StarterKitDownloadStep(_umbContext.Application), + new StarterKitDownloadStep(_umbContext.Application, _umbContext.Security), new StarterKitInstallStep(_umbContext.Application, _umbContext.HttpContext), new StarterKitCleanupStep(_umbContext.Application), new SetUmbracoVersionStep(_umbContext.Application, _umbContext.HttpContext), @@ -146,7 +146,7 @@ namespace Umbraco.Web.Install { get { - var databaseSettings = ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName]; + var databaseSettings = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName]; if (GlobalSettings.ConfigurationStatus.IsNullOrWhiteSpace() && _umbContext.Application.DatabaseContext.IsConnectionStringConfigured(databaseSettings) == false) { diff --git a/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs index 4854919efb..ce28a26176 100644 --- a/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs @@ -83,7 +83,7 @@ namespace Umbraco.Web.Install.InstallSteps private bool ShouldDisplayView() { //If the connection string is already present in web.config we don't need to show the settings page and we jump to installing/upgrading. - var databaseSettings = ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName]; + var databaseSettings = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName]; if (_applicationContext.DatabaseContext.IsConnectionStringConfigured(databaseSettings)) { diff --git a/src/Umbraco.Web/Install/InstallSteps/DatabaseInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/DatabaseInstallStep.cs index 57c8e67465..b75a310546 100644 --- a/src/Umbraco.Web/Install/InstallSteps/DatabaseInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/DatabaseInstallStep.cs @@ -48,13 +48,13 @@ namespace Umbraco.Web.Install.InstallSteps internal static void HandleConnectionStrings() { // Remove legacy umbracoDbDsn configuration setting if it exists and connectionstring also exists - if (ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName] != null) + if (ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName] != null) { - GlobalSettings.RemoveSetting(GlobalSettings.UmbracoConnectionName); + GlobalSettings.RemoveSetting(Constants.System.UmbracoConnectionName); } else { - var ex = new ArgumentNullException(string.Format("ConfigurationManager.ConnectionStrings[{0}]", GlobalSettings.UmbracoConnectionName), "Install / upgrade did not complete successfully, umbracoDbDSN was not set in the connectionStrings section"); + var ex = new ArgumentNullException(string.Format("ConfigurationManager.ConnectionStrings[{0}]", Constants.System.UmbracoConnectionName), "Install / upgrade did not complete successfully, umbracoDbDSN was not set in the connectionStrings section"); LogHelper.Error("", ex); throw ex; } diff --git a/src/Umbraco.Web/Install/InstallSteps/DatabaseUpgradeStep.cs b/src/Umbraco.Web/Install/InstallSteps/DatabaseUpgradeStep.cs index 678ae427e5..c3ae313898 100644 --- a/src/Umbraco.Web/Install/InstallSteps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/DatabaseUpgradeStep.cs @@ -58,7 +58,7 @@ namespace Umbraco.Web.Install.InstallSteps return false; } - var databaseSettings = ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName]; + var databaseSettings = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName]; if (_applicationContext.DatabaseContext.IsConnectionStringConfigured(databaseSettings)) { diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index 85dd58d697..7a6a54f9e7 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -118,7 +118,7 @@ namespace Umbraco.Web.Install.InstallSteps public override bool RequiresExecution(UserModel model) { //now we have to check if this is really a new install, the db might be configured and might contain data - var databaseSettings = ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName]; + var databaseSettings = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName]; //if there's already a version then there should def be a user but in some cases someone may have // left a version number in there but cleared out their db conn string, in that case, it's really a new install. diff --git a/src/Umbraco.Web/Install/InstallSteps/StarterKitDownloadStep.cs b/src/Umbraco.Web/Install/InstallSteps/StarterKitDownloadStep.cs index e8397136a9..a8f55d3b2c 100644 --- a/src/Umbraco.Web/Install/InstallSteps/StarterKitDownloadStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/StarterKitDownloadStep.cs @@ -4,7 +4,9 @@ using System.Linq; using System.Web; using umbraco.cms.businesslogic.packager; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Web.Install.Models; +using Umbraco.Web.Security; namespace Umbraco.Web.Install.InstallSteps { @@ -13,10 +15,12 @@ namespace Umbraco.Web.Install.InstallSteps internal class StarterKitDownloadStep : InstallSetupStep { private readonly ApplicationContext _applicationContext; + private readonly WebSecurity _security; - public StarterKitDownloadStep(ApplicationContext applicationContext) + public StarterKitDownloadStep(ApplicationContext applicationContext, WebSecurity security) { _applicationContext = applicationContext; + _security = security; } private const string RepoGuid = "65194810-1f85-11dd-bd0b-0800200c9a66"; @@ -50,19 +54,13 @@ namespace Umbraco.Web.Install.InstallSteps } private Tuple DownloadPackageFiles(Guid kitGuid) - { - var repo = global::umbraco.cms.businesslogic.packager.repositories.Repository.getByGuid(RepoGuid); - if (repo == null) - { - throw new InstallException("No repository found with id " + RepoGuid); - } - if (repo.HasConnection() == false) - { - throw new InstallException("Cannot connect to repository"); - } + { var installer = new Installer(); - var tempFile = installer.Import(repo.fetch(kitGuid.ToString())); + //Go get the package file from the package repo + var packageFile = _applicationContext.Services.PackagingService.FetchPackageFile(kitGuid, UmbracoVersion.Current, _security.GetUserId()); + + var tempFile = installer.Import(packageFile); installer.LoadConfig(tempFile); var pId = installer.CreateManifest(tempFile, kitGuid.ToString(), RepoGuid); diff --git a/src/Umbraco.Web/Media/ImageUrl.cs b/src/Umbraco.Web/Media/ImageUrl.cs index dff9358a38..2af7fa8b9b 100644 --- a/src/Umbraco.Web/Media/ImageUrl.cs +++ b/src/Umbraco.Web/Media/ImageUrl.cs @@ -11,6 +11,7 @@ using umbraco; namespace Umbraco.Web.Media { + [Obsolete("This is no longer used and will be removed in future versions")] public class ImageUrl { [Obsolete("Use TryGetImageUrl() instead")] diff --git a/src/Umbraco.Web/Media/ImageUrlProviderResolver.cs b/src/Umbraco.Web/Media/ImageUrlProviderResolver.cs index c52d937d8f..dfed61e69d 100644 --- a/src/Umbraco.Web/Media/ImageUrlProviderResolver.cs +++ b/src/Umbraco.Web/Media/ImageUrlProviderResolver.cs @@ -8,9 +8,10 @@ using Umbraco.Web.Media.ImageUrlProviders; namespace Umbraco.Web.Media { - internal sealed class ImageUrlProviderResolver : ManyObjectsResolverBase + [Obsolete("IImageUrlProvider is no longer used and will be removed in future versions")] + internal sealed class ImageUrlProviderResolver : LazyManyObjectsResolverBase { - internal ImageUrlProviderResolver(IServiceProvider serviceProvider, ILogger logger, IEnumerable value) + internal ImageUrlProviderResolver(IServiceProvider serviceProvider, ILogger logger, Func> value) : base(serviceProvider, logger, value) { } diff --git a/src/Umbraco.Web/Media/ImageUrlProviders/ImageUrlProvider.cs b/src/Umbraco.Web/Media/ImageUrlProviders/ImageUrlProvider.cs index 9777e39fe6..a8d541cebb 100644 --- a/src/Umbraco.Web/Media/ImageUrlProviders/ImageUrlProvider.cs +++ b/src/Umbraco.Web/Media/ImageUrlProviders/ImageUrlProvider.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Xml.XPath; using Umbraco.Core.Configuration; using Umbraco.Core.Media; @@ -7,6 +8,7 @@ using Umbraco.Core; namespace Umbraco.Web.Media.ImageUrlProviders { + [Obsolete("IImageUrlProvider is no longer used and will be removed in future versions")] public class ImageUrlProvider : IImageUrlProvider { public const string DefaultName = "umbracoUpload"; diff --git a/src/Umbraco.Web/Media/ThumbnailProviders/ThumbnailProvidersResolver.cs b/src/Umbraco.Web/Media/ThumbnailProviders/ThumbnailProvidersResolver.cs index 2513e8c1f4..f172dc3b7e 100644 --- a/src/Umbraco.Web/Media/ThumbnailProviders/ThumbnailProvidersResolver.cs +++ b/src/Umbraco.Web/Media/ThumbnailProviders/ThumbnailProvidersResolver.cs @@ -12,7 +12,8 @@ using umbraco.BusinessLogic.Utils; namespace Umbraco.Web.Media.ThumbnailProviders { - public sealed class ThumbnailProvidersResolver : ManyObjectsResolverBase + [Obsolete("Thumbnails are generated by ImageProcessor, use that instead")] + public sealed class ThumbnailProvidersResolver : LazyManyObjectsResolverBase { /// /// Constructor @@ -20,7 +21,7 @@ namespace Umbraco.Web.Media.ThumbnailProviders /// /// /// - internal ThumbnailProvidersResolver(IServiceProvider serviceProvider, ILogger logger, IEnumerable providers) + internal ThumbnailProvidersResolver(IServiceProvider serviceProvider, ILogger logger, Func> providers) : base(serviceProvider, logger, providers) { diff --git a/src/Umbraco.Web/MediaPropertyExtensions.cs b/src/Umbraco.Web/MediaPropertyExtensions.cs deleted file mode 100644 index cc6d018174..0000000000 --- a/src/Umbraco.Web/MediaPropertyExtensions.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.IO; -using Umbraco.Core.Models; - -namespace Umbraco.Web -{ - internal static class MediaPropertyExtensions - { - - internal static void AutoPopulateFileMetaDataProperties(this IContentBase model, string propertyAlias, string relativefilePath = null) - { - var mediaFileSystem = FileSystemProviderManager.Current.GetFileSystemProvider(); - var uploadFieldConfigNode = - UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties - .FirstOrDefault(x => x.Alias == propertyAlias); - - if (uploadFieldConfigNode != null && model.Properties.Contains(propertyAlias)) - { - if (relativefilePath == null) - relativefilePath = model.GetValue(propertyAlias); - - //now we need to check if there is a path - if (!string.IsNullOrEmpty(relativefilePath)) - { - var fullPath = mediaFileSystem.GetFullPath(mediaFileSystem.GetRelativePath(relativefilePath)); - var umbracoFile = new UmbracoMediaFile(fullPath); - FillProperties(uploadFieldConfigNode, model, umbracoFile); - } - else - { - //for now I'm just resetting this - ResetProperties(uploadFieldConfigNode, model); - } - } - } - - internal static void ResetFileMetaDataProperties(this IContentBase content, IImagingAutoFillUploadField uploadFieldConfigNode) - { - if (uploadFieldConfigNode == null) throw new ArgumentNullException("uploadFieldConfigNode"); - ResetProperties(uploadFieldConfigNode, content); - } - - private static void ResetProperties(IImagingAutoFillUploadField uploadFieldConfigNode, IContentBase content) - { - if (content.Properties.Contains(uploadFieldConfigNode.WidthFieldAlias)) - content.Properties[uploadFieldConfigNode.WidthFieldAlias].Value = string.Empty; - - if (content.Properties.Contains(uploadFieldConfigNode.HeightFieldAlias)) - content.Properties[uploadFieldConfigNode.HeightFieldAlias].Value = string.Empty; - - if (content.Properties.Contains(uploadFieldConfigNode.LengthFieldAlias)) - content.Properties[uploadFieldConfigNode.LengthFieldAlias].Value = string.Empty; - - if (content.Properties.Contains(uploadFieldConfigNode.ExtensionFieldAlias)) - content.Properties[uploadFieldConfigNode.ExtensionFieldAlias].Value = string.Empty; - } - - - internal static void PopulateFileMetaDataProperties(this IContentBase content, IImagingAutoFillUploadField uploadFieldConfigNode, string relativeFilePath) - { - if (uploadFieldConfigNode == null) throw new ArgumentNullException("uploadFieldConfigNode"); - if (relativeFilePath.IsNullOrWhiteSpace() == false) - { - var mediaFileSystem = FileSystemProviderManager.Current.GetFileSystemProvider(); - var fullPath = mediaFileSystem.GetFullPath(mediaFileSystem.GetRelativePath(relativeFilePath)); - var umbracoFile = new UmbracoMediaFile(fullPath); - FillProperties(uploadFieldConfigNode, content, umbracoFile); - } - else - { - //for now I'm just resetting this since we cant detect a file - ResetProperties(uploadFieldConfigNode, content); - } - } - - private static void FillProperties(IImagingAutoFillUploadField uploadFieldConfigNode, IContentBase content, UmbracoMediaFile um) - { - if (uploadFieldConfigNode == null) throw new ArgumentNullException("uploadFieldConfigNode"); - if (content == null) throw new ArgumentNullException("content"); - if (um == null) throw new ArgumentNullException("um"); - var size = um.SupportsResizing ? (Size?)um.GetDimensions() : null; - - if (content.Properties.Contains(uploadFieldConfigNode.WidthFieldAlias)) - content.Properties[uploadFieldConfigNode.WidthFieldAlias].Value = size.HasValue ? size.Value.Width.ToInvariantString() : string.Empty; - - if (content.Properties.Contains(uploadFieldConfigNode.HeightFieldAlias)) - content.Properties[uploadFieldConfigNode.HeightFieldAlias].Value = size.HasValue ? size.Value.Height.ToInvariantString() : string.Empty; - - if (content.Properties.Contains(uploadFieldConfigNode.LengthFieldAlias)) - content.Properties[uploadFieldConfigNode.LengthFieldAlias].Value = um.Length; - - if (content.Properties.Contains(uploadFieldConfigNode.ExtensionFieldAlias)) - content.Properties[uploadFieldConfigNode.ExtensionFieldAlias].Value = um.Extension; - } - - - } -} diff --git a/src/Umbraco.Web/MembershipProviderExtensions.cs b/src/Umbraco.Web/MembershipProviderExtensions.cs index b0fc939480..2a32384732 100644 --- a/src/Umbraco.Web/MembershipProviderExtensions.cs +++ b/src/Umbraco.Web/MembershipProviderExtensions.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Web; using System.Web.Security; +using Umbraco.Core.Models; using Umbraco.Core.Security; +using Umbraco.Core.Services; namespace Umbraco.Web { @@ -14,16 +13,19 @@ namespace Umbraco.Web /// Returns the configuration of the membership provider used to configure change password editors /// /// + /// /// public static IDictionary GetConfiguration( - this MembershipProvider membershipProvider) + this MembershipProvider membershipProvider, IUserService userService) { var baseProvider = membershipProvider as MembershipProviderBase; - + + var canReset = membershipProvider.CanResetPassword(userService); + return new Dictionary { {"minPasswordLength", membershipProvider.MinRequiredPasswordLength}, - {"enableReset", membershipProvider.EnablePasswordReset}, + {"enableReset", canReset}, {"enablePasswordRetrieval", membershipProvider.EnablePasswordRetrieval}, {"requiresQuestionAnswer", membershipProvider.RequiresQuestionAndAnswer}, {"allowManuallyChangingPassword", baseProvider != null && baseProvider.AllowManuallyChangingPassword} diff --git a/src/Umbraco.Web/Models/ContentEditing/CodeFileDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/CodeFileDisplay.cs new file mode 100644 index 0000000000..34ad507836 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/CodeFileDisplay.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "scriptFile", Namespace = "")] + public class CodeFileDisplay : INotificationModel, IValidatableObject + { + public CodeFileDisplay() + { + Notifications = new List(); + } + + /// + /// VirtualPath is the path to the file on disk + /// /views/partials/file.cshtml + /// + [DataMember(Name = "virtualPath", IsRequired = true)] + public string VirtualPath { get; set; } + + /// + /// Path represents the path used by the backoffice tree + /// For files stored on disk, this is a urlencoded, comma seperated + /// path to the file, always starting with -1. + /// + /// -1,Partials,Parials%2FFolder,Partials%2FFolder%2FFile.cshtml + /// + [DataMember(Name = "path")] + [ReadOnly(true)] + public string Path { get; set; } + + [DataMember(Name = "name", IsRequired = true)] + public string Name { get; set; } + + [DataMember(Name = "content", IsRequired = true)] + public string Content { get; set; } + + [DataMember(Name = "fileType", IsRequired = true)] + public string FileType { get; set; } + + [DataMember(Name = "snippet")] + [ReadOnly(true)] + public string Snippet { get; set; } + + [DataMember(Name = "id")] + [ReadOnly(true)] + public string Id { get; set; } + + public List Notifications { get; private set; } + + /// + /// Some custom validation is required for valid file names + /// + /// + /// + public IEnumerable Validate(ValidationContext validationContext) + { + var illegalChars = System.IO.Path.GetInvalidFileNameChars(); + if (Name.ContainsAny(illegalChars)) + { + yield return new ValidationResult( + "The file name cannot contain illegal characters", + new[] { "Name" }); + } + else if (System.IO.Path.GetFileNameWithoutExtension(Name).IsNullOrWhiteSpace()) + { + yield return new ValidationResult( + "The file name cannot be empty", + new[] { "Name" }); + } + } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs index 87eff89b58..74c6a3ea7e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs @@ -84,17 +84,21 @@ namespace Umbraco.Web.Models.ContentEditing } /// - /// The real persisted content object + /// The real persisted content object - used during inbound model binding /// + /// + /// This is not used for outgoing model information. + /// [JsonIgnore] internal TPersisted PersistedContent { get; set; } /// - /// The DTO object used to gather all required content data including data type information etc... for use with validation + /// The DTO object used to gather all required content data including data type information etc... for use with validation - used during inbound model binding /// /// /// We basically use this object to hydrate all required data from the database into one object so we can validate everything we need /// instead of having to look up all the data individually. + /// This is not used for outgoing model information. /// [JsonIgnore] internal ContentItemDto ContentDto { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs index 40d884d653..9f6e5b28da 100644 --- a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs @@ -6,7 +6,10 @@ using System.Linq; using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json; +using Umbraco.Core; using Umbraco.Core.Models.Validation; +using Umbraco.Core.Serialization; namespace Umbraco.Web.Models.ContentEditing { @@ -25,7 +28,12 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "id", IsRequired = true)] [Required] public object Id { get; set; } - + + [DataMember(Name = "udi")] + [ReadOnly(true)] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi Udi { get; set; } + [DataMember(Name = "icon")] public string Icon { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs index d9bc169c8f..61d61f2108 100644 --- a/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs +++ b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs @@ -3,7 +3,18 @@ namespace Umbraco.Web.Models.ContentEditing public class GetAvailableCompositionsFilter { public int ContentTypeId { get; set; } + + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// public string[] FilterPropertyTypes { get; set; } + + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// public string[] FilterContentTypes { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/PreValueFieldDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/PreValueFieldDisplay.cs index 1e8a7ba088..6879701bde 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PreValueFieldDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PreValueFieldDisplay.cs @@ -1,4 +1,5 @@ -using System.Runtime.Serialization; +using System.Collections.Generic; +using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing { @@ -33,5 +34,11 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "view", IsRequired = true)] public string View { get; set; } + /// + /// This allows for custom configuration to be injected into the pre-value editor + /// + [DataMember(Name = "config")] + public IDictionary Config { get; set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/SnippetDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/SnippetDisplay.cs new file mode 100644 index 0000000000..e05f8c5c89 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/SnippetDisplay.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "scriptFile", Namespace = "")] + public class SnippetDisplay + { + [DataMember(Name = "name", IsRequired = true)] + public string Name { get; set; } + + [DataMember(Name = "fileName", IsRequired = true)] + public string FileName { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs new file mode 100644 index 0000000000..91c1aefdb0 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models.Validation; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "template", Namespace = "")] + public class TemplateDisplay : INotificationModel + { + + [DataMember(Name = "id")] + public int Id { get; set; } + + [Required] + [DataMember(Name = "name")] + public string Name { get; set; } + + [Required] + [DataMember(Name = "alias")] + public string Alias { get; set; } + + [DataMember(Name = "content")] + public string Content { get; set; } + + [DataMember(Name = "path")] + public string Path { get; set; } + + [DataMember(Name = "virtualPath")] + public string VirtualPath { get; set; } + + [DataMember(Name = "masterTemplateAlias")] + public string MasterTemplateAlias { get; set; } + + [DataMember(Name = "isMasterTemplate")] + public bool IsMasterTemplate { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/UmbracoEntityTypes.cs b/src/Umbraco.Web/Models/ContentEditing/UmbracoEntityTypes.cs index 824dcb2b91..c68ee3c655 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UmbracoEntityTypes.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UmbracoEntityTypes.cs @@ -1,4 +1,7 @@ -namespace Umbraco.Web.Models.ContentEditing +using System; +using System.ComponentModel; + +namespace Umbraco.Web.Models.ContentEditing { /// /// Represents the type's of Umbraco entities that can be resolved from the EntityController @@ -53,6 +56,8 @@ /// /// Content Item /// + [Obsolete("This is not used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] ContentItem, /// diff --git a/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs b/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs index 624c641d3b..2b9047c284 100644 --- a/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using AutoMapper; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Web.Models.ContentEditing; @@ -10,9 +11,22 @@ namespace Umbraco.Web.Models.Mapping { internal class AvailablePropertyEditorsResolver : ValueResolver> { + private readonly IContentSection _contentSection; + + public AvailablePropertyEditorsResolver(IContentSection contentSection) + { + _contentSection = contentSection; + } + protected override IEnumerable ResolveCore(IDataTypeDefinition source) { return PropertyEditorResolver.Current.PropertyEditors + .Where(x => + { + if (_contentSection.ShowDeprecatedPropertyEditors) + return true; + return source.PropertyEditorAlias == x.Alias || x.IsDeprecated == false; + }) .OrderBy(x => x.Name) .Select(Mapper.Map); } diff --git a/src/Umbraco.Web/Models/Mapping/CodeFileDisplayMapper.cs b/src/Umbraco.Web/Models/Mapping/CodeFileDisplayMapper.cs new file mode 100644 index 0000000000..dec0930c07 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/CodeFileDisplayMapper.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models.Mapping; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + public class CodeFileDisplayMapper : MapperConfiguration + { + public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) + { + config.CreateMap() + .ForMember(x => x.FileType, exp => exp.Ignore()) + .ForMember(x => x.Notifications, exp => exp.Ignore()) + .ForMember(x => x.Path, exp => exp.Ignore()) + .ForMember(x => x.Snippet, exp => exp.Ignore()); + + config.CreateMap() + .ForMember(x => x.FileType, exp => exp.Ignore()) + .ForMember(x => x.Notifications, exp => exp.Ignore()) + .ForMember(x => x.Path, exp => exp.Ignore()) + .ForMember(x => x.Snippet, exp => exp.Ignore()); + + config.CreateMap() + .ForMember(x => x.DeletedDate, exp => exp.Ignore()) + .ForMember(x => x.Id, exp => exp.Ignore()) + .ForMember(x => x.Key, exp => exp.Ignore()) + .ForMember(x => x.Path, exp => exp.Ignore()) + .ForMember(x => x.CreateDate, exp => exp.Ignore()) + .ForMember(x => x.UpdateDate, exp => exp.Ignore()) + .ForMember(x => x.Path, exp => exp.Ignore()) + .ForMember(x => x.Alias, exp => exp.Ignore()) + .ForMember(x => x.Name, exp => exp.Ignore()) + .ForMember(x => x.OriginalPath, exp => exp.Ignore()) + .ForMember(x => x.HasIdentity, exp => exp.Ignore()); + + config.CreateMap() + .ForMember(x => x.DeletedDate, exp => exp.Ignore()) + .ForMember(x => x.Id, exp => exp.Ignore()) + .ForMember(x => x.Key, exp => exp.Ignore()) + .ForMember(x => x.Path, exp => exp.Ignore()) + .ForMember(x => x.CreateDate, exp => exp.Ignore()) + .ForMember(x => x.UpdateDate, exp => exp.Ignore()) + .ForMember(x => x.Path, exp => exp.Ignore()) + .ForMember(x => x.Alias, exp => exp.Ignore()) + .ForMember(x => x.Name, exp => exp.Ignore()) + .ForMember(x => x.OriginalPath, exp => exp.Ignore()) + .ForMember(x => x.HasIdentity, exp => exp.Ignore()); + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index 8942d52360..37b002d297 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -28,6 +28,7 @@ namespace Umbraco.Web.Models.Mapping //FROM IContent TO ContentItemDisplay config.CreateMap() + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Document, content.Key))) .ForMember(display => display.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(display => display.Updater, expression => expression.ResolveUsing(new CreatorResolver())) .ForMember(display => display.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) @@ -58,6 +59,7 @@ namespace Umbraco.Web.Models.Mapping //FROM IContent TO ContentItemBasic config.CreateMap>() + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Document, content.Key))) .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(dto => dto.Updater, expression => expression.ResolveUsing(new CreatorResolver())) .ForMember(dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) @@ -68,6 +70,7 @@ namespace Umbraco.Web.Models.Mapping //FROM IContent TO ContentItemDto config.CreateMap>() + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Document, content.Key))) .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(dto => dto.HasPublishedVersion, expression => expression.MapFrom(content => content.HasPublishedVersion)) .ForMember(dto => dto.Updater, expression => expression.Ignore()) @@ -99,7 +102,7 @@ namespace Umbraco.Web.Models.Mapping //map the tree node url if (HttpContext.Current != null) { - var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); + var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); var url = urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(display.Id.ToString(), null)); display.TreeNodeUrl = url; } @@ -162,16 +165,10 @@ namespace Umbraco.Web.Models.Mapping { {"items", templateItemConfig} } - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("content/urls"), - Value = string.Join(",", display.Urls), - View = "urllist" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor } }; + TabsAndPropertiesResolver.MapGenericProperties(content, display, localizedText, properties.ToArray(), genericProperties => { @@ -203,6 +200,15 @@ namespace Umbraco.Web.Models.Mapping //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor docTypeProperty.View = "urllist"; } + + // inject 'Link to document' as the first generic property + genericProperties.Insert(0, new ContentPropertyDisplay + { + Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("content/urls"), + Value = string.Join(",", display.Urls), + View = "urllist" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor + }); }); } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs index bf0ec1f457..24620509e9 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs @@ -48,6 +48,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(type => type.Key, expression => expression.Ignore()) .ForMember(type => type.CreateDate, expression => expression.Ignore()) .ForMember(type => type.UpdateDate, expression => expression.Ignore()) + .ForMember(type => type.DeletedDate, expression => expression.Ignore()) .ForMember(type => type.HasIdentity, expression => expression.Ignore()); config.CreateMap() @@ -72,7 +73,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() //do the base mapping .MapBaseContentTypeSaveToEntity(applicationContext) - .ConstructUsing((source) => new MediaType(source.ParentId)) + .ConstructUsing((source) => new MediaType(source.ParentId)) .AfterMap((source, dest) => { ContentTypeModelMapperExtensions.AfterMapMediaTypeSaveToEntity(source, dest, applicationContext); @@ -161,9 +162,12 @@ namespace Umbraco.Web.Models.Mapping }); - config.CreateMap(); - config.CreateMap(); - config.CreateMap(); + config.CreateMap() + .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.MemberType, content.Key))); + config.CreateMap() + .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.MediaType, content.Key))); + config.CreateMap() + .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.DocumentType, content.Key))); config.CreateMap() diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs index 52f2dbad4b..274771fead 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs @@ -31,6 +31,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.HasIdentity, map => map.Ignore()) .ForMember(dest => dest.CreateDate, map => map.Ignore()) .ForMember(dest => dest.UpdateDate, map => map.Ignore()) + .ForMember(dest => dest.DeletedDate, map => map.Ignore()) .ForMember(dest => dest.PropertyTypes, map => map.Ignore()); } @@ -126,6 +127,7 @@ namespace Umbraco.Web.Models.Mapping where TPropertyTypeDisplay : PropertyTypeDisplay, new() { return mapping + .ForMember(x => x.Udi, expression => expression.ResolveUsing(new ContentTypeUdiResolver())) .ForMember(display => display.Notifications, expression => expression.Ignore()) .ForMember(display => display.Errors, expression => expression.Ignore()) .ForMember(display => display.AllowAsRoot, expression => expression.MapFrom(type => type.AllowedAsRoot)) @@ -174,6 +176,7 @@ namespace Umbraco.Web.Models.Mapping //These get persisted as part of the saving procedure, nothing to do with the display model .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) + .ForMember(dto => dto.DeletedDate, expression => expression.Ignore()) .ForMember(dto => dto.AllowedAsRoot, expression => expression.MapFrom(display => display.AllowAsRoot)) .ForMember(dto => dto.CreatorId, expression => expression.Ignore()) diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeUdiResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeUdiResolver.cs new file mode 100644 index 0000000000..2d33b17155 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeUdiResolver.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// Resolves a UDI for a content type based on it's type + /// + internal class ContentTypeUdiResolver : ValueResolver + { + protected override Udi ResolveCore(IContentTypeComposition source) + { + if (source == null) return null; + + return Udi.Create( + source.GetType() == typeof(IMemberType) + ? Constants.UdiEntityType.MemberType + : source.GetType() == typeof(IMediaType) + ? Constants.UdiEntityType.MediaType : Constants.UdiEntityType.DocumentType, source.Key); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs index ac2faa82dc..7e9a00f760 100644 --- a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using AutoMapper; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; using Umbraco.Core.PropertyEditors; @@ -34,6 +35,7 @@ namespace Umbraco.Web.Models.Mapping }; config.CreateMap() + .ForMember(x => x.Udi, expression => expression.Ignore()) .ForMember(x => x.HasPrevalues, expression => expression.Ignore()) .ForMember(x => x.IsSystemDataType, expression => expression.Ignore()) .ForMember(x => x.Id, expression => expression.Ignore()) @@ -44,6 +46,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(x => x.AdditionalData, expression => expression.Ignore()); config.CreateMap() + .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.DataType, content.Key))) .ForMember(x => x.HasPrevalues, expression => expression.Ignore()) .ForMember(x => x.Icon, expression => expression.Ignore()) .ForMember(x => x.Alias, expression => expression.Ignore()) @@ -61,7 +64,8 @@ namespace Umbraco.Web.Models.Mapping }); config.CreateMap() - .ForMember(display => display.AvailableEditors, expression => expression.ResolveUsing(new AvailablePropertyEditorsResolver())) + .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.DataType, content.Key))) + .ForMember(display => display.AvailableEditors, expression => expression.ResolveUsing(new AvailablePropertyEditorsResolver(UmbracoConfig.For.UmbracoSettings().Content))) .ForMember(display => display.PreValues, expression => expression.ResolveUsing( new PreValueDisplayResolver(lazyDataTypeService))) .ForMember(display => display.SelectedEditor, expression => expression.MapFrom( @@ -104,6 +108,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(x => x.Level, expression => expression.Ignore()) .ForMember(x => x.SortOrder, expression => expression.Ignore()) .ForMember(x => x.CreateDate, expression => expression.Ignore()) + .ForMember(x => x.DeletedDate, expression => expression.Ignore()) .ForMember(x => x.UpdateDate, expression => expression.Ignore()); //Converts a property editor to a new list of pre-value fields - used when creating a new data type or changing a data type with new pre-vals diff --git a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs index 5610a70008..5be9d550e5 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using AutoMapper; using Examine; +using Examine.LuceneEngine.Providers; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; @@ -17,11 +18,20 @@ namespace Umbraco.Web.Models.Mapping public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { config.CreateMap() + .ForMember(x => x.Udi, expression => expression.MapFrom(x => Udi.Create(UmbracoObjectTypesExtensions.GetUdiType(x.NodeObjectTypeId), x.Key))) .ForMember(basic => basic.Icon, expression => expression.MapFrom(entity => entity.ContentTypeIcon)) .ForMember(dto => dto.Trashed, expression => expression.Ignore()) - .ForMember(x => x.Alias, expression => expression.Ignore()); + .ForMember(x => x.Alias, expression => expression.Ignore()) + .AfterMap((entity, basic) => + { + if (entity.NodeObjectTypeId == Constants.ObjectTypes.MemberGuid && basic.Icon.IsNullOrWhiteSpace()) + { + basic.Icon = "icon-user"; + } + }); config.CreateMap() + .ForMember(x => x.Udi, expression => expression.Ignore()) .ForMember(basic => basic.Icon, expression => expression.UseValue("icon-box")) .ForMember(basic => basic.Path, expression => expression.UseValue("")) .ForMember(basic => basic.ParentId, expression => expression.UseValue(-1)) @@ -29,6 +39,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(x => x.AdditionalData, expression => expression.Ignore()); config.CreateMap() + .ForMember(x => x.Udi, expression => expression.Ignore()) .ForMember(basic => basic.Icon, expression => expression.UseValue("icon-tab")) .ForMember(basic => basic.Path, expression => expression.UseValue("")) .ForMember(basic => basic.ParentId, expression => expression.UseValue(-1)) @@ -38,6 +49,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(x => x.AdditionalData, expression => expression.Ignore()); config.CreateMap() + .ForMember(x => x.Udi, expression => expression.Ignore()) .ForMember(basic => basic.Icon, expression => expression.UseValue("icon-user")) .ForMember(basic => basic.Path, expression => expression.UseValue("")) .ForMember(basic => basic.ParentId, expression => expression.UseValue(-1)) @@ -46,6 +58,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(x => x.AdditionalData, expression => expression.Ignore()); config.CreateMap() + .ForMember(x => x.Udi, expression => expression.MapFrom(x => Udi.Create(Constants.UdiEntityType.Template, x.Key))) .ForMember(basic => basic.Icon, expression => expression.UseValue("icon-layout")) .ForMember(basic => basic.Path, expression => expression.MapFrom(template => template.Path)) .ForMember(basic => basic.ParentId, expression => expression.UseValue(-1)) @@ -70,6 +83,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(x => x.SortOrder, expression => expression.Ignore()); config.CreateMap() + .ForMember(x => x.Udi, expression => expression.ResolveUsing(new ContentTypeUdiResolver())) .ForMember(basic => basic.Path, expression => expression.MapFrom(x => x.Path)) .ForMember(basic => basic.ParentId, expression => expression.MapFrom(x => x.ParentId)) .ForMember(dto => dto.Trashed, expression => expression.Ignore()) @@ -77,6 +91,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() //default to document icon + .ForMember(x => x.Udi, expression => expression.Ignore()) .ForMember(x => x.Icon, expression => expression.Ignore()) .ForMember(x => x.Id, expression => expression.MapFrom(result => result.Id)) .ForMember(x => x.Name, expression => expression.Ignore()) @@ -87,21 +102,40 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dto => dto.Trashed, expression => expression.Ignore()) .ForMember(x => x.AdditionalData, expression => expression.Ignore()) .AfterMap((result, basic) => - { + { + //get the icon if there is one basic.Icon = result.Fields.ContainsKey(UmbracoContentIndexer.IconFieldName) ? result.Fields[UmbracoContentIndexer.IconFieldName] : "icon-document"; basic.Name = result.Fields.ContainsKey("nodeName") ? result.Fields["nodeName"] : "[no name]"; - if (result.Fields.ContainsKey("__NodeKey")) + if (result.Fields.ContainsKey(UmbracoContentIndexer.NodeKeyFieldName)) { Guid key; - if (Guid.TryParse(result.Fields["__NodeKey"], out key)) + if (Guid.TryParse(result.Fields[UmbracoContentIndexer.NodeKeyFieldName], out key)) { basic.Key = key; + + //need to set the UDI + if (result.Fields.ContainsKey(LuceneIndexer.IndexTypeFieldName)) + { + switch (result.Fields[LuceneIndexer.IndexTypeFieldName]) + { + case IndexTypes.Member: + basic.Udi = new GuidUdi(Constants.UdiEntityType.Member, basic.Key); + break; + case IndexTypes.Content: + basic.Udi = new GuidUdi(Constants.UdiEntityType.Document, basic.Key); + break; + case IndexTypes.Media: + basic.Udi = new GuidUdi(Constants.UdiEntityType.Media, basic.Key); + break; + } + } } } + if (result.Fields.ContainsKey("parentID")) { int parentId; @@ -114,7 +148,7 @@ namespace Umbraco.Web.Models.Mapping basic.ParentId = -1; } } - basic.Path = result.Fields.ContainsKey("__Path") ? result.Fields["__Path"] : ""; + basic.Path = result.Fields.ContainsKey(UmbracoContentIndexer.IndexPathFieldName) ? result.Fields[UmbracoContentIndexer.IndexPathFieldName] : ""; if (result.Fields.ContainsKey(UmbracoContentIndexer.NodeTypeAliasFieldName)) { @@ -124,6 +158,9 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap>() .ConvertUsing(results => results.Select(Mapper.Map).ToList()); + + config.CreateMap, IEnumerable>() + .ConvertUsing(results => results.Select(Mapper.Map).ToList()); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/MacroModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MacroModelMapper.cs index 54ebca6e68..8ef4432cab 100644 --- a/src/Umbraco.Web/Models/Mapping/MacroModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MacroModelMapper.cs @@ -20,6 +20,7 @@ namespace Umbraco.Web.Models.Mapping { //FROM IMacro TO EntityBasic config.CreateMap() + .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Macro, content.Key))) .ForMember(entityBasic => entityBasic.Icon, expression => expression.UseValue("icon-settings-alt")) .ForMember(dto => dto.ParentId, expression => expression.UseValue(-1)) .ForMember(dto => dto.Path, expression => expression.ResolveUsing(macro => "-1," + macro.Id)) @@ -36,8 +37,8 @@ namespace Umbraco.Web.Models.Mapping .AfterMap((property, parameter) => { //map the view and the config - - var paramEditor = ParameterEditorResolver.Current.GetByAlias(property.EditorAlias); + // we need to show the depracated ones for backwards compatibility + var paramEditor = ParameterEditorResolver.Current.GetByAlias(property.EditorAlias, true); if (paramEditor == null) { //we'll just map this to a text box diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index 867ae2f00b..384ef332e2 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -25,6 +25,7 @@ namespace Umbraco.Web.Models.Mapping { //FROM IMedia TO MediaItemDisplay config.CreateMap() + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Media, content.Key))) .ForMember(display => display.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(display => display.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) .ForMember(display => display.ContentTypeAlias, expression => expression.MapFrom(content => content.ContentType.Alias)) @@ -45,6 +46,7 @@ namespace Umbraco.Web.Models.Mapping //FROM IMedia TO ContentItemBasic config.CreateMap>() + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Media, content.Key))) .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) .ForMember(dto => dto.Trashed, expression => expression.MapFrom(content => content.Trashed)) @@ -56,6 +58,7 @@ namespace Umbraco.Web.Models.Mapping //FROM IMedia TO ContentItemDto config.CreateMap>() + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Media, content.Key))) .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(dto => dto.Published, expression => expression.Ignore()) .ForMember(dto => dto.Updater, expression => expression.Ignore()) @@ -70,20 +73,20 @@ namespace Umbraco.Web.Models.Mapping //map the IsChildOfListView (this is actually if it is a descendant of a list view!) var parent = media.Parent(); display.IsChildOfListView = parent != null && (parent.ContentType.IsContainer || contentTypeService.HasContainerInPath(parent.Path)); - + //map the tree node url if (HttpContext.Current != null) { - var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); + var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); var url = urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(display.Id.ToString(), null)); display.TreeNodeUrl = url; } - + if (media.ContentType.IsContainer) { TabsAndPropertiesResolver.AddListView(display, "media", dataTypeService, localizedText); } - + var genericProperties = new List { new ContentPropertyDisplay @@ -95,20 +98,6 @@ namespace Umbraco.Web.Models.Mapping } }; - var links = media.GetUrls(UmbracoConfig.For.UmbracoSettings().Content, logger); - - if (links.Any()) - { - var link = new ContentPropertyDisplay - { - Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("media/urls"), - Value = string.Join(",", links), - View = "urllist" - }; - genericProperties.Add(link); - } - TabsAndPropertiesResolver.MapGenericProperties(media, display, localizedText, genericProperties, properties => { if (HttpContext.Current != null && UmbracoContext.Current != null && UmbracoContext.Current.Security.CurrentUser != null @@ -130,6 +119,20 @@ namespace Umbraco.Web.Models.Mapping }; docTypeProperty.View = "urllist"; } + + // inject 'Link to media' as the first generic property + var links = media.GetUrls(UmbracoConfig.For.UmbracoSettings().Content, logger); + if (links.Any()) + { + var link = new ContentPropertyDisplay + { + Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("media/urls"), + Value = string.Join(",", links), + View = "urllist" + }; + properties.Insert(0, link); + } }); } } diff --git a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs index edb44d36ce..976026bd97 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs @@ -55,12 +55,14 @@ namespace Umbraco.Web.Models.Mapping .ForMember(member => member.SortOrder, expression => expression.Ignore()) .ForMember(member => member.AdditionalData, expression => expression.Ignore()) .ForMember(member => member.FailedPasswordAttempts, expression => expression.Ignore()) - //TODO: Support these eventually - .ForMember(member => member.PasswordQuestion, expression => expression.Ignore()) + .ForMember(member => member.DeletedDate, expression => expression.Ignore()) + //TODO: Support these eventually + .ForMember(member => member.PasswordQuestion, expression => expression.Ignore()) .ForMember(member => member.RawPasswordAnswerValue, expression => expression.Ignore()); //FROM IMember TO MediaItemDisplay config.CreateMap() + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Member, content.Key))) .ForMember(display => display.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(display => display.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) .ForMember(display => display.ContentTypeAlias, expression => expression.MapFrom(content => content.ContentType.Alias)) @@ -80,10 +82,11 @@ namespace Umbraco.Web.Models.Mapping .ForMember(display => display.IsContainer, expression => expression.Ignore()) .ForMember(display => display.TreeNodeUrl, expression => expression.Ignore()) .ForMember(display => display.HasPublishedVersion, expression => expression.Ignore()) - .AfterMap((member, display) => MapGenericCustomProperties(applicationContext.Services.MemberService, member, display, applicationContext.Services.TextService)); + .AfterMap((member, display) => MapGenericCustomProperties(applicationContext.Services.MemberService, applicationContext.Services.UserService, member, display, applicationContext.Services.TextService)); //FROM IMember TO MemberBasic config.CreateMap() + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Member, content.Key))) .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) .ForMember(dto => dto.ContentTypeAlias, expression => expression.MapFrom(content => content.ContentType.Alias)) @@ -96,9 +99,10 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dto => dto.HasPublishedVersion, expression => expression.Ignore()); //FROM MembershipUser TO MemberBasic - config.CreateMap() + config.CreateMap() //we're giving this entity an ID of 0 - we cannot really map it but it needs an id so the system knows it's not a new entity .ForMember(member => member.Id, expression => expression.MapFrom(user => int.MaxValue)) + .ForMember(display => display.Udi, expression => expression.Ignore()) .ForMember(member => member.CreateDate, expression => expression.MapFrom(user => user.CreationDate)) .ForMember(member => member.UpdateDate, expression => expression.MapFrom(user => user.LastActivityDate)) .ForMember(member => member.Key, expression => expression.MapFrom(user => user.ProviderUserKey.TryConvertTo().Result.ToString("N"))) @@ -121,6 +125,7 @@ namespace Umbraco.Web.Models.Mapping //FROM IMember TO ContentItemDto config.CreateMap>() + .ForMember(display => display.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.Member, content.Key))) .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) .ForMember(dto => dto.Published, expression => expression.Ignore()) .ForMember(dto => dto.Updater, expression => expression.Ignore()) @@ -135,20 +140,21 @@ namespace Umbraco.Web.Models.Mapping /// Maps the generic tab with custom properties for content /// /// + /// /// /// /// /// /// If this is a new entity and there is an approved field then we'll set it to true by default. /// - private static void MapGenericCustomProperties(IMemberService memberService, IMember member, MemberDisplay display, ILocalizedTextService localizedText) + private static void MapGenericCustomProperties(IMemberService memberService, IUserService userService, IMember member, MemberDisplay display, ILocalizedTextService localizedText) { var membersProvider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); //map the tree node url if (HttpContext.Current != null) { - var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); + var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); var url = urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(display.Key.ToString("N"), null)); display.TreeNodeUrl = url; } @@ -185,7 +191,7 @@ namespace Umbraco.Web.Models.Mapping //TODO: Hard coding this because the changepassword doesn't necessarily need to be a resolvable (real) property editor View = "changepassword", //initialize the dictionary with the configuration from the default membership provider - Config = new Dictionary(membersProvider.GetConfiguration()) + Config = new Dictionary(membersProvider.GetConfiguration(userService)) { //the password change toggle will only be displayed if there is already a password assigned. {"hasPassword", member.RawPasswordValue.IsNullOrWhiteSpace() == false} diff --git a/src/Umbraco.Web/Models/Mapping/OwnerResolver.cs b/src/Umbraco.Web/Models/Mapping/OwnerResolver.cs index 5f3697c3d5..067d495591 100644 --- a/src/Umbraco.Web/Models/Mapping/OwnerResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/OwnerResolver.cs @@ -1,5 +1,6 @@ using AutoMapper; using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; diff --git a/src/Umbraco.Web/Models/Mapping/TemplateModelMapper.cs b/src/Umbraco.Web/Models/Mapping/TemplateModelMapper.cs new file mode 100644 index 0000000000..19677fccc8 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/TemplateModelMapper.cs @@ -0,0 +1,29 @@ +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Mapping; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + internal class TemplateModelMapper : MapperConfiguration + { + public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) + { + config.CreateMap() + .ForMember(x => x.Notifications, exp => exp.Ignore()); + + config.CreateMap() + .ForMember(x => x.DeletedDate, exp => exp.Ignore()) + .ForMember(x => x.Key, exp => exp.Ignore()) + .ForMember(x => x.Path, exp => exp.Ignore()) + .ForMember(x => x.CreateDate, exp => exp.Ignore()) + .ForMember(x => x.UpdateDate, exp => exp.Ignore()) + .ForMember(x => x.VirtualPath, exp => exp.Ignore()) + .ForMember(x => x.Path, exp => exp.Ignore()) + .ForMember(x => x.MasterTemplateId, exp => exp.Ignore()) // ok, assigned when creating the template + .ForMember(x => x.IsMasterTemplate, exp => exp.Ignore()) + .ForMember(x => x.HasIdentity, exp => exp.Ignore()); + } + } +} diff --git a/src/Umbraco.Web/Models/RelatedLink.cs b/src/Umbraco.Web/Models/RelatedLink.cs new file mode 100644 index 0000000000..884bdebeef --- /dev/null +++ b/src/Umbraco.Web/Models/RelatedLink.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Web.Models +{ + public class RelatedLink : RelatedLinkBase + { + public int? Id { get; internal set; } + internal bool IsDeleted { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/RelatedLinkBase.cs b/src/Umbraco.Web/Models/RelatedLinkBase.cs new file mode 100644 index 0000000000..b347e25e0a --- /dev/null +++ b/src/Umbraco.Web/Models/RelatedLinkBase.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models +{ + public abstract class RelatedLinkBase + { + [JsonProperty("caption")] + public string Caption { get; set; } + [JsonProperty("link")] + public string Link { get; set; } + [JsonProperty("newWindow")] + public bool NewWindow { get; set; } + [JsonProperty("isInternal")] + public bool IsInternal { get; set; } + [JsonProperty("type")] + public RelatedLinkType Type { get; set; } + [JsonIgnore] + public IPublishedContent Content { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/RelatedLinkType.cs b/src/Umbraco.Web/Models/RelatedLinkType.cs new file mode 100644 index 0000000000..eec7817ab6 --- /dev/null +++ b/src/Umbraco.Web/Models/RelatedLinkType.cs @@ -0,0 +1,27 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Umbraco +// +// +// Defines the RelatedLinkType type. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Umbraco.Web.Models +{ + /// + /// The related link type. + /// + public enum RelatedLinkType + { + /// + /// Internal link type + /// + Internal, + + /// + /// External link type + /// + External + } +} diff --git a/src/Umbraco.Web/Models/RelatedLinks.cs b/src/Umbraco.Web/Models/RelatedLinks.cs new file mode 100644 index 0000000000..afccdfb31f --- /dev/null +++ b/src/Umbraco.Web/Models/RelatedLinks.cs @@ -0,0 +1,42 @@ +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace Umbraco.Web.Models +{ + [TypeConverter(typeof(RelatedLinksTypeConverter))] + public class RelatedLinks : IEnumerable + { + private readonly string _propertyData; + + private readonly IEnumerable _relatedLinks; + + public RelatedLinks(IEnumerable relatedLinks, string propertyData) + { + _relatedLinks = relatedLinks; + _propertyData = propertyData; + } + + /// + /// Gets the property data. + /// + internal string PropertyData + { + get + { + return this._propertyData; + } + } + + public IEnumerator GetEnumerator() + { + return _relatedLinks.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } +} diff --git a/src/Umbraco.Web/Models/SendCodeViewModel.cs b/src/Umbraco.Web/Models/SendCodeViewModel.cs new file mode 100644 index 0000000000..31c6644089 --- /dev/null +++ b/src/Umbraco.Web/Models/SendCodeViewModel.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + /// + /// Used for 2FA verification + /// + [DataContract(Name = "code", Namespace = "")] + public class Verify2FACodeModel + { + [Required] + [DataMember(Name = "code", IsRequired = true)] + public string Code { get; set; } + + [Required] + [DataMember(Name = "provider", IsRequired = true)] + public string Provider { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs b/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs index 284518bf1e..36593736d3 100644 --- a/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs +++ b/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs @@ -11,16 +11,48 @@ internal static class QueryConditionExtensions { - private static string MakeBinaryOperation(this QueryCondition condition, string operand, int token) + + public static string BuildTokenizedCondition(this QueryCondition condition, int token) { - return string.Format("{0}{1}@{2}", condition.Property.Name, operand, token); + return condition.BuildConditionString(string.Empty, token); } - - public static string BuildCondition(this QueryCondition condition, int token) + public static string BuildCondition(this QueryCondition condition, string parameterAlias) { + return condition.BuildConditionString(parameterAlias + "."); + } + + private static string BuildConditionString(this QueryCondition condition, string prefix, int token = -1) + { + + + var operand = string.Empty; var value = string.Empty; + var constraintValue = string.Empty; + + + //if a token is used, use a token placeholder, otherwise, use the actual value + if(token >= 0){ + constraintValue = string.Format("@{0}", token); + }else { + + //modify the format of the constraint value + switch (condition.Property.Type) + { + case "string": + constraintValue = string.Format("\"{0}\"", condition.ConstraintValue); + break; + case "datetime": + constraintValue = string.Format("DateTime.Parse(\"{0}\")", condition.ConstraintValue); + break; + default: + constraintValue = condition.ConstraintValue; + break; + } + + // constraintValue = condition.Property.Type == "string" ? string.Format("\"{0}\"", condition.ConstraintValue) : condition.ConstraintValue; + } switch (condition.Term.Operathor) { @@ -43,17 +75,23 @@ operand = " <= "; break; case Operathor.Contains: - value = string.Format("{0}.Contains(@{1})", condition.Property.Name, token); + value = string.Format("{0}{1}.Contains({2})", prefix, condition.Property.Alias, constraintValue); break; case Operathor.NotContains: - value = string.Format("!{0}.Contains(@{1})", condition.Property.Name, token); + value = string.Format("!{0}{1}.Contains({2})", prefix, condition.Property.Alias, constraintValue); break; default : operand = " == "; break; } - return string.IsNullOrEmpty(value) ? condition.MakeBinaryOperation(operand, token) : value; + + if (string.IsNullOrEmpty(value) == false) + return value; + + + + return string.Format("{0}{1}{2}{3}", prefix, condition.Property.Alias, operand, constraintValue); } } diff --git a/src/Umbraco.Web/Mvc/PluginController.cs b/src/Umbraco.Web/Mvc/PluginController.cs index 48608fc9a1..4ff4b961b8 100644 --- a/src/Umbraco.Web/Mvc/PluginController.cs +++ b/src/Umbraco.Web/Mvc/PluginController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Web.Mvc; +using umbraco.interfaces; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Services; @@ -12,7 +13,7 @@ namespace Umbraco.Web.Mvc /// /// A base class for all plugin controllers to inherit from /// - public abstract class PluginController : Controller + public abstract class PluginController : Controller, IDiscoverable { /// /// stores the metadata about plugin controllers diff --git a/src/Umbraco.Web/Mvc/Strings.Designer.cs b/src/Umbraco.Web/Mvc/Strings.Designer.cs index 02a44fbbd7..243a7f7dd9 100644 --- a/src/Umbraco.Web/Mvc/Strings.Designer.cs +++ b/src/Umbraco.Web/Mvc/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. @@ -65,8 +65,8 @@ namespace Umbraco.Web.Mvc { ///<configuration> /// /// <configSections> - /// <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"> - /// <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" /> + /// <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"> + /// <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" /> /// <section name="page [rest of string was truncated]";. /// internal static string WebConfigTemplate { diff --git a/src/Umbraco.Web/Profiling/WebProfiler.cs b/src/Umbraco.Web/Profiling/WebProfiler.cs index 62d69019d6..b1c2d9eefb 100644 --- a/src/Umbraco.Web/Profiling/WebProfiler.cs +++ b/src/Umbraco.Web/Profiling/WebProfiler.cs @@ -12,16 +12,16 @@ namespace Umbraco.Web.Profiling { /// /// A profiler used for web based activity based on the MiniProfiler framework - /// + /// internal class WebProfiler : IProfiler { private const string BootRequestItemKey = "Umbraco.Web.Profiling.WebProfiler__isBootRequest"; - private WebProfilerProvider _provider; + private readonly WebProfilerProvider _provider; private int _first; /// /// Constructor - /// + /// internal WebProfiler() { // create our own provider, which can provide a profiler even during boot @@ -78,7 +78,7 @@ namespace Umbraco.Web.Profiling { // if this is the boot request, or if we should profile this request, stop // (the boot request is always profiled, no matter what) - var isBootRequest = ((HttpApplication)sender).Context.Items[BootRequestItemKey] != null; // fixme perfs + var isBootRequest = ((HttpApplication)sender).Context.Items[BootRequestItemKey] != null; if (isBootRequest) _provider.EndBootRequest(); if (isBootRequest || ShouldProfile(sender)) @@ -87,11 +87,11 @@ namespace Umbraco.Web.Profiling private bool ShouldProfile(object sender) { - if (GlobalSettings.DebugMode == false) + if (GlobalSettings.DebugMode == false) return false; - + //will not run in medium trust - if (SystemUtilities.GetCurrentTrustLevel() < AspNetHostingPermissionLevel.High) + if (SystemUtilities.GetCurrentTrustLevel() < AspNetHostingPermissionLevel.High) return false; var request = TryGetRequest(sender); @@ -109,7 +109,7 @@ namespace Umbraco.Web.Profiling } /// - /// Render the UI to display the profiler + /// Render the UI to display the profiler /// /// /// @@ -137,7 +137,7 @@ namespace Umbraco.Web.Profiling /// Start the profiler /// public void Start() - { + { MiniProfiler.Start(); } @@ -145,7 +145,7 @@ namespace Umbraco.Web.Profiling /// Start the profiler /// /// - /// set discardResults to false when you want to abandon all profiling, this is useful for + /// set discardResults to false when you want to abandon all profiling, this is useful for /// when someone is not authenticated or you want to clear the results based on some other mechanism. /// public void Stop(bool discardResults = false) diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs index 3509b2e650..c9e792fcc6 100644 --- a/src/Umbraco.Web/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs @@ -35,7 +35,15 @@ using System.Security; [assembly: InternalsVisibleTo("Concorde.Sync")] [assembly: InternalsVisibleTo("Umbraco.Courier.Core")] [assembly: InternalsVisibleTo("Umbraco.Courier.Persistence")] +[assembly: InternalsVisibleTo("Umbraco.Deploy")] +[assembly: InternalsVisibleTo("Umbraco.Deploy.UI")] +[assembly: InternalsVisibleTo("Umbraco.Deploy.Cloud")] [assembly: InternalsVisibleTo("Umbraco.VisualStudio")] [assembly: InternalsVisibleTo("Umbraco.ModelsBuilder")] [assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.AspNet")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] + +[assembly: InternalsVisibleTo("Umbraco.Forms.Core")] +[assembly: InternalsVisibleTo("Umbraco.Forms.Core.Providers")] +[assembly: InternalsVisibleTo("Umbraco.Forms.Web")] + diff --git a/src/Umbraco.Web/Properties/Settings1.Designer.cs b/src/Umbraco.Web/Properties/Settings1.Designer.cs index 3f63c8d5cd..1b72efc115 100644 --- a/src/Umbraco.Web/Properties/Settings1.Designer.cs +++ b/src/Umbraco.Web/Properties/Settings1.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34003 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -12,7 +12,7 @@ namespace Umbraco.Web.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs new file mode 100644 index 0000000000..5b99264113 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Content property editor that stores UDI + /// + [PropertyEditor(Constants.PropertyEditors.ContentPicker2Alias, "Content Picker", PropertyEditorValueTypes.String, "contentpicker", IsParameterEditor = true, Group = "Pickers")] + public class ContentPicker2PropertyEditor : PropertyEditor + { + + public ContentPicker2PropertyEditor() + { + InternalPreValues = new Dictionary + { + {"startNodeId", "-1"}, + {"showOpenButton", "0"}, + {"showEditButton", "0"}, + {"showPathOnHover", "0"}, + {"idType", "udi"} + }; + } + + internal IDictionary InternalPreValues; + public override IDictionary DefaultPreValues + { + get { return InternalPreValues; } + set { InternalPreValues = value; } + } + + protected override PreValueEditor CreatePreValueEditor() + { + return new ContentPickerPreValueEditor(); + } + + internal class ContentPickerPreValueEditor : PreValueEditor + { + public ContentPickerPreValueEditor() + { + //create the fields + Fields.Add(new PreValueField() + { + Key = "showOpenButton", + View = "boolean", + Name = "Show open button (this feature is in preview!)", + Description = "Opens the node in a dialog" + }); + Fields.Add(new PreValueField() + { + Key = "startNodeId", + View = "treepicker", + Name = "Start node", + Config = new Dictionary + { + {"idType", "udi"} + } + }); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index 4d58060164..52e6e1d6bd 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -1,49 +1,32 @@ -using System.Collections.Generic; +using System; +using System.Linq; using Umbraco.Core; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.ContentPickerAlias, "Content Picker", PropertyEditorValueTypes.Integer, "contentpicker", IsParameterEditor = true, Group = "Pickers")] - public class ContentPickerPropertyEditor : PropertyEditor - { + /// + /// Legacy content property editor that stores Integer Ids + /// + [Obsolete("This editor is obsolete, use ContentPickerPropertyEditor2 instead which stores UDI")] + [PropertyEditor(Constants.PropertyEditors.ContentPickerAlias, "(Obsolete) Content Picker", PropertyEditorValueTypes.Integer, "contentpicker", IsParameterEditor = true, Group = "Pickers", IsDeprecated = true)] + public class ContentPickerPropertyEditor : ContentPicker2PropertyEditor + { public ContentPickerPropertyEditor() { - _internalPreValues = new Dictionary - { - {"startNodeId", "-1"}, - {"showOpenButton", "0"}, - {"showEditButton", "0"}, - {"showPathOnHover", "0"} - }; - } - - private IDictionary _internalPreValues; - public override IDictionary DefaultPreValues - { - get { return _internalPreValues; } - set { _internalPreValues = value; } + InternalPreValues["idType"] = "int"; } + /// + /// overridden to change the pre-value picker to use INT ids + /// + /// protected override PreValueEditor CreatePreValueEditor() { - return new ContentPickerPreValueEditor(); - } - - internal class ContentPickerPreValueEditor : PreValueEditor - { - [PreValueField("showOpenButton", "Show open button", "boolean")] - public string ShowOpenButton { get; set; } - - [PreValueField("showEditButton", "Show edit button (this feature is in preview!)", "boolean")] - public string ShowEditButton { get; set; } - - [PreValueField("startNodeId", "Start node", "treepicker")] - public int StartNodeId { get; set; } - - [PreValueField("showPathOnHover", "Show path when hovering items", "boolean")] - public bool ShowPathOnHover { get; set; } + var preValEditor = base.CreatePreValueEditor(); + preValEditor.Fields.Single(x => x.Key == "startNodeId").Config["idType"] = "int"; + return preValEditor; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 3c30586383..9ff4aaa1a2 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -1,19 +1,10 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Drawing; -using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; -using System.Xml; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using umbraco.cms.businesslogic.Files; using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; -using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; @@ -23,282 +14,189 @@ namespace Umbraco.Web.PropertyEditors [PropertyEditor(Constants.PropertyEditors.UploadFieldAlias, "File upload", "fileupload", Icon = "icon-download-alt", Group = "media")] public class FileUploadPropertyEditor : PropertyEditor, IApplicationEventHandler { + private static MediaFileSystem MediaFileSystem + { + // v8 will get rid of singletons + get { return FileSystemProviderManager.Current.MediaFileSystem; } + } /// - /// Creates our custom value editor + /// Creates the corresponding property value editor. /// - /// + /// The corresponding property value editor. protected override PropertyValueEditor CreateValueEditor() { - var baseEditor = base.CreateValueEditor(); + var baseEditor = base.CreateValueEditor(); baseEditor.Validators.Add(new UploadFileTypeValidator()); - return new FileUploadPropertyValueEditor(baseEditor); + return new FileUploadPropertyValueEditor(baseEditor, MediaFileSystem); } - - protected override PreValueEditor CreatePreValueEditor() + + /// + /// Gets a value indicating whether a property is an upload field. + /// + /// The property. + /// A value indicating whether to check that the property has a non-empty value. + /// A value indicating whether a property is an upload field, and (optionaly) has a non-empty value. + private static bool IsUploadField(Property property, bool ensureValue) { - return new FileUploadPreValueEditor(); + if (property.PropertyType.PropertyEditorAlias != Constants.PropertyEditors.UploadFieldAlias) + return false; + if (ensureValue == false) + return true; + return property.Value is string && string.IsNullOrWhiteSpace((string) property.Value) == false; } /// - /// Ensures any files associated are removed + /// Gets the files that need to be deleted when entities are deleted. /// - /// - static IEnumerable ServiceEmptiedRecycleBin(Dictionary> allPropertyData) + /// The properties that were deleted. + static IEnumerable GetFilesToDelete(IEnumerable properties) { - var list = new List(); - //Get all values for any image croppers found - foreach (var uploadVal in allPropertyData - .SelectMany(x => x.Value) - .Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias) - .Select(x => x.Value) - .WhereNotNull()) - { - if (uploadVal.ToString().IsNullOrWhiteSpace() == false) - { - list.Add(uploadVal.ToString()); - } - } - return list; + return properties + .Where(x => IsUploadField(x, true)) + .Select(x => MediaFileSystem.GetRelativePath((string) x.Value)) + .ToList(); } /// - /// Ensures any files associated are removed + /// After a content has been copied, also copy uploaded files. /// - /// - static IEnumerable ServiceDeleted(IEnumerable deletedEntities) + /// The event sender. + /// The event arguments. + static void ContentServiceCopied(IContentService sender, Core.Events.CopyEventArgs args) { - var list = new List(); - foreach (var property in deletedEntities.SelectMany(deletedEntity => deletedEntity - .Properties - .Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias - && x.Value != null - && string.IsNullOrEmpty(x.Value.ToString()) == false))) + // get the upload field properties with a value + var properties = args.Original.Properties.Where(x => IsUploadField(x, true)); + + // copy files + var isUpdated = false; + foreach (var property in properties) { - if (property.Value != null && property.Value.ToString().IsNullOrWhiteSpace() == false) - { - list.Add(property.Value.ToString()); - } + var sourcePath = MediaFileSystem.GetRelativePath((string) property.Value); + var copyPath = MediaFileSystem.CopyFile(args.Copy, property.PropertyType, sourcePath); + args.Copy.SetValue(property.Alias, MediaFileSystem.GetUrl(copyPath)); + isUpdated = true; } - return list; + + // if updated, re-save the copy with the updated value + if (isUpdated) + sender.Save(args.Copy); } /// - /// After the content is copied we need to check if there are files that also need to be copied + /// After a media has been created, auto-fill the properties. /// - /// - /// - static void ContentServiceCopied(IContentService sender, Core.Events.CopyEventArgs e) + /// The event sender. + /// The event arguments. + static void MediaServiceCreated(IMediaService sender, Core.Events.NewEventArgs args) { - if (e.Original.Properties.Any(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias)) - { - bool isUpdated = false; - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - - //Loop through properties to check if the content contains media that should be deleted - foreach (var property in e.Original.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias - && x.Value != null - && string.IsNullOrEmpty(x.Value.ToString()) == false)) - { - if (fs.FileExists(fs.GetRelativePath(property.Value.ToString()))) - { - var currentPath = fs.GetRelativePath(property.Value.ToString()); - var propertyId = e.Copy.Properties.First(x => x.Alias == property.Alias).Id; - var newPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(currentPath)); - - fs.CopyFile(currentPath, newPath); - e.Copy.SetValue(property.Alias, fs.GetUrl(newPath)); - - //Copy thumbnails - foreach (var thumbPath in fs.GetThumbnails(currentPath)) - { - var newThumbPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(thumbPath)); - fs.CopyFile(thumbPath, newThumbPath); - } - isUpdated = true; - } - } - - if (isUpdated) - { - //need to re-save the copy with the updated path value - sender.Save(e.Copy); - } - } - } - - static void MediaServiceCreating(IMediaService sender, Core.Events.NewEventArgs e) - { - AutoFillProperties(e.Entity); - } - - static void MediaServiceSaving(IMediaService sender, Core.Events.SaveEventArgs e) - { - foreach (var m in e.SavedEntities) - { - AutoFillProperties(m); - } - } - - static void AutoFillProperties(IContentBase model) - { - foreach (var p in model.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias)) - { - var uploadFieldConfigNode = - UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties - .FirstOrDefault(x => x.Alias == p.Alias); - - if (uploadFieldConfigNode != null) - { - model.PopulateFileMetaDataProperties(uploadFieldConfigNode, p.Value == null ? string.Empty : p.Value.ToString()); - } - } + AutoFillProperties(args.Entity); } /// - /// A custom pre-val editor to ensure that the data is stored how the legacy data was stored in + /// After a media has been saved, auto-fill the properties. /// - internal class FileUploadPreValueEditor : ValueListPreValueEditor + /// The event sender. + /// The event arguments. + static void MediaServiceSaving(IMediaService sender, Core.Events.SaveEventArgs args) { - public FileUploadPreValueEditor() - : base() - { - var field = Fields.First(); - field.Description = "Enter a max width/height for each thumbnail"; - field.Name = "Add thumbnail size"; - //need to have some custom validation happening here - field.Validators.Add(new ThumbnailListValidator()); - } - - /// - /// Format the persisted value to work with our multi-val editor. - /// - /// - /// - /// - public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, PreValueCollection persistedPreVals) - { - var result = new List(); - - //the pre-values just take up one field with a semi-colon delimiter so we'll just parse - var dictionary = persistedPreVals.FormatAsDictionary(); - if (dictionary.Any()) - { - //there should only be one val - var delimited = dictionary.First().Value.Value.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - for (var index = 0; index < delimited.Length; index++) - { - result.Add(new PreValue(index, delimited[index])); - } - } - - //the items list will be a dictionary of it's id -> value we need to use the id for persistence for backwards compatibility - return new Dictionary { { "items", result.ToDictionary(x => x.Id, x => PreValueAsDictionary(x)) } }; - } - - private IDictionary PreValueAsDictionary(PreValue preValue) - { - return new Dictionary { { "value", preValue.Value }, { "sortOrder", preValue.SortOrder } }; - } - /// - /// Take the posted values and convert them to a semi-colon separated list so that its backwards compatible - /// - /// - /// - /// - public override IDictionary ConvertEditorToDb(IDictionary editorValue, PreValueCollection currentValue) - { - var result = base.ConvertEditorToDb(editorValue, currentValue); - - //this should just be a dictionary of values, we want to re-format this so that it is just one value in the dictionary that is - // semi-colon delimited - var values = result.Select(item => item.Value.Value).ToList(); - - result.Clear(); - result.Add("thumbs", new PreValue(string.Join(";", values))); - return result; - } - - internal class ThumbnailListValidator : IPropertyValidator - { - public IEnumerable Validate(object value, PreValueCollection preValues, PropertyEditor editor) - { - var json = value as JArray; - if (json == null) yield break; - - //validate each item which is a json object - for (var index = 0; index < json.Count; index++) - { - var i = json[index]; - var jItem = i as JObject; - if (jItem == null || jItem["value"] == null) continue; - - //NOTE: we will be removing empty values when persisting so no need to validate - var asString = jItem["value"].ToString(); - if (asString.IsNullOrWhiteSpace()) continue; - - int parsed; - if (int.TryParse(asString, out parsed) == false) - { - yield return new ValidationResult("The value " + asString + " is not a valid number", new[] - { - //we'll make the server field the index number of the value so it can be wired up to the view - "item_" + index.ToInvariantString() - }); - } - } - } - } + foreach (var entity in args.SavedEntities) + AutoFillProperties(entity); } + /// + /// After a content item has been saved, auto-fill the properties. + /// + /// The event sender. + /// The event arguments. + static void ContentServiceSaving(IContentService sender, Core.Events.SaveEventArgs args) + { + foreach (var entity in args.SavedEntities) + AutoFillProperties(entity); + } + + /// + /// Auto-fill properties (or clear). + /// + /// The content. + static void AutoFillProperties(IContentBase content) + { + var properties = content.Properties.Where(x => IsUploadField(x, false)); + + foreach (var property in properties) + { + var autoFillConfig = MediaFileSystem.UploadAutoFillProperties.GetConfig(property.Alias); + if (autoFillConfig == null) continue; + + var svalue = property.Value as string; + if (string.IsNullOrWhiteSpace(svalue)) + MediaFileSystem.UploadAutoFillProperties.Reset(content, autoFillConfig); + else + MediaFileSystem.UploadAutoFillProperties.Populate(content, autoFillConfig, MediaFileSystem.GetRelativePath(svalue)); + } + } + #region Application event handler, used to bind to events on startup - private readonly FileUploadPropertyEditorApplicationStartup _applicationStartup = new FileUploadPropertyEditorApplicationStartup(); - - /// - /// we're using a sub -class because this has the logic to prevent it from executing if the application is not configured - /// - private class FileUploadPropertyEditorApplicationStartup : ApplicationEventHandler - { - /// - /// We're going to bind to the MediaService Saving event so that we can populate the umbracoFile size, type, etc... label fields - /// if we find any attached to the current media item. - /// - protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) - { - MediaService.Saving += MediaServiceSaving; - MediaService.Created += MediaServiceCreating; - ContentService.Copied += ContentServiceCopied; - - MediaService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - MediaService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - ContentService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - ContentService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - MemberService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - } - } + // The FileUploadPropertyEditor properties own files and as such must manage these files, + // so we are binding to events in order to make sure that + // - files are deleted when the owning content/media is + // - files are copied when the owning content is + // - populate the auto-fill properties when the owning content/media is saved + // + // NOTE: + // although some code fragments seem to want to support uploading multiple files, + // this is NOT a feature of the FileUploadPropertyEditor and is NOT supported + // + // auto-fill properties are recalculated EVERYTIME the content/media is saved, + // even if the property has NOT been modified (it could be the same filename but + // a different file) - this is accepted (auto-fill props should die) + // + // TODO in v8: + // for some weird backward compatibility reasons, + // - media copy is not supported + // - auto-fill properties are not supported for content items + // - auto-fill runs on MediaService.Created which makes no sense (no properties yet) public void OnApplicationInitialized(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - //wrap - _applicationStartup.OnApplicationInitialized(umbracoApplication, applicationContext); + // nothing } + public void OnApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - //wrap - _applicationStartup.OnApplicationStarting(umbracoApplication, applicationContext); + // nothing } + public void OnApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - //wrap - _applicationStartup.OnApplicationStarted(umbracoApplication, applicationContext); - } - #endregion + // only if the app is configured + // see ApplicationEventHandler.ShouldExecute + if (applicationContext.IsConfigured == false || applicationContext.DatabaseContext.IsDatabaseConfigured == false) + return; + MediaService.Created += MediaServiceCreated; // see above - makes no sense + MediaService.Saving += MediaServiceSaving; + //MediaService.Copied += MediaServiceCopied; // see above - missing + + ContentService.Copied += ContentServiceCopied; + //ContentService.Saving += ContentServiceSaving; // see above - missing + MediaService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); + + MediaService.EmptiedRecycleBin += (sender, args) => args.Files.AddRange( + GetFilesToDelete(args.AllPropertyData.SelectMany(x => x.Value))); + + ContentService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); + + ContentService.EmptiedRecycleBin += (sender, args) => args.Files.AddRange( + GetFilesToDelete(args.AllPropertyData.SelectMany(x => x.Value))); + + MemberService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); + } + + #endregion } } diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs index 221dde865f..777a14b768 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -1,178 +1,140 @@ using System; using System.Collections.Generic; using System.Drawing; -using System.Globalization; using System.IO; using System.Linq; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Media; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; using Umbraco.Web.Models.ContentEditing; -using umbraco; -using umbraco.cms.businesslogic.Files; -using Umbraco.Core; namespace Umbraco.Web.PropertyEditors { /// - /// The editor for the file upload property editor + /// The value editor for the file upload property editor. /// internal class FileUploadPropertyValueEditor : PropertyValueEditorWrapper { - public FileUploadPropertyValueEditor(PropertyValueEditor wrapped) : base(wrapped) + private readonly MediaFileSystem _mediaFileSystem; + + public FileUploadPropertyValueEditor(PropertyValueEditor wrapped, MediaFileSystem mediaFileSystem) + : base(wrapped) { + _mediaFileSystem = mediaFileSystem; } /// - /// Overrides the deserialize value so that we can save the file accordingly + /// Converts the value received from the editor into the value can be stored in the database. /// - /// - /// This is value passed in from the editor. We normally don't care what the editorValue.Value is set to because - /// we are more interested in the files collection associated with it, however we do care about the value if we - /// are clearing files. By default the editorValue.Value will just be set to the name of the file (but again, we - /// just ignore this and deal with the file collection in editorValue.AdditionalData.ContainsKey("files") ) - /// - /// - /// The current value persisted for this property. This will allow us to determine if we want to create a new - /// file path or use the existing file path. - /// - /// + /// The value received from the editor. + /// The current value of the property + /// The converted value. + /// + /// The is used to re-use the folder, if possible. + /// The is value passed in from the editor. We normally don't care what + /// the editorValue.Value is set to because we are more interested in the files collection associated with it, + /// however we do care about the value if we are clearing files. By default the editorValue.Value will just + /// be set to the name of the file - but again, we just ignore this and deal with the file collection in + /// editorValue.AdditionalData.ContainsKey("files") + /// We only process ONE file. We understand that the current value may contain more than one file, + /// and that more than one file may be uploaded, so we take care of them all, but we only store ONE file. + /// Other places (FileUploadPropertyEditor...) do NOT deal with multiple files, and our logic for reusing + /// folders would NOT work, etc. + /// public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) { - if (currentValue == null) - { - currentValue = string.Empty; - } + currentValue = currentValue ?? string.Empty; - //if the value is the same then just return the current value so we don't re-process everything - if (string.IsNullOrEmpty(currentValue.ToString()) == false && editorValue.Value == currentValue.ToString()) - { + // at that point, + // currentValue is either empty or "/media/path/to/img.jpg" + // editorValue.Value is { "clearFiles": true } or { "selectedFiles": "img1.jpg,img2.jpg" } + // comparing them makes little sense + + // check the editorValue value to see whether we need to clear files + var editorJsonValue = editorValue.Value as JObject; + var clears = editorJsonValue != null && editorJsonValue["clearFiles"] != null && editorJsonValue["clearFiles"].Value(); + var uploads = editorValue.AdditionalData.ContainsKey("files") && editorValue.AdditionalData["files"] is IEnumerable; + + // nothing = no changes, return what we have already (leave existing files intact) + if (clears == false && uploads == false) return currentValue; + + // get the current file paths + var currentPaths = currentValue.ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => _mediaFileSystem.GetRelativePath(x)) // get the fs-relative path + .ToArray(); + + // if clearing, remove these files and return + if (clears) + { + foreach (var pathToRemove in currentPaths) + _mediaFileSystem.DeleteFile(pathToRemove, true); + return string.Empty; // no more files } - //check the editorValue value to see if we need to clear the files or not. - var clear = false; - var json = editorValue.Value as JObject; - if (json != null && json["clearFiles"] != null && json["clearFiles"].Value()) + // ensure we have the required guids + if (editorValue.AdditionalData.ContainsKey("cuid") == false // for the content item + || editorValue.AdditionalData.ContainsKey("puid") == false) // and the property type + throw new Exception("Missing cuid/puid additional data."); + var cuido = editorValue.AdditionalData["cuid"]; + var puido = editorValue.AdditionalData["puid"]; + if ((cuido is Guid) == false || (puido is Guid) == false) + throw new Exception("Invalid cuid/puid additional data."); + var cuid = (Guid) cuido; + var puid = (Guid) puido; + if (cuid == Guid.Empty || puid == Guid.Empty) + throw new Exception("Invalid cuid/puid additional data."); + + // process the files + var files = ((IEnumerable) editorValue.AdditionalData["files"]).ToArray(); + + var newPaths = new List(); + const int maxLength = 1; // we only process ONE file + for (var i = 0; i < maxLength /*files.Length*/; i++) { - clear = json["clearFiles"].Value(); - } + var file = files[i]; - var currentPersistedValues = new string[] { }; - if (string.IsNullOrEmpty(currentValue.ToString()) == false) - { - currentPersistedValues = currentValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } + // skip invalid files + if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) + continue; - var newValue = new List(); + // get the filepath + // in case we are using the old path scheme, try to re-use numbers (bah...) + var reuse = i < currentPaths.Length ? currentPaths[i] : null; // this would be WRONG with many files + var filepath = _mediaFileSystem.GetMediaPath(file.FileName, reuse, cuid, puid); // fs-relative path - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - - if (clear) - { - //Remove any files that are saved for this item - foreach (var toRemove in currentPersistedValues) + using (var filestream = File.OpenRead(file.TempFilePath)) { - fs.DeleteFile(fs.GetRelativePath(toRemove), true); - } - return ""; - } + _mediaFileSystem.AddFile(filepath, filestream, true); // must overwrite! - //check for any files - if (editorValue.AdditionalData.ContainsKey("files")) - { - var files = editorValue.AdditionalData["files"] as IEnumerable; - if (files != null) - { - //now we just need to move the files to where they should be - var filesAsArray = files.ToArray(); - //a list of all of the newly saved files so we can compare with the current saved files and remove the old ones - var savedFilePaths = new List(); - for (var i = 0; i < filesAsArray.Length; i++) + var ext = _mediaFileSystem.GetExtension(filepath); + if (_mediaFileSystem.IsImageFile(ext)) { - var file = filesAsArray[i]; - - //don't continue if this is not allowed! - if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) - { - continue; - } - - //TODO: ALl of this naming logic needs to be put into the ImageHelper and then we need to change ContentExtensions to do the same! - - var currentPersistedFile = currentPersistedValues.Length >= (i + 1) - ? currentPersistedValues[i] - : ""; - - var name = IOHelper.SafeFileName(file.FileName.Substring(file.FileName.LastIndexOf(IOHelper.DirSepChar) + 1, file.FileName.Length - file.FileName.LastIndexOf(IOHelper.DirSepChar) - 1).ToLower()); - - var subfolder = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? currentPersistedFile.Replace(fs.GetUrl("/"), "").Split('/')[0] - : currentPersistedFile.Substring(currentPersistedFile.LastIndexOf("/", StringComparison.Ordinal) + 1).Split('-')[0]; - - int subfolderId; - var numberedFolder = int.TryParse(subfolder, out subfolderId) - ? subfolderId.ToString(CultureInfo.InvariantCulture) - : MediaSubfolderCounter.Current.Increment().ToString(CultureInfo.InvariantCulture); - - var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? Path.Combine(numberedFolder, name) - : numberedFolder + "-" + name; - - using (var fileStream = File.OpenRead(file.TempFilePath)) - { - var umbracoFile = UmbracoMediaFile.Save(fileStream, fileName); - - if (umbracoFile.SupportsResizing) - { - var additionalSizes = new List(); - //get the pre-vals value - var thumbs = editorValue.PreValues.FormatAsDictionary(); - if (thumbs.Any()) - { - var thumbnailSizes = thumbs.First().Value.Value; - // additional thumbnails configured as prevalues on the DataType - foreach (var thumb in thumbnailSizes.Split(new[] { ";", "," }, StringSplitOptions.RemoveEmptyEntries)) - { - int thumbSize; - if (thumb == "" || int.TryParse(thumb, out thumbSize) == false) continue; - additionalSizes.Add(thumbSize); - } - } - - using (var image = Image.FromStream(fileStream)) - { - ImageHelper.GenerateMediaThumbnails(fs, fileName, umbracoFile.Extension, image, additionalSizes); - } - } - - newValue.Add(umbracoFile.Url); - //add to the saved paths - savedFilePaths.Add(umbracoFile.Url); - } - //now remove the temp file - File.Delete(file.TempFilePath); + var preValues = editorValue.PreValues.FormatAsDictionary(); + var sizes = preValues.Any() ? preValues.First().Value.Value : string.Empty; + using (var image = Image.FromStream(filestream)) + _mediaFileSystem.GenerateThumbnails(image, filepath, sizes); } - //Remove any files that are no longer saved for this item - foreach (var toRemove in currentPersistedValues.Except(savedFilePaths)) - { - fs.DeleteFile(fs.GetRelativePath(toRemove), true); - } + // all related properties (auto-fill) are managed by FileUploadPropertyEditor + // when the content is saved (through event handlers) - - return string.Join(",", newValue); + newPaths.Add(filepath); } } - //if we've made it here, we had no files to save and we were not clearing anything so just persist the same value we had before - return currentValue; + // remove all temp files + foreach (var file in files) + File.Delete(file.TempFilePath); + + // remove files that are not there anymore + foreach (var pathToRemove in currentPaths.Except(newPaths)) + _mediaFileSystem.DeleteFile(pathToRemove, true); + + + return string.Join(",", newPaths.Select(x => _mediaFileSystem.GetUrl(x))); } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/FolderBrowserPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FolderBrowserPropertyEditor.cs index bfdbe368d1..5c89b868b8 100644 --- a/src/Umbraco.Web/PropertyEditors/FolderBrowserPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FolderBrowserPropertyEditor.cs @@ -9,7 +9,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { [Obsolete("This is no longer used by default, use the ListViewPropertyEditor instead")] - [PropertyEditor(Constants.PropertyEditors.FolderBrowserAlias, "(Obsolete) Folder Browser", "folderbrowser", HideLabel=true, Icon="icon-folder", Group="media")] + [PropertyEditor(Constants.PropertyEditors.FolderBrowserAlias, "(Obsolete) Folder Browser", "folderbrowser", HideLabel=true, Icon="icon-folder", Group="media", IsDeprecated = true)] public class FolderBrowserPropertyEditor : PropertyEditor { diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index db9792572f..af30b4ceeb 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -31,6 +31,8 @@ namespace Umbraco.Web.PropertyEditors { try { + //TODO: We should deserialize this to Umbraco.Core.Models.GridValue instead of doing the below + var json = JsonConvert.DeserializeObject(e.Fields[field.Name]); //check if this is formatted for grid json diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs index 14c267cf7d..5196fda0ee 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs @@ -3,12 +3,10 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; @@ -18,22 +16,18 @@ namespace Umbraco.Web.PropertyEditors [PropertyEditor(Constants.PropertyEditors.ImageCropperAlias, "Image Cropper", "imagecropper", ValueType = PropertyEditorValueTypes.Json, HideLabel = false, Group="media", Icon="icon-crop")] public class ImageCropperPropertyEditor : PropertyEditor, IApplicationEventHandler { + // preValues + private IDictionary _internalPreValues; + + public override IDictionary DefaultPreValues + { + get { return _internalPreValues; } + set { _internalPreValues = value; } + } + /// - /// Creates our custom value editor + /// Initializes a new instance of the class. /// - /// - protected override PropertyValueEditor CreateValueEditor() - { - var baseEditor = base.CreateValueEditor(); - return new ImageCropperPropertyValueEditor(baseEditor); - } - - protected override PreValueEditor CreatePreValueEditor() - { - return new ImageCropperPreValueEditor(); - } - - public ImageCropperPropertyEditor() { _internalPreValues = new Dictionary @@ -43,195 +37,194 @@ namespace Umbraco.Web.PropertyEditors }; } - /// - /// Ensures any files associated are removed - /// - /// - static IEnumerable ServiceEmptiedRecycleBin(Dictionary> allPropertyData) + private static MediaFileSystem MediaFileSystem { - var list = new List(); - //Get all values for any image croppers found - foreach (var cropperVal in allPropertyData - .SelectMany(x => x.Value) - .Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias) - .Select(x => x.Value) - .WhereNotNull()) + // v8 will get rid of singletons + get { return FileSystemProviderManager.Current.MediaFileSystem; } + } + + /// + /// Creates the corresponding property value editor. + /// + /// The corresponding property value editor. + protected override PropertyValueEditor CreateValueEditor() + { + var baseEditor = base.CreateValueEditor(); + return new ImageCropperPropertyValueEditor(baseEditor, MediaFileSystem); + } + + /// + /// Creates the corresponding preValue editor. + /// + /// The corresponding preValue editor. + protected override PreValueEditor CreatePreValueEditor() + { + return new ImageCropperPreValueEditor(); + } + + /// + /// Gets a value indicating whether a property is an image cropper field. + /// + /// The property. + /// A value indicating whether to check that the property has a non-empty value. + /// A value indicating whether a property is an image cropper field, and (optionaly) has a non-empty value. + private static bool IsCropperField(Property property, bool ensureValue) + { + if (property.PropertyType.PropertyEditorAlias != Constants.PropertyEditors.ImageCropperAlias) + return false; + if (ensureValue == false) + return true; + return property.Value is string && string.IsNullOrWhiteSpace((string)property.Value) == false; + } + + /// + /// Parses the property value into a json object. + /// + /// The property value. + /// A value indicating whether to log the error. + /// The json object corresponding to the property value. + /// In case of an error, optionaly logs the error and returns null. + private static JObject GetJObject(string value, bool writeLog) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + try { - JObject json; - try + return JsonConvert.DeserializeObject(value); + } + catch (Exception ex) + { + if (writeLog) + LogHelper.Error("Could not parse image cropper value \"" + value + "\"", ex); + return null; + } + } + + /// + /// Gets the files that need to be deleted when entities are deleted. + /// + /// The properties that were deleted. + static IEnumerable GetFilesToDelete(IEnumerable properties) + { + return properties.Where(x => IsCropperField(x, true)).Select(x => + { + var jo = GetJObject((string) x.Value, true); + if (jo == null || jo["src"] == null) return null; + var src = jo["src"].Value(); + return string.IsNullOrWhiteSpace(src) ? null : MediaFileSystem.GetRelativePath(src); + }).WhereNotNull(); + } + + /// + /// After a content has been copied, also copy uploaded files. + /// + /// The event sender. + /// The event arguments. + static void ContentServiceCopied(IContentService sender, Core.Events.CopyEventArgs args) + { + // get the image cropper field properties with a value + var properties = args.Original.Properties.Where(x => IsCropperField(x, true)); + + // copy files + var isUpdated = false; + foreach (var property in properties) + { + var jo = GetJObject((string) property.Value, true); + if (jo == null || jo["src"] == null) continue; + + var src = jo["src"].Value(); + if (string.IsNullOrWhiteSpace(src)) continue; + + var sourcePath = MediaFileSystem.GetRelativePath(src); + var copyPath = MediaFileSystem.CopyFile(args.Copy, property.PropertyType, sourcePath); + jo["src"] = MediaFileSystem.GetUrl(copyPath); + args.Copy.SetValue(property.Alias, jo.ToString()); + isUpdated = true; + } + + // if updated, re-save the copy with the updated value + if (isUpdated) + sender.Save(args.Copy); + } + + /// + /// After a media has been created, auto-fill the properties. + /// + /// The event sender. + /// The event arguments. + static void MediaServiceCreated(IMediaService sender, Core.Events.NewEventArgs args) + { + AutoFillProperties(args.Entity); + } + + /// + /// After a media has been saved, auto-fill the properties. + /// + /// The event sender. + /// The event arguments. + static void MediaServiceSaving(IMediaService sender, Core.Events.SaveEventArgs args) + { + foreach (var entity in args.SavedEntities) + AutoFillProperties(entity); + } + + /// + /// After a content item has been saved, auto-fill the properties. + /// + /// The event sender. + /// The event arguments. + static void ContentServiceSaving(IContentService sender, Core.Events.SaveEventArgs args) + { + foreach (var entity in args.SavedEntities) + AutoFillProperties(entity); + } + + /// + /// Auto-fill properties (or clear). + /// + /// The content. + static void AutoFillProperties(IContentBase content) + { + var properties = content.Properties.Where(x => IsCropperField(x, false)); + + foreach (var property in properties) + { + var autoFillConfig = MediaFileSystem.UploadAutoFillProperties.GetConfig(property.Alias); + if (autoFillConfig == null) continue; + + var svalue = property.Value as string; + if (string.IsNullOrWhiteSpace(svalue)) { - json = JsonConvert.DeserializeObject(cropperVal.ToString()); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred parsing the value stored in the image cropper value: " + cropperVal, ex); + MediaFileSystem.UploadAutoFillProperties.Reset(content, autoFillConfig); continue; } - if (json["src"] != null && json["src"].ToString().IsNullOrWhiteSpace() == false) + var jo = GetJObject(svalue, false); + string src; + if (jo == null) { - list.Add(json["src"].ToString()); + // so we have a non-empty string value that cannot be parsed into a json object + // see http://issues.umbraco.org/issue/U4-4756 + // it can happen when an image is uploaded via the folder browser, in which case + // the property value will be the file source eg '/media/23454/hello.jpg' and we + // are fixing that anomaly here - does not make any sense at all but... bah... + var config = ApplicationContext.Current.Services.DataTypeService + .GetPreValuesByDataTypeId(property.PropertyType.DataTypeDefinitionId).FirstOrDefault(); + var crops = string.IsNullOrWhiteSpace(config) ? "[]" : config; + src = svalue; + property.Value = "{\"src\": \"" + svalue + "\", \"crops\": " + crops + "}"; } + else + { + src = jo["src"] == null ? null : jo["src"].Value(); + } + + if (src == null) + MediaFileSystem.UploadAutoFillProperties.Reset(content, autoFillConfig); + else + MediaFileSystem.UploadAutoFillProperties.Populate(content, autoFillConfig, MediaFileSystem.GetRelativePath(src)); } - return list; - } - - /// - /// Ensures any files associated are removed - /// - /// - static IEnumerable ServiceDeleted(IEnumerable deletedEntities) - { - var list = new List(); - foreach (var property in deletedEntities.SelectMany(deletedEntity => deletedEntity - .Properties - .Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias - && x.Value != null - && string.IsNullOrEmpty(x.Value.ToString()) == false))) - { - JObject json; - try - { - json = JsonConvert.DeserializeObject(property.Value.ToString()); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred parsing the value stored in the image cropper value: " + property.Value, ex); - continue; - } - - if (json["src"] != null && json["src"].ToString().IsNullOrWhiteSpace() == false) - { - list.Add(json["src"].ToString()); - } - } - return list; - } - - /// - /// After the content is copied we need to check if there are files that also need to be copied - /// - /// - /// - static void ContentServiceCopied(IContentService sender, Core.Events.CopyEventArgs e) - { - if (e.Original.Properties.Any(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias)) - { - bool isUpdated = false; - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - - //Loop through properties to check if the content contains media that should be deleted - foreach (var property in e.Original.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias - && x.Value != null - && string.IsNullOrEmpty(x.Value.ToString()) == false)) - { - JObject json; - try - { - json = JsonConvert.DeserializeObject(property.Value.ToString()); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred parsing the value stored in the image cropper value: " + property.Value.ToString(), ex); - continue; - } - - if (json["src"] != null && json["src"].ToString().IsNullOrWhiteSpace() == false) - { - if (fs.FileExists(fs.GetRelativePath(json["src"].ToString()))) - { - var currentPath = fs.GetRelativePath(json["src"].ToString()); - var propertyId = e.Copy.Properties.First(x => x.Alias == property.Alias).Id; - var newPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(currentPath)); - - fs.CopyFile(currentPath, newPath); - json["src"] = fs.GetUrl(newPath); - e.Copy.SetValue(property.Alias, json.ToString()); - - //Copy thumbnails - foreach (var thumbPath in fs.GetThumbnails(currentPath)) - { - var newThumbPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(thumbPath)); - fs.CopyFile(thumbPath, newThumbPath); - } - isUpdated = true; - } - } - - - } - - if (isUpdated) - { - //need to re-save the copy with the updated path value - sender.Save(e.Copy); - } - } - } - - static void MediaServiceCreated(IMediaService sender, Core.Events.NewEventArgs e) - { - AutoFillProperties(e.Entity); - } - - static void MediaServiceSaving(IMediaService sender, Core.Events.SaveEventArgs e) - { - foreach (var m in e.SavedEntities) - { - AutoFillProperties(m); - } - } - - static void AutoFillProperties(IContentBase model) - { - foreach (var p in model.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias)) - { - var uploadFieldConfigNode = - UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties - .FirstOrDefault(x => x.Alias == p.Alias); - - if (uploadFieldConfigNode != null) - { - if (p.Value != null) - { - JObject json = null; - try - { - json = JObject.Parse((string)p.Value); - } - catch (JsonException) - { - //note: we are swallowing this exception because in some cases a normal string/non json value will be passed in which will just be the - // file path like /media/23454/hello.jpg - // This will happen everytime an image is uploaded via the folder browser and we don't really want to pollute the log since it's not actually - // a problem and we take care of this below. - // see: http://issues.umbraco.org/issue/U4-4756 - } - if (json != null && json["src"] != null) - { - model.PopulateFileMetaDataProperties(uploadFieldConfigNode, json["src"].Value()); - } - else if (p.Value is string) - { - var src = p.Value == null ? string.Empty : p.Value.ToString(); - var config = ApplicationContext.Current.Services.DataTypeService.GetPreValuesByDataTypeId(p.PropertyType.DataTypeDefinitionId).FirstOrDefault(); - var crops = string.IsNullOrEmpty(config) == false ? config : "[]"; - p.Value = "{src: '" + p.Value + "', crops: " + crops + "}"; - //Only provide the source path, not the whole JSON value - model.PopulateFileMetaDataProperties(uploadFieldConfigNode, src); - } - } - else - model.ResetFileMetaDataProperties(uploadFieldConfigNode); - } - } - } - - private IDictionary _internalPreValues; - public override IDictionary DefaultPreValues - { - get { return _internalPreValues; } - set { _internalPreValues = value; } } internal class ImageCropperPreValueEditor : PreValueEditor @@ -242,51 +235,66 @@ namespace Umbraco.Web.PropertyEditors #region Application event handler, used to bind to events on startup - private readonly FileUploadPropertyEditorApplicationStartup _applicationStartup = new FileUploadPropertyEditorApplicationStartup(); - - /// - /// we're using a sub -class because this has the logic to prevent it from executing if the application is not configured - /// - private class FileUploadPropertyEditorApplicationStartup : ApplicationEventHandler - { - /// - /// We're going to bind to the MediaService Saving event so that we can populate the umbracoFile size, type, etc... label fields - /// if we find any attached to the current media item. - /// - protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) - { - MediaService.Saving += MediaServiceSaving; - MediaService.Created += MediaServiceCreated; - ContentService.Copied += ContentServiceCopied; - - MediaService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - MediaService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - ContentService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - ContentService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - MemberService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - } - } + // The ImageCropperPropertyEditor properties own files and as such must manage these files, + // so we are binding to events in order to make sure that + // - files are deleted when the owning content/media is + // - files are copied when the owning content is (NOTE: not supporting media copy here!) + // - populate the auto-fill properties when files are changing + // - populate the auto-fill properties when the owning content/media is saved + // + // NOTE: + // uploading multiple files is NOT a feature of the ImageCropperPropertyEditor + // + // auto-fill properties are recalculated EVERYTIME the content/media is saved, + // even if the property has NOT been modified (it could be the same filename but + // a different file) - this is accepted (auto-fill props should die) + // + // TODO in v8: + // for some weird backward compatibility reasons, + // - media copy is not supported + // - auto-fill properties are not supported for content items + // - auto-fill runs on MediaService.Created which makes no sense (no properties yet) public void OnApplicationInitialized(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - //wrap - _applicationStartup.OnApplicationInitialized(umbracoApplication, applicationContext); + // nothing } + public void OnApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - //wrap - _applicationStartup.OnApplicationStarting(umbracoApplication, applicationContext); + // nothing } + public void OnApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - //wrap - _applicationStartup.OnApplicationStarted(umbracoApplication, applicationContext); + // only if the app is configured + // see ApplicationEventHandler.ShouldExecute + if (applicationContext.IsConfigured == false || applicationContext.DatabaseContext.IsDatabaseConfigured == false) + return; + + MediaService.Created += MediaServiceCreated; // see above - makes no sense + MediaService.Saving += MediaServiceSaving; + //MediaService.Copied += MediaServiceCopied; // see above - missing + + ContentService.Copied += ContentServiceCopied; + //ContentService.Saving += ContentServiceSaving; // see above - missing + + MediaService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); + + MediaService.EmptiedRecycleBin += (sender, args) => args.Files.AddRange( + GetFilesToDelete(args.AllPropertyData.SelectMany(x => x.Value))); + + ContentService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); + + ContentService.EmptiedRecycleBin += (sender, args) => args.Files.AddRange( + GetFilesToDelete(args.AllPropertyData.SelectMany(x => x.Value))); + + MemberService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); } + #endregion } } diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index a32a764929..0e6500f3d8 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -1,37 +1,38 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; +using System.Drawing; using System.Linq; -using System.Text; -using System.Threading.Tasks; - using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; -using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; +using File = System.IO.File; namespace Umbraco.Web.PropertyEditors { + /// + /// The value editor for the image cropper property editor. + /// internal class ImageCropperPropertyValueEditor : PropertyValueEditorWrapper { - public ImageCropperPropertyValueEditor(PropertyValueEditor wrapped) : base(wrapped) - { + private readonly MediaFileSystem _mediaFileSystem; + public ImageCropperPropertyValueEditor(PropertyValueEditor wrapped, MediaFileSystem mediaFileSystem) + : base(wrapped) + { + _mediaFileSystem = mediaFileSystem; } /// /// This is called to merge in the prevalue crops with the value that is saved - similar to the property value converter for the front-end /// - + public override object ConvertDbToEditor(Property property, PropertyType propertyType, IDataTypeService dataTypeService) { var val = base.ConvertDbToEditor(property, propertyType, dataTypeService); @@ -45,139 +46,144 @@ namespace Umbraco.Web.PropertyEditors return val; } - /// - /// Overrides the deserialize value so that we can save the file accordingly + /// Converts the value received from the editor into the value can be stored in the database. /// - /// - /// This is value passed in from the editor. We normally don't care what the editorValue.Value is set to because - /// we are more interested in the files collection associated with it, however we do care about the value if we - /// are clearing files. By default the editorValue.Value will just be set to the name of the file (but again, we - /// just ignore this and deal with the file collection in editorValue.AdditionalData.ContainsKey("files") ) - /// - /// - /// The current value persisted for this property. This will allow us to determine if we want to create a new - /// file path or use the existing file path. - /// - /// + /// The value received from the editor. + /// The current value of the property + /// The converted value. + /// + /// The is used to re-use the folder, if possible. + /// editorValue.Value is used to figure out editorFile and, if it has been cleared, remove the old file - but + /// it is editorValue.AdditionalData["files"] that is used to determine the actual file that has been uploaded. + /// public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) { - - - string oldFile = string.Empty; - string newFile = string.Empty; - JObject newJson = null; - JObject oldJson = null; - - //get the old src path - if (currentValue != null && string.IsNullOrEmpty(currentValue.ToString()) == false) + // get the current path + var currentPath = string.Empty; + try { - try - { - oldJson = JObject.Parse(currentValue.ToString()); - } - catch (Exception ex) - { - //for some reason the value is invalid so continue as if there was no value there - LogHelper.WarnWithException("Could not parse current db value to a JObject", ex); - } - - if (oldJson != null && oldJson["src"] != null) - { - oldFile = oldJson["src"].Value(); - } + var svalue = currentValue as string; + var currentJson = string.IsNullOrWhiteSpace(svalue) ? null : JObject.Parse(svalue); + if (currentJson != null && currentJson["src"] != null) + currentPath = currentJson["src"].Value(); } + catch (Exception ex) + { + // for some reason the value is invalid so continue as if there was no value there + LogHelper.WarnWithException("Could not parse current db value to a JObject.", ex); + } + if (string.IsNullOrWhiteSpace(currentPath) == false) + currentPath = _mediaFileSystem.GetRelativePath(currentPath); - //get the new src path + // get the new json and path + JObject editorJson = null; + var editorFile = string.Empty; if (editorValue.Value != null) { - newJson = editorValue.Value as JObject; - if (newJson != null && newJson["src"] != null) - { - newFile = newJson["src"].Value(); - } + editorJson = editorValue.Value as JObject; + if (editorJson != null && editorJson["src"] != null) + editorFile = editorJson["src"].Value(); } - //compare old and new src path - //if not alike, that means we have a new file, or delete the current one... - if (string.IsNullOrEmpty(newFile) || editorValue.AdditionalData.ContainsKey("files")) + // ensure we have the required guids + if (editorValue.AdditionalData.ContainsKey("cuid") == false // for the content item + || editorValue.AdditionalData.ContainsKey("puid") == false) // and the property type + throw new Exception("Missing cuid/puid additional data."); + var cuido = editorValue.AdditionalData["cuid"]; + var puido = editorValue.AdditionalData["puid"]; + if ((cuido is Guid) == false || (puido is Guid) == false) + throw new Exception("Invalid cuid/puid additional data."); + var cuid = (Guid)cuido; + var puid = (Guid)puido; + if (cuid == Guid.Empty || puid == Guid.Empty) + throw new Exception("Invalid cuid/puid additional data."); + + // editorFile is empty whenever a new file is being uploaded + // or when the file is cleared (in which case editorJson is null) + // else editorFile contains the unchanged value + + var uploads = editorValue.AdditionalData.ContainsKey("files") && editorValue.AdditionalData["files"] is IEnumerable; + var files = uploads ? ((IEnumerable)editorValue.AdditionalData["files"]).ToArray() : new ContentItemFile[0]; + var file = uploads ? files.FirstOrDefault() : null; + + if (file == null) // not uploading a file { - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - - //if we have an existing file, delete it - if (string.IsNullOrEmpty(oldFile) == false) - fs.DeleteFile(fs.GetRelativePath(oldFile), true); - else - oldFile = string.Empty; - - //if we have a new file, add it to the media folder and set .src - - if (editorValue.AdditionalData.ContainsKey("files")) + // if editorFile is empty then either there was nothing to begin with, + // or it has been cleared and we need to remove the file - else the + // value is unchanged. + if (string.IsNullOrWhiteSpace(editorFile) && string.IsNullOrWhiteSpace(currentPath) == false) { - var files = editorValue.AdditionalData["files"] as IEnumerable; - if (files != null && files.Any()) - { - var file = files.First(); - - if (UploadFileTypeValidator.ValidateFileExtension(file.FileName)) - { - //create name and folder number - var name = IOHelper.SafeFileName(file.FileName.Substring(file.FileName.LastIndexOf(IOHelper.DirSepChar) + 1, file.FileName.Length - file.FileName.LastIndexOf(IOHelper.DirSepChar) - 1).ToLower()); - - //try to reuse the folder number from the current file - var subfolder = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? oldFile.Replace(fs.GetUrl("/"), "").Split('/')[0] - : oldFile.Substring(oldFile.LastIndexOf("/", StringComparison.Ordinal) + 1).Split('-')[0]; - - //if we dont find one, create a new one - int subfolderId; - var numberedFolder = int.TryParse(subfolder, out subfolderId) - ? subfolderId.ToString(CultureInfo.InvariantCulture) - : MediaSubfolderCounter.Current.Increment().ToString(CultureInfo.InvariantCulture); - - //set a file name or full path - var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? Path.Combine(numberedFolder, name) - : numberedFolder + "-" + name; - - //save file and assign to the json - using (var fileStream = System.IO.File.OpenRead(file.TempFilePath)) - { - var umbracoFile = UmbracoMediaFile.Save(fileStream, fileName); - newJson["src"] = umbracoFile.Url; - - return newJson.ToString(); - } - } - } + _mediaFileSystem.DeleteFile(currentPath, true); + return null; // clear } + + return editorJson == null ? null : editorJson.ToString(); // unchanged } - //incase we submit nothing back - if (editorValue.Value == null) + // process the file + var filepath = editorJson == null ? null : ProcessFile(editorValue, file, currentPath, cuid, puid); + + // remove all temp files + foreach (var f in files) + File.Delete(f.TempFilePath); + + // remove current file if replaced + if (currentPath != filepath && string.IsNullOrWhiteSpace(currentPath) == false) + _mediaFileSystem.DeleteFile(currentPath, true); + + // update json and return + if (editorJson == null) return null; + editorJson["src"] = filepath == null ? string.Empty : _mediaFileSystem.GetUrl(filepath); + return editorJson.ToString(); + } + + private string ProcessFile(ContentPropertyData editorValue, ContentItemFile file, string currentPath, Guid cuid, Guid puid) + { + // process the file + // no file, invalid file, reject change + if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) return null; - return editorValue.Value.ToString(); - } - - + // get the filepath + // in case we are using the old path scheme, try to re-use numbers (bah...) + var filepath = _mediaFileSystem.GetMediaPath(file.FileName, currentPath, cuid, puid); // fs-relative path - public override string ConvertDbToString(Property property, PropertyType propertyType, Core.Services.IDataTypeService dataTypeService) + using (var filestream = File.OpenRead(file.TempFilePath)) + { + _mediaFileSystem.AddFile(filepath, filestream, true); // must overwrite! + + var ext = _mediaFileSystem.GetExtension(filepath); + if (_mediaFileSystem.IsImageFile(ext)) + { + var preValues = editorValue.PreValues.FormatAsDictionary(); + var sizes = preValues.Any() ? preValues.First().Value.Value : string.Empty; + using (var image = Image.FromStream(filestream)) + _mediaFileSystem.GenerateThumbnails(image, filepath, sizes); + } + + // all related properties (auto-fill) are managed by ImageCropperPropertyEditor + // when the content is saved (through event handlers) + } + + return filepath; + } + + public override string ConvertDbToString(Property property, PropertyType propertyType, IDataTypeService dataTypeService) { - if(property.Value == null || string.IsNullOrEmpty(property.Value.ToString())) + if (property.Value == null || string.IsNullOrEmpty(property.Value.ToString())) return null; - //if we dont have a json structure, we will get it from the property type + // if we dont have a json structure, we will get it from the property type var val = property.Value.ToString(); if (val.DetectIsJson()) return val; + // more magic here ;-( var config = dataTypeService.GetPreValuesByDataTypeId(propertyType.DataTypeDefinitionId).FirstOrDefault(); - var crops = !string.IsNullOrEmpty(config) ? config : "[]"; - var newVal = "{src: '" + val + "', crops: " + crops + "}"; + var crops = string.IsNullOrEmpty(config) ? "[]" : config; + var newVal = "{src: '" + val + "', crops: " + crops + "}"; return newVal; } } - - } diff --git a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs index 3ac5788dd2..b3e5390b64 100644 --- a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs @@ -9,7 +9,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MacroContainerAlias, "Macro container", "macrocontainer", ValueType = PropertyEditorValueTypes.Text, Group="rich content", Icon="icon-settings-alt")] + [PropertyEditor(Constants.PropertyEditors.MacroContainerAlias, "Macro Picker", "macrocontainer", ValueType = PropertyEditorValueTypes.Text, Group="rich content", Icon="icon-settings-alt", IsDeprecated = true)] public class MacroContainerPropertyEditor : PropertyEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs new file mode 100644 index 0000000000..2d9ee68b3b --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Media picker property editors that stores UDI + /// + [PropertyEditor(Constants.PropertyEditors.MediaPicker2Alias, "Media Picker", PropertyEditorValueTypes.Text, "mediapicker", IsParameterEditor = true, Group = "media", Icon = "icon-picture")] + public class MediaPicker2PropertyEditor : PropertyEditor + { + public MediaPicker2PropertyEditor() + { + InternalPreValues = new Dictionary + { + {"idType", "udi"} + }; + } + + internal IDictionary InternalPreValues; + + public override IDictionary DefaultPreValues + { + get { return InternalPreValues; } + set { InternalPreValues = value; } + } + + protected override PreValueEditor CreatePreValueEditor() + { + return new MediaPickerPreValueEditor(); + } + + internal class MediaPickerPreValueEditor : PreValueEditor + { + public MediaPickerPreValueEditor() + { + //create the fields + Fields.Add(new PreValueField() + { + Key = "multiPicker", + View = "boolean", + Name = "Pick multiple items" + }); + Fields.Add(new PreValueField() + { + Key = "onlyImages", + View = "boolean", + Name = "Pick only images", + Description = "Only let the editor choose images from media." + }); + Fields.Add(new PreValueField() + { + Key = "disableFolderSelect", + View = "boolean", + Name = "Disable folder select", + Description = "Do not allow folders to be picked." + }); + Fields.Add(new PreValueField() + { + Key = "startNodeId", + View = "mediapicker", + Name = "Start node", + Config = new Dictionary + { + {"idType", "udi"} + } + }); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 7e966d31ad..8828f89a0b 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -9,34 +9,23 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MediaPickerAlias, "Legacy Media Picker", PropertyEditorValueTypes.Integer, "mediapicker", Group="media", Icon="icon-picture")] - public class MediaPickerPropertyEditor : PropertyEditor + /// + /// Legacy media property editor that stores Integer Ids + /// + [Obsolete("This editor is obsolete, use ContentPicker2PropertyEditor instead which stores UDI")] + [PropertyEditor(Constants.PropertyEditors.MediaPickerAlias, "(Obsolete) Media Picker", PropertyEditorValueTypes.Integer, "mediapicker", Group = "media", Icon = "icon-picture", IsDeprecated = true)] + public class MediaPickerPropertyEditor : MediaPicker2PropertyEditor { public MediaPickerPropertyEditor() { InternalPreValues = new Dictionary { {"multiPicker", "0"}, - {"onlyImages", "0"} + {"onlyImages", "0"}, + {"idType", "int"} }; } - protected IDictionary InternalPreValues; - - protected override PropertyValueEditor CreateValueEditor() - { - //TODO: Need to add some validation to the ValueEditor to ensure that any media chosen actually exists! - return base.CreateValueEditor(); - } - - - - public override IDictionary DefaultPreValues - { - get { return InternalPreValues; } - set { InternalPreValues = value; } - } - protected override PreValueEditor CreatePreValueEditor() { return new SingleMediaPickerPreValueEditor(); @@ -44,8 +33,19 @@ namespace Umbraco.Web.PropertyEditors internal class SingleMediaPickerPreValueEditor : PreValueEditor { - [PreValueField("startNodeId", "Start node", "mediapicker")] - public int StartNodeId { get; set; } + public SingleMediaPickerPreValueEditor() + { + Fields.Add(new PreValueField() + { + Key = "startNodeId", + View = "mediapicker", + Name = "Start node", + Config = new Dictionary + { + {"idType", "int"} + } + }); + } } } } diff --git a/src/Umbraco.Web/PropertyEditors/MemberPicker2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MemberPicker2PropertyEditor.cs new file mode 100644 index 0000000000..eefa4debe5 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MemberPicker2PropertyEditor.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + [PropertyEditor(Constants.PropertyEditors.MemberPicker2Alias, "Member Picker", PropertyEditorValueTypes.String, "memberpicker", Group = "People", Icon = "icon-user")] + public class MemberPicker2PropertyEditor : PropertyEditor + { + public MemberPicker2PropertyEditor() + { + InternalPreValues = new Dictionary + { + {"idType", "udi"} + }; + } + + internal IDictionary InternalPreValues; + public override IDictionary DefaultPreValues + { + get { return InternalPreValues; } + set { InternalPreValues = value; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs index 7dadbfe24c..f39f824ca1 100644 --- a/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -8,8 +7,14 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MemberPickerAlias, "Member Picker", PropertyEditorValueTypes.Integer, "memberpicker", Group = "People", Icon = "icon-user")] - public class MemberPickerPropertyEditor : PropertyEditor + + [Obsolete("This editor is obsolete, use MemberPickerPropertyEditor2 instead which stores UDI")] + [PropertyEditor(Constants.PropertyEditors.MemberPickerAlias, "(Obsolete) Member Picker", PropertyEditorValueTypes.Integer, "memberpicker", Group = "People", Icon = "icon-user", IsDeprecated = true)] + public class MemberPickerPropertyEditor : MemberPicker2PropertyEditor { + public MemberPickerPropertyEditor() + { + InternalPreValues["idType"] = "int"; + } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs new file mode 100644 index 0000000000..f43ecd48be --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + [PropertyEditor(Constants.PropertyEditors.MultiNodeTreePicker2Alias, "Multinode Treepicker", PropertyEditorValueTypes.Text, "contentpicker", Group="pickers", Icon="icon-page-add")] + public class MultiNodeTreePicker2PropertyEditor : PropertyEditor + { + public MultiNodeTreePicker2PropertyEditor() + { + InternalPreValues = new Dictionary + { + {"multiPicker", "1"}, + {"showOpenButton", "0"}, + {"showEditButton", "0"}, + {"showPathOnHover", "0"}, + {"idType", "udi"} + }; + } + + protected override PreValueEditor CreatePreValueEditor() + { + return new MultiNodePickerPreValueEditor(); + } + + internal IDictionary InternalPreValues; + public override IDictionary DefaultPreValues + { + get { return InternalPreValues; } + set { InternalPreValues = value; } + } + + internal class MultiNodePickerPreValueEditor : PreValueEditor + { + public MultiNodePickerPreValueEditor() + { + //create the fields + Fields.Add(new PreValueField() + { + Key = "startNode", + View = "treesource", + Name = "Node type", + Config = new Dictionary + { + {"idType", "udi"} + } + }); + Fields.Add(new PreValueField() + { + Key = "filter", + View = "textstring", + Name = "Allow items of type", + Description = "Separate with comma" + }); + Fields.Add(new PreValueField() + { + Key = "minNumber", + View = "number", + Name = "Minimum number of items" + }); + Fields.Add(new PreValueField() + { + Key = "maxNumber", + View = "number", + Name = "Maximum number of items" + }); + Fields.Add(new PreValueField() + { + Key = "showOpenButton", + View = "boolean", + Name = "Show open button (this feature is in preview!)", + Description = "Opens the node in a dialog" + }); + } + + /// + /// This ensures the multiPicker pre-val is set based on the maxNumber of nodes set + /// + /// + /// + /// + /// + /// Due to compatibility with 7.0.0 the multiPicker pre-val might already exist in the db, but we've removed that setting in 7.0.1 so we need to detect it and if it is + /// there, then we'll set the maxNumber to '1' + /// + public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, PreValueCollection persistedPreVals) + { + var result = base.ConvertDbToEditor(defaultPreVals, persistedPreVals); + + //backwards compatibility check + if (result.ContainsKey("multiPicker") && result["multiPicker"].ToString() == "0") + { + result["maxNumber"] = "1"; + } + + //set the multiPicker val correctly depending on the maxNumber + if (result.ContainsKey("maxNumber")) + { + var asNumber = result["maxNumber"].TryConvertTo(); + if (asNumber.Success) + { + if (asNumber.Result <= 1) + { + result["multiPicker"] = "0"; + } + else + { + result["multiPicker"] = "1"; + } + } + } + + + return result; + } + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index b3d3f02448..a3c1e64af0 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -1,101 +1,28 @@ -using System.Collections.Generic; +using System; +using System.Linq; using Umbraco.Core; -using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MultiNodeTreePickerAlias, "Multinode Treepicker", "contentpicker", Group="pickers", Icon="icon-page-add")] - public class MultiNodeTreePickerPropertyEditor : PropertyEditor + [Obsolete("This editor is obsolete, use MultiNodeTreePickerPropertyEditor2 instead which stores UDI")] + [PropertyEditor(Constants.PropertyEditors.MultiNodeTreePickerAlias, "(Obsolete) Multinode Treepicker", "contentpicker", Group = "pickers", Icon = "icon-page-add", IsDeprecated = true)] + public class MultiNodeTreePickerPropertyEditor : MultiNodeTreePicker2PropertyEditor { public MultiNodeTreePickerPropertyEditor() { - _internalPreValues = new Dictionary - { - {"multiPicker", "1"}, - {"showOpenButton", "0"}, - {"showEditButton", "0"}, - {"showPathOnHover", "0"} - }; + InternalPreValues["idType"] = "int"; } - + + /// + /// overridden to change the pre-value picker to use INT ids + /// + /// protected override PreValueEditor CreatePreValueEditor() { - return new MultiNodePickerPreValueEditor(); - } - - private IDictionary _internalPreValues; - public override IDictionary DefaultPreValues - { - get { return _internalPreValues; } - set { _internalPreValues = value; } - } - - internal class MultiNodePickerPreValueEditor : PreValueEditor - { - [PreValueField("startNode", "Node type", "treesource")] - public string StartNode { get; set; } - - [PreValueField("filter", "Allow items of type", "textstring", Description = "Separate with comma")] - public string Filter { get; set; } - - [PreValueField("minNumber", "Minimum number of items", "number")] - public string MinNumber { get; set; } - - [PreValueField("maxNumber", "Maximum number of items", "number")] - public string MaxNumber { get; set; } - - - [PreValueField("showOpenButton", "Show open button", "boolean")] - public string ShowOpenButton { get; set; } - - [PreValueField("showEditButton", "Show edit button (this feature is in preview!)", "boolean")] - public string ShowEditButton { get; set; } - - [PreValueField("showPathOnHover", "Show path when hovering items", "boolean")] - public bool ShowPathOnHover { get; set; } - - /// - /// This ensures the multiPicker pre-val is set based on the maxNumber of nodes set - /// - /// - /// - /// - /// - /// Due to compatibility with 7.0.0 the multiPicker pre-val might already exist in the db, but we've removed that setting in 7.0.1 so we need to detect it and if it is - /// there, then we'll set the maxNumber to '1' - /// - public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, PreValueCollection persistedPreVals) - { - var result = base.ConvertDbToEditor(defaultPreVals, persistedPreVals); - - //backwards compatibility check - if (result.ContainsKey("multiPicker") && result["multiPicker"].ToString() == "0") - { - result["maxNumber"] = "1"; - } - - //set the multiPicker val correctly depending on the maxNumber - if (result.ContainsKey("maxNumber")) - { - var asNumber = result["maxNumber"].TryConvertTo(); - if (asNumber.Success) - { - if (asNumber.Result <= 1) - { - result["multiPicker"] = "0"; - } - else - { - result["multiPicker"] = "1"; - } - } - } - - - return result; - } - + var preValEditor = base.CreatePreValueEditor(); + preValEditor.Fields.Single(x => x.Key == "startNode").Config["idType"] = "int"; + return preValEditor; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs index 37587d1c54..268ad30ffb 100644 --- a/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs @@ -1,35 +1,30 @@ -using Umbraco.Core; +using System; +using System.Linq; +using Umbraco.Core; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MultipleMediaPickerAlias, "Media Picker", "mediapicker", Group = "media", Icon = "icon-pictures-alt-2")] - public class MultipleMediaPickerPropertyEditor : MediaPickerPropertyEditor - { + [Obsolete("This editor is obsolete, use MultipleMediaPickerPropertyEditor2 instead which stores UDI")] + [PropertyEditor(Constants.PropertyEditors.MultipleMediaPickerAlias, "(Obsolete) Media Picker", "mediapicker", Group = "media", Icon = "icon-pictures-alt-2", IsDeprecated = true)] + public class MultipleMediaPickerPropertyEditor : MediaPicker2PropertyEditor + { public MultipleMediaPickerPropertyEditor() { - //clear the pre-values so it defaults to a multiple picker. - InternalPreValues.Clear(); - } - - protected override PreValueEditor CreatePreValueEditor() - { - return new MediaPickerPreValueEditor(); - } - - internal class MediaPickerPreValueEditor : PreValueEditor - { - [PreValueField("multiPicker", "Pick multiple items", "boolean")] - public bool MultiPicker { get; set; } - - [PreValueField("onlyImages", "Pick only images", "boolean", Description = "Only let the editor choose images from media.")] - public bool OnlyImages { get; set; } + //default it to multi picker + InternalPreValues["multiPicker"] = "1"; + InternalPreValues["idType"] = "int"; + } - [PreValueField("disableFolderSelect", "Disable folder select", "boolean", Description = "Do not allow folders to be picked.")] - public bool DisableFolderSelect { get; set; } - - [PreValueField("startNodeId", "Start node", "mediapicker")] - public int StartNodeId { get; set; } + /// + /// overridden to change the pre-value picker to use INT ids + /// + /// + protected override PreValueEditor CreatePreValueEditor() + { + var preValEditor = base.CreatePreValueEditor(); + preValEditor.Fields.Single(x => x.Key == "startNodeId").Config["idType"] = "int"; + return preValEditor; } - } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs new file mode 100644 index 0000000000..3aaf191fd2 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + [PropertyEditor(Constants.PropertyEditors.RelatedLinks2Alias, "Related links", "relatedlinks", ValueType = PropertyEditorValueTypes.Json, Icon = "icon-thumbnail-list", Group = "pickers")] + public class RelatedLinks2PropertyEditor : PropertyEditor + { + public RelatedLinks2PropertyEditor() + { + InternalPreValues = new Dictionary + { + {"idType", "udi"} + }; + } + + internal IDictionary InternalPreValues; + public override IDictionary DefaultPreValues + { + get { return InternalPreValues; } + set { InternalPreValues = value; } + } + + protected override PreValueEditor CreatePreValueEditor() + { + return new RelatedLinksPreValueEditor(); + } + + internal class RelatedLinksPreValueEditor : PreValueEditor + { + [PreValueField("max", "Maximum number of links", "number", Description = "Enter the maximum amount of links to be added, enter 0 for unlimited")] + public int Maximum { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/RelatedLinksPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RelatedLinksPropertyEditor.cs index cf9d4100a4..e4a7d99d07 100644 --- a/src/Umbraco.Web/PropertyEditors/RelatedLinksPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RelatedLinksPropertyEditor.cs @@ -8,18 +8,16 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.RelatedLinksAlias, "Related links", "relatedlinks", ValueType = PropertyEditorValueTypes.Json, Icon="icon-thumbnail-list", Group="pickers")] - public class RelatedLinksPropertyEditor : PropertyEditor + [Obsolete("This editor is obsolete, use RelatedLinks2PropertyEditor instead which stores UDI")] + [PropertyEditor(Constants.PropertyEditors.RelatedLinksAlias, "(Obsolete) Related links", "relatedlinks", ValueType = PropertyEditorValueTypes.Json, Icon="icon-thumbnail-list", Group="pickers", IsDeprecated = true)] + public class RelatedLinksPropertyEditor : RelatedLinks2PropertyEditor { - protected override PreValueEditor CreatePreValueEditor() + public RelatedLinksPropertyEditor() { - return new RelatedLinksPreValueEditor(); - } - - internal class RelatedLinksPreValueEditor : PreValueEditor - { - [PreValueField("max", "Maximum number of links", "number", Description = "Enter the maximum amount of links to be added, enter 0 for unlimited")] - public int Maximum { get; set; } + InternalPreValues = new Dictionary + { + {"idType", "int"} + }; } } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerPropertyConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerPropertyConverter.cs new file mode 100644 index 0000000000..ca147698ec --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerPropertyConverter.cs @@ -0,0 +1,154 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Umbraco +// +// +// The content picker property editor converter. +// +// -------------------------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; + +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Extensions; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + /// + /// The content picker property value converter. + /// + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(IPublishedContent))] + [PropertyValueCache(PropertyCacheValue.Object, PropertyCacheLevel.ContentCache)] + [PropertyValueCache(PropertyCacheValue.Source, PropertyCacheLevel.Content)] + [PropertyValueCache(PropertyCacheValue.XPath, PropertyCacheLevel.Content)] + public class ContentPickerPropertyConverter : PropertyValueConverterBase + { + /// + /// The properties to exclude. + /// + private static readonly List PropertiesToExclude = new List() + { + Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), + Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture) + }; + + /// + /// 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.ContentPickerAlias) + || propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.ContentPicker2Alias); + } + return false; + } + + /// + /// Convert the raw string into a nodeId integer + /// + /// + /// The published property type. + /// + /// + /// The value of the property + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + var attemptConvertInt = source.TryConvertTo(); + if (attemptConvertInt.Success) + return attemptConvertInt.Result; + var attemptConvertUdi = source.TryConvertTo(); + if (attemptConvertUdi.Success) + return attemptConvertUdi.Result; + return null; + } + + /// + /// Convert the source nodeId into a IPublishedContent (or DynamicPublishedContent) + /// + /// + /// The published property type. + /// + /// + /// The value of the property + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) + { + return null; + } + + if (UmbracoContext.Current != null) + { + if ((propertyType.PropertyTypeAlias != null && PropertiesToExclude.Contains(propertyType.PropertyTypeAlias.ToLower(CultureInfo.InvariantCulture))) == false) + { + IPublishedContent content; + if (source is int) + { + var sourceInt = (int)source; + content = UmbracoContext.Current.ContentCache.GetById(sourceInt); + if(content != null) + return content; + } + else + { + var sourceUdi = source as Udi; + content = sourceUdi.ToPublishedContent(); + if (content != null) + return content; + } + } + } + return source; + } + + /// + /// The convert source to xPath. + /// + /// + /// The property type. + /// + /// + /// The source. + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertSourceToXPath(PublishedPropertyType propertyType, object source, bool preview) + { + return source.ToString(); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropDataSetConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropDataSetConverter.cs index 82278676cd..19e87f7d23 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropDataSetConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropDataSetConverter.cs @@ -13,14 +13,14 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// public class ImageCropDataSetConverter : TypeConverter { + private static readonly Type[] ConvertableTypes = new[] + { + typeof(JObject) + }; + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { - var convertableTypes = new[] - { - typeof(JObject) - }; - - return convertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType)) + return ConvertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType)) || base.CanConvertFrom(context, destinationType); } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/LegacyRelatedLinksEditorValueConvertor.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/LegacyRelatedLinksEditorValueConvertor.cs new file mode 100644 index 0000000000..ee64bf6c15 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/LegacyRelatedLinksEditorValueConvertor.cs @@ -0,0 +1,127 @@ +using System; +using System.Linq; +using System.Xml; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.Extensions; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter(typeof(JsonValueConverter))] //this shadows the JsonValueConverter + [PropertyValueType(typeof(JArray))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class LegacyRelatedLinksEditorValueConvertor : PropertyValueConverterBase + { + private static readonly string[] MatchingEditors = { + Constants.PropertyEditors.RelatedLinksAlias, + Constants.PropertyEditors.RelatedLinks2Alias + }; + + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters == false) + { + return MatchingEditors.Contains(propertyType.PropertyEditorAlias); + } + return false; + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) return null; + var sourceString = source.ToString(); + + if (sourceString.DetectIsJson()) + { + try + { + var obj = JsonConvert.DeserializeObject(sourceString); + //update the internal links if we have a context + if (UmbracoContext.Current != null) + { + var helper = new UmbracoHelper(UmbracoContext.Current); + foreach (var a in obj) + { + var type = a.Value("type"); + if (type.IsNullOrWhiteSpace() == false) + { + if (type == "internal") + { + switch (propertyType.PropertyEditorAlias) + { + case Constants.PropertyEditors.RelatedLinksAlias: + var intLinkId = a.Value("link"); + var intLink = helper.NiceUrl(intLinkId); + a["link"] = intLink; + break; + case Constants.PropertyEditors.RelatedLinks2Alias: + var strLinkId = a.Value("link"); + var udiAttempt = strLinkId.TryConvertTo(); + if (udiAttempt) + { + var content = udiAttempt.Result.ToPublishedContent(); + a["link"] = helper.NiceUrl(content.Id); + } + break; + } + } + } + } + } + return obj; + } + catch (Exception ex) + { + LogHelper.Error("Could not parse the string " + sourceString + " to a json object", ex); + } + } + + //it's not json, just return the string + return sourceString; + } + + public override object ConvertSourceToXPath(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) return null; + var sourceString = source.ToString(); + + if (sourceString.DetectIsJson()) + { + try + { + var obj = JsonConvert.DeserializeObject(sourceString); + + var d = new XmlDocument(); + var e = d.CreateElement("links"); + d.AppendChild(e); + + foreach (dynamic link in obj) + { + var ee = d.CreateElement("link"); + ee.SetAttribute("title", link.title); + ee.SetAttribute("link", link.link); + ee.SetAttribute("type", link.type); + ee.SetAttribute("newwindow", link.newWindow); + + e.AppendChild(ee); + } + + return d.CreateNavigator(); + } + catch (Exception ex) + { + LogHelper.Error("Could not parse the string " + sourceString + " to a json object", ex); + } + } + + //it's not json, just return the string + return sourceString; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerPropertyConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerPropertyConverter.cs new file mode 100644 index 0000000000..4b3db67025 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerPropertyConverter.cs @@ -0,0 +1,204 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Umbraco +// +// +// The media picker 2 value converter +// +// -------------------------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web.Extensions; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + /// + /// The media picker property value converter. + /// + [DefaultPropertyValueConverter] + public class MediaPickerPropertyConverter : PropertyValueConverterBase, IPropertyValueConverterMeta + { + private readonly IDataTypeService _dataTypeService; + + //TODO: Remove this ctor in v8 since the other one will use IoC + public MediaPickerPropertyConverter() + : this(ApplicationContext.Current.Services.DataTypeService) + { + } + + public MediaPickerPropertyConverter(IDataTypeService dataTypeService) + { + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + _dataTypeService = dataTypeService; + } + + public Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return IsMultipleDataType(propertyType.DataTypeId, propertyType.PropertyEditorAlias) ? typeof(IEnumerable) : typeof(IPublishedContent); + } + + public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue) + { + PropertyCacheLevel returnLevel; + switch (cacheValue) + { + case PropertyCacheValue.Object: + returnLevel = PropertyCacheLevel.ContentCache; + break; + case PropertyCacheValue.Source: + returnLevel = PropertyCacheLevel.Content; + break; + case PropertyCacheValue.XPath: + returnLevel = PropertyCacheLevel.Content; + break; + default: + returnLevel = PropertyCacheLevel.None; + break; + } + + return returnLevel; + } + + /// + /// The is multiple data type. + /// + /// + /// The data type id. + /// + /// + /// + /// The . + /// + private bool IsMultipleDataType(int dataTypeId, string propertyEditorAlias) + { + // 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 preVals = _dataTypeService.GetPreValuesCollectionByDataTypeId(id).PreValuesAsDictionary; + + if (preVals.ContainsKey("multiPicker")) + { + var preValue = preVals + .FirstOrDefault(x => string.Equals(x.Key, "multiPicker", StringComparison.InvariantCultureIgnoreCase)) + .Value; + + return preValue != null && preValue.Value.TryConvertTo().Result; + } + + //in some odd cases, the pre-values in the db won't exist but their default pre-values contain this key so check there + var propertyEditor = PropertyEditorResolver.Current.GetByAlias(propertyEditorAlias); + if (propertyEditor != null) + { + var preValue = propertyEditor.DefaultPreValues + .FirstOrDefault(x => string.Equals(x.Key, "multiPicker", StringComparison.InvariantCultureIgnoreCase)) + .Value; + + return preValue != null && preValue.TryConvertTo().Result; + } + + return false; + }); + } + + /// + /// 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) + { + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MediaPicker2Alias); + } + + /// + /// Convert the raw string into a nodeId integer + /// + /// + /// The published property type. + /// + /// + /// The value of the property + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + var nodeIds = source.ToString() + .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(Udi.Parse) + .ToArray(); + return nodeIds; + } + + /// + /// Convert the source nodeId into a IPublishedContent (or DynamicPublishedContent) + /// + /// + /// The published property type. + /// + /// + /// The value of the property + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) + { + return null; + } + + var udis = (Udi[])source; + var mediaItems = new List(); + if (udis.Any()) + { + foreach (var udi in udis) + { + var item = udi.ToPublishedContent(); + if (item != null) + mediaItems.Add(item); + } + if (IsMultipleDataType(propertyType.DataTypeId, propertyType.PropertyEditorAlias)) + { + return mediaItems; + } + else + { + return mediaItems.FirstOrDefault(); + } + } + + return source; + } + + private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); + + internal static void ClearCaches() + { + Storages.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerPropertyConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerPropertyConverter.cs new file mode 100644 index 0000000000..4467c464f5 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerPropertyConverter.cs @@ -0,0 +1,67 @@ +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Extensions; +using Umbraco.Web.Security; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + [PropertyValueType(typeof(IPublishedContent))] + [PropertyValueCache(PropertyCacheValue.Object, PropertyCacheLevel.ContentCache)] + [PropertyValueCache(PropertyCacheValue.Source, PropertyCacheLevel.Content)] + [PropertyValueCache(PropertyCacheValue.XPath, PropertyCacheLevel.Content)] + public class MemberPickerPropertyConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.MemberPickerAlias) + || propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.MemberPicker2Alias); + } + return false; + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + var attemptConvertInt = source.TryConvertTo(); + if (attemptConvertInt.Success) + return attemptConvertInt.Result; + var attemptConvertUdi = source.TryConvertTo(); + if (attemptConvertUdi.Success) + return attemptConvertUdi.Result; + return null; + } + + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) + return null; + + if (UmbracoContext.Current != null) + { + IPublishedContent member; + if (source is int) + { + var membershipHelper = new MembershipHelper(UmbracoContext.Current); + member = membershipHelper.GetById((int)source); + if (member != null) + return member; + } + else + { + var sourceUdi = source as Udi; + member = sourceUdi.ToPublishedContent(); + if (member != null) + return member; + } + } + + return source; + } + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerPropertyConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerPropertyConverter.cs new file mode 100644 index 0000000000..c0c0321e91 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerPropertyConverter.cs @@ -0,0 +1,215 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Umbraco +// +// +// The multi node tree picker property editor value converter. +// +// -------------------------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.Extensions; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + + /// + /// The multi node tree picker property editor value converter. + /// + [DefaultPropertyValueConverter(typeof(MustBeStringValueConverter))] + [PropertyValueType(typeof(IEnumerable))] + [PropertyValueCache(PropertyCacheValue.Object, PropertyCacheLevel.ContentCache)] + [PropertyValueCache(PropertyCacheValue.Source, PropertyCacheLevel.Content)] + [PropertyValueCache(PropertyCacheValue.XPath, PropertyCacheLevel.Content)] + public class MultiNodeTreePickerPropertyConverter : PropertyValueConverterBase + { + /// + /// The properties to exclude. + /// + private static readonly List PropertiesToExclude = new List() + { + Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), + Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture) + }; + + /// + /// 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.MultiNodeTreePickerAlias) + || propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePicker2Alias); + } + return false; + } + + /// + /// Convert the raw string into a nodeId integer array + /// + /// + /// The published property type. + /// + /// + /// The value of the property + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePickerAlias)) + { + var nodeIds = source.ToString() + .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(int.Parse) + .ToArray(); + return nodeIds; + } + if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePicker2Alias)) + { + var nodeIds = source.ToString() + .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(Udi.Parse) + .ToArray(); + return nodeIds; + } + return null; + } + + /// + /// Convert the source nodeId into a IEnumerable of IPublishedContent (or DynamicPublishedContent) + /// + /// + /// The published property type. + /// + /// + /// The value of the property + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) + { + return null; + } + + //TODO: Inject an UmbracoHelper and create a GetUmbracoHelper method based on either injected or singleton + if (UmbracoContext.Current != null) + { + if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePickerAlias)) + { + var nodeIds = (int[])source; + + if ((propertyType.PropertyTypeAlias != null && PropertiesToExclude.InvariantContains(propertyType.PropertyTypeAlias)) == false) + { + var multiNodeTreePicker = new List(); + + if (nodeIds.Length > 0) + { + var umbHelper = new UmbracoHelper(UmbracoContext.Current); + var objectType = UmbracoObjectTypes.Unknown; + + foreach (var nodeId in nodeIds) + { + var multiNodeTreePickerItem = + GetPublishedContent(nodeId, ref objectType, UmbracoObjectTypes.Document, umbHelper.TypedContent) + ?? GetPublishedContent(nodeId, ref objectType, UmbracoObjectTypes.Media, umbHelper.TypedMedia) + ?? GetPublishedContent(nodeId, ref objectType, UmbracoObjectTypes.Member, umbHelper.TypedMember); + + if (multiNodeTreePickerItem != null) + { + multiNodeTreePicker.Add(multiNodeTreePickerItem); + } + } + } + + return multiNodeTreePicker; + } + + // return the first nodeId as this is one of the excluded properties that expects a single id + return nodeIds.FirstOrDefault(); + } + + if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePicker2Alias)) + { + var udis = (Udi[])source; + + if ((propertyType.PropertyTypeAlias != null && PropertiesToExclude.InvariantContains(propertyType.PropertyTypeAlias)) == false) + { + var multiNodeTreePicker = new List(); + + if (udis.Length > 0) + { + foreach (var udi in udis) + { + var item = udi.ToPublishedContent(); + if (item != null) + { + multiNodeTreePicker.Add(item); + } + } + } + + return multiNodeTreePicker; + } + + // return the first nodeId as this is one of the excluded properties that expects a single id + return udis.FirstOrDefault(); + } + } + return source; + } + + /// + /// Attempt to get an IPublishedContent instance based on ID and content type + /// + /// The content node ID + /// The type of content being requested + /// The type of content expected/supported by + /// A function to fetch content of type + /// The requested content, or null if either it does not exist or does not match + private IPublishedContent GetPublishedContent(int nodeId, ref UmbracoObjectTypes actualType, UmbracoObjectTypes expectedType, Func contentFetcher) + { + // is the actual type supported by the content fetcher? + if (actualType != UmbracoObjectTypes.Unknown && actualType != expectedType) + { + // no, return null + return null; + } + + // attempt to get the content + var content = contentFetcher(nodeId); + if (content != null) + { + // if we found the content, assign the expected type to the actual type so we don't have to keep looking for other types of content + actualType = expectedType; + } + return content; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultipleMediaPickerPropertyConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultipleMediaPickerPropertyConverter.cs new file mode 100644 index 0000000000..161e7178e7 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultipleMediaPickerPropertyConverter.cs @@ -0,0 +1,267 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Umbraco +// +// +// The multiple media picker property editor converter. +// +// -------------------------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + /// + /// The multiple media picker property value converter. + /// + [DefaultPropertyValueConverter(typeof(MustBeStringValueConverter))] + public class MultipleMediaPickerPropertyConverter : PropertyValueConverterBase, IPropertyValueConverterMeta + { + private readonly IDataTypeService _dataTypeService; + + //TODO: Remove this ctor in v8 since the other one will use IoC + public MultipleMediaPickerPropertyConverter() + : this(ApplicationContext.Current.Services.DataTypeService) + { + } + + public MultipleMediaPickerPropertyConverter(IDataTypeService dataTypeService) + { + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + _dataTypeService = dataTypeService; + } + + /// + /// Checks if this converter can convert the property editor and registers if it can. + /// + /// + /// The property type. + /// + /// + /// The . + /// + public override bool IsConverter(PublishedPropertyType propertyType) + { + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultipleMediaPickerAlias); + } + return false; + } + + /// + /// Convert the raw string into a nodeId integer array or a single integer + /// + /// + /// The published property type. + /// + /// + /// The value of the property + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + if (IsMultipleDataType(propertyType.DataTypeId, propertyType.PropertyEditorAlias)) + { + var nodeIds = + source.ToString() + .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(int.Parse) + .ToArray(); + return nodeIds; + } + + var attemptConvertInt = source.TryConvertTo(); + if (attemptConvertInt.Success) + { + return attemptConvertInt.Result; + } + else + { + var nodeIds = + source.ToString() + .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(int.Parse) + .ToArray(); + + if (nodeIds.Length > 0) + { + var error = + string.Format( + "Data type \"{0}\" is not set to allow multiple items but appears to contain multiple items, check the setting and save the data type again", + ApplicationContext.Current.Services.DataTypeService.GetDataTypeDefinitionById( + propertyType.DataTypeId).Name); + + LogHelper.Warn(error); + throw new Exception(error); + } + } + + return null; + } + + /// + /// Convert the source nodeId into a IPublishedContent or IEnumerable of IPublishedContent (or DynamicPublishedContent) depending on data type setting + /// + /// + /// The published property type. + /// + /// + /// The value of the property + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) + { + return null; + } + + if (UmbracoContext.Current == null) + { + return null; + } + + var umbHelper = new UmbracoHelper(UmbracoContext.Current); + + if (IsMultipleDataType(propertyType.DataTypeId, propertyType.PropertyEditorAlias)) + { + var nodeIds = (int[])source; + var multiMediaPicker = Enumerable.Empty(); + if (nodeIds.Length > 0) + { + multiMediaPicker = umbHelper.TypedMedia(nodeIds).Where(x => x != null); + } + + // in v8 should return multiNodeTreePickerEnumerable but for v7 need to return as PublishedContentEnumerable so that string can be returned for legacy compatibility + return new PublishedContentEnumerable(multiMediaPicker); + } + + // single value picker + var nodeId = (int)source; + + return umbHelper.TypedMedia(nodeId); + } + + /// + /// The get property cache level. + /// + /// + /// The property type. + /// + /// + /// The cache value. + /// + /// + /// The . + /// + public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue) + { + PropertyCacheLevel returnLevel; + switch (cacheValue) + { + case PropertyCacheValue.Object: + returnLevel = PropertyCacheLevel.ContentCache; + break; + case PropertyCacheValue.Source: + returnLevel = PropertyCacheLevel.Content; + break; + case PropertyCacheValue.XPath: + returnLevel = PropertyCacheLevel.Content; + break; + default: + returnLevel = PropertyCacheLevel.None; + break; + } + + return returnLevel; + } + + /// + /// The get property value type. + /// + /// + /// The property type. + /// + /// + /// The . + /// + public Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return IsMultipleDataType(propertyType.DataTypeId, propertyType.PropertyEditorAlias) ? typeof(IEnumerable) : typeof(IPublishedContent); + } + + /// + /// The is multiple data type. + /// + /// + /// The data type id. + /// + /// + /// + /// The . + /// + private bool IsMultipleDataType(int dataTypeId, string propertyEditorAlias) + { + // 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 preVals = _dataTypeService.GetPreValuesCollectionByDataTypeId(id).PreValuesAsDictionary; + + if (preVals.ContainsKey("multiPicker")) + { + var preValue = preVals + .FirstOrDefault(x => string.Equals(x.Key, "multiPicker", StringComparison.InvariantCultureIgnoreCase)) + .Value; + + return preValue != null && preValue.Value.TryConvertTo().Result; + } + + //in some odd cases, the pre-values in the db won't exist but their default pre-values contain this key so check there + var propertyEditor = PropertyEditorResolver.Current.GetByAlias(propertyEditorAlias); + if (propertyEditor != null) + { + var preValue = propertyEditor.DefaultPreValues + .FirstOrDefault(x => string.Equals(x.Key, "multiPicker", StringComparison.InvariantCultureIgnoreCase)) + .Value; + + return preValue != null && preValue.TryConvertTo().Result; + } + + return false; + }); + } + + private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); + + internal static void ClearCaches() + { + Storages.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs index a0368db769..b95644d9ca 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs @@ -1,106 +1,164 @@ -using System; +// -------------------------------------------------------------------------------------------------------------------- +// +// Umbraco +// +// +// Defines the RelatedLinksPropertyConverter type. +// +// -------------------------------------------------------------------------------------------------------------------- + +using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.Extensions; +using Umbraco.Web.Models; +using Umbraco.Web.Routing; namespace Umbraco.Web.PropertyEditors.ValueConverters { - [PropertyValueType(typeof(JArray))] - [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] - [DefaultPropertyValueConverter(typeof(JsonValueConverter))] //this shadows the JsonValueConverter - public class RelatedLinksEditorValueConvertor : PropertyValueConverterBase + /// + /// The related links property value converter. + /// + [DefaultPropertyValueConverter(typeof(LegacyRelatedLinksEditorValueConvertor), typeof(JsonValueConverter))] + [PropertyValueType(typeof(RelatedLinks))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.ContentCache)] + public class RelatedLinksPropertyConverter : PropertyValueConverterBase { + private readonly UrlProvider _urlProvider; + + public RelatedLinksPropertyConverter() + { + } + + public RelatedLinksPropertyConverter(UrlProvider urlProvider) + { + if (urlProvider == null) throw new ArgumentNullException("urlProvider"); + _urlProvider = urlProvider; + } + + /// + /// Checks if this converter can convert the property editor and registers if it can. + /// + /// + /// The property type. + /// + /// + /// The . + /// public override bool IsConverter(PublishedPropertyType propertyType) { - return Constants.PropertyEditors.RelatedLinksAlias.Equals(propertyType.PropertyEditorAlias); + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.RelatedLinksAlias) + || propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.RelatedLinks2Alias); + } + return false; } - public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + /// + /// Convert the source nodeId into a RelatedLinks object + /// + /// + /// The published property type. + /// + /// + /// The value of the property + /// + /// + /// The preview. + /// + /// + /// The . + /// + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) { - if (source == null) return null; + if (source == null) + { + return null; + } + var sourceString = source.ToString(); - if (sourceString.DetectIsJson()) + var relatedLinksData = JsonConvert.DeserializeObject>(sourceString); + var relatedLinks = new List(); + + foreach (var linkData in relatedLinksData) { - try + var relatedLink = new RelatedLink { - var obj = JsonConvert.DeserializeObject(sourceString); - //update the internal links if we have a context - if (UmbracoContext.Current != null) + Caption = linkData.Caption, + NewWindow = linkData.NewWindow, + IsInternal = linkData.IsInternal, + Type = linkData.Type, + Link = linkData.Link + }; + + int contentId; + if (int.TryParse(relatedLink.Link, out contentId)) + { + relatedLink.Id = contentId; + relatedLink = CreateLink(relatedLink); + } + else + { + var strLinkId = linkData.Link; + var udiAttempt = strLinkId.TryConvertTo(); + if (udiAttempt.Success) { - var helper = new UmbracoHelper(UmbracoContext.Current); - foreach (var a in obj) + var content = udiAttempt.Result.ToPublishedContent(); + if (content != null) { - var type = a.Value("type"); - if (type.IsNullOrWhiteSpace() == false) - { - if (type == "internal") - { - var linkId = a.Value("link"); - var link = helper.NiceUrl(linkId); - a["link"] = link; - } - } - } + relatedLink.Id = content.Id; + relatedLink = CreateLink(relatedLink); + relatedLink.Content = content; + } } - return obj; } - catch (Exception ex) + + if (relatedLink.IsDeleted == false) { - LogHelper.Error("Could not parse the string " + sourceString + " to a json object", ex); + relatedLinks.Add(relatedLink); + } + else + { + LogHelper.Warn( + string.Format("Related Links value converter skipped a link as the node has been unpublished/deleted (Internal Link NodeId: {0}, Link Caption: \"{1}\")", relatedLink.Link, relatedLink.Caption)); } } - //it's not json, just return the string - return sourceString; + return new RelatedLinks(relatedLinks, sourceString); } - public override object ConvertSourceToXPath(PublishedPropertyType propertyType, object source, bool preview) + private RelatedLink CreateLink(RelatedLink link) { - if (source == null) return null; - var sourceString = source.ToString(); - - if (sourceString.DetectIsJson()) + if (link.IsInternal && link.Id != null) { - try + if (_urlProvider == null && UmbracoContext.Current == null) { - var obj = JsonConvert.DeserializeObject(sourceString); - - var d = new XmlDocument(); - var e = d.CreateElement("links"); - d.AppendChild(e); - - var values = (IEnumerable)source; - foreach (dynamic link in obj) - { - var ee = d.CreateElement("link"); - ee.SetAttribute("title", link.title); - ee.SetAttribute("link", link.link); - ee.SetAttribute("type", link.type); - ee.SetAttribute("newwindow", link.newWindow); - - e.AppendChild(ee); - } - - return d.CreateNavigator(); + return null; } - catch (Exception ex) + + var urlProvider = _urlProvider ?? UmbracoContext.Current.UrlProvider; + + link.Link = urlProvider.GetUrl((int)link.Id); + if (link.Link.Equals("#")) { - LogHelper.Error("Could not parse the string " + sourceString + " to a json object", ex); + link.IsDeleted = true; + link.Link = link.Id.ToString(); + } + else + { + link.IsDeleted = false; } } - //it's not json, just return the string - return sourceString; + return link; } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index 2802a4d631..a6ea79d283 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -15,14 +15,15 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { /// - /// A value converter for TinyMCE that will ensure any macro content is rendered properly even when - /// used dynamically. - /// + /// A value converter for TinyMCE that will ensure any macro content is rendered properly even when + /// used dynamically. + /// // because that version of RTE converter parses {locallink} and executes macros, when going from // data to source, its source value has to be cached at the request level, because we have no idea // what the macros may depend on actually. An so, object and xpath need to follow... request, too. // note: the TinyMceValueConverter is NOT inherited, so the PropertyValueCache attribute here is not // actually required (since Request is default) but leave it here to be absolutely explicit. + [DefaultPropertyValueConverter] [PropertyValueType(typeof(IHtmlString))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Request)] public class RteMacroRenderingValueConverter : TinyMceValueConverter diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs index 8938325c35..8354ca629d 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -10,11 +10,12 @@ using Umbraco.Web.Templates; namespace Umbraco.Web.PropertyEditors.ValueConverters { + [DefaultPropertyValueConverter] [PropertyValueType(typeof(string))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Request)] 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.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index bdf02ba5ca..3b9f184d1c 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -148,11 +148,6 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - public override Task RunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } - public override bool IsAsync { get { return false; } diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 5caa08729a..36d1306ab6 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -540,7 +540,7 @@ namespace Umbraco.Web var filtered = dynamicDocumentList.Where(predicate); return filtered.Count() == 1; } - + #endregion #region AsDynamic @@ -806,7 +806,7 @@ namespace Umbraco.Web public static HtmlString IsOdd(this IPublishedContent content, string valueIfTrue, string valueIfFalse) { return new HtmlString(content.IsOdd() ? valueIfTrue : valueIfFalse); - } + } #endregion @@ -836,7 +836,7 @@ namespace Umbraco.Web { return content.IsNotEqual(other, valueIfTrue, string.Empty); } - + public static HtmlString IsNotEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) { return new HtmlString(content.IsNotEqual(other) ? valueIfTrue : valueIfFalse); @@ -1125,7 +1125,7 @@ namespace Umbraco.Web { return content.Ancestors(maxLevel).FirstOrDefault(); } - + /// /// Gets the content or its nearest ancestor. /// @@ -1186,7 +1186,7 @@ namespace Umbraco.Web { return content.AncestorsOrSelf(maxLevel).FirstOrDefault(); } - + internal static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func func) { var ancestorsOrSelf = content.EnumerateAncestors(orSelf); @@ -1237,7 +1237,7 @@ namespace Umbraco.Web where T : class, IPublishedContent { return parentNodes.SelectMany(x => x.DescendantsOrSelf()); - } + } // as per XPath 1.0 specs 2.2, @@ -1285,7 +1285,7 @@ namespace Umbraco.Web { return content.Descendants(level).OfType(); } - + public static IEnumerable DescendantsOrSelf(this IPublishedContent content) { return content.DescendantsOrSelf(true, null); @@ -1366,7 +1366,7 @@ namespace Umbraco.Web { return content.DescendantOrSelf(level) as T; } - + internal static IEnumerable DescendantsOrSelf(this IPublishedContent content, bool orSelf, Func func) { return content.EnumerateDescendants(orSelf).Where(x => func == null || func(x)); @@ -1390,7 +1390,7 @@ namespace Umbraco.Web foreach (var child2 in child.EnumerateDescendants()) yield return child2; } - + #endregion #region Axes: following-sibling, preceding-sibling, following, preceding + pseudo-axes up, down, next, previous @@ -1413,8 +1413,8 @@ namespace Umbraco.Web public static IPublishedContent Up(this IPublishedContent content, string contentTypeAlias) { - return string.IsNullOrEmpty(contentTypeAlias) - ? content.Parent + return string.IsNullOrEmpty(contentTypeAlias) + ? content.Parent : content.Ancestor(contentTypeAlias); } @@ -1777,6 +1777,17 @@ namespace Umbraco.Web return content.Children().FirstOrDefault(); } + /// + /// Gets the first child of the content, of a given content type. + /// + /// The content. + /// The content type alias. + /// The first child of content, of the given content type. + public static IPublishedContent FirstChild(this IPublishedContent content, string alias) + { + return content.Children( alias ).FirstOrDefault(); + } + public static IPublishedContent FirstChild(this IPublishedContent content, Func predicate) { return content.Children(predicate).FirstOrDefault(); @@ -1788,13 +1799,19 @@ namespace Umbraco.Web return content.Children().FirstOrDefault(); } - /// - /// Gets the children of the content in a DataTable. - /// + public static IPublishedContent FirstChild(this IPublishedContent content, Func predicate) + where T : class, IPublishedContent + { + return content.Children().FirstOrDefault(predicate); + } + + /// + /// Gets the children of the content in a DataTable. + /// /// The content. /// An optional content type alias. /// The children of the content. - public static DataTable ChildrenAsTable(this IPublishedContent content, string contentTypeAliasFilter = "") + public static DataTable ChildrenAsTable(this IPublishedContent content, string contentTypeAliasFilter = "") { return GenerateDataTable(content, contentTypeAliasFilter); } @@ -1813,7 +1830,7 @@ namespace Umbraco.Web : null : content.Children.FirstOrDefault(x => x.DocumentTypeAlias == contentTypeAliasFilter); if (firstNode == null) - return new DataTable(); //no children found + return new DataTable(); //no children found //use new utility class to create table so that we don't have to maintain code in many places, just one var dt = Core.DataTableExtensions.GenerateDataTable( @@ -1953,7 +1970,7 @@ namespace Umbraco.Web public static CultureInfo GetCulture(this IPublishedContent content, Uri current = null) { return Models.ContentExtensions.GetCulture(UmbracoContext.Current, - ApplicationContext.Current.Services.DomainService, + ApplicationContext.Current.Services.DomainService, ApplicationContext.Current.Services.LocalizationService, ApplicationContext.Current.Services.ContentService, content.Id, content.Path, diff --git a/src/Umbraco.Web/RelatedLinksTypeConverter.cs b/src/Umbraco.Web/RelatedLinksTypeConverter.cs new file mode 100644 index 0000000000..2debe5f3ac --- /dev/null +++ b/src/Umbraco.Web/RelatedLinksTypeConverter.cs @@ -0,0 +1,97 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Linq; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Web.Models; + +namespace Umbraco.Web +{ + public class RelatedLinksTypeConverter : TypeConverter + { + private readonly UmbracoHelper _umbracoHelper; + + public RelatedLinksTypeConverter(UmbracoHelper umbracoHelper) + { + _umbracoHelper = umbracoHelper; + } + + public RelatedLinksTypeConverter() + { + + } + + private static readonly Type[] ConvertableTypes = new[] + { + typeof(JArray) + }; + + 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 relatedLinks = value as RelatedLinks; + if (relatedLinks == null) + return null; + + if (TypeHelper.IsTypeAssignableFrom(destinationType)) + { + // Conversion to JArray taken from old value converter + + var obj = JsonConvert.DeserializeObject(relatedLinks.PropertyData); + + var umbracoHelper = GetUmbracoHelper(); + + //update the internal links if we have a context + if (umbracoHelper != null) + { + foreach (var a in obj) + { + var type = a.Value("type"); + if (type.IsNullOrWhiteSpace() == false) + { + if (type == "internal") + { + var linkId = a.Value("link"); + var link = umbracoHelper.NiceUrl(linkId); + a["link"] = link; + } + } + } + } + return obj; + + } + + return base.ConvertTo(context, culture, value, destinationType); + } + + private UmbracoHelper GetUmbracoHelper() + { + if (_umbracoHelper != null) + return _umbracoHelper; + + if (UmbracoContext.Current == null) + { + LogHelper.Warn("Cannot create an UmbracoHelper the UmbracoContext is null"); + return null; + } + + //DO NOT assign to _umbracoHelper variable, this is a singleton class and we cannot assign this based on an UmbracoHelper which is request based + return new UmbracoHelper(UmbracoContext.Current); + } + } +} diff --git a/src/Umbraco.Web/RequestLifespanMessagesFactory.cs b/src/Umbraco.Web/RequestLifespanMessagesFactory.cs deleted file mode 100644 index 26ac3bd5df..0000000000 --- a/src/Umbraco.Web/RequestLifespanMessagesFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Umbraco.Core.Events; - -namespace Umbraco.Web -{ - /// - /// Stores the instance of EventMessages in the current request so all events will share the same instance - /// - internal class RequestLifespanMessagesFactory : IEventMessagesFactory - { - private readonly IHttpContextAccessor _httpAccessor; - - public RequestLifespanMessagesFactory(IHttpContextAccessor httpAccessor) - { - if (httpAccessor == null) throw new ArgumentNullException("httpAccessor"); - _httpAccessor = httpAccessor; - } - - public EventMessages Get() - { - if (_httpAccessor.Value.Items[typeof (RequestLifespanMessagesFactory).Name] == null) - { - _httpAccessor.Value.Items[typeof(RequestLifespanMessagesFactory).Name] = new EventMessages(); - } - return (EventMessages)_httpAccessor.Value.Items[typeof (RequestLifespanMessagesFactory).Name]; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index 2bb012d207..f237a7d886 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -5,6 +5,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Web.PublishedCache; using Umbraco.Core; +using Umbraco.Core.Models; namespace Umbraco.Web.Routing { @@ -24,6 +25,7 @@ namespace Umbraco.Web.Routing public UrlProvider(UmbracoContext umbracoContext, IWebRoutingSection routingSettings, IEnumerable urlProviders) { if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); + if (routingSettings == null) throw new ArgumentNullException("routingSettings"); _umbracoContext = umbracoContext; _urlProviders = urlProviders; @@ -65,6 +67,72 @@ namespace Umbraco.Web.Routing #region GetUrl + /// + /// Gets the url of a published content. + /// + /// The published content identifier. + /// The url for the published content. + /// + /// The url is absolute or relative depending on Mode and on the current url. + /// If the provider is unable to provide a url, it returns "#". + /// + public string GetUrl(Guid id) + { + var intId = _umbracoContext.Application.Services.EntityService.GetIdForKey(id, UmbracoObjectTypes.Document); + return GetUrl(intId.Success ? intId.Result : -1); + } + + /// + /// Gets the nice url of a published content. + /// + /// The published content identifier. + /// A value indicating whether the url should be absolute in any case. + /// The url for the published content. + /// + /// The url is absolute or relative depending on Mode and on current, unless + /// absolute is true, in which case the url is always absolute. + /// If the provider is unable to provide a url, it returns "#". + /// + public string GetUrl(Guid id, bool absolute) + { + var intId = _umbracoContext.Application.Services.EntityService.GetIdForKey(id, UmbracoObjectTypes.Document); + return GetUrl(intId.Success ? intId.Result : -1, absolute); + } + + /// + /// Gets the nice url of a published content. + /// + /// The published content id. + /// The current absolute url. + /// A value indicating whether the url should be absolute in any case. + /// The url for the published content. + /// + /// The url is absolute or relative depending on Mode and on current, unless + /// absolute is true, in which case the url is always absolute. + /// If the provider is unable to provide a url, it returns "#". + /// + public string GetUrl(Guid id, Uri current, bool absolute) + { + var intId = _umbracoContext.Application.Services.EntityService.GetIdForKey(id, UmbracoObjectTypes.Document); + return GetUrl(intId.Success ? intId.Result : -1, current, absolute); + } + + /// + /// Gets the nice url of a published content. + /// + /// The published content identifier. + /// The url mode. + /// The url for the published content. + /// + /// The url is absolute or relative depending on mode and on the current url. + /// If the provider is unable to provide a url, it returns "#". + /// + public string GetUrl(Guid id, UrlProviderMode mode) + { + var intId = _umbracoContext.Application.Services.EntityService.GetIdForKey(id, UmbracoObjectTypes.Document); + return GetUrl(intId.Success ? intId.Result : -1, mode); + } + /// /// Gets the url of a published content. /// diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index d63fbb4606..3ec1f06260 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -1,15 +1,19 @@ using System; -using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; using System.Web.Hosting; +using Umbraco.Core; using Umbraco.Core.Events; using Umbraco.Core.Logging; namespace Umbraco.Web.Scheduling { - // exists for logging purposes - internal class BackgroundTaskRunner + /// + /// Manages a queue of tasks and runs them in the background. + /// + /// This class exists for logging purposes - the one you want to use is BackgroundTaskRunner{T}. + public abstract class BackgroundTaskRunner { } /// @@ -18,68 +22,68 @@ namespace Umbraco.Web.Scheduling /// The type of the managed tasks. /// The task runner is web-aware and will ensure that it shuts down correctly when the AppDomain /// shuts down (ie is unloaded). - internal class BackgroundTaskRunner : BackgroundTaskRunner, IBackgroundTaskRunner + public class BackgroundTaskRunner : BackgroundTaskRunner, IBackgroundTaskRunner where T : class, IBackgroundTask { + // do not remove this comment! + // + // if you plan to do anything on this class, first go and read + // http://blog.stephencleary.com/2012/12/dont-block-in-asynchronous-code.html + // http://stackoverflow.com/questions/19481964/calling-taskcompletionsource-setresult-in-a-non-blocking-manner + // http://stackoverflow.com/questions/21225361/is-there-anything-like-asynchronous-blockingcollectiont + // and more, and more, and more + // and remember: async is hard + private readonly string _logPrefix; private readonly BackgroundTaskRunnerOptions _options; private readonly ILogger _logger; - private readonly BlockingCollection _tasks = new BlockingCollection(); private readonly object _locker = new object(); - // that event is used to stop the pump when it is alive and waiting - // on a latched task - so it waits on the latch, the cancellation token, - // and the completed event - private readonly ManualResetEventSlim _completedEvent = new ManualResetEventSlim(false); + private readonly BufferBlock _tasks = new BufferBlock(new DataflowBlockOptions { }); // in various places we are testing these vars outside a lock, so make them volatile private volatile bool _isRunning; // is running - private volatile bool _isCompleted; // does not accept tasks anymore, may still be running + private volatile bool _completed; // does not accept tasks anymore, may still be running - private Task _runningTask; - private CancellationTokenSource _tokenSource; + private Task _runningTask; // the threading task that is currently executing background tasks + private CancellationTokenSource _shutdownTokenSource; // used to cancel everything and shutdown + private CancellationTokenSource _cancelTokenSource; // used to cancel the current task + private CancellationToken _shutdownToken; private bool _terminating; // ensures we raise that event only once private bool _terminated; // remember we've terminated - private TaskCompletionSource _terminatedSource; // awaitable source - - internal event TypedEventHandler, TaskEventArgs> TaskError; - internal event TypedEventHandler, TaskEventArgs> TaskStarting; - internal event TypedEventHandler, TaskEventArgs> TaskCompleted; - internal event TypedEventHandler, TaskEventArgs> TaskCancelled; - - // triggers when the runner stops (but could start again if a task is added to it) - internal event TypedEventHandler, EventArgs> Stopped; - - // triggers when the hosting environment requests that the runner terminates - internal event TypedEventHandler, EventArgs> Terminating; - - // triggers when the runner terminates (no task can be added, no task is running) - internal event TypedEventHandler, EventArgs> Terminated; + private readonly TaskCompletionSource _terminatedSource = new TaskCompletionSource(); // enable awaiting termination /// /// Initializes a new instance of the class. /// - public BackgroundTaskRunner(ILogger logger) - : this(typeof (T).FullName, new BackgroundTaskRunnerOptions(), logger) + /// A logger. + /// An optional action to execute when the main domain status is aquired. + /// An optional action to execute when the main domain status is released. + public BackgroundTaskRunner(ILogger logger, Action mainDomInstall = null, Action mainDomRelease = null) + : this(typeof(T).FullName, new BackgroundTaskRunnerOptions(), logger, mainDomInstall, mainDomRelease) { } /// /// Initializes a new instance of the class. /// /// The name of the runner. - /// - public BackgroundTaskRunner(string name, ILogger logger) - : this(name, new BackgroundTaskRunnerOptions(), logger) + /// A logger. + /// An optional action to execute when the main domain status is aquired. + /// An optional action to execute when the main domain status is released. + public BackgroundTaskRunner(string name, ILogger logger, Action mainDomInstall = null, Action mainDomRelease = null) + : this(name, new BackgroundTaskRunnerOptions(), logger, mainDomInstall, mainDomRelease) { } /// /// Initializes a new instance of the class with a set of options. /// /// The set of options. - /// - public BackgroundTaskRunner(BackgroundTaskRunnerOptions options, ILogger logger) - : this(typeof (T).FullName, options, logger) + /// A logger. + /// An optional action to execute when the main domain status is aquired. + /// An optional action to execute when the main domain status is released. + public BackgroundTaskRunner(BackgroundTaskRunnerOptions options, ILogger logger, Action mainDomInstall = null, Action mainDomRelease = null) + : this(typeof(T).FullName, options, logger, mainDomInstall, mainDomRelease) { } /// @@ -87,8 +91,10 @@ namespace Umbraco.Web.Scheduling /// /// The name of the runner. /// The set of options. - /// - public BackgroundTaskRunner(string name, BackgroundTaskRunnerOptions options, ILogger logger) + /// A logger. + /// An optional action to execute when the main domain status is aquired. + /// An optional action to execute when the main domain status is released. + public BackgroundTaskRunner(string name, BackgroundTaskRunnerOptions options, ILogger logger, Action mainDomInstall = null, Action mainDomRelease = null) { if (options == null) throw new ArgumentNullException("options"); if (logger == null) throw new ArgumentNullException("logger"); @@ -99,7 +105,18 @@ namespace Umbraco.Web.Scheduling if (options.Hosted) HostingEnvironment.RegisterObject(this); - if (options.AutoStart) + if (mainDomInstall != null || mainDomRelease != null) + { + var appContext = ApplicationContext.Current; + var mainDom = appContext == null ? null : appContext.MainDom; + var reg = mainDom == null || ApplicationContext.Current.MainDom.Register(mainDomInstall, mainDomRelease); + if (reg == false) + _completed = _terminated = true; + if (reg && mainDom == null && mainDomInstall != null) + mainDomInstall(); + } + + if (options.AutoStart && _terminated == false) StartUp(); } @@ -112,7 +129,7 @@ namespace Umbraco.Web.Scheduling } /// - /// Gets a value indicating whether a task is currently running. + /// Gets a value indicating whether a threading task is currently running. /// public bool IsRunning { @@ -124,27 +141,26 @@ namespace Umbraco.Web.Scheduling /// public bool IsCompleted { - get { return _isCompleted; } + get { return _completed; } } /// - /// Gets the running task as an immutable object. + /// Gets the running threading task as an immutable awaitable. /// /// There is no running task. /// - /// Unless the AutoStart option is true, there will be no running task until - /// a background task is added to the queue. Unless the KeepAlive option is true, there - /// will be no running task when the queue is empty. + /// Unless the AutoStart option is true, there will be no current threading task until + /// a background task is added to the queue, and there will be no current threading task + /// when the queue is empty. In which case this method returns null. + /// The returned value can be awaited and that is all (eg no continuation). /// - public ThreadingTaskImmutable CurrentThreadingTask + internal ThreadingTaskImmutable CurrentThreadingTask { get { lock (_locker) { - if (_runningTask == null) - throw new InvalidOperationException("There is no current Threading.Task."); - return new ThreadingTaskImmutable(_runningTask); + return _runningTask == null ? null : new ThreadingTaskImmutable(_runningTask); } } } @@ -154,7 +170,8 @@ namespace Umbraco.Web.Scheduling /// /// An awaitable instance. /// Used to wait until the runner is no longer running (IsRunning == false), - /// though the runner could be started again afterwards by adding tasks to it. + /// though the runner could be started again afterwards by adding tasks to it. If + /// the runner is not running, returns a completed awaitable. public ThreadingTaskImmutable StoppedAwaitable { get @@ -168,20 +185,21 @@ namespace Umbraco.Web.Scheduling } /// - /// Gets an awaitable used to await the runner. + /// Gets an awaitable object that can be used to await for the runner to terminate. /// - /// An awaitable instance. - /// Used to wait until the runner is terminated. - public ThreadingTaskImmutable TerminatedAwaitable + /// An awaitable object. + /// + /// Used to wait until the runner has terminated. + /// This is for unit tests and should not be used otherwise. In most cases when the runner + /// has terminated, the application domain is going down and it is not the right time to do things. + /// + internal ThreadingTaskImmutable TerminatedAwaitable { get { lock (_locker) { - if (_terminatedSource == null && _terminated == false) - _terminatedSource = new TaskCompletionSource(); - var task = _terminatedSource == null ? Task.FromResult(0) : _terminatedSource.Task; - return new ThreadingTaskImmutable(task); + return new ThreadingTaskImmutable(_terminatedSource.Task); } } } @@ -195,12 +213,12 @@ namespace Umbraco.Web.Scheduling { lock (_locker) { - if (_isCompleted) + if (_completed) throw new InvalidOperationException("The task runner has completed."); // add task - _logger.Debug(_logPrefix + "Task added {0}", task.GetType); - _tasks.Add(task); + _logger.Debug(_logPrefix + "Task added {0}", () => task.GetType().FullName); + _tasks.Post(task); // start StartUpLocked(); @@ -217,15 +235,15 @@ namespace Umbraco.Web.Scheduling { lock (_locker) { - if (_isCompleted) + if (_completed) { - _logger.Debug(_logPrefix + "Task cannot be added {0}, the task runner is already shutdown", task.GetType); + _logger.Debug(_logPrefix + "Task cannot be added {0}, the task runner has already shutdown", () => task.GetType().FullName); return false; } // add task - _logger.Debug(_logPrefix + "Task added {0}", task.GetType); - _tasks.Add(task); + _logger.Debug(_logPrefix + "Task added {0}", () => task.GetType().FullName); + _tasks.Post(task); // start StartUpLocked(); @@ -234,18 +252,33 @@ namespace Umbraco.Web.Scheduling } } + /// + /// Cancels to current task, if any. + /// + /// Has no effect if the task runs synchronously, or does not want to cancel. + public void CancelCurrentBackgroundTask() + { + lock (_locker) + { + if (_completed) + throw new InvalidOperationException("The task runner has completed."); + if (_cancelTokenSource != null) + _cancelTokenSource.Cancel(); + } + } + /// /// Starts the tasks runner, if not already running. /// /// Is invoked each time a task is added, to ensure it is going to be processed. /// The task runner has completed. - public void StartUp() + internal void StartUp() { if (_isRunning) return; lock (_locker) { - if (_isCompleted) + if (_completed) throw new InvalidOperationException("The task runner has completed."); StartUpLocked(); @@ -258,13 +291,15 @@ namespace Umbraco.Web.Scheduling /// Must be invoked within lock(_locker) and with _isCompleted being false. private void StartUpLocked() { - // double check + // double check if (_isRunning) return; _isRunning = true; // create a new token source since this is a new process - _tokenSource = new CancellationTokenSource(); - _runningTask = PumpIBackgroundTasks(Task.Factory, _tokenSource.Token); + _shutdownTokenSource = new CancellationTokenSource(); + _shutdownToken = _shutdownTokenSource.Token; + _runningTask = Task.Run(async () => await Pump().ConfigureAwait(false), _shutdownToken); + _logger.Debug(_logPrefix + "Starting"); } @@ -279,28 +314,27 @@ namespace Umbraco.Web.Scheduling { lock (_locker) { - _isCompleted = true; // do not accept new tasks + _completed = true; // do not accept new tasks if (_isRunning == false) return; // done already } - // try to be nice - // assuming multiple threads can do these without problems - _completedEvent.Set(); - _tasks.CompleteAdding(); + // complete the queue + // will stop waiting on the queue or on a latch + _tasks.Complete(); if (force) { // we must bring everything down, now - Thread.Sleep(100); // give time to CompleteAdding() + Thread.Sleep(100); // give time to Complete() lock (_locker) { - // was CompleteAdding() enough? + // was Complete() enough? if (_isRunning == false) return; } // try to cancel running async tasks (cannot do much about sync tasks) - // break delayed tasks delay - // truncate running queues - _tokenSource.Cancel(false); // false is the default + // break latched tasks + // stop processing the queue + _shutdownTokenSource.Cancel(false); // false is the default } // tasks in the queue will be executed... @@ -310,145 +344,152 @@ namespace Umbraco.Web.Scheduling _runningTask.Wait(); // wait for whatever is running to end... } - /// - /// Runs background tasks for as long as there are background tasks in the queue, with an asynchronous operation. - /// - /// The supporting . - /// A cancellation token. - /// The asynchronous operation. - private Task PumpIBackgroundTasks(TaskFactory factory, CancellationToken token) + private async Task Pump() { - var taskSource = new TaskCompletionSource(factory.CreationOptions); - var enumerator = _options.KeepAlive ? _tasks.GetConsumingEnumerable(token).GetEnumerator() : null; - - // ReSharper disable once MethodSupportsCancellation // always run - var taskSourceContinuing = taskSource.Task.ContinueWith(t => + while (true) { - // because the pump does not lock, there's a race condition, - // the pump may stop and then we still have tasks to process, - // and then we must restart the pump - lock to avoid race cond - var onStopped = false; + // get the next task + // if it returns null the runner is going down, stop + var bgTask = await GetNextBackgroundTask(_shutdownToken); + if (bgTask == null) return; + + // set a cancellation source so that the current task can be cancelled + // link from _shutdownToken so that we can use _cancelTokenSource for both lock (_locker) { - if (token.IsCancellationRequested || _tasks.Count == 0) - { - _logger.Debug(_logPrefix + "Stopping"); - - if (_options.PreserveRunningTask == false) - _runningTask = null; - - // stopped - _isRunning = false; - onStopped = true; - } + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken); } - if (onStopped) + // wait for latch should return the task + // if it returns null it's either that the task has been cancelled + // or the whole runner is going down - in both cases, continue, + // and GetNextBackgroundTask will take care of shutdowns + bgTask = await WaitForLatch(bgTask, _cancelTokenSource.Token); + if (bgTask == null) continue; + + // executes & be safe - RunAsync should NOT throw but only raise an event, + // but... just make sure we never ever take everything down + try { - OnEvent(Stopped, "Stopped"); - return; + await RunAsync(bgTask, _cancelTokenSource.Token).ConfigureAwait(false); } - - // if _runningTask is taskSource.Task then we must keep continuing it, - // not starting a new taskSource, else _runningTask would complete and - // something may be waiting on it - //PumpIBackgroundTasks(factory, token); // restart - // ReSharper disable MethodSupportsCancellation // always run - t.ContinueWithTask(_ => PumpIBackgroundTasks(factory, token)); // restart - // ReSharper restore MethodSupportsCancellation - }); - - Action pump = null; - pump = task => - { - // RunIBackgroundTaskAsync does NOT throw exceptions, just raises event - // so if we have an exception here, really, wtf? - must read the exception - // anyways so it does not bubble up and kill everything - if (task != null && task.IsFaulted) + catch (Exception e) { - var exception = task.Exception; - _logger.Error(_logPrefix + "Task runner exception.", exception); + _logger.Error(_logPrefix + "Task runner exception.", e); } - // is it ok to run? - if (TaskSourceCanceled(taskSource, token)) return; - - // try to get a task - // the blocking MoveNext will end if token is cancelled or collection is completed - T bgTask; - var hasBgTask = _options.KeepAlive - // ReSharper disable once PossibleNullReferenceException - ? (bgTask = enumerator.MoveNext() ? enumerator.Current : null) != null // blocking - : _tasks.TryTake(out bgTask); // non-blocking - - // no task, signal the runner we're done - if (hasBgTask == false) + // done + lock (_locker) { - TaskSourceCompleted(taskSource, token); - return; + _cancelTokenSource = null; } - - // wait for latched task, supporting cancellation - var dbgTask = bgTask as ILatchedBackgroundTask; - if (dbgTask != null && dbgTask.IsLatched) - { - WaitHandle.WaitAny(new[] { dbgTask.Latch, token.WaitHandle, _completedEvent.WaitHandle }); - if (TaskSourceCanceled(taskSource, token)) return; - // else run now, either because latch ok or runner is completed - // still latched & not running on shutdown = stop here - if (dbgTask.IsLatched && dbgTask.RunsOnShutdown == false) - { - dbgTask.Dispose(); // will not run - TaskSourceCompleted(taskSource, token); - return; - } - } - - // run the task as first task, or a continuation - task = task == null - ? RunIBackgroundTaskAsync(bgTask, token) - // ReSharper disable once MethodSupportsCancellation // always run - : task.ContinueWithTask(_ => RunIBackgroundTaskAsync(bgTask, token)); - - // and pump - // ReSharper disable once MethodSupportsCancellation // always run - task.ContinueWith(t => pump(t)); - }; - - // start it all - factory.StartNew(() => pump(null), - token, - _options.LongRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.None, - TaskScheduler.Default); - - return taskSourceContinuing; - } - - private static bool TaskSourceCanceled(TaskCompletionSource taskSource, CancellationToken token) - { - if (token.IsCancellationRequested) - { - taskSource.SetCanceled(); - return true; } - return false; } - private static void TaskSourceCompleted(TaskCompletionSource taskSource, CancellationToken token) + // gets the next background task from the buffer + private async Task GetNextBackgroundTask(CancellationToken token) { - if (token.IsCancellationRequested) - taskSource.SetCanceled(); - else - taskSource.SetResult(null); + while (true) + { + var task = await GetNextBackgroundTask2(token); + if (task != null) return task; + + lock (_locker) + { + // deal with race condition + if (_shutdownToken.IsCancellationRequested == false && _tasks.Count > 0) continue; + + // if we really have nothing to do, stop + _logger.Debug(_logPrefix + "Stopping"); + + if (_options.PreserveRunningTask == false) + _runningTask = null; + _isRunning = false; + _shutdownToken = CancellationToken.None; + } + + OnEvent(Stopped, "Stopped"); + return null; + } } - /// - /// Runs a background task asynchronously. - /// - /// The background task. - /// A cancellation token. - /// The asynchronous operation. - internal async Task RunIBackgroundTaskAsync(T bgTask, CancellationToken token) + private async Task GetNextBackgroundTask2(CancellationToken shutdownToken) + { + // exit if cancelling + if (shutdownToken.IsCancellationRequested) + return null; + + // if keepalive is false then don't block, exit if there is + // no task in the buffer - yes, there is a race cond, which + // we'll take care of + if (_options.KeepAlive == false && _tasks.Count == 0) + return null; + + try + { + // A Task that informs of whether and when more output is available. If, when the + // task completes, its Result is true, more output is available in the source (though another + // consumer of the source may retrieve the data). If it returns false, more output is not + // and will never be available, due to the source completing prior to output being available. + + var output = await _tasks.OutputAvailableAsync(shutdownToken); // block until output or cancelled + if (output == false) return null; + } + catch (TaskCanceledException) + { + return null; + } + + try + { + // A task that represents the asynchronous receive operation. When an item value is successfully + // received from the source, the returned task is completed and its Result returns the received + // value. If an item value cannot be retrieved because the source is empty and completed, an + // InvalidOperationException exception is thrown in the returned task. + + // the source cannot be empty *and* completed here - we know we have output + return await _tasks.ReceiveAsync(shutdownToken); + } + catch (TaskCanceledException) + { + return null; + } + } + + // if bgTask is not a latched background task, or if it is not latched, returns immediately + // else waits for the latch, taking care of completion and shutdown and whatnot + private async Task WaitForLatch(T bgTask, CancellationToken token) + { + var latched = bgTask as ILatchedBackgroundTask; + if (latched == null || latched.IsLatched == false) return bgTask; + + // support cancelling awaiting + // read https://github.com/dotnet/corefx/issues/2704 + // read http://stackoverflow.com/questions/27238232/how-can-i-cancel-task-whenall + var tokenTaskSource = new TaskCompletionSource(); + token.Register(s => ((TaskCompletionSource)s).SetResult(true), tokenTaskSource); + + // returns the task that completed + // - latched.Latch completes when the latch releases + // - _tasks.Completion completes when the runner completes + // - tokenTaskSource.Task completes when this task, or the whole runner, is cancelled + var task = await Task.WhenAny(latched.Latch, _tasks.Completion, tokenTaskSource.Task); + + // ok to run now + if (task == latched.Latch) + return bgTask; + + // if shutting down, return the task only if it runs on shutdown + if (_shutdownToken.IsCancellationRequested == false && latched.RunsOnShutdown) return bgTask; + + // else, either it does not run on shutdown or it's been cancelled, dispose + latched.Dispose(); + return null; + } + + // runs the background task, taking care of shutdown (as far as possible - cannot abort + // a non-async Run for example, so we'll do our best) + private async Task RunAsync(T bgTask, CancellationToken token) { try { @@ -464,7 +505,7 @@ namespace Umbraco.Web.Scheduling else bgTask.Run(); } - finally // ensure we disposed - unless latched (again) + finally // ensure we disposed - unless latched again ie wants to re-run { var lbgTask = bgTask as ILatchedBackgroundTask; if (lbgTask == null || lbgTask.IsLatched == false) @@ -482,11 +523,32 @@ namespace Umbraco.Web.Scheduling catch (Exception ex) { _logger.Error(_logPrefix + "Task has failed", ex); - } + } } #region Events + // triggers when a background task starts + public event TypedEventHandler, TaskEventArgs> TaskStarting; + + // triggers when a background task has completed + public event TypedEventHandler, TaskEventArgs> TaskCompleted; + + // triggers when a background task throws + public event TypedEventHandler, TaskEventArgs> TaskError; + + // triggers when a background task is cancelled + public event TypedEventHandler, TaskEventArgs> TaskCancelled; + + // triggers when the runner stops (but could start again if a task is added to it) + internal event TypedEventHandler, EventArgs> Stopped; + + // triggers when the hosting environment requests that the runner terminates + internal event TypedEventHandler, EventArgs> Terminating; + + // triggers when the runner has terminated (no task can be added, no task is running) + internal event TypedEventHandler, EventArgs> Terminated; + private void OnEvent(TypedEventHandler, EventArgs> handler, string name) { if (handler == null) return; @@ -526,7 +588,7 @@ namespace Umbraco.Web.Scheduling { OnEvent(TaskCancelled, "TaskCancelled", e); - //dispose it + // dispose it e.Task.Dispose(); } @@ -608,9 +670,7 @@ namespace Umbraco.Web.Scheduling _logger.Info(_logPrefix + "Waiting for tasks to complete"); Shutdown(false, false); // do not accept any more tasks, flush the queue, do not wait - // raise the completed event only after the running task has completed - // and there's no more task running - + // raise the completed event only after the running threading task has completed lock (_locker) { if (_runningTask != null) @@ -632,6 +692,7 @@ namespace Umbraco.Web.Scheduling } } + // called by Stop either immediately or eventually private void Terminate(bool immediate) { // signal the environment we have terminated @@ -640,8 +701,6 @@ namespace Umbraco.Web.Scheduling // complete the awaitable completion source, if any HostingEnvironment.UnregisterObject(this); - _logger.Info(_logPrefix + "Tasks " + (immediate ? "cancelled" : "completed") + ", terminated"); - OnEvent(Terminated, "Terminated"); TaskCompletionSource terminatedSource; lock (_locker) @@ -649,8 +708,12 @@ namespace Umbraco.Web.Scheduling _terminated = true; terminatedSource = _terminatedSource; } - if (terminatedSource != null) - terminatedSource.SetResult(0); + + _logger.Info(_logPrefix + "Tasks " + (immediate ? "cancelled" : "completed") + ", terminated"); + + OnEvent(Terminated, "Terminated"); + + terminatedSource.SetResult(0); } } } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs index 55df42d3b7..28c814db24 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs @@ -3,7 +3,7 @@ namespace Umbraco.Web.Scheduling /// /// Provides options to the class. /// - internal class BackgroundTaskRunnerOptions + public class BackgroundTaskRunnerOptions { //TODO: Could add options for using a stack vs queue if required diff --git a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs index 4e646c0623..2e67ed1790 100644 --- a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs +++ b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs @@ -7,7 +7,7 @@ namespace Umbraco.Web.Scheduling /// /// Represents a background task. /// - internal interface IBackgroundTask : IDisposable + public interface IBackgroundTask : IDisposable { /// /// Runs the background task. diff --git a/src/Umbraco.Web/Scheduling/IBackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/IBackgroundTaskRunner.cs index c4e2dab35d..8c0117541b 100644 --- a/src/Umbraco.Web/Scheduling/IBackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/IBackgroundTaskRunner.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.Scheduling /// /// The type of the managed tasks. /// The interface is not complete and exists only to have the contravariance on T. - internal interface IBackgroundTaskRunner : IDisposable, IRegisteredObject + public interface IBackgroundTaskRunner : IDisposable, IRegisteredObject where T : class, IBackgroundTask { bool IsCompleted { get; } diff --git a/src/Umbraco.Web/Scheduling/ILatchedBackgroundTask.cs b/src/Umbraco.Web/Scheduling/ILatchedBackgroundTask.cs index 79379cb966..c761e5f4de 100644 --- a/src/Umbraco.Web/Scheduling/ILatchedBackgroundTask.cs +++ b/src/Umbraco.Web/Scheduling/ILatchedBackgroundTask.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using System.Threading.Tasks; namespace Umbraco.Web.Scheduling { @@ -13,10 +14,10 @@ namespace Umbraco.Web.Scheduling internal interface ILatchedBackgroundTask : IBackgroundTask { /// - /// Gets a wait handle on the task condition. + /// Gets a task on latch. /// /// The task is not latched. - WaitHandle Latch { get; } + Task Latch { get; } /// /// Gets a value indicating whether the task is latched. diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index 380ae85401..763e28b608 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -20,11 +20,6 @@ namespace Umbraco.Web.Scheduling _appContext = appContext; } - public override bool PerformRun() - { - throw new NotImplementedException(); - } - public override async Task PerformRunAsync(CancellationToken token) { if (_appContext == null) return true; // repeat... @@ -69,10 +64,5 @@ namespace Umbraco.Web.Scheduling { get { return true; } } - - public override bool RunsOnShutdown - { - get { return false; } - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs index 3315fa7c34..e0cda08e47 100644 --- a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs @@ -5,59 +5,70 @@ using Umbraco.Core; namespace Umbraco.Web.Scheduling { - internal abstract class LatchedBackgroundTaskBase : DisposableObject, ILatchedBackgroundTask + /// + /// Provides a base class for latched background tasks. + /// + /// Implement by overriding Run or RunAsync and then IsAsync accordingly, + /// depending on whether the task is implemented as a sync or async method, and then + /// optionnally overriding RunsOnShutdown, to indicate whether the latched task should run + /// immediately on shutdown, or just be abandonned (default). + public abstract class LatchedBackgroundTaskBase : DisposableObject, ILatchedBackgroundTask { - private readonly ManualResetEventSlim _latch; + private TaskCompletionSource _latch; protected LatchedBackgroundTaskBase() { - _latch = new ManualResetEventSlim(false); + _latch = new TaskCompletionSource(); } /// /// Implements IBackgroundTask.Run(). /// - public abstract void Run(); + public virtual void Run() + { + throw new NotSupportedException("This task cannot run synchronously."); + } /// /// Implements IBackgroundTask.RunAsync(). /// - public abstract Task RunAsync(CancellationToken token); + public virtual Task RunAsync(CancellationToken token) + { + throw new NotSupportedException("This task cannot run asynchronously."); + } /// /// Indicates whether the background task can run asynchronously. /// public abstract bool IsAsync { get; } - public WaitHandle Latch + public Task Latch { - get { return _latch.WaitHandle; } + get { return _latch.Task; } } public bool IsLatched { - get { return _latch.IsSet == false; } + get { return _latch.Task.IsCompleted == false; } } protected void Release() { - _latch.Set(); + _latch.SetResult(true); } protected void Reset() { - _latch.Reset(); + _latch = new TaskCompletionSource(); } - public abstract bool RunsOnShutdown { get; } + public virtual bool RunsOnShutdown { get { return false; } } // the task is going to be disposed after execution, // unless it is latched again, thus indicating it wants to // remain active protected override void DisposeResources() - { - _latch.Dispose(); - } + { } } } diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index b3a5f303e3..9000fc72cf 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -16,7 +16,7 @@ namespace Umbraco.Web.Scheduling private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - public LogScrubber(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + public LogScrubber(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { @@ -77,27 +77,21 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } + // running on a background task, and Log.CleanLogs uses the old SqlHelper, + // better wrap in a scope and ensure it's all cleaned up and nothing leaks + using (var scope = ApplicationContext.Current.ScopeProvider.CreateScope()) using (DisposableTimer.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) { Log.CleanLogs(GetLogScrubbingMaximumAge(_settings)); + scope.Complete(); } return true; // repeat } - public override Task PerformRunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } - public override bool IsAsync { get { return false; } } - - public override bool RunsOnShutdown - { - get { return false; } - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index 567f85f1f5..d9bd33dd30 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Threading.Tasks; @@ -6,12 +7,16 @@ namespace Umbraco.Web.Scheduling /// /// Provides a base class for recurring background tasks. /// - internal abstract class RecurringTaskBase : LatchedBackgroundTaskBase + /// Implement by overriding PerformRun or PerformRunAsync and then IsAsync accordingly, + /// depending on whether the task is implemented as a sync or async method. Run nor RunAsync are + /// sealed here as overriding them would break recurrence. And then optionnally override + /// RunsOnShutdown, in order to indicate whether the latched task should run immediately on + /// shutdown, or just be abandonned (default). + public abstract class RecurringTaskBase : LatchedBackgroundTaskBase { private readonly IBackgroundTaskRunner _runner; private readonly int _periodMilliseconds; private readonly Timer _timer; - private bool _disposed; /// /// Initializes a new instance of the class. @@ -37,7 +42,7 @@ namespace Umbraco.Web.Scheduling /// Implements IBackgroundTask.Run(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public override void Run() + public sealed override void Run() { var shouldRepeat = PerformRun(); if (shouldRepeat) Repeat(); @@ -47,7 +52,7 @@ namespace Umbraco.Web.Scheduling /// Implements IBackgroundTask.RunAsync(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public override async Task RunAsync(CancellationToken token) + public sealed override async Task RunAsync(CancellationToken token) { var shouldRepeat = await PerformRunAsync(token); if (shouldRepeat) Repeat(); @@ -74,7 +79,10 @@ namespace Umbraco.Web.Scheduling /// Runs the background task. /// /// A value indicating whether to repeat the task. - public abstract bool PerformRun(); + public virtual bool PerformRun() + { + throw new NotSupportedException("This task cannot run synchronously."); + } /// /// Runs the task asynchronously. @@ -82,7 +90,10 @@ namespace Umbraco.Web.Scheduling /// A cancellation token. /// A instance representing the execution of the background task, /// and returning a value indicating whether to repeat the task. - public abstract Task PerformRunAsync(CancellationToken token); + public virtual Task PerformRunAsync(CancellationToken token) + { + throw new NotSupportedException("This task cannot run asynchronously."); + } protected override void DisposeResources() { diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 6649c3c474..bf49e335f6 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -23,13 +23,8 @@ namespace Umbraco.Web.Scheduling _settings = settings; } - public override bool PerformRun() - { - throw new NotImplementedException(); - } - public override async Task PerformRunAsync(CancellationToken token) - { + { if (_appContext == null) return true; // repeat... switch (_appContext.GetCurrentServerRole()) @@ -70,19 +65,27 @@ namespace Umbraco.Web.Scheduling var url = umbracoAppUrl + "/RestServices/ScheduledPublish/Index"; using (DisposableTimer.DebugDuration( - () => string.Format("Scheduled publishing executing @ {0}", url), + () => string.Format("Scheduled publishing executing @ {0}", url), () => "Scheduled publishing complete")) - { + { try - { + { using (var wc = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = new StringContent(string.Empty) }; - //pass custom the authorization header - request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); + + // running on a background task, requires its own (safe) scope + // (GetAuthenticationHeaderValue uses UserService to load the current user, hence requires a database) + // (might not need a scope but we don't know really) + using (var scope = ApplicationContext.Current.ScopeProvider.CreateScope()) + { + //pass custom the authorization header + request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); + scope.Complete(); + } var result = await wc.SendAsync(request, token); } @@ -100,10 +103,5 @@ namespace Umbraco.Web.Scheduling { get { return true; } } - - public override bool RunsOnShutdown - { - get { return false; } - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index 3f0a9f2a97..c69d42bd27 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -81,11 +81,6 @@ namespace Umbraco.Web.Scheduling } } - public override bool PerformRun() - { - throw new NotImplementedException(); - } - public override async Task PerformRunAsync(CancellationToken token) { if (_appContext == null) return true; // repeat... @@ -126,10 +121,5 @@ namespace Umbraco.Web.Scheduling { get { return true; } } - - public override bool RunsOnShutdown - { - get { return false; } - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/TaskEventArgs.cs b/src/Umbraco.Web/Scheduling/TaskEventArgs.cs index 27e5174616..5e83b14231 100644 --- a/src/Umbraco.Web/Scheduling/TaskEventArgs.cs +++ b/src/Umbraco.Web/Scheduling/TaskEventArgs.cs @@ -2,21 +2,41 @@ namespace Umbraco.Web.Scheduling { - internal class TaskEventArgs : EventArgs + /// + /// Provides arguments for task runner events. + /// + /// The type of the task. + public class TaskEventArgs : EventArgs where T : IBackgroundTask { - public T Task { get; private set; } - public Exception Exception { get; private set; } - + /// + /// Initializes a new instance of the class with a task. + /// + /// The task. public TaskEventArgs(T task) { Task = task; } + /// + /// Initializes a new instance of the class with a task and an exception. + /// + /// The task. + /// An exception. public TaskEventArgs(T task, Exception exception) { Task = task; Exception = exception; } + + /// + /// Gets or sets the task. + /// + public T Task { get; private set; } + + /// + /// Gets or sets the exception. + /// + public Exception Exception { get; private set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ThreadingTaskImmutable.cs b/src/Umbraco.Web/Scheduling/ThreadingTaskImmutable.cs index e8ccbeac0e..1bb5fcbf41 100644 --- a/src/Umbraco.Web/Scheduling/ThreadingTaskImmutable.cs +++ b/src/Umbraco.Web/Scheduling/ThreadingTaskImmutable.cs @@ -5,10 +5,10 @@ using System.Threading.Tasks; namespace Umbraco.Web.Scheduling { /// - /// Wraps a Task within an object that gives access to its GetAwaiter method and Status + /// Wraps a within an object that gives access to its GetAwaiter method and Status /// property while ensuring that it cannot be modified in any way. /// - internal class ThreadingTaskImmutable + public class ThreadingTaskImmutable { private readonly Task _task; diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index c69b483a3a..7e4c9c45aa 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Web; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; +using Microsoft.AspNet.SignalR; using Microsoft.Owin; using Microsoft.Owin.Extensions; using Microsoft.Owin.Logging; @@ -53,7 +54,22 @@ namespace Umbraco.Web.Security.Identity { app.SetLoggerFactory(new OwinLoggerFactory()); } - + + /// + /// This maps a Signal path/hub + /// + /// + /// + public static IAppBuilder UseSignalR(this IAppBuilder app) + { + + // TODO: Move this method in v8, it doesn't belong in this namespace/extension class + var umbracoPath = GlobalSettings.UmbracoMvcArea; + + return app.MapSignalR(HttpRuntime.AppDomainAppVirtualPath + + umbracoPath + "/BackOffice/signalr", new HubConfiguration { EnableDetailedErrors = true }); + } + /// /// Configure Default Identity User Manager for Umbraco /// diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index 158910a794..605b5137d8 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -663,7 +663,8 @@ namespace Umbraco.Web.Security //Are we resetting the password?? if (passwordModel.Reset.HasValue && passwordModel.Reset.Value) { - if (membershipProvider.EnablePasswordReset == false) + var canReset = membershipProvider.CanResetPassword(_applicationContext.Services.UserService); + if (canReset == false) { return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not enabled", new[] { "resetPassword" }) }); } diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index f0c0861059..cda9f04fad 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -189,13 +189,13 @@ namespace Umbraco.Web.Security } /// - /// Returns the back office IUser instance for the username specified + /// Gets (and creates if not found) the back office instance for the username specified /// /// /// /// - /// This will return an Iuser instance no matter what membership provider is installed for the back office, it will automatically - /// create any missing Iuser accounts if one is not found and a custom membership provider is being used. + /// This will return an instance no matter what membership provider is installed for the back office, it will automatically + /// create any missing accounts if one is not found and a custom membership provider or is being used. /// internal IUser GetBackOfficeUser(string username) { diff --git a/src/Umbraco.Web/SingletonHttpContextAccessor.cs b/src/Umbraco.Web/SingletonHttpContextAccessor.cs index cdeafa48e1..3d6ac159fb 100644 --- a/src/Umbraco.Web/SingletonHttpContextAccessor.cs +++ b/src/Umbraco.Web/SingletonHttpContextAccessor.cs @@ -6,7 +6,11 @@ namespace Umbraco.Web { public HttpContextBase Value { - get { return new HttpContextWrapper(HttpContext.Current); } + get + { + var httpContext = HttpContext.Current; + return httpContext == null ? null : new HttpContextWrapper(httpContext); + } } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs index 4a316fe96f..f1d7f761af 100644 --- a/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs +++ b/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Persistence.Migrations; using Umbraco.Web.WebApi.Filters; using umbraco.interfaces; +using Umbraco.Core; using Umbraco.Core.Configuration; namespace Umbraco.Web.Strategies.Migrations @@ -15,7 +16,7 @@ namespace Umbraco.Web.Strategies.Migrations { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { - if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + if (e.ProductName != Constants.System.UmbracoMigrationName) return; if (HttpContext.Current == null) return; diff --git a/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs index cf9ddeafb2..52144ae383 100644 --- a/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs +++ b/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web.Strategies.Migrations { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { - if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + if (e.ProductName != Constants.System.UmbracoMigrationName) return; var target70 = new Version(7, 0, 0); diff --git a/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs b/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs index 270fe4eace..45eb0a92b3 100644 --- a/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs +++ b/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web.Strategies.Migrations { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { - if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + if (e.ProductName != Constants.System.UmbracoMigrationName) return; var target720 = new Version(7, 2, 0); diff --git a/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs b/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs index 9a8b6a6044..a4e4349372 100644 --- a/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs +++ b/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs @@ -23,7 +23,7 @@ namespace Umbraco.Web.Strategies.Migrations { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { - if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + if (e.ProductName != Constants.System.UmbracoMigrationName) return; var target73 = new Version(7, 3, 0); diff --git a/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs b/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs index 7be8d818d3..70f0b58f1a 100644 --- a/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs +++ b/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web.Strategies.Migrations { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { - if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + if (e.ProductName != Constants.System.UmbracoMigrationName) return; var target = new Version(6, 0, 0); if (e.ConfiguredVersion < target) diff --git a/src/Umbraco.Web/Strategies/Migrations/RebuildXmlCachesAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/RebuildXmlCachesAfterUpgrade.cs index e62a738675..5f8f8b8593 100644 --- a/src/Umbraco.Web/Strategies/Migrations/RebuildXmlCachesAfterUpgrade.cs +++ b/src/Umbraco.Web/Strategies/Migrations/RebuildXmlCachesAfterUpgrade.cs @@ -23,7 +23,7 @@ namespace Umbraco.Web.Strategies.Migrations { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { - if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + if (e.ProductName != Constants.System.UmbracoMigrationName) return; var v730 = new Semver.SemVersion(new Version(7, 3, 0)); diff --git a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs index a6dfa07be1..dff5c6730c 100644 --- a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs +++ b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs @@ -35,7 +35,7 @@ namespace Umbraco.Web.Strategies protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { _registrar = ServerRegistrarResolver.Current.Registrar as DatabaseServerRegistrar; - + // only for the DatabaseServerRegistrar if (_registrar == null) return; @@ -65,10 +65,10 @@ namespace Umbraco.Web.Strategies case EnsureRoutableOutcome.IsRoutable: case EnsureRoutableOutcome.NotDocumentRequest: RegisterBackgroundTasks(e); - break; + break; } } - + private void RegisterBackgroundTasks(UmbracoRequestEventArgs e) { //remove handler, we're done @@ -84,7 +84,7 @@ namespace Umbraco.Web.Strategies 15000, //delay before first execution _registrar.Options.RecurringSeconds*1000, //amount of ms between executions svc, _registrar, serverAddress); - + //Perform the rest async, we don't want to block the startup sequence // this will just reoccur on a background thread _backgroundTaskRunner.TryAdd(task); @@ -124,11 +124,6 @@ namespace Umbraco.Web.Strategies get { return false; } } - public override bool RunsOnShutdown - { - get { return false; } - } - /// /// Runs the background task. /// @@ -137,6 +132,8 @@ namespace Umbraco.Web.Strategies { try { + // TouchServer uses a proper unit of work etc underneath so even in a + // background task it is safe to call it without dealing with any scope _svc.TouchServer(_serverAddress, _svc.CurrentServerIdentity, _registrar.Options.StaleServerTimeout); return true; // repeat @@ -148,11 +145,6 @@ namespace Umbraco.Web.Strategies return false; // probably stop if we have an error } } - - public override Task PerformRunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } } } } diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index c56d7b5b8a..a7e6738374 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -4,6 +4,7 @@ using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Web.Routing; namespace Umbraco.Web.Templates { @@ -39,11 +40,51 @@ namespace Umbraco.Web.Templates /// Parses the string looking for the {localLink} syntax and updates them to their correct links. /// /// + /// /// - public static string ParseInternalLinks(string text) + public static string ParseInternalLinks(string text, UrlProvider urlProvider) { - //TODO: Pass in an Umbraco context!!!!!!!! Don't rely on the singleton so things are more testable, better yet, pass in urlprovider, routing context, separately + if (urlProvider == null) throw new ArgumentNullException("urlProvider"); + // Parse internal links + var tags = LocalLinkPattern.Matches(text); + foreach (Match tag in tags) + { + if (tag.Groups.Count > 0) + { + var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); + + //The id could be an int or a UDI + Udi udi; + if (Udi.TryParse(id, out udi)) + { + var guidUdi = udi as GuidUdi; + if (guidUdi != null) + { + var newLink = urlProvider.GetUrl(guidUdi.Guid); + text = text.Replace(tag.Value, "href=\"" + newLink); + } + } + int intId; + if (int.TryParse(id, out intId)) + { + var newLink = urlProvider.GetUrl(intId); + text = text.Replace(tag.Value, "href=\"" + newLink); + } + } + } + + return text; + } + + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + [Obsolete("Use the overload specifying all dependencies instead")] + public static string ParseInternalLinks(string text) + { //don't attempt to proceed without a context as we cannot lookup urls without one if (UmbracoContext.Current == null || UmbracoContext.Current.RoutingContext == null) { @@ -51,22 +92,14 @@ namespace Umbraco.Web.Templates } var urlProvider = UmbracoContext.Current.UrlProvider; - - // Parse internal links - var tags = Regex.Matches(text, @"href=""[/]?(?:\{|\%7B)localLink:([0-9]+)(?:\}|\%7D)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - foreach (Match tag in tags) - if (tag.Groups.Count > 0) - { - var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); - var newLink = urlProvider.GetUrl(int.Parse(id)); - text = text.Replace(tag.Value, "href=\"" + newLink); - } - - return text; + return ParseInternalLinks(text, urlProvider); } - + // static compiled regex for faster performance - private readonly static Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", + private static readonly Regex LocalLinkPattern = new Regex(@"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", + RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); /// diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 1b90ae0dd8..f6b5e82fc3 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -30,16 +30,17 @@ namespace Umbraco.Web.Trees /// The application to load tree for /// An optional single tree alias, if specified will only load the single tree for the request app /// + /// An optional bool (defaults to true), if set to false it will also load uninitialized trees /// [HttpQueryStringFilter("queryStrings")] - public async Task GetApplicationTrees(string application, string tree, FormDataCollection queryStrings) + public async Task GetApplicationTrees(string application, string tree, FormDataCollection queryStrings, bool onlyInitialized = true) { if (string.IsNullOrEmpty(application)) throw new HttpResponseException(HttpStatusCode.NotFound); var rootId = Constants.System.Root.ToString(CultureInfo.InvariantCulture); //find all tree definitions that have the current application alias - var appTrees = ApplicationContext.Current.Services.ApplicationTreeService.GetApplicationTrees(application, true).ToArray(); + var appTrees = ApplicationContext.Current.Services.ApplicationTreeService.GetApplicationTrees(application, onlyInitialized).ToArray(); if (appTrees.Count() == 1 || string.IsNullOrEmpty(tree) == false ) { diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 9a5f90ff89..a9f0bda805 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -84,11 +84,10 @@ namespace Umbraco.Web.Trees var isContainer = e.IsContainer(); // && (queryStrings.Get("isDialog") != "true"); var node = CreateTreeNode( - e.Id.ToInvariantString(), + entity, + Constants.ObjectTypes.DocumentGuid, parentId, - queryStrings, - e.Name, - entity.ContentTypeIcon, + queryStrings, entity.HasChildren && (isContainer == false)); node.AdditionalData.Add("contentType", entity.ContentTypeAlias); @@ -208,7 +207,11 @@ namespace Umbraco.Web.Trees return false; } - IContent content = Services.ContentService.GetById(entity.Id); + var content = Services.ContentService.GetById(entity.Id); + if (content == null) + { + return false; + } return Security.CurrentUser.HasPathAccess(content); } diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 5e8172b29f..97c5daed96 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -300,24 +300,31 @@ namespace Umbraco.Web.Trees } /// - /// Get an entity via an id that can be either an integer or a Guid + /// Get an entity via an id that can be either an integer, Guid or UDI /// /// /// internal IUmbracoEntity GetEntityFromId(string id) { IUmbracoEntity entity; - Guid idGuid = Guid.Empty; + + Guid idGuid; int idInt; + Udi idUdi; + if (Guid.TryParse(id, out idGuid)) { entity = Services.EntityService.GetByKey(idGuid, UmbracoObjectType); - } else if (int.TryParse(id, out idInt)) { entity = Services.EntityService.Get(idInt, UmbracoObjectType); } + else if (Udi.TryParse(id, out idUdi)) + { + var guidUdi = idUdi as GuidUdi; + entity = guidUdi != null ? Services.EntityService.GetByKey(guidUdi.Guid, UmbracoObjectType) : null; + } else { return null; diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index 7de352de79..6df71341cd 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees .OrderBy(entity => entity.Name) .Select(dt => { - var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-item-arrangement", + var node = CreateTreeNode(dt, Constants.ObjectTypes.DocumentTypeGuid, id, queryStrings, "icon-item-arrangement", //NOTE: Since 7.4+ child type creation is enabled by a config option. It defaults to on, but can be disabled if we decide to. //We need this check to keep supporting sites where childs have already been created. dt.HasChildren()); diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index 35ae7cabfd..0baadaf6ca 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -37,7 +37,7 @@ namespace Umbraco.Web.Trees .OrderBy(entity => entity.Name) .Select(dt => { - var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-folder", dt.HasChildren(), ""); + var node = CreateTreeNode(dt, Constants.ObjectTypes.DataTypeGuid, id, queryStrings, "icon-folder", dt.HasChildren()); node.Path = dt.Path; node.NodeType = "container"; //TODO: This isn't the best way to ensure a noop process for clicking a node but it works for now. diff --git a/src/Umbraco.Web/Trees/FileSystemTreeController.cs b/src/Umbraco.Web/Trees/FileSystemTreeController.cs index f9ebaaf2d6..54979990b7 100644 --- a/src/Umbraco.Web/Trees/FileSystemTreeController.cs +++ b/src/Umbraco.Web/Trees/FileSystemTreeController.cs @@ -1,94 +1,128 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Net.Http.Formatting; +using umbraco.BusinessLogic.Actions; +using Umbraco.Core; using Umbraco.Core.IO; +using Umbraco.Core.Services; using Umbraco.Web.Models.Trees; +using System.Web; namespace Umbraco.Web.Trees { public abstract class FileSystemTreeController : TreeController { - protected abstract string FilePath { get; } - protected abstract string FileSearchPattern { get; } + protected abstract IFileSystem2 FileSystem { get; } + protected abstract string[] Extensions { get; } + protected abstract string FileIcon { get; } /// /// Inheritors can override this method to modify the file node that is created. /// - /// + /// protected virtual void OnRenderFileNode(ref TreeNode treeNode) { } /// /// Inheritors can override this method to modify the folder node that is created. /// - /// + /// protected virtual void OnRenderFolderNode(ref TreeNode treeNode) { } - protected override Models.Trees.TreeNodeCollection GetTreeNodes(string id, System.Net.Http.Formatting.FormDataCollection queryStrings) + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { - string orgPath = ""; - string path = ""; - if (!string.IsNullOrEmpty(id) && id != "-1") - { - orgPath = id; - path = IOHelper.MapPath(FilePath + "/" + orgPath); - orgPath += "/"; - } - else - { - path = IOHelper.MapPath(FilePath); - } + var path = string.IsNullOrEmpty(id) == false && id != Constants.System.Root.ToInvariantString() + ? HttpUtility.UrlDecode(id).TrimStart("/") + : ""; - DirectoryInfo dirInfo = new DirectoryInfo(path); - DirectoryInfo[] dirInfos = dirInfo.GetDirectories(); + var directories = FileSystem.GetDirectories(path); var nodes = new TreeNodeCollection(); - foreach (DirectoryInfo dir in dirInfos) + foreach (var directory in directories) { - if ((dir.Attributes & FileAttributes.Hidden) == 0) - { - var HasChildren = dir.GetFiles().Length > 0 || dir.GetDirectories().Length > 0; - var node = CreateTreeNode(orgPath + dir.Name, orgPath, queryStrings, dir.Name, "icon-folder", HasChildren); + var hasChildren = FileSystem.GetFiles(directory).Any() || FileSystem.GetDirectories(directory).Any(); - OnRenderFolderNode(ref node); - if(node != null) - nodes.Add(node); - } + var name = Path.GetFileName(directory); + var node = CreateTreeNode(HttpUtility.UrlEncode(directory), path, queryStrings, name, "icon-folder", hasChildren); + OnRenderFolderNode(ref node); + if(node != null) + nodes.Add(node); } //this is a hack to enable file system tree to support multiple file extension look-up //so the pattern both support *.* *.xml and xml,js,vb for lookups - string[] allowedExtensions = new string[0]; - bool filterByMultipleExtensions = FileSearchPattern.Contains(","); - FileInfo[] fileInfo; - - if (filterByMultipleExtensions) + var files = FileSystem.GetFiles(path).Where(x => { - fileInfo = dirInfo.GetFiles(); - allowedExtensions = FileSearchPattern.ToLower().Split(','); - } - else - fileInfo = dirInfo.GetFiles(FileSearchPattern); + var extension = Path.GetExtension(x); + return extension != null && Extensions.Contains(extension.Trim('.'), StringComparer.InvariantCultureIgnoreCase); + }); - foreach (FileInfo file in fileInfo) - { - if ((file.Attributes & FileAttributes.Hidden) == 0) + foreach (var file in files) + { + var withoutExt = Path.GetFileNameWithoutExtension(file); + if (withoutExt.IsNullOrWhiteSpace() == false) { - if (filterByMultipleExtensions && Array.IndexOf(allowedExtensions, file.Extension.ToLower().Trim('.')) < 0) - continue; - - var node = CreateTreeNode(orgPath + file.Name, orgPath, queryStrings, file.Name, "icon-file", false); - + var name = Path.GetFileName(file); + var node = CreateTreeNode(HttpUtility.UrlEncode(file), path, queryStrings, name, FileIcon, false); OnRenderFileNode(ref node); - - if(node != null) + if (node != null) nodes.Add(node); } } return nodes; - } + } + + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + //if root node no need to visit the filesystem so lets just create the menu and return it + if (id == Constants.System.Root.ToInvariantString()) + { + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + //create action + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + //refresh action + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + + return menu; + } + + var path = string.IsNullOrEmpty(id) == false && id != Constants.System.Root.ToInvariantString() + ? System.Web.HttpUtility.UrlDecode(id).TrimStart("/") + : ""; + + var isFile = FileSystem.FileExists(path); + var isDirectory = FileSystem.DirectoryExists(path); + + if (isDirectory) + { + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + //create action + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + + var hasChildren = FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any(); + + //We can only delete folders if it doesn't have any children (folders or files) + if (hasChildren == false) + { + //delete action + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); + } + + //refresh action + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + } + else if (isFile) + { + //if it's not a directory then we only allow to delete the item + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + } + + return menu; + } } } diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index b08db4cacd..ed72857a40 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -73,11 +73,10 @@ namespace Umbraco.Web.Trees var isContainer = e.IsContainer(); // && (queryStrings.Get("isDialog") != "true"); var node = CreateTreeNode( - e.Id.ToInvariantString(), + entity, + Constants.ObjectTypes.MediaGuid, parentId, queryStrings, - e.Name, - entity.ContentTypeIcon, entity.HasChildren && (isContainer == false)); node.AdditionalData.Add("contentType", entity.ContentTypeAlias); @@ -131,7 +130,7 @@ namespace Umbraco.Web.Trees menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); //if the media item is in the recycle bin, don't have a default menu, just show the regular menu - if (item.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) + if (item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) { menu.DefaultMenuAlias = null; } @@ -152,7 +151,13 @@ namespace Umbraco.Web.Trees /// protected override bool HasPathAccess(string id, FormDataCollection queryStrings) { - var media = Services.MediaService.GetById(int.Parse(id)); + var entity = GetEntityFromId(id); + if (entity == null) + { + return false; + } + + var media = Services.MediaService.GetById(entity.Id); if (media == null) { return false; diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index 31fb899f81..e116e43825 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees .OrderBy(entity => entity.Name) .Select(dt => { - var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-item-arrangement", + var node = CreateTreeNode(dt, Constants.ObjectTypes.MediaTypeGuid, id, queryStrings, "icon-item-arrangement", //NOTE: Since 7.4+ child type creation is enabled by a config option. It defaults to on, but can be disabled if we decide to. //We need this check to keep supporting sites where childs have already been created. dt.HasChildren()); diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index fa6eb64571..fe7aa8a689 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -79,7 +79,9 @@ namespace Umbraco.Web.Trees queryStrings, member.Name, "icon-user", - false); + false, + "", + Udi.Create(UmbracoObjectTypesExtensions.GetUdiType(Constants.ObjectTypes.MemberGuid), member.Key)); node.AdditionalData.Add("contentType", member.ContentTypeAlias); node.AdditionalData.Add("isContainer", true); diff --git a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs index d739c2a661..1e0c32a4b4 100644 --- a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Trees nodes.AddRange( Services.MemberTypeService.GetAll() .OrderBy(x => x.Name) - .Select(dt => CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-item-arrangement", false))); + .Select(dt => CreateTreeNode(dt, Constants.ObjectTypes.MemberTypeGuid, id, queryStrings, "icon-item-arrangement", false))); return nodes; } diff --git a/src/Umbraco.Web/Trees/PartialViewMacrosTree.cs b/src/Umbraco.Web/Trees/PartialViewMacrosTree.cs deleted file mode 100644 index cf13a24d36..0000000000 --- a/src/Umbraco.Web/Trees/PartialViewMacrosTree.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Text; -using Umbraco.Core.IO; -using umbraco.businesslogic; -using umbraco.cms.presentation.Trees; -using Umbraco.Core; - -namespace Umbraco.Web.Trees -{ - /// - /// Tree for displaying partial view macros in the developer app - /// - [Tree(Constants.Applications.Developer, "partialViewMacros", null, sortOrder: 6)] - public class PartialViewMacrosTree : PartialViewsTree - { - public PartialViewMacrosTree(string application) : base(application) - { - } - - protected override string FilePath - { - get { return SystemDirectories.MvcViews + "/MacroPartials/"; } - } - - public override void RenderJS(ref StringBuilder javascript) - { - javascript.Append( - @" - function openMacroPartialView(id) { - UmbClientMgr.contentFrame('Settings/Views/EditView.aspx?treeType=partialViewMacros&file=' + id); - } - "); - - }/// - /// Ensures that no folders can be added - /// - /// - protected override void OnRenderFolderNode(ref XmlTreeNode xNode) - { - base.OnRenderFolderNode(ref xNode); - - xNode.NodeType = "partialViewMacrosFolder"; - } - - protected override void ChangeNodeAction(XmlTreeNode xNode) - { - xNode.Action = xNode.Action.Replace("openFile", "openMacroPartialView"); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/PartialViewMacrosTreeController.cs b/src/Umbraco.Web/Trees/PartialViewMacrosTreeController.cs new file mode 100644 index 0000000000..b4cc2eb2fd --- /dev/null +++ b/src/Umbraco.Web/Trees/PartialViewMacrosTreeController.cs @@ -0,0 +1,36 @@ +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Web.Models.Trees; + +namespace Umbraco.Web.Trees +{ + /// + /// Tree for displaying partial view macros in the developer app + /// + [Tree(Constants.Applications.Developer, "partialViewMacros", "Partial View Macro Files", sortOrder: 6)] + public class PartialViewMacrosTreeController : FileSystemTreeController + { + protected override IFileSystem2 FileSystem + { + get { return FileSystemProviderManager.Current.MacroPartialsFileSystem; } + } + + private static readonly string[] ExtensionsStatic = { "cshtml" }; + + protected override string[] Extensions + { + get { return ExtensionsStatic; } + } + + protected override string FileIcon + { + get { return "icon-article"; } + } + + protected override void OnRenderFolderNode(ref TreeNode treeNode) + { + //TODO: This isn't the best way to ensure a noop process for clicking a node but it works for now. + treeNode.AdditionalData["jsClickCallback"] = "javascript:void(0);"; + } + } +} diff --git a/src/Umbraco.Web/Trees/PartialViewsTree.cs b/src/Umbraco.Web/Trees/PartialViewsTree.cs deleted file mode 100644 index 594355f6c7..0000000000 --- a/src/Umbraco.Web/Trees/PartialViewsTree.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Web; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using umbraco.BusinessLogic.Actions; -using umbraco.businesslogic; -using umbraco.cms.businesslogic.template; -using umbraco.cms.presentation.Trees; -using umbraco.interfaces; - -namespace Umbraco.Web.Trees -{ - /// - /// Tree for displaying partial views in the settings app - /// - [Tree(Constants.Applications.Settings, "partialViews", null, sortOrder: 2)] - public class PartialViewsTree : FileSystemTree - { - public PartialViewsTree(string application) : base(application) { } - - public override void RenderJS(ref StringBuilder javascript) - { - javascript.Append( - @" - function openPartialView(id) { - UmbClientMgr.contentFrame('Settings/Views/EditView.aspx?treeType=partialViews&file=' + id); - } - "); - } - - protected override void CreateRootNode(ref XmlTreeNode rootNode) - { - rootNode.NodeType = TreeAlias; - rootNode.NodeID = "init"; - } - - protected override string FilePath - { - get { return SystemDirectories.MvcViews + "/Partials/"; } - } - - protected override string FileSearchPattern - { - get { return "*.cshtml"; } - } - - /// - /// Ensures that no folders can be added - /// - /// - protected override void OnRenderFolderNode(ref XmlTreeNode xNode) - { - // We should allow folder hierarchy for organization in large sites. - xNode.Action = "javascript:void(0);"; - xNode.NodeType = "partialViewsFolder"; - xNode.Menu = new List(new IAction[] - { - ActionNew.Instance, - ContextMenuSeperator.Instance, - ActionDelete.Instance, - ContextMenuSeperator.Instance, - ActionRefresh.Instance - }); - - } - - protected virtual void ChangeNodeAction(XmlTreeNode xNode) - { - xNode.Action = xNode.Action.Replace("openFile", "openPartialView"); - } - - protected override void OnRenderFileNode(ref XmlTreeNode xNode) - { - ChangeNodeAction(xNode); - xNode.Icon = "icon-article"; - xNode.OpenIcon = "icon-article"; - - xNode.Text = xNode.Text.StripFileExtension(); - } - - - } -} diff --git a/src/Umbraco.Web/Trees/PartialViewsTreeController.cs b/src/Umbraco.Web/Trees/PartialViewsTreeController.cs new file mode 100644 index 0000000000..264b44ad73 --- /dev/null +++ b/src/Umbraco.Web/Trees/PartialViewsTreeController.cs @@ -0,0 +1,36 @@ +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Web.Models.Trees; + +namespace Umbraco.Web.Trees +{ + /// + /// Tree for displaying partial views in the settings app + /// + [Tree(Constants.Applications.Settings, "partialViews", "Partial Views", sortOrder: 2)] + public class PartialViewsTreeController : FileSystemTreeController + { + protected override IFileSystem2 FileSystem + { + get { return FileSystemProviderManager.Current.PartialViewsFileSystem; } + } + + private static readonly string[] ExtensionsStatic = { "cshtml" }; + + protected override string[] Extensions + { + get { return ExtensionsStatic; } + } + + protected override string FileIcon + { + get { return "icon-article"; } + } + + protected override void OnRenderFolderNode(ref TreeNode treeNode) + { + //TODO: This isn't the best way to ensure a noop process for clicking a node but it works for now. + treeNode.AdditionalData["jsClickCallback"] = "javascript:void(0);"; + } + } +} diff --git a/src/Umbraco.Web/Trees/ScriptTreeController.cs b/src/Umbraco.Web/Trees/ScriptTreeController.cs new file mode 100644 index 0000000000..616106a625 --- /dev/null +++ b/src/Umbraco.Web/Trees/ScriptTreeController.cs @@ -0,0 +1,32 @@ +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Web.Models.Trees; + +namespace Umbraco.Web.Trees +{ + [Tree(Constants.Applications.Settings, "scripts", "Scripts", sortOrder: 4)] + public class ScriptTreeController : FileSystemTreeController + { + protected override IFileSystem2 FileSystem + { + get { return FileSystemProviderManager.Current.ScriptsFileSystem; } + } + + private static readonly string[] ExtensionsStatic = { "js" }; + + protected override string[] Extensions + { + get { return ExtensionsStatic; } + } + protected override string FileIcon + { + get { return "icon-script"; } + } + + protected override void OnRenderFolderNode(ref TreeNode treeNode) + { + //TODO: This isn't the best way to ensure a noop process for clicking a node but it works for now. + treeNode.AdditionalData["jsClickCallback"] = "javascript:void(0);"; + } + } +} diff --git a/src/Umbraco.Web/Trees/TemplatesTreeController.cs b/src/Umbraco.Web/Trees/TemplatesTreeController.cs index 73f9389150..3869f58a2a 100644 --- a/src/Umbraco.Web/Trees/TemplatesTreeController.cs +++ b/src/Umbraco.Web/Trees/TemplatesTreeController.cs @@ -53,7 +53,9 @@ namespace Umbraco.Web.Trees template.Name, template.IsMasterTemplate ? "icon-newspaper" : "icon-newspaper-alt", template.IsMasterTemplate, - GetEditorPath(template, queryStrings)))); + GetEditorPath(template, queryStrings), + Udi.Create(UmbracoObjectTypesExtensions.GetUdiType(Constants.ObjectTypes.TemplateTypeGuid), template.Key) + ))); return nodes; } @@ -67,15 +69,14 @@ namespace Umbraco.Web.Trees protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { var menu = new MenuItemCollection(); + //Create the normal create action + var item = menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)); + item.NavigateToRoute(string.Format("{0}/templates/edit/{1}?create=true", queryStrings.GetValue("application"), id)); + if (id == Constants.System.Root.ToInvariantString()) { - //Create the normal create action - menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)) - //Since we haven't implemented anything for templates in angular, this needs to be converted to - //use the legacy format - .ConvertLegacyMenuItem(null, "inittemplates", queryStrings.GetValue("application")); - + //refresh action menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); @@ -86,12 +87,6 @@ namespace Umbraco.Web.Trees if (template == null) return new MenuItemCollection(); var entity = FromTemplate(template); - //Create the create action for creating sub layouts - menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)) - //Since we haven't implemented anything for templates in angular, this needs to be converted to - //use the legacy format - .ConvertLegacyMenuItem(entity, "templates", queryStrings.GetValue("application")); - //don't allow delete if it has child layouts if (template.IsMasterTemplate == false) { @@ -132,8 +127,7 @@ namespace Umbraco.Web.Trees return Services.FileService.DetermineTemplateRenderingEngine(template) == RenderingEngine.WebForms ? "/" + queryStrings.GetValue("application") + "/framed/" + Uri.EscapeDataString("settings/editTemplate.aspx?templateID=" + template.Id) - : "/" + queryStrings.GetValue("application") + "/framed/" + - Uri.EscapeDataString("settings/Views/EditView.aspx?treeType=" + Constants.Trees.Templates + "&templateID=" + template.Id); + : null; } } } diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 91191a165d..751f83db16 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -8,6 +8,7 @@ using Umbraco.Web.Models.Trees; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; namespace Umbraco.Web.Trees { @@ -229,6 +230,41 @@ namespace Umbraco.Web.Trees return node; } + /// + /// Helper method to create tree nodes and automatically generate the json url + UDI + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(UmbracoEntity entity, Guid entityObjectType, string parentId, FormDataCollection queryStrings, bool hasChildren) + { + var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, entity.ContentTypeIcon); + treeNode.Udi = Udi.Create(UmbracoObjectTypesExtensions.GetUdiType(entityObjectType), entity.Key); + treeNode.HasChildren = hasChildren; + return treeNode; + } + + /// + /// Helper method to create tree nodes and automatically generate the json url + UDI + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(IUmbracoEntity entity, Guid entityObjectType, string parentId, FormDataCollection queryStrings, string icon, bool hasChildren) + { + var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, icon); + treeNode.Udi = Udi.Create(UmbracoObjectTypesExtensions.GetUdiType(entityObjectType), entity.Key); + treeNode.HasChildren = hasChildren; + return treeNode; + } + /// /// Helper method to create tree nodes and automatically generate the json url /// @@ -265,6 +301,27 @@ namespace Umbraco.Web.Trees return treeNode; } + /// + /// Helper method to create tree nodes and automatically generate the json url + UDI + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormDataCollection queryStrings, string title, string icon, bool hasChildren, string routePath, Udi udi) + { + var treeNode = CreateTreeNode(id, parentId, queryStrings, title, icon); + treeNode.HasChildren = hasChildren; + treeNode.RoutePath = routePath; + treeNode.Udi = udi; + return treeNode; + } + #endregion /// diff --git a/src/Umbraco.Web/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web/Trees/UrlHelperExtensions.cs index 0931fc0997..4b94f548ca 100644 --- a/src/Umbraco.Web/Trees/UrlHelperExtensions.cs +++ b/src/Umbraco.Web/Trees/UrlHelperExtensions.cs @@ -1,5 +1,8 @@ using System; +using System.Linq; using System.Net.Http.Formatting; +using System.Text; +using System.Web; using System.Web.Http.Routing; using Umbraco.Core; @@ -30,5 +33,35 @@ namespace Umbraco.Web.Trees return actionUrl; } + + internal static string GetTreePathFromFilePath(this UrlHelper urlHelper, string virtualPath, string basePath = "") + { + //This reuses the Logic from umbraco.cms.helpers.DeepLink class + //to convert a filepath to a tree syncing path string. + + //removes the basepath from the path + //and normalises paths - / is used consistently between trees and editors + basePath = basePath.TrimStart("~"); + virtualPath = virtualPath.TrimStart("~"); + virtualPath = virtualPath.Substring(basePath.Length); + virtualPath = virtualPath.Replace('\\', '/'); + + //-1 is the default root id for trees + var sb = new StringBuilder("-1"); + + //split the virtual path and iterate through it + var pathPaths = virtualPath.Split('/'); + + for (var p = 0; p < pathPaths.Length; p++) + { + var path = HttpUtility.UrlEncode(string.Join("/", pathPaths.Take(p + 1))); + if (string.IsNullOrEmpty(path) == false) + { + sb.Append(","); + sb.Append(path); + } + } + return sb.ToString().TrimEnd(","); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/UI/Controls/UmbracoControl.cs b/src/Umbraco.Web/UI/Controls/UmbracoControl.cs index bc633b22a3..b84deaa232 100644 --- a/src/Umbraco.Web/UI/Controls/UmbracoControl.cs +++ b/src/Umbraco.Web/UI/Controls/UmbracoControl.cs @@ -81,7 +81,7 @@ namespace Umbraco.Web.UI.Controls /// 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.Web/UI/Controls/UmbracoUserControl.cs b/src/Umbraco.Web/UI/Controls/UmbracoUserControl.cs index 2a18413ace..246b9d45c2 100644 --- a/src/Umbraco.Web/UI/Controls/UmbracoUserControl.cs +++ b/src/Umbraco.Web/UI/Controls/UmbracoUserControl.cs @@ -119,7 +119,7 @@ namespace Umbraco.Web.UI.Controls /// 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.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 0314c65fae..3be8a58983 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -16,6 +16,8 @@ 'lib/ng-file-upload/ng-file-upload.min.js', 'lib/angular-local-storage/angular-local-storage.min.js', + //"lib/ace-builds/src-min-noconflict/ace.js", + 'lib/bootstrap/js/bootstrap.2.3.2.min.js', 'lib/bootstrap-tabdrop/bootstrap-tabdrop.js', 'lib/umbraco/Extensions.js', diff --git a/src/Umbraco.Web/UI/JavaScript/Resources.Designer.cs b/src/Umbraco.Web/UI/JavaScript/Resources.Designer.cs index 3b8bfd9385..8c7087939d 100644 --- a/src/Umbraco.Web/UI/JavaScript/Resources.Designer.cs +++ b/src/Umbraco.Web/UI/JavaScript/Resources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18444 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -62,21 +62,21 @@ namespace Umbraco.Web.UI.JavaScript { /// /// Looks up a localized string similar to [ - /// 'lib/jquery/jquery-2.0.3.min.js', + /// 'lib/jquery/jquery.min.js', /// 'lib/angular/1.1.5/angular.min.js', - /// 'lib/underscore/underscore.js', + /// 'lib/underscore/underscore-min.js', /// - /// 'lib/jquery/jquery-ui-1.10.3.custom.min.js', + /// 'lib/jquery-ui/jquery-ui.min.js', + /// 'lib/jquery-ui-touch-punch/jquery.ui.touch-punch.js', /// /// 'lib/angular/1.1.5/angular-cookies.min.js', /// 'lib/angular/1.1.5/angular-mobile.js', /// 'lib/angular/1.1.5/angular-sanitize.min.js', /// /// 'lib/angular/angular-ui-sortable.js', - /// - /// 'lib/jquery/jquery.upload/js/jquery.fileupload.js', - /// 'lib/jquery/jquery.upload/js/load-image.min.js', - /// 'lib/jquery/jquery.upload/js/ [rest of string was truncated]";. + /// + /// 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', + /// 'lib/ng-file-upload/ng-file-upload.min. [rest of string was truncated]";. /// internal static string JsInitialize { get { @@ -90,7 +90,9 @@ namespace Umbraco.Web.UI.JavaScript { /// UmbClientMgr.setUmbracoPath('"##UmbracoPath##"'); /// /// jQuery(document).ready(function () { + /// /// angular.bootstrap(document, ['umbraco']); + /// /// }); ///});. /// diff --git a/src/Umbraco.Web/UI/Pages/BasePage.cs b/src/Umbraco.Web/UI/Pages/BasePage.cs index b9eec4982b..e2c64b7ece 100644 --- a/src/Umbraco.Web/UI/Pages/BasePage.cs +++ b/src/Umbraco.Web/UI/Pages/BasePage.cs @@ -69,7 +69,7 @@ namespace Umbraco.Web.UI.Pages /// 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)); } } private HtmlHelper _html; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b3137b47dc..d3e31fa366 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1,5 +1,5 @@  - + 9.0.30729 @@ -99,68 +99,90 @@ ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.dll + True ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll + True ..\packages\ClientDependency.1.9.2\lib\net45\ClientDependency.Core.dll + True ..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll + True - - ..\packages\dotless.1.4.1.0\lib\dotless.Core.dll + + ..\packages\dotless.1.5.2\lib\dotless.Core.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\Markdown.1.14.7\lib\net45\MarkdownSharp.dll + True ..\packages\Microsoft.AspNet.Identity.Core.2.2.1\lib\net45\Microsoft.AspNet.Identity.Core.dll + True ..\packages\Microsoft.AspNet.Identity.Owin.2.2.1\lib\net45\Microsoft.AspNet.Identity.Owin.dll + True + + + ..\packages\Microsoft.AspNet.SignalR.Core.2.2.1\lib\net45\Microsoft.AspNet.SignalR.Core.dll + True ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + True ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + True ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll + True ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll + True ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + True ..\packages\MiniProfiler.2.1.0\lib\net40\MiniProfiler.dll - - - ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll True + + ..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll + ..\packages\Owin.1.0\lib\net40\Owin.dll + True ..\packages\semver.1.1.2\lib\net45\Semver.dll @@ -168,6 +190,7 @@ System + @@ -183,9 +206,13 @@ ..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll + True + + ..\packages\System.Threading.Tasks.Dataflow.4.7.0\lib\portable-net45+win8+wpa81\System.Threading.Tasks.Dataflow.dll + 3.5 @@ -197,30 +224,38 @@ ..\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 System.Web.Services ..\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 @@ -261,9 +296,6 @@ {D7636876-0756-43CB-A192-138C6F0D5E42} umbraco.providers - - ..\packages\UrlRewritingNet.UrlRewriter.2.0.7\lib\UrlRewritingNet.UrlRewriter.dll - @@ -276,8 +308,12 @@ + + + + @@ -328,11 +364,22 @@ + + + + + + + + + + + @@ -348,16 +395,27 @@ + + + + + + + + + + + - + @@ -381,6 +439,11 @@ + + + + ASPXCodeBehind + @@ -439,7 +502,6 @@ - @@ -502,7 +564,6 @@ - @@ -679,9 +740,6 @@ ASPXCodeBehind - - ASPXCodeBehind - ASPXCodeBehind @@ -1037,8 +1095,8 @@ - - + + @@ -1460,13 +1518,6 @@ autoDoc.aspx - - BrowseRepository.aspx - ASPXCodeBehind - - - BrowseRepository.aspx - editPackage.aspx ASPXCodeBehind @@ -1474,27 +1525,6 @@ editPackage.aspx - - installedPackage.aspx - ASPXCodeBehind - - - installedPackage.aspx - - - LoadNitros.ascx - ASPXCodeBehind - - - LoadNitros.ascx - - - SubmitPackage.aspx - ASPXCodeBehind - - - SubmitPackage.aspx - getXsltStatus.asmx Component @@ -2014,15 +2044,9 @@ - ASPXCodeBehind - - ASPXCodeBehind - - - ASPXCodeBehind @@ -2207,6 +2231,7 @@ umbraco_org_umbraco_update_CheckForUpgrade + 11.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v11.0 diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index d878ac3d1d..90f4e4df03 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -10,6 +10,7 @@ using Umbraco.Web.Routing; using Umbraco.Web.Security; using umbraco.BusinessLogic; using umbraco.presentation.preview; +using Umbraco.Core.CodeAnnotations; using GlobalSettings = umbraco.GlobalSettings; using IOHelper = Umbraco.Core.IO.IOHelper; using SystemDirectories = Umbraco.Core.IO.SystemDirectories; @@ -324,7 +325,7 @@ namespace Umbraco.Web /// /// Gets the current ApplicationContext /// - [Obsolete("Do not access the ApplicationContext via the UmbracoContext, either inject the ApplicationContext into the services you need or access it via it's own Singleton accessor ApplicationContext.Current")] + [UmbracoWillObsolete("Do not access the ApplicationContext via the UmbracoContext, either inject the ApplicationContext into the services you need or access it via it's own Singleton accessor ApplicationContext.Current")] [EditorBrowsable(EditorBrowsableState.Never)] public ApplicationContext Application { get; private set; } diff --git a/src/Umbraco.Web/UmbracoContextExtensions.cs b/src/Umbraco.Web/UmbracoContextExtensions.cs index 8cd38df6d1..3e5c9d355e 100644 --- a/src/Umbraco.Web/UmbracoContextExtensions.cs +++ b/src/Umbraco.Web/UmbracoContextExtensions.cs @@ -53,9 +53,8 @@ namespace Umbraco.Web /// public static EventMessages GetCurrentEventMessages(this UmbracoContext umbracoContext) { - var msgs = umbracoContext.HttpContext.Items[typeof (RequestLifespanMessagesFactory).Name]; - if (msgs == null) return null; - return (EventMessages) msgs; + var eventMessagesFactory = umbracoContext.Application.Services.EventMessagesFactory as ScopeLifespanMessagesFactory; + return eventMessagesFactory == null ? null : eventMessagesFactory.TryGet(); } } diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index 046b2167d5..c11252d2ca 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -1,11 +1,7 @@ using System; -using System.Web; using Microsoft.Owin; -using Microsoft.Owin.Extensions; -using Microsoft.Owin.Logging; using Owin; using Umbraco.Core; -using Umbraco.Core.Logging; using Umbraco.Core.Security; using Umbraco.Web; using Umbraco.Web.Security.Identity; @@ -42,11 +38,11 @@ namespace Umbraco.Web { app.SetUmbracoLoggerFactory(); - //Configure the Identity user manager for use with Umbraco Back office + //Configure the Identity user manager for use with Umbraco Back office // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) app.ConfigureUserManagerForUmbracoBackOffice( ApplicationContext, - Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); + Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); } /// @@ -61,6 +57,7 @@ namespace Umbraco.Web .UseUmbracoBackOfficeCookieAuthentication(ApplicationContext, PipelineStage.Authenticate) .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext, PipelineStage.Authenticate) .UseUmbracoPreviewAuthentication(ApplicationContext, PipelineStage.Authorize) + .UseSignalR() .FinalizeMiddlewareConfiguration(); } diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 629d69ed4a..614c815f79 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -101,7 +101,12 @@ namespace Umbraco.Web /// Add a serialised date of the last edit of the item to ensure client cache refresh when updated /// /// - /// The further options. + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// /// /// /// Use a dimension as a ratio @@ -113,7 +118,7 @@ namespace Umbraco.Web /// /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be /// set to false if using the result of this method for CSS. - /// + /// /// /// The . /// @@ -177,7 +182,12 @@ namespace Umbraco.Web /// Add a serialised date of the last edit of the item to ensure client cache refresh when updated /// /// - /// The further options. + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// /// /// /// Use a dimension as a ratio @@ -189,7 +199,7 @@ namespace Umbraco.Web /// /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be /// set to false if using the result of this method for CSS. - /// + /// /// /// The . /// @@ -333,7 +343,5 @@ namespace Umbraco.Web { return url.SurfaceAction(action, typeof (T), additionalRouteVals); } - - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs b/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs index ad6323da86..5e753fa7f8 100644 --- a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs +++ b/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34003 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -9,7 +9,7 @@ //------------------------------------------------------------------------------ // -// This source code was auto-generated by Microsoft.VSDesigner, Version 4.0.30319.34003. +// This source code was auto-generated by Microsoft.VSDesigner, Version 4.0.30319.42000. // #pragma warning disable 1591 @@ -23,7 +23,7 @@ namespace Umbraco.Web.org.umbraco.our { /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Web.Services.WebServiceBindingAttribute(Name="RepositorySoap", Namespace="http://packages.umbraco.org/webservices/")] @@ -480,7 +480,7 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1586.0")] [System.SerializableAttribute()] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] @@ -549,7 +549,7 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1586.0")] [System.SerializableAttribute()] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] @@ -738,7 +738,7 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1586.0")] [System.SerializableAttribute()] [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] public enum SubmitStatus { @@ -757,11 +757,11 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void CategoriesCompletedEventHandler(object sender, CategoriesCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class CategoriesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -783,11 +783,11 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void ModulesCompletedEventHandler(object sender, ModulesCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class ModulesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -809,11 +809,11 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void ModulesCategorizedCompletedEventHandler(object sender, ModulesCategorizedCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class ModulesCategorizedCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -835,11 +835,11 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void NitrosCompletedEventHandler(object sender, NitrosCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class NitrosCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -861,11 +861,11 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void NitrosCategorizedCompletedEventHandler(object sender, NitrosCategorizedCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class NitrosCategorizedCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -887,11 +887,11 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void authenticateCompletedEventHandler(object sender, authenticateCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class authenticateCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -913,11 +913,11 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void fetchPackageCompletedEventHandler(object sender, fetchPackageCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class fetchPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -939,11 +939,11 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void fetchPackageByVersionCompletedEventHandler(object sender, fetchPackageByVersionCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class fetchPackageByVersionCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -965,11 +965,11 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void fetchProtectedPackageCompletedEventHandler(object sender, fetchProtectedPackageCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class fetchProtectedPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -991,11 +991,11 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void SubmitPackageCompletedEventHandler(object sender, SubmitPackageCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class SubmitPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { @@ -1017,11 +1017,11 @@ namespace Umbraco.Web.org.umbraco.our { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void PackageByGuidCompletedEventHandler(object sender, PackageByGuidCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.33440")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class PackageByGuidCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/Reference.cs b/src/Umbraco.Web/Web References/org.umbraco.update/Reference.cs index e68ae400f5..9e35deb713 100644 --- a/src/Umbraco.Web/Web References/org.umbraco.update/Reference.cs +++ b/src/Umbraco.Web/Web References/org.umbraco.update/Reference.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18444 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -9,7 +9,7 @@ //------------------------------------------------------------------------------ // -// This source code was auto-generated by Microsoft.VSDesigner, Version 4.0.30319.18444. +// This source code was auto-generated by Microsoft.VSDesigner, Version 4.0.30319.42000. // #pragma warning disable 1591 @@ -23,7 +23,7 @@ namespace Umbraco.Web.org.umbraco.update { /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.18408")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Web.Services.WebServiceBindingAttribute(Name="CheckForUpgradeSoap", Namespace="http://update.umbraco.org/")] @@ -180,7 +180,7 @@ namespace Umbraco.Web.org.umbraco.update { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.0.30319.18408")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1586.0")] [System.SerializableAttribute()] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] @@ -225,7 +225,7 @@ namespace Umbraco.Web.org.umbraco.update { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.0.30319.18408")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1586.0")] [System.SerializableAttribute()] [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://update.umbraco.org/")] public enum UpgradeType { @@ -253,15 +253,15 @@ namespace Umbraco.Web.org.umbraco.update { } /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.18408")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void InstallCompletedEventHandler(object sender, System.ComponentModel.AsyncCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.18408")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] public delegate void CheckUpgradeCompletedEventHandler(object sender, CheckUpgradeCompletedEventArgs e); /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.0.30319.18408")] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.6.1586.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] public partial class CheckUpgradeCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs index cb0f07f796..e126d42c21 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs @@ -22,7 +22,6 @@ namespace Umbraco.Web.WebApi.Filters { private readonly int? _nodeId; private readonly string _paramName; - private DictionarySource _source; public enum DictionarySource { @@ -43,14 +42,12 @@ namespace Umbraco.Web.WebApi.Filters { Mandate.ParameterNotNullOrEmpty(paramName, "paramName"); _paramName = paramName; - _source = DictionarySource.ActionArguments; } public EnsureUserPermissionForMediaAttribute(string paramName, DictionarySource source) { Mandate.ParameterNotNullOrEmpty(paramName, "paramName"); _paramName = paramName; - _source = source; } public override bool AllowMultiple @@ -58,6 +55,33 @@ namespace Umbraco.Web.WebApi.Filters get { return true; } } + private int GetNodeIdFromParameter(object parameterValue) + { + if (parameterValue is int) + { + return (int) parameterValue; + } + + var guidId = Guid.Empty; + if (parameterValue is Guid) + { + guidId = (Guid)parameterValue; + } + else if (parameterValue is GuidUdi) + { + guidId = ((GuidUdi) parameterValue).Guid; + } + + if (guidId != Guid.Empty) + { + var found = ApplicationContext.Current.Services.EntityService.GetIdForKey(guidId, UmbracoObjectTypes.Media); + if (found) + return found.Result; + } + + throw new InvalidOperationException("The id type: " + parameterValue.GetType() + " is not a supported id"); + } + public override void OnActionExecuting(HttpActionContext actionContext) { if (UmbracoContext.Current.Security.CurrentUser == null) @@ -68,7 +92,7 @@ namespace Umbraco.Web.WebApi.Filters int nodeId; if (_nodeId.HasValue == false) { - var parts = _paramName.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries); + var parts = _paramName.Split(new [] { '.' }, StringSplitOptions.RemoveEmptyEntries); if (actionContext.ActionArguments[parts[0]] == null) { @@ -77,7 +101,7 @@ namespace Umbraco.Web.WebApi.Filters if (parts.Length == 1) { - nodeId = (int)actionContext.ActionArguments[parts[0]]; + nodeId = GetNodeIdFromParameter(actionContext.ActionArguments[parts[0]]); } else { @@ -88,7 +112,7 @@ namespace Umbraco.Web.WebApi.Filters { throw new InvalidOperationException("No argument found for the current action with the name: " + _paramName); } - nodeId = (int)prop.GetValue(actionContext.ActionArguments[parts[0]]); + nodeId = GetNodeIdFromParameter(prop.GetValue(actionContext.ActionArguments[parts[0]])); } } else @@ -109,22 +133,7 @@ namespace Umbraco.Web.WebApi.Filters } } - - //private object GetValueFromSource(HttpActionContext actionContext, string key) - //{ - // switch (_source) - // { - // case DictionarySource.ActionArguments: - // return actionContext.ActionArguments[key]; - // case DictionarySource.RequestForm: - // return actionContext.Request.Properties - // case DictionarySource.RequestQueryString: - // break; - // default: - // throw new ArgumentOutOfRangeException(); - // } - //} - + } diff --git a/src/Umbraco.Web/WebApi/JsonCamelCaseFormatter.cs b/src/Umbraco.Web/WebApi/JsonCamelCaseFormatter.cs index b54d6102c0..de1383706e 100644 --- a/src/Umbraco.Web/WebApi/JsonCamelCaseFormatter.cs +++ b/src/Umbraco.Web/WebApi/JsonCamelCaseFormatter.cs @@ -8,7 +8,7 @@ using Newtonsoft.Json.Serialization; namespace Umbraco.Web.WebApi { /// - /// Applying this attribute to any webapi controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. + /// Applying this attribute to any webapi controller will ensure that it only contains one json formatter with a camelCase formatter /// public class JsonCamelCaseFormatter : Attribute, IControllerConfiguration { diff --git a/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs index e018881f8a..2593acfbe0 100644 --- a/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs +++ b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs @@ -6,7 +6,7 @@ using System.Web.Http.Validation; namespace Umbraco.Web.WebApi { /// - /// Applying this attribute to any webapi controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. + /// Applying this attribute to any webapi controller will ensure that the is of type /// internal class PrefixlessBodyModelValidatorAttribute : Attribute, IControllerConfiguration { diff --git a/src/Umbraco.Web/WebApi/UmbracoApiController.cs b/src/Umbraco.Web/WebApi/UmbracoApiController.cs index b850532011..0356432c66 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiController.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Web.Http; using System.Web.Http.ModelBinding; +using umbraco.interfaces; using Umbraco.Core.Models; using Umbraco.Core.Models.Validation; using Umbraco.Web.Models.ContentEditing; @@ -11,7 +12,7 @@ namespace Umbraco.Web.WebApi /// /// The base class for auto-routed API controllers for Umbraco /// - public abstract class UmbracoApiController : UmbracoApiControllerBase + public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable { protected UmbracoApiController() { diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 38f896381f..29565065e2 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -42,9 +42,11 @@ using Umbraco.Web.UI.JavaScript; using Umbraco.Web.WebApi; using umbraco.BusinessLogic; using Umbraco.Core.Cache; +using Umbraco.Core.Events; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Publishing; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Web.Editors; using Umbraco.Web.HealthCheck; @@ -86,17 +88,15 @@ namespace Umbraco.Web /// Creates and returns the service context for the app /// /// - /// + /// /// - protected override ServiceContext CreateServiceContext(DatabaseContext dbContext, IDatabaseFactory dbFactory) + protected override ServiceContext CreateServiceContext(DatabaseContext dbContext, IScopeProvider scopeProvider) { //use a request based messaging factory - var evtMsgs = new RequestLifespanMessagesFactory(new SingletonHttpContextAccessor()); + var evtMsgs = new ScopeLifespanMessagesFactory(new SingletonHttpContextAccessor(), scopeProvider); return new ServiceContext( new RepositoryFactory(ApplicationCache, ProfilingLogger.Logger, dbContext.SqlSyntax, UmbracoConfig.For.UmbracoSettings()), - new PetaPocoUnitOfWorkProvider(dbFactory), - new FileUnitOfWorkProvider(), - new PublishingStrategy(evtMsgs, ProfilingLogger.Logger), + new PetaPocoUnitOfWorkProvider(scopeProvider), ApplicationCache, ProfilingLogger.Logger, evtMsgs); @@ -533,11 +533,11 @@ namespace Umbraco.Web ThumbnailProvidersResolver.Current = new ThumbnailProvidersResolver( ServiceProvider, LoggerResolver.Current.Logger, - PluginManager.ResolveThumbnailProviders()); + () => PluginManager.ResolveThumbnailProviders()); ImageUrlProviderResolver.Current = new ImageUrlProviderResolver( ServiceProvider, LoggerResolver.Current.Logger, - PluginManager.ResolveImageUrlProviders()); + () => PluginManager.ResolveImageUrlProviders()); CultureDictionaryFactoryResolver.Current = new CultureDictionaryFactoryResolver( new DefaultCultureDictionaryFactory()); diff --git a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs index 1a4f5d4b5d..ca49f7a9f7 100644 --- a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs +++ b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs @@ -74,7 +74,11 @@ namespace Umbraco.Web.WebServices /// public IEnumerable GetIndexerDetails() { - return ExamineManager.Instance.IndexProviderCollection.Select(CreateModel); + return ExamineManager.Instance.IndexProviderCollection.Select(CreateModel).OrderBy(x => + { + //order by name , but strip the "Indexer" from the end if it exists + return x.Name.TrimEnd("Indexer"); + }); } /// @@ -99,6 +103,10 @@ namespace Umbraco.Web.WebServices indexerModel.ProviderProperties.Add(p.Name, p.GetValue(searcher, null).ToString()); } return indexerModel; + }).OrderBy(x => + { + //order by name , but strip the "Searcher" from the end if it exists + return x.Name.TrimEnd("Searcher"); })); return model; } diff --git a/src/Umbraco.Web/WebServices/FolderBrowserService.cs b/src/Umbraco.Web/WebServices/FolderBrowserService.cs index bf5a3f1bd5..15a6c10880 100644 --- a/src/Umbraco.Web/WebServices/FolderBrowserService.cs +++ b/src/Umbraco.Web/WebServices/FolderBrowserService.cs @@ -15,7 +15,7 @@ using Tag = umbraco.cms.businesslogic.Tags.Tag; namespace Umbraco.Web.WebServices { //TODO: Can we convert this to MVC please instead of /base? - + [Obsolete("Thumbnails are generated by ImageProcessor, use that instead")] [RestExtension("FolderBrowserService")] public class FolderBrowserService { diff --git a/src/Umbraco.Web/WebServices/UmbracoHttpHandler.cs b/src/Umbraco.Web/WebServices/UmbracoHttpHandler.cs index d50f28b350..d315f0d20d 100644 --- a/src/Umbraco.Web/WebServices/UmbracoHttpHandler.cs +++ b/src/Umbraco.Web/WebServices/UmbracoHttpHandler.cs @@ -71,7 +71,7 @@ namespace Umbraco.Web.WebServices /// public UrlHelper Url { - get { return _url ?? (_url = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData()))); } + get { return _url ?? (_url = new UrlHelper(HttpContext.Current.Request.RequestContext)); } } /// diff --git a/src/Umbraco.Web/WebServices/UmbracoWebService.cs b/src/Umbraco.Web/WebServices/UmbracoWebService.cs index e92e85de1e..55b2d54295 100644 --- a/src/Umbraco.Web/WebServices/UmbracoWebService.cs +++ b/src/Umbraco.Web/WebServices/UmbracoWebService.cs @@ -72,7 +72,7 @@ namespace Umbraco.Web.WebServices /// 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.Web/app.config b/src/Umbraco.Web/app.config index 2b15194775..b4178cfb18 100644 --- a/src/Umbraco.Web/app.config +++ b/src/Umbraco.Web/app.config @@ -41,7 +41,7 @@ - + @@ -67,6 +67,10 @@ + + + + - \ No newline at end of file + diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 6aefb6f220..b7625770a3 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -2,15 +2,16 @@ - + - + + @@ -23,10 +24,10 @@ - + - + \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/SafeXmlReaderWriter.cs b/src/Umbraco.Web/umbraco.presentation/SafeXmlReaderWriter.cs new file mode 100644 index 0000000000..581a6a8983 --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/SafeXmlReaderWriter.cs @@ -0,0 +1,144 @@ +using System; +using System.Xml; +using Umbraco.Core; +using Umbraco.Core.Scoping; + +// ReSharper disable once CheckNamespace +namespace umbraco +{ + // provides safe access to the Xml cache + internal class SafeXmlReaderWriter : IDisposable + { + private readonly bool _scoped; + private readonly Action _refresh; + private readonly Action _apply; + private IDisposable _releaser; + private bool _isWriter; + private bool _applyChanges; + private XmlDocument _xml, _origXml; + private bool _using; + private bool _registerXmlChange; + + private SafeXmlReaderWriter(IDisposable releaser, XmlDocument xml, Action refresh, Action apply, bool isWriter, bool scoped) + { + _releaser = releaser; + _refresh = refresh; + _apply = apply; + _isWriter = isWriter; + _scoped = scoped; + + _xml = _isWriter ? Clone(xml) : xml; + } + + public static SafeXmlReaderWriter Get(IScopeProviderInternal scopeProvider, AsyncLock xmlLock, XmlDocument xml, Action refresh, Action apply, bool writer) + { + var scopeContext = scopeProvider.Context; + + // no real scope = just create a reader/writer instance + if (scopeContext == null) + { + // obtain exclusive access to xml and create reader/writer + var releaser = xmlLock.Lock(); + return new SafeXmlReaderWriter(releaser, xml, refresh, apply, writer, false); + } + + // get or create an enlisted reader/writer + var rw = scopeContext.Enlist("safeXmlReaderWriter", + () => // creator + { + // obtain exclusive access to xml and create reader/writer + var releaser = xmlLock.Lock(); + return new SafeXmlReaderWriter(releaser, xml, refresh, apply, writer, true); + }, + (completed, item) => // action + { + item.DisposeForReal(completed); + }); + + // ensure it's not already in-use - should never happen, just being super safe + if (rw._using) + throw new InvalidOperationException(); + rw._using = true; + + return rw; + } + + public bool IsWriter { get { return _isWriter; } } + + public void UpgradeToWriter() + { + if (_isWriter) + throw new InvalidOperationException("Already a writer."); + _isWriter = true; + + _xml = Clone(_xml); + } + + internal static Action Cloning { get; set; } + + private XmlDocument Clone(XmlDocument xml) + { + if (Cloning != null) Cloning(); + if (_origXml != null) + throw new Exception("panic."); + _origXml = xml; + return xml == null ? null : (XmlDocument) xml.CloneNode(true); + } + + public XmlDocument Xml + { + get { return _xml; } + set + { + if (_isWriter == false) + throw new InvalidOperationException("Not a writer."); + _xml = value; + } + } + + // registerXmlChange indicates whether to do what should be done when Xml changes, + // that is, to request that the file be written to disk - something we don't want + // to do if we're committing Xml precisely after we've read from disk! + public void AcceptChanges(bool registerXmlChange = true) + { + if (_isWriter == false) + throw new InvalidOperationException("Not a writer."); + + _applyChanges = true; + _registerXmlChange |= registerXmlChange; + } + + private void DisposeForReal(bool completed) + { + if (_isWriter) + { + // apply changes, or restore the original xml for the current request + if (_applyChanges && completed) + _apply(_xml, _registerXmlChange); + else + _refresh(_origXml); + } + + // release the lock + _releaser.Dispose(); + _releaser = null; + } + + public void Dispose() + { + _using = false; + + if (_scoped == false) + { + // really dispose + DisposeForReal(true); + } + else + { + // don't really dispose, + // just apply the changes for the current request + _refresh(_xml); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index b92b6a0b0e..a96863ea09 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -1,15 +1,12 @@ using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.IO; using System.Text; -using System.Threading; -using System.Threading.Tasks; using System.Web; using System.Xml; -using System.Xml.Linq; -using System.Xml.XPath; using umbraco.BusinessLogic; using umbraco.cms.businesslogic; using umbraco.cms.businesslogic.web; @@ -22,12 +19,12 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Profiling; +using Umbraco.Core.Scoping; using Umbraco.Web; using Umbraco.Web.PublishedCache.XmlPublishedCache; using Umbraco.Web.Scheduling; using File = System.IO.File; using Node = umbraco.NodeFactory.Node; -using Task = System.Threading.Tasks.Task; namespace umbraco { @@ -36,6 +33,7 @@ namespace umbraco /// public class content { + private readonly IScopeProviderInternal _scopeProvider = (IScopeProviderInternal) ApplicationContext.Current.ScopeProvider; private XmlCacheFilePersister _persisterTask; private volatile bool _released; @@ -84,7 +82,7 @@ namespace umbraco } // initialize content - populate the cache - using (var safeXml = GetSafeXmlWriter(false)) + using (var safeXml = GetSafeXmlWriter()) { bool registerXmlChange; @@ -93,7 +91,7 @@ namespace umbraco LoadXmlLocked(safeXml, out registerXmlChange); // if we use the file and registerXmlChange is true this will // write to file, else it will not - safeXml.Commit(registerXmlChange); + safeXml.AcceptChanges(registerXmlChange); } } @@ -119,10 +117,10 @@ namespace umbraco // (not refactoring that part at the moment) private static readonly object DbReadSyncLock = new object(); - private const string XmlContextContentItemKey = "UmbracoXmlContextContent"; - private const string XmlContextClonedContentItemKey = "UmbracoXmlContextContent.cloned"; + internal const string XmlContextContentItemKey = "UmbracoXmlContextContent"; private static string _umbracoXmlDiskCacheFileName = string.Empty; - private volatile XmlDocument _xmlContent; + // internal for SafeXmlReaderWriter + internal volatile XmlDocument _xmlContent; /// /// Gets the path of the umbraco XML disk cache file. @@ -149,7 +147,11 @@ namespace umbraco // not work as expected for a double check lock because properties are treated differently in the clr. public virtual bool isInitializing { - get { return _xmlContent == null; } + get + { + // ok to access _xmlContent here + return _xmlContent == null; + } } /// @@ -179,15 +181,17 @@ namespace umbraco var e = new RefreshContentEventArgs(); FireBeforeRefreshContent(e); - if (!e.Cancel) + if (e.Cancel) return; + + using (var safeXml = GetSafeXmlWriter()) { - using (var safeXml = GetSafeXmlWriter()) - { - safeXml.Xml = LoadContentFromDatabase(); - } + safeXml.Xml = LoadContentFromDatabase(); + safeXml.AcceptChanges(); } } + internal static bool TestingUpdateSitemapProvider = true; + /// /// Used by all overloaded publish methods to do the actual "noderepresentation to xml" /// @@ -196,6 +200,8 @@ namespace umbraco /// public static XmlDocument PublishNodeDo(Document d, XmlDocument xmlContentCopy, bool updateSitemapProvider) { + updateSitemapProvider &= TestingUpdateSitemapProvider; + // check if document *is* published, it could be unpublished by an event if (d.Published) { @@ -251,7 +257,7 @@ namespace umbraco /// The parent node identifier. public void SortNodes(int parentId) { - using (var safeXml = GetSafeXmlWriter(false)) + using (var safeXml = GetSafeXmlWriter()) { var parentNode = parentId == -1 ? safeXml.Xml.DocumentElement @@ -266,7 +272,7 @@ namespace umbraco if (sorted == false) return; - safeXml.Commit(); + safeXml.AcceptChanges(); } } @@ -289,22 +295,20 @@ namespace umbraco var e = new DocumentCacheEventArgs(); FireBeforeUpdateDocumentCache(d, e); - if (!e.Cancel) + if (e.Cancel) return; + + // lock the xml cache so no other thread can write to it at the same time + // note that some threads could read from it while we hold the lock, though + using (var safeXml = GetSafeXmlWriter()) { - // lock the xml cache so no other thread can write to it at the same time - // note that some threads could read from it while we hold the lock, though - using (var safeXml = GetSafeXmlWriter()) - { - safeXml.Xml = PublishNodeDo(d, safeXml.Xml, true); - } - - ClearContextCache(); - - var cachedFieldKeyStart = string.Format("{0}{1}_", CacheKeys.ContentItemCacheKey, d.Id); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(cachedFieldKeyStart); - - FireAfterUpdateDocumentCache(d, e); + safeXml.Xml = PublishNodeDo(d, safeXml.Xml, true); + safeXml.AcceptChanges(); } + + var cachedFieldKeyStart = string.Format("{0}{1}_", CacheKeys.ContentItemCacheKey, d.Id); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(cachedFieldKeyStart); + + FireAfterUpdateDocumentCache(d, e); } internal virtual void UpdateSortOrder(int contentId) @@ -339,7 +343,7 @@ namespace umbraco if (c.HasPublishedVersion == false) return; if (c.WasPropertyDirty("SortOrder") == false) return; - using (var safeXml = GetSafeXmlWriter(false)) + using (var safeXml = GetSafeXmlWriter()) { //TODO: This can be null: safeXml.Xml!!!! @@ -354,7 +358,7 @@ namespace umbraco // only if node was actually modified attr.Value = sortOrder; - safeXml.Commit(); + safeXml.AcceptChanges(); } } @@ -365,20 +369,14 @@ namespace umbraco [Obsolete("This is not used and will be removed from the codebase in future versions")] public virtual void UpdateDocumentCache(List Documents) { - // We need to lock content cache here, because we cannot allow other threads - // making changes at the same time, they need to be queued - int parentid = Documents[0].Id; - - using (var safeXml = GetSafeXmlWriter()) { - foreach (Document d in Documents) + foreach (var d in Documents) { safeXml.Xml = PublishNodeDo(d, safeXml.Xml, true); } + safeXml.AcceptChanges(); } - - ClearContextCache(); } [Obsolete("Method obsolete in version 4.1 and later, please use UpdateDocumentCache", true)] @@ -442,8 +440,6 @@ namespace umbraco // clear xml cache ClearDocumentXmlCache(doc.Id); - ClearContextCache(); - //SD: changed to fire event BEFORE running the sitemap!! argh. FireAfterClearDocumentCache(doc, e); @@ -467,7 +463,8 @@ namespace umbraco if (x == null) return; - safeXml.UpgradeToWriter(false); + if (safeXml.IsWriter == false) + safeXml.UpgradeToWriter(); // Find the document in the xml cache x = safeXml.Xml.GetElementById(id.ToString()); @@ -475,7 +472,7 @@ namespace umbraco { // The document already exists in cache, so repopulate it x.ParentNode.RemoveChild(x); - safeXml.Commit(); + safeXml.AcceptChanges(); } } } @@ -494,14 +491,38 @@ namespace umbraco #region Protected & Private methods - /// - /// Clear HTTPContext cache if any - /// + // 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(); + } + } + + // clear the current xml capture in http context + // used when applying changes from SafeXmlReaderWriter, + // to force a new capture - so that changes become + // visible for the current request private void ClearContextCache() { - // If running in a context very important to reset context cache orelse new nodes are missing - if (UmbracoContext.Current != null && UmbracoContext.Current.HttpContext != null && UmbracoContext.Current.HttpContext.Items.Contains(XmlContextContentItemKey)) - UmbracoContext.Current.HttpContext.Items.Remove(XmlContextContentItemKey); + var items = HttpContextItems; + if (items == null || items.Contains(XmlContextContentItemKey)) return; + items.Remove(XmlContextContentItemKey); + } + + // replaces the current xml capture in http context + // used for temp changes from SafeXmlReaderWriter + // so the current request immediately sees changes + private void SetContextCache(XmlDocument xml) + { + var items = HttpContextItems; + if (items == null) return; + items[XmlContextContentItemKey] = xml; } /// @@ -573,12 +594,6 @@ namespace umbraco get { return XmlFileEnabled && UmbracoConfig.For.UmbracoSettings().Content.XmlContentCheckForDiskChanges; } } - // whether _xml is immutable or not (achieved by cloning before changing anything) - private static bool XmlIsImmutable - { - get { return UmbracoConfig.For.UmbracoSettings().Content.CloneXmlContent; } - } - // whether to use the legacy schema private static bool UseLegacySchema { @@ -589,7 +604,8 @@ namespace umbraco #region Xml - private readonly AsyncLock _xmlLock = new AsyncLock(); // protects _xml + // internal for SafeXmlReaderWriter + internal readonly AsyncLock _xmlLock = new AsyncLock(); // protects _xml /// /// Get content. First call to this property will initialize xmldoc @@ -601,13 +617,17 @@ namespace umbraco { get { - if (UmbracoContext.Current == null || UmbracoContext.Current.HttpContext == null) + var items = HttpContextItems; + if (items == null) return XmlContentInternal; - var content = UmbracoContext.Current.HttpContext.Items[XmlContextContentItemKey] as XmlDocument; + + // capture or return the current xml in http context + // so that it remains stable over the entire request + var content = (XmlDocument) items[XmlContextContentItemKey]; if (content == null) { content = XmlContentInternal; - UmbracoContext.Current.HttpContext.Items[XmlContextContentItemKey] = content; + items[XmlContextContentItemKey] = content; } return content; } @@ -620,6 +640,7 @@ namespace umbraco } // to be used by content.Instance + // ok to access _xmlContent here - just capturing protected internal virtual XmlDocument XmlContentInternal { get @@ -630,7 +651,9 @@ namespace umbraco } // assumes xml lock - private void SetXmlLocked(XmlDocument xml, bool registerXmlChange) + // ok to access _xmlContent here since this is called from the safe reader/writer + // internal for SafeXmlReaderWriter + internal void SetXmlLocked(XmlDocument xml, bool registerXmlChange) { // this is the ONLY place where we write to _xmlContent _xmlContent = xml; @@ -642,11 +665,6 @@ namespace umbraco _persisterTask = _persisterTask.Touch(); // _persisterTask != null because SyncToXmlFile == true } - private static XmlDocument Clone(XmlDocument xmlDoc) - { - return xmlDoc == null ? null : (XmlDocument)xmlDoc.CloneNode(true); - } - private static XmlDocument EnsureSchema(string contentTypeAlias, XmlDocument xml) { string subset = null; @@ -703,91 +721,25 @@ namespace umbraco // gets a locked safe read access to the main xml private SafeXmlReaderWriter GetSafeXmlReader() { - var releaser = _xmlLock.Lock(); - return SafeXmlReaderWriter.GetReader(this, releaser); + return SafeXmlReaderWriter.Get(_scopeProvider, _xmlLock, _xmlContent, + SetContextCache, + (xml, registerXmlChange) => + { + SetXmlLocked(xml, registerXmlChange); + ClearContextCache(); + }, false); } // gets a locked safe write access to the main xml (cloned) - private SafeXmlReaderWriter GetSafeXmlWriter(bool auto = true) + private SafeXmlReaderWriter GetSafeXmlWriter() { - var releaser = _xmlLock.Lock(); - return SafeXmlReaderWriter.GetWriter(this, releaser, auto); - } - - private class SafeXmlReaderWriter : IDisposable - { - private readonly content _instance; - private IDisposable _releaser; - private bool _isWriter; - private bool _auto; - private bool _committed; - private XmlDocument _xml; - - private SafeXmlReaderWriter(content instance, IDisposable releaser, bool isWriter, bool auto) - { - _instance = instance; - _releaser = releaser; - _isWriter = isWriter; - _auto = auto; - - // cloning for writer is not an option anymore (see XmlIsImmutable) - _xml = _isWriter ? Clone(instance._xmlContent) : instance._xmlContent; - } - - public static SafeXmlReaderWriter GetReader(content instance, IDisposable releaser) - { - return new SafeXmlReaderWriter(instance, releaser, false, false); - } - - public static SafeXmlReaderWriter GetWriter(content instance, IDisposable releaser, bool auto) - { - return new SafeXmlReaderWriter(instance, releaser, true, auto); - } - - public void UpgradeToWriter(bool auto) - { - if (_isWriter) - throw new InvalidOperationException("Already writing."); - _isWriter = true; - _auto = auto; - _xml = Clone(_xml); // cloning for writer is not an option anymore (see XmlIsImmutable) - } - - public XmlDocument Xml - { - get + return SafeXmlReaderWriter.Get(_scopeProvider, _xmlLock, _xmlContent, + SetContextCache, + (xml, registerXmlChange) => { - return _xml; - } - set - { - if (_isWriter == false) - throw new InvalidOperationException("Not writing."); - _xml = value; - } - } - - // registerXmlChange indicates whether to do what should be done when Xml changes, - // that is, to request that the file be written to disk - something we don't want - // to do if we're committing Xml precisely after we've read from disk! - public void Commit(bool registerXmlChange = true) - { - if (_isWriter == false) - throw new InvalidOperationException("Not writing."); - _instance.SetXmlLocked(Xml, registerXmlChange); - _committed = true; - } - - public void Dispose() - { - if (_releaser == null) - return; - if (_isWriter && _auto && _committed == false) - Commit(); - _releaser.Dispose(); - _releaser = null; - } - + SetXmlLocked(xml, registerXmlChange); + ClearContextCache(); + }, true); } private static string ChildNodesXPath @@ -846,7 +798,8 @@ namespace umbraco LogHelper.Info("Save Xml to file..."); try { - var xml = _xmlContent; // capture (atomic + volatile), immutable anyway + // ok to access _xmlContent here - capture (atomic + volatile), immutable anyway + var xml = _xmlContent; if (xml == null) return; // delete existing file, if any @@ -975,11 +928,11 @@ namespace umbraco // time to read - using (var safeXml = GetSafeXmlWriter(false)) + using (var safeXml = GetSafeXmlWriter()) { bool registerXmlChange; LoadXmlLocked(safeXml, out registerXmlChange); // updates _lastFileRead - safeXml.Commit(registerXmlChange); + safeXml.AcceptChanges(registerXmlChange); } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadTemplates.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadTemplates.cs index bf7c04bb27..ad03d99223 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadTemplates.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadTemplates.cs @@ -39,7 +39,7 @@ namespace umbraco { public loadTemplates(string application) : base(application) {} - private ViewHelper _viewHelper = new ViewHelper(new PhysicalFileSystem(SystemDirectories.MvcViews)); + private ViewHelper _viewHelper = new ViewHelper(FileSystemProviderManager.Current.MvcViewsFileSystem); protected override void CreateRootNode(ref XmlTreeNode rootNode) { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs index 6eefec8ab0..b4fa6ed573 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/UmbracoMetaWeblogAPI.cs @@ -450,7 +450,7 @@ namespace umbraco.presentation.channels Property fileObject = m.getProperty(userChannel.MediaTypeFileProperty); var filename = file.name.Replace("/", "_"); - var relativeFilePath = _fs.GetRelativePath(fileObject.Id, filename); + var relativeFilePath = UmbracoMediaFactory.GetRelativePath(fileObject.Id, filename); fileObject.Value = _fs.GetUrl(relativeFilePath); fileUrl.url = fileObject.Value.ToString(); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/ImageViewer.ascx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/ImageViewer.ascx.cs index 98362dd638..09ac8518cc 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/ImageViewer.ascx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/ImageViewer.ascx.cs @@ -7,7 +7,8 @@ using Umbraco.Core; namespace umbraco.controls.Images { - public partial class ImageViewer : UserControl + [Obsolete("This is no longer used and will be removed in future versions")] + public partial class ImageViewer : UserControl { public ImageViewer() diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/ImageViewerUpdater.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/ImageViewerUpdater.asmx.cs index d03a2a668b..4818edfd81 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/ImageViewerUpdater.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Images/ImageViewerUpdater.asmx.cs @@ -21,7 +21,8 @@ namespace umbraco.controls.Images [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [ToolboxItem(false)] [ScriptService] - public class ImageViewerUpdater : System.Web.Services.WebService + [Obsolete("This is no longer used and will be removed in future versions")] + public class ImageViewerUpdater : System.Web.Services.WebService { /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/create/templateTasks.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/create/templateTasks.cs index bbddcd9de8..8c1cab8e78 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/create/templateTasks.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/create/templateTasks.cs @@ -13,8 +13,7 @@ using umbraco.cms.businesslogic.member; namespace umbraco { public class templateTasks : LegacyDialogTask - { - + { public override bool PerformSave() { var masterId = ParentID; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs index 0c375893d6..b40515578b 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs @@ -209,8 +209,9 @@ namespace umbraco.cms.presentation.developer } protected IEnumerable GetMacroParameterEditors() - { - return ParameterEditorResolver.Current.ParameterEditors; + { + // we need to show the depracated ones for backwards compatibility + return ParameterEditorResolver.Current.GetParameterEditors(true); } public void macroPropertyCreate(object sender, EventArgs e) @@ -233,7 +234,7 @@ namespace umbraco.cms.presentation.developer var macroPropertyAliasNew = (TextBox)((Control)sender).Parent.FindControl("macroPropertyAliasNew"); var macroPropertyNameNew = (TextBox)((Control)sender).Parent.FindControl("macroPropertyNameNew"); var macroPropertyTypeNew = (DropDownList)((Control)sender).Parent.FindControl("macroPropertyTypeNew"); - + if (macroPropertyAliasNew.Text != ui.Text("general", "new", UmbracoUser) + " " + ui.Text("general", "alias", UmbracoUser)) { if (_macro.Properties.ContainsKey(macroPropertyAliasNew.Text.Trim())) @@ -287,7 +288,7 @@ namespace umbraco.cms.presentation.developer } protected override void OnInit(EventArgs e) - { + { base.OnInit(e); EnsureChildControls(); } @@ -332,7 +333,9 @@ namespace umbraco.cms.presentation.developer SetMacroValuesFromPostBack(_macro, Convert.ToInt32(tempCachePeriod), tempMacroAssembly, tempMacroType); - // Save elements + // save elements + // this is oh so completely broken + var aliases = new Dictionary(); foreach (RepeaterItem item in macroProperties.Items) { var macroPropertyId = (HtmlInputHidden)item.FindControl("macroPropertyID"); @@ -345,15 +348,27 @@ namespace umbraco.cms.presentation.developer var sortOrder = 0; int.TryParse(macroElementSortOrder.Text, out sortOrder); + var alias = macroElementAlias.Text.Trim(); + if (prop.Alias != alias) // changing the alias + { + // use a temp alias to avoid collision if eg swapping aliases + var tempAlias = Guid.NewGuid().ToString("N").Substring(0, 8); + aliases[tempAlias] = alias; + alias = tempAlias; + } + _macro.Properties.UpdateProperty( prop.Alias, macroElementName.Text.Trim(), sortOrder, - macroElementType.SelectedValue, - macroElementAlias.Text.Trim()); - + macroElementType.SelectedValue, + alias); } + // now apply the real aliases, should not collide + foreach (var kvp in aliases) + _macro.Properties.UpdateProperty(kvp.Key, newAlias: kvp.Value); + Services.MacroService.Save(_macro); ClientTools.ShowSpeechBubble(speechBubbleIcon.save, "Macro saved", ""); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/BrowseRepository.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/BrowseRepository.aspx deleted file mode 100644 index 076c767b01..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/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/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 f71434e7e4..b66388d3a0 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/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/PermissionEditor.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx index 815a420f88..75b5d6e66a 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/PermissionEditor.aspx @@ -1,5 +1,5 @@ <%@ Page Language="C#" AutoEventWireup="true" MasterPageFile="../masterpages/umbracoPage.Master" CodeBehind="PermissionEditor.aspx.cs" Inherits="umbraco.cms.presentation.user.PermissionEditor" %> - +<%@ Import Namespace="Umbraco.Web" %> <%@ Register Src="../controls/Tree/TreeControl.ascx" TagName="TreeControl" TagPrefix="umbraco" %> <%@ Register Src="NodePermissions.ascx" TagName="NodePermissions" TagPrefix="user" %> <%@ Register TagPrefix="ui" Namespace="umbraco.uicontrols" Assembly="controls" %> @@ -27,7 +27,7 @@