diff --git a/.editorconfig b/.editorconfig
index e99299d69937..208052cbb7ad 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -3,7 +3,7 @@ root=true
[*]
end_of_line = lf
indent_style = space
-indent_size = 2
+indent_size = 4
trim_trailing_whitespace = true
[*.{cs,cshtml,csx,vb,vbx,vbhtml,fs,fsx,txt,ps1,sql}]
diff --git a/.gitignore b/.gitignore
index 18239100adac..808b964260ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,13 +46,7 @@ src/Umbraco.Web.UI/Web.*.config.transformed
umbraco/presentation/umbraco/plugins/uComponents/uComponentsInstaller.ascx
umbraco/presentation/packages/uComponents/MultiNodePicker/CustomTreeService.asmx
-_BuildOutput/*
*.ncrunchsolution
-build/UmbracoCms.AllBinaries*zip
-build/UmbracoCms.WebPI*zip
-build/UmbracoCms*zip
-build/UmbracoExamine.PDF*zip
-build/*.nupkg
src/Umbraco.Tests/config/applications.config
src/Umbraco.Tests/config/trees.config
src/Umbraco.Web.UI/web.config
@@ -68,9 +62,9 @@ src/packages/repositories.config
src/Umbraco.Web.UI/[Ww]eb.config
*.transformed
-webpihash.txt
node_modules
+lib-bower
src/Umbraco.Web.UI/[Uu]mbraco/[Ll]ib/*
src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/umbraco.*
@@ -95,7 +89,6 @@ src/Umbraco.Web.UI/[Uu]mbraco/[Aa]ssets/*
src/Umbraco.Web.UI.Client/[Bb]uild/*
src/Umbraco.Web.UI.Client/[Bb]uild/[Bb]elle/
src/Umbraco.Web.UI/[Uu]ser[Cc]ontrols/
-build/_BuildOutput/
src/Umbraco.Web.UI.Client/src/[Ll]ess/*.css
tools/NDepend/
@@ -130,7 +123,6 @@ src/*.boltdata/
/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.config.js
/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.front.js
src/umbraco.sln.ide/*
-build/UmbracoCms.*/
src/.vs/
src/Umbraco.Web.UI/umbraco/js/install.loader.js
src/Umbraco.Tests/media
@@ -140,8 +132,11 @@ apidocs/api/*
build/docs.zip
build/ui-docs.zip
build/csharp-docs.zip
-build/msbuild.log
.vs/
src/packages/
-build/tools/
src/PrecompiledWeb/*
+
+
+build.out/
+build.tmp/
+build/Modules/*/temp/
\ No newline at end of file
diff --git a/BUILD.md b/BUILD.md
new file mode 100644
index 000000000000..bc8ab500b1c6
--- /dev/null
+++ b/BUILD.md
@@ -0,0 +1,149 @@
+Umbraco Cms Build
+--
+----
+
+# Quick!
+
+To build Umbraco, fire PowerShell and move to Umbraco's repository root (the directory that contains `src`, `build`, `README.md`...). There, trigger the build with the following command:
+
+ build\build.ps1
+
+By default, this builds the current version. It is possible to specify a different version as a parameter to the build script:
+
+ build\build.ps1 7.6.44
+
+Valid version strings are defined in the `Set-UmbracoVersion` documentation below.
+
+## Notes
+
+Git might have issues dealing with long file paths during build. You may want/need to enable `core.longpaths` support (see [this page](https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path) for details).
+
+# Build
+
+The Umbraco Build solution relies on a PowerShell module. The module needs to be imported into PowerShell. From within Umbraco's repository root:
+
+ build\build.ps1 -ModuleOnly
+
+Or the abbreviated form:
+
+ build\build.ps1 -mo
+
+Once the module has been imported, a set of commands are added to PowerShell.
+
+## Get-UmbracoBuildEnv
+
+Gets the Umbraco build environment ie NuGet, Semver, Visual Studio, etc. Downloads things that can be downloaded such as NuGet. Examples:
+
+ $uenv = Get-UmbracoBuildEnv
+ Write-Host $uenv.SolutionRoot
+ &$uenv.NuGet help
+
+The object exposes the following properties:
+
+* `SolutionRoot`: the absolute path to the solution root
+* `VisualStudio`: a Visual Studio object (see below)
+* `NuGet`: the absolute path to the NuGet executable
+* `Zip`: the absolute path to the 7Zip executable
+* `VsWhere`: the absolute path to the VsWhere executable
+* `NodePath`: the absolute path to the Node install
+* `NpmPath`: the absolute path to the Npm install
+
+The Visual Studio object is `null` when Visual Studio has not been detected (eg on VSTS). When not null, the object exposes the following properties:
+
+* `Path`: Visual Studio installation path (eg some place under `Program Files`)
+* `Major`: Visual Studio major version (eg `15` for VS 2017)
+* `Minor`: Visual Studio minor version
+* `MsBUild`: the absolute path to the MsBuild executable
+
+## Get-UmbracoVersion
+
+Gets an object representing the current Umbraco version. Example:
+
+ $v = Get-UmbracoVersion
+ Write-Host $v.Semver
+
+The object exposes the following properties:
+
+* `Semver`: the semver object representing the version
+* `Release`: the main part of the version (eg `7.6.33`)
+* `Comment`: the pre release part of the version (eg `alpha02`)
+* `Build`: the build number part of the version (eg `1234`)
+
+## Set-UmbracoVersion
+
+Modifies Umbraco files with the new version.
+
+>This entirely replaces the legacy `UmbracoVersion.txt` file.
+
+The version must be a valid semver version. It can include a *pre release* part (eg `alpha02`) and/or a *build number* (eg `1234`). Examples:
+
+ Set-UmbracoVersion 7.6.33
+ Set-UmbracoVersion 7.6.33-alpha02
+ Set-UmbracoVersion 7.6.33+1234
+ Set-UmbracoVersion 7.6.33-beta05+5678
+
+Note that `Set-UmbracoVersion` enforces a slightly more restrictive naming scheme than what semver would tolerate. The pre release part can only be composed of a-z and 0-9, therefore `alpha033` is considered valid but not `alpha.033` nor `alpha033-preview` nor `RC2` (would need to be lowercased `rc2`).
+
+>It is considered best to add trailing zeroes to pre releases, else NuGet gets the order of versions wrong. So if you plan to have more than 10, but no more that 100 alpha versions, number the versions `alpha00`, `alpha01`, etc.
+
+## Build-Umbraco
+
+Builds Umbraco. Temporary files are generated in `build.tmp` while the actual artifacts (zip files, NuGet packages...) are produced in `build.out`. Example:
+
+ Build-Umbraco
+
+Some log files, such as MsBuild logs, are produced in `build.tmp` too. The `build` directory should remain clean during a build.
+
+### web.config
+
+Building Umbraco requires a clean `web.config` file in the `Umbraco.Web.UI` project. If a `web.config` file already exists, the `pre-build` task (see below) will save it as `web.config.temp-build` and replace it with a clean copy of `web.Template.config`. The original file is replaced once it is safe to do so, by the `pre-packages` task.
+
+## Build-UmbracoDocs
+
+Builds umbraco documentation. Temporary files are generated in `build.tmp` while the actual artifacts (docs...) are produced in `build.out`. Example:
+
+ Build-UmbracoDocs
+
+Some log files, such as MsBuild logs, are produced in `build.tmp` too. The `build` directory should remain clean during a build.
+
+## Verify-NuGet
+
+Verifies that projects all require the same version of their dependencies, and that NuSpec files require versions that are consistent with projects. Example:
+
+ Verify-NuGet
+
+# VSTS
+
+Continuous integration, nightly builds and release builds run on VSTS.
+
+VSTS uses the `Build-Umbraco` command several times, each time passing a different *target* parameter. The supported targets are:
+
+* `pre-build`: prepares the build
+* `compile-belle`: compiles Belle
+* `compile-umbraco`: compiles Umbraco
+* `pre-tests`: prepares the tests
+* `compile-tests`: compiles the tests
+* `pre-packages`: prepares the packages
+* `pkg-zip`: creates the zip files
+* `pre-nuget`: prepares NuGet packages
+* `pkg-nuget`: creates NuGet packages
+
+All these targets are executed when `Build-Umbraco` is invoked without a parameter (or with the `all` parameter). On VSTS, compilations (of Umbraco and tests) are performed by dedicated VSTS tasks. Similarly, creating the NuGet packages is also performed by dedicated VSTS tasks.
+
+Finally, the produced artifacts are published in two containers that can be downloaded from VSTS: `zips` contains the zip files while `nuget` contains the NuGet packages.
+
+>During a VSTS build, some environment `UMBRACO_*` variables are exported by the `pre-build` target and can be reused in other targets *and* in VSTS tasks. The `UMBRACO_TMP` environment variable is used in `Umbraco.Tests` to disable some tests that have issues with VSTS at the moment.
+
+# Notes
+
+*This part needs to be cleaned up*
+
+Nightlies should use some sort of build number.
+
+We should increment versions as soon as a version is released. Ie, as soon as `7.6.33` is released, we should `Set-UmbracoVersion 7.6.34-alpha` and push.
+
+NuGet / NuSpec consistency checks are performed in tests. We should move it so it is done as part of the PowerShell script even before we try to compile and run the tests.
+
+There are still a few commands in `build` (to build docs, install Git or cleanup the install) that will need to be migrated to PowerShell.
+
+/eof
\ No newline at end of file
diff --git a/README.md b/README.md
index a9559c185027..ceed6967b8e7 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![Build status](https://ci.appveyor.com/api/projects/status/ikpgqxiw9v8opltv/branch/dev-v7?svg=true)](https://ci.appveyor.com/project/Umbraco/umbraco-cms-hs8dx/branch/dev-v7)
+[![Build status](https://ci.appveyor.com/api/projects/status/6by6harxtxt0ocdx/branch/dev-v7?svg=true)](https://ci.appveyor.com/project/Umbraco/umbraco-cms-b2cri/branch/dev-v7)
Umbraco CMS
===========
@@ -12,9 +12,7 @@ Umbraco is a free open source Content Management System built on the ASP.NET pla
## Building Umbraco from source ##
-The easiest way to get started is to run `build/build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `grunt vs` in `src\Umbraco.Web.UI.Client`.
-
-If you're interested in making changes to Belle without running Visual Studio make sure to read the [Belle ReadMe file](src/Umbraco.Web.UI.Client/README.md).
+The easiest way to get started is to run `build/build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`.
Note that you can always [download a nightly build](http://nightly.umbraco.org/?container=umbraco-750) so you don't have to build the code yourself.
diff --git a/appveyor.yml b/appveyor.yml
index dc6e22edbf0b..4c9a33fd2310 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,5 +1,9 @@
version: '{build}'
shallow_clone: true
+
+init:
+ - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
+
build_script:
- cmd: >-
SET SLN=%CD%
diff --git a/build.bat b/build.bat
new file mode 100644
index 000000000000..da9c4e137dd7
--- /dev/null
+++ b/build.bat
@@ -0,0 +1,15 @@
+@ECHO OFF
+powershell .\build\build.ps1
+
+IF ERRORLEVEL 1 (
+ GOTO :error
+) ELSE (
+ GOTO :EOF
+)
+
+:error
+ECHO.
+ECHO Can not run build\build.ps1.
+ECHO If this is due to a SecurityError then make sure to run the following command from an administrator command prompt:
+ECHO.
+ECHO powershell Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
\ No newline at end of file
diff --git a/build/Build.bat b/build/Build.bat
deleted file mode 100644
index f26c4877cd2a..000000000000
--- a/build/Build.bat
+++ /dev/null
@@ -1,217 +0,0 @@
-@ECHO OFF
-
-:: UMBRACO BUILD FILE
-
-
-:: ensure we have UmbracoVersion.txt
-IF NOT EXIST UmbracoVersion.txt (
- 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
-
-REM process args
-
-SET INTEGRATION=0
-SET nuGetFolder=%CD%\..\src\packages
-SET SKIPNUGET=0
-
-:processArgs
-
-:: grab the first parameter as a whole eg "/action:start"
-:: end if no more parameter
-SET SWITCHPARSE=%1
-IF [%SWITCHPARSE%] == [] goto endProcessArgs
-
-:: 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
-RD ..\src\Umbraco.Web.UI.Client\build /Q /S
-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 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
-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
-SET PATH="C:\Program Files (x86)\Git\cmd";"C:\Program Files\Git\cmd";%PATH%
-
-SET toolsFolder=%CD%\tools\
-IF NOT EXIST "%toolsFolder%" (
- MD tools
-)
-
-SET nuGetExecutable=%CD%\tools\nuget.exe
-IF NOT EXIST "%nuGetExecutable%" (
- ECHO Getting NuGet so we can fetch some tools
- ECHO Downloading https://dist.nuget.org/win-x86-commandline/latest/nuget.exe to %nuGetExecutable%
- powershell -Command "(New-Object Net.WebClient).DownloadFile('https://dist.nuget.org/win-x86-commandline/latest/nuget.exe', '%nuGetExecutable%')"
-)
-
-:: We need 7za.exe for BuildBelle.bat
-IF NOT EXIST "%toolsFolder%7za.exe" (
- ECHO 7zip not found - fetching now
- "%nuGetExecutable%" install 7-Zip.CommandLine -OutputDirectory tools -Verbosity quiet
-)
-
-:: We need vswhere.exe for VS2017+
-IF NOT EXIST "%toolsFolder%vswhere.exe" (
- ECHO vswhere not found - fetching now
- "%nuGetExecutable%" install vswhere -OutputDirectory tools -Verbosity quiet
-)
-
-:: Put 7za.exe and vswhere.exe in a predictable path (not version specific)
-FOR /f "delims=" %%A in ('dir "%toolsFolder%7-Zip.CommandLine.*" /b') DO SET "sevenZipExePath=%toolsFolder%%%A\"
-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"
-
-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
-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% /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
-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
-
-: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 %INTEGRATION% NEQ 1 PAUSE
diff --git a/build/Build.proj b/build/Build.proj
deleted file mode 100644
index 1f57ca591551..000000000000
--- a/build/Build.proj
+++ /dev/null
@@ -1,345 +0,0 @@
-
-
-
-
-
- ..\MSBuildCommunityTasks
- ..\UmbracoMSBuildTasks
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- b.ToString("x2"))));
- }
- }
- }
- }
- }
- ]]>
-
-
-
-
-
-
-
-
-
- .$(BUILD_NUMBER)
-
-
- .$(BUILD_RELEASE)
-
-
- .$(BUILD_RELEASE)-$(BUILD_COMMENT)
-
-
- .$(BUILD_RELEASE)-$(BUILD_NIGHTLY)
-
-
- .$(BUILD_RELEASE)-$(BUILD_COMMENT)-$(BUILD_NIGHTLY)
-
-
-
- Release
- _BuildOutput\
- UmbracoCms$(DECIMAL_BUILD_NUMBER).zip
- UmbracoCms.AllBinaries$(DECIMAL_BUILD_NUMBER).zip
- UmbracoCms.WebPI$(DECIMAL_BUILD_NUMBER).zip
- False
- ..\..\build\$(BuildFolder)
- $(MSBuildProjectDirectory)\$(BuildFolder)
- $(BuildFolder)bin\
- $(BuildFolder)WebApp\
- $(BuildFolder)WebPi\
- $(BuildFolder)Configs\
- $(BuildFolderRelativeToProjects)bin\
- $(BuildFolderAbsolutePath)bin\
- $(BuildFolderRelativeToProjects)WebApp\
- $(BuildFolderAbsolutePath)WebApp\
- $(BuildFolderRelativeToProjects)WebPi\
- $(BuildFolderAbsolutePath)WebPi\
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- $(BUILD_RELEASE)
- $(BUILD_RELEASE)-$(BUILD_COMMENT)
- $(BUILD_RELEASE)-$(BUILD_NIGHTLY)
- $(BUILD_RELEASE)-$(BUILD_COMMENT)-$(BUILD_NIGHTLY)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat
deleted file mode 100644
index 78b7736c2b70..000000000000
--- a/build/BuildBelle.bat
+++ /dev/null
@@ -1,59 +0,0 @@
-@ECHO OFF
-SETLOCAL
- :: SETLOCAL is on, so changes to the path not persist to the actual user's path
-
-SET toolsFolder=%CD%\tools\
-ECHO Current folder: %CD%
-
-SET nodeFileName=node-v6.9.1-win-x86.7z
-SET nodeExtractFolder=%toolsFolder%node.js.691
-
-IF NOT EXIST "%nodeExtractFolder%" (
- ECHO Downloading http://nodejs.org/dist/v6.9.1/%nodeFileName% to %toolsFolder%%nodeFileName%
- powershell -Command "(New-Object Net.WebClient).DownloadFile('http://nodejs.org/dist/v6.9.1/%nodeFileName%', '%toolsFolder%%nodeFileName%')"
- ECHO Extracting %nodeFileName% to %nodeExtractFolder%
- "%toolsFolder%\7za.exe" x "%toolsFolder%\%nodeFileName%" -o"%nodeExtractFolder%" -aos > nul
-)
-FOR /f "delims=" %%A in ('dir "%nodeExtractFolder%\node*" /b') DO SET "nodePath=%nodeExtractFolder%\%%A"
-
-
-SET nuGetExecutable=%CD%\tools\nuget.exe
-IF NOT EXIST "%nuGetExecutable%" (
- ECHO Downloading https://dist.nuget.org/win-x86-commandline/latest/nuget.exe to %nuGetExecutable%
- powershell -Command "(New-Object Net.WebClient).DownloadFile('https://dist.nuget.org/win-x86-commandline/latest/nuget.exe', '%nuGetExecutable%')"
-)
-
-SET drive=%CD:~0,2%
-SET nuGetFolder=%drive%\packages\
-FOR /f "delims=" %%A in ('dir "%nuGetFolder%npm.*" /b') DO SET "npmPath=%nuGetFolder%%%A\"
-
-IF [%npmPath%] == [] GOTO :installnpm
-IF NOT [%npmPath%] == [] GOTO :build
-
-:installnpm
- ECHO Downloading npm
- ECHO Configured packages folder: %nuGetFolder%
- ECHO Installing Npm NuGet Package
- "%nuGetExecutable%" install Npm -OutputDirectory %nuGetFolder% -Verbosity detailed
- REM Ensures that we look for the just downloaded NPM, not whatever the user has installed on their machine
- FOR /f "delims=" %%A in ('dir %nuGetFolder%npm.* /b') DO SET "npmPath=%nuGetFolder%%%A\"
- GOTO :build
-
-:build
- ECHO Adding Npm and Node to path
- REM SETLOCAL is on, so changes to the path not persist to the actual user's path
- PATH="%npmPath%";"%nodePath%";%PATH%
- SET buildFolder=%CD%
-
- ECHO Change directory to %CD%\..\src\Umbraco.Web.UI.Client\
- CD %CD%\..\src\Umbraco.Web.UI.Client\
-
- ECHO Do npm install and the grunt build of Belle
- call npm cache clean --quiet
- call npm install --quiet
- call npm install -g grunt-cli --quiet
- call npm install -g bower --quiet
- call grunt build --buildversion=%release%
-
- ECHO Move back to the build folder
- CD "%buildFolder%"
\ No newline at end of file
diff --git a/build/BuildDocs.bat b/build/BuildDocs.bat
deleted file mode 100644
index 9d0a04e1cd29..000000000000
--- a/build/BuildDocs.bat
+++ /dev/null
@@ -1,20 +0,0 @@
-@ECHO OFF
-SETLOCAL
-
-SET release=%1
-ECHO Installing Npm NuGet Package
-
-SET nuGetFolder=%CD%\..\src\packages\
-ECHO Configured packages folder: %nuGetFolder%
-ECHO Current folder: %CD%
-
-%CD%\..\src\.nuget\NuGet.exe install Npm.js -OutputDirectory %nuGetFolder% -Verbosity quiet
-
-for /f "delims=" %%A in ('dir %nuGetFolder%node.js.* /b') do set "nodePath=%nuGetFolder%%%A\"
-for /f "delims=" %%A in ('dir %nuGetFolder%npm.js.* /b') do set "npmPath=%nuGetFolder%%%A\tools\"
-
-ECHO Adding Npm and Node to path
-REM SETLOCAL is on, so changes to the path not persist to the actual user's path
-PATH=%npmPath%;%nodePath%;%PATH%
-
-Powershell.exe -ExecutionPolicy Unrestricted -File .\BuildDocs.ps1
\ No newline at end of file
diff --git a/build/BuildDocs.ps1 b/build/BuildDocs.ps1
deleted file mode 100644
index 7b13f98ca449..000000000000
--- a/build/BuildDocs.ps1
+++ /dev/null
@@ -1,114 +0,0 @@
-$PSScriptFilePath = (Get-Item $MyInvocation.MyCommand.Path);
-$RepoRoot = (get-item $PSScriptFilePath).Directory.Parent.FullName;
-$SolutionRoot = Join-Path -Path $RepoRoot "src";
-$ToolsRoot = Join-Path -Path $RepoRoot "tools";
-$DocFx = Join-Path -Path $ToolsRoot "docfx\docfx.exe"
-$DocFxFolder = (Join-Path -Path $ToolsRoot "docfx")
-$DocFxJson = Join-Path -Path $RepoRoot "apidocs\docfx.json"
-$7Zip = Join-Path -Path $ToolsRoot "7zip\7za.exe"
-$DocFxSiteOutput = Join-Path -Path $RepoRoot "apidocs\_site\*.*"
-$NgDocsSiteOutput = Join-Path -Path $RepoRoot "src\Umbraco.Web.UI.Client\docs\api\*.*"
-$ProgFiles86 = [Environment]::GetEnvironmentVariable("ProgramFiles(x86)");
-$MSBuild = "$ProgFiles86\MSBuild\14.0\Bin\MSBuild.exe"
-
-
-################ Do the UI docs
-
-"Changing to Umbraco.Web.UI.Client folder"
-cd ..
-cd src\Umbraco.Web.UI.Client
-Write-Host $(Get-Location)
-
-"Creating build folder so MSBuild doesn't run the whole grunt build"
-if (-Not (Test-Path "build")) {
- md "build"
-}
-
-"Installing node"
-# Check if Install-Product exists, should only exist on the build server
-if (Get-Command Install-Product -errorAction SilentlyContinue)
-{
- Install-Product node ''
-}
-
-"Installing node modules"
-& npm install
-
-"Installing grunt"
-& npm install -g grunt-cli
-
-"Moving back to build folder"
-cd ..
-cd ..
-cd build
-Write-Host $(Get-Location)
-
- & grunt --gruntfile ../src/umbraco.web.ui.client/gruntfile.js docs
-
-# change baseUrl
-$BaseUrl = "https://our.umbraco.org/apidocs/ui/"
-$IndexPath = "../src/umbraco.web.ui.client/docs/api/index.html"
-(Get-Content $IndexPath).replace('location.href.replace(rUrl, indexFile)', "`'" + $BaseUrl + "`'") | Set-Content $IndexPath
-# zip it
-
-& $7Zip a -tzip ui-docs.zip $NgDocsSiteOutput -r
-
-################ Do the c# docs
-
-# Build the solution in debug mode
-$SolutionPath = Join-Path -Path $SolutionRoot -ChildPath "umbraco.sln"
-
-# Go get nuget.exe if we don't hae it
-$NuGet = "$ToolsRoot\nuget.exe"
-$FileExists = Test-Path $NuGet
-If ($FileExists -eq $False) {
- Write-Host "Retrieving nuget.exe..."
- $SourceNugetExe = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
- Invoke-WebRequest $SourceNugetExe -OutFile $NuGet
-}
-
-#restore nuget packages
-Write-Host "Restoring nuget packages..."
-& $NuGet restore $SolutionPath
-
-& $MSBuild "$SolutionPath" /p:Configuration=Debug /maxcpucount /t:Clean
-if (-not $?)
-{
- throw "The MSBuild process returned an error code."
-}
-& $MSBuild "$SolutionPath" /p:Configuration=Debug /maxcpucount
-if (-not $?)
-{
- throw "The MSBuild process returned an error code."
-}
-
-# Go get docfx if we don't hae it
-$FileExists = Test-Path $DocFx
-If ($FileExists -eq $False) {
-
- If(!(Test-Path $DocFxFolder))
- {
- New-Item $DocFxFolder -type directory
- }
-
- $DocFxZip = Join-Path -Path $ToolsRoot "docfx\docfx.zip"
- $DocFxSource = "https://github.com/dotnet/docfx/releases/download/v1.9.4/docfx.zip"
- Invoke-WebRequest $DocFxSource -OutFile $DocFxZip
-
- #unzip it
- & $7Zip e $DocFxZip "-o$DocFxFolder"
-}
-
-#clear site
-If(Test-Path(Join-Path -Path $RepoRoot "apidocs\_site"))
-{
- Remove-Item $DocFxSiteOutput -recurse
-}
-
-# run it!
-& $DocFx metadata $DocFxJson
-& $DocFx build $DocFxJson
-
-# zip it
-
-& $7Zip a -tzip csharp-docs.zip $DocFxSiteOutput -r
diff --git a/build/Modules/Umbraco.Build/Build-UmbracoDocs.ps1 b/build/Modules/Umbraco.Build/Build-UmbracoDocs.ps1
new file mode 100644
index 000000000000..f0691fb5ae44
--- /dev/null
+++ b/build/Modules/Umbraco.Build/Build-UmbracoDocs.ps1
@@ -0,0 +1,111 @@
+#
+
+function Build-UmbracoDocs
+{
+ $uenv = Get-UmbracoBuildEnv
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $out = "$($uenv.SolutionRoot)\build.out"
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+
+ $buildTemp = "$PSScriptRoot\temp"
+ $cache = 2
+
+ Prepare-Build -keep $uenv
+
+ ################ Do the UI docs
+ # get a temp clean node env (will restore)
+ Sandbox-Node $uenv
+
+ Write-Host "Executing gulp docs"
+
+ push-location "$($uenv.SolutionRoot)\src\Umbraco.Web.UI.Client"
+ write "node version is:" > $tmp\belle-docs.log
+ &node -v >> $tmp\belle-docs.log 2>&1
+ write "npm version is:" >> $tmp\belle-docs.log 2>&1
+ &npm -v >> $tmp\belle-docs.log 2>&1
+ write "executing npm install" >> $tmp\belle-docs.log 2>&1
+ &npm install >> $tmp\belle-docs.log 2>&1
+ write "executing bower install" >> $tmp\belle-docs.log 2>&1
+ &npm install -g bower >> $tmp\belle-docs.log 2>&1
+ write "installing gulp" >> $tmp\belle-docs.log 2>&1
+ &npm install -g gulp >> $tmp\belle-docs.log 2>&1
+ write "installing gulp-cli" >> $tmp\belle-docs.log 2>&1
+ &npm install -g gulp-cli --quiet >> $tmp\belle-docs.log 2>&1
+ write "building docs using gulp" >> $tmp\belle-docs.log 2>&1
+ &gulp docs >> $tmp\belle-docs.log 2>&1
+ pop-location
+
+ Write-Host "Completed gulp docs build"
+
+ # fixme - should we filter the log to find errors?
+ #get-content .\build.tmp\belle-docs.log | %{ if ($_ -match "build") { write $_}}
+
+ # change baseUrl
+ $baseUrl = "https://our.umbraco.org/apidocs/ui/"
+ $indexPath = "$src/Umbraco.Web.UI.Client/docs/api/index.html"
+ (Get-Content $indexPath).Replace("location.href.replace(rUrl, indexFile)", "'$baseUrl'") `
+ | Set-Content $indexPath
+
+ # restore
+ Restore-Node
+
+ # zip
+ &$uenv.Zip a -tzip -r "$out\ui-docs.zip" "$src\Umbraco.Web.UI.Client\docs\api\*.*" `
+ > $null
+
+ ################ Do the c# docs
+
+ Write-Host "Build C# documentation"
+
+ # Build the solution in debug mode
+ # FIXME no only a simple compilation should be enough!
+ # FIXME we MUST handle msbuild & co error codes!
+ # FIXME deal with weird things in gitconfig?
+ #Build-Umbraco -Configuration Debug
+ Restore-NuGet $uenv
+ Compile-Umbraco $uenv "Debug" # FIXME different log file!
+ Restore-WebConfig "$src\Umbraco.Web.UI"
+
+ # ensure we have docfx
+ Get-DocFx $uenv $buildTemp
+
+ # clear
+ $docFxOutput = "$($uenv.SolutionRoot)\apidocs\_site"
+ if (test-path($docFxOutput))
+ {
+ Remove-Directory $docFxOutput
+ }
+
+ # run
+ $docFxJson = "$($uenv.SolutionRoot)\apidocs\docfx.json"
+ push-location "$($uenv.SolutionRoot)\build" # silly docfx.json wants this
+
+ Write-Host "Run DocFx metadata"
+ Write-Host "Logging to $tmp\docfx.metadata.log"
+ &$uenv.DocFx metadata $docFxJson > "$tmp\docfx.metadata.log"
+ Write-Host "Run DocFx build"
+ Write-Host "Logging to $tmp\docfx.build.log"
+ &$uenv.DocFx build $docFxJson > "$tmp\docfx.build.log"
+
+ pop-location
+
+ # zip
+ &$uenv.Zip a -tzip -r "$out\csharp-docs.zip" "$docFxOutput\*.*" `
+ > $null
+}
+
+function Get-DocFx($uenv, $buildTemp)
+{
+ $docFx = "$buildTemp\docfx"
+ if (-not (test-path $docFx))
+ {
+ Write-Host "Download DocFx..."
+ $source = "https://github.com/dotnet/docfx/releases/download/v2.19.2/docfx.zip"
+ Invoke-WebRequest $source -OutFile "$buildTemp\docfx.zip"
+
+ &$uenv.Zip x "$buildTemp\docfx.zip" -o"$buildTemp\docfx" -aos > $nul
+ Remove-File "$buildTemp\docfx.zip"
+ }
+ $uenv | add-member -memberType NoteProperty -name DocFx -value "$docFx\docfx.exe"
+}
\ No newline at end of file
diff --git a/build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1 b/build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1
new file mode 100644
index 000000000000..d1aaf7582f58
--- /dev/null
+++ b/build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1
@@ -0,0 +1,180 @@
+#
+# Get-UmbracoBuildEnv
+# Gets the Umbraco build environment
+# Downloads tools if necessary
+#
+function Get-UmbracoBuildEnv
+{
+ # store tools in the module's directory
+ # and cache them for two days
+ $path = "$PSScriptRoot\temp"
+ $cache = 2
+
+ if (-not (test-path $path))
+ {
+ mkdir $path > $null
+ }
+
+ # ensure we have NuGet
+ $nuget = "$path\nuget.exe"
+ $source = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
+ if ((test-path $nuget) -and ((ls $nuget).CreationTime -lt [DateTime]::Now.AddDays(-$cache)))
+ {
+ Remove-File $nuget
+ }
+ if (-not (test-path $nuget))
+ {
+ Write-Host "Download NuGet..."
+ Invoke-WebRequest $source -OutFile $nuget
+ }
+
+ # ensure we have 7-Zip
+ $sevenZip = "$path\7za.exe"
+ if ((test-path $sevenZip) -and ((ls $sevenZip).CreationTime -lt [DateTime]::Now.AddDays(-$cache)))
+ {
+ Remove-File $sevenZip
+ }
+ if (-not (test-path $sevenZip))
+ {
+ Write-Host "Download 7-Zip..."
+ &$nuget install 7-Zip.CommandLine -OutputDirectory $path -Verbosity quiet
+ $dir = ls "$path\7-Zip.CommandLine.*" | sort -property Name -descending | select -first 1
+ $file = ls -path "$dir" -name 7za.exe -recurse
+ mv "$dir\$file" $sevenZip
+ Remove-Directory $dir
+ }
+
+ # ensure we have vswhere
+ $vswhere = "$path\vswhere.exe"
+ if ((test-path $vswhere) -and ((ls $vswhere).CreationTime -lt [DateTime]::Now.AddDays(-$cache)))
+ {
+ Remove-File $vswhere
+ }
+ if (-not (test-path $vswhere))
+ {
+ Write-Host "Download VsWhere..."
+ &$nuget install vswhere -OutputDirectory $path -Verbosity quiet
+ $dir = ls "$path\vswhere.*" | sort -property Name -descending | select -first 1
+ $file = ls -path "$dir" -name vswhere.exe -recurse
+ mv "$dir\$file" $vswhere
+ Remove-Directory $dir
+ }
+
+ # ensure we have semver
+ $semver = "$path\Semver.dll"
+ if ((test-path $semver) -and ((ls $semver).CreationTime -lt [DateTime]::Now.AddDays(-$cache)))
+ {
+ Remove-File $semver
+ }
+ if (-not (test-path $semver))
+ {
+ Write-Host "Download Semver..."
+ &$nuget install semver -OutputDirectory $path -Verbosity quiet
+ $dir = ls "$path\semver.*" | sort -property Name -descending | select -first 1
+ $file = "$dir\lib\net452\Semver.dll"
+ if (-not (test-path $file))
+ {
+ Write-Error "Failed to file $file"
+ return
+ }
+ mv "$file" $semver
+ Remove-Directory $dir
+ }
+
+ try
+ {
+ [Reflection.Assembly]::LoadFile($semver) > $null
+ }
+ catch
+ {
+ Write-Error -Exception $_.Exception -Message "Failed to load $semver"
+ break
+ }
+
+ # ensure we have node
+ $node = "$path\node-v6.9.1-win-x86"
+ $source = "http://nodejs.org/dist/v6.9.1/node-v6.9.1-win-x86.7z"
+ if (-not (test-path $node))
+ {
+ Write-Host "Download Node..."
+ Invoke-WebRequest $source -OutFile "$path\node-v6.9.1-win-x86.7z"
+ &$sevenZip x "$path\node-v6.9.1-win-x86.7z" -o"$path" -aos > $nul
+ Remove-File "$path\node-v6.9.1-win-x86.7z"
+ }
+
+ # note: why? node already brings everything we need!
+ ## ensure we have npm
+ #$npm = "$path\npm.*"
+ #$getNpm = $true
+ #if (test-path $npm)
+ #{
+ # $getNpm = $false
+ # $tmpNpm = ls "$path\npm.*" | sort -property Name -descending | select -first 1
+ # if ($tmpNpm.CreationTime -lt [DateTime]::Now.AddDays(-$cache))
+ # {
+ # $getNpm = $true
+ # }
+ # else
+ # {
+ # $npm = $tmpNpm.ToString()
+ # }
+ #}
+ #if ($getNpm)
+ #{
+ # Write-Host "Download Npm..."
+ # &$nuget install npm -OutputDirectory $path -Verbosity quiet
+ # $npm = ls "$path\npm.*" | sort -property Name -descending | select -first 1
+ # $npm.CreationTime = [DateTime]::Now
+ # $npm = $npm.ToString()
+ #}
+
+ # find visual studio
+ # will not work on VSO but VSO does not need it
+ $vsPath = ""
+ $vsVer = ""
+ $msBuild = $null
+ &$vswhere | foreach {
+ if ($_.StartsWith("installationPath:")) { $vsPath = $_.SubString("installationPath:".Length).Trim() }
+ if ($_.StartsWith("installationVersion:")) { $vsVer = $_.SubString("installationVersion:".Length).Trim() }
+ }
+ if ($vsPath -ne "")
+ {
+ $vsVerParts = $vsVer.Split('.')
+ $vsMajor = [int]::Parse($vsVerParts[0])
+ $vsMinor = [int]::Parse($vsVerParts[1])
+ if ($vsMajor -eq 15) {
+ $msBuild = "$vsPath\MSBuild\$vsMajor.0\Bin"
+ }
+ elseif ($vsMajor -eq 14) {
+ $msBuild = "c:\Program Files (x86)\MSBuild\$vsMajor\Bin"
+ }
+ else
+ {
+ $msBuild = $null
+ }
+ }
+
+ $vs = $null
+ if ($msBuild)
+ {
+ $vs = new-object -typeName PsObject
+ $vs | add-member -memberType NoteProperty -name Path -value $vsPath
+ $vs | add-member -memberType NoteProperty -name Major -value $vsMajor
+ $vs | add-member -memberType NoteProperty -name Minor -value $vsMinor
+ $vs | add-member -memberType NoteProperty -name MsBuild -value "$msBuild\MsBuild.exe"
+ }
+
+ $solutionRoot = Get-FullPath "$PSScriptRoot\..\..\.."
+
+ $uenv = new-object -typeName PsObject
+ $uenv | add-member -memberType NoteProperty -name SolutionRoot -value $solutionRoot
+ $uenv | add-member -memberType NoteProperty -name VisualStudio -value $vs
+ $uenv | add-member -memberType NoteProperty -name NuGet -value $nuget
+ $uenv | add-member -memberType NoteProperty -name Zip -value $sevenZip
+ $uenv | add-member -memberType NoteProperty -name VsWhere -value $vswhere
+ $uenv | add-member -memberType NoteProperty -name Semver -value $semver
+ $uenv | add-member -memberType NoteProperty -name NodePath -value $node
+ #$uenv | add-member -memberType NoteProperty -name NpmPath -value $npm
+
+ return $uenv
+}
diff --git a/build/Modules/Umbraco.Build/Get-UmbracoVersion.ps1 b/build/Modules/Umbraco.Build/Get-UmbracoVersion.ps1
new file mode 100644
index 000000000000..a3ce784f146b
--- /dev/null
+++ b/build/Modules/Umbraco.Build/Get-UmbracoVersion.ps1
@@ -0,0 +1,26 @@
+#
+# Get-UmbracoVersion
+# Gets the Umbraco version
+#
+function Get-UmbracoVersion
+{
+ $uenv = Get-UmbracoBuildEnv
+
+ # parse SolutionInfo and retrieve the version string
+ $filepath = "$($uenv.SolutionRoot)\src\SolutionInfo.cs"
+ $text = [System.IO.File]::ReadAllText($filepath)
+ $match = [System.Text.RegularExpressions.Regex]::Matches($text, "AssemblyInformationalVersion\(`"(.+)?`"\)")
+ $version = $match.Groups[1]
+
+ # semver-parse the version string
+ $semver = [SemVer.SemVersion]::Parse($version)
+ $release = "" + $semver.Major + "." + $semver.Minor + "." + $semver.Patch
+
+ $versions = new-object -typeName PsObject
+ $versions | add-member -memberType NoteProperty -name Semver -value $semver
+ $versions | add-member -memberType NoteProperty -name Release -value $release
+ $versions | add-member -memberType NoteProperty -name Comment -value $semver.PreRelease
+ $versions | add-member -memberType NoteProperty -name Build -value $semver.Build
+
+ return $versions
+}
diff --git a/build/Modules/Umbraco.Build/Get-VisualStudio.ps1 b/build/Modules/Umbraco.Build/Get-VisualStudio.ps1
new file mode 100644
index 000000000000..cc88984eb210
--- /dev/null
+++ b/build/Modules/Umbraco.Build/Get-VisualStudio.ps1
@@ -0,0 +1,30 @@
+# finds msbuild
+function Get-VisualStudio($vswhere)
+{
+ $vsPath = ""
+ $vsVer = ""
+ &$vswhere | foreach {
+ if ($_.StartsWith("installationPath:")) { $vsPath = $_.SubString("installationPath:".Length).Trim() }
+ if ($_.StartsWith("installationVersion:")) { $vsVer = $_.SubString("installationVersion:".Length).Trim() }
+ }
+ if ($vsPath -eq "") { return $null }
+
+ $vsVerParts = $vsVer.Split('.')
+ $vsMajor = [int]::Parse($vsVerParts[0])
+ $vsMinor = [int]::Parse($vsVerParts[1])
+ if ($vsMajor -eq 15) {
+ $msBuild = "$vsPath\MSBuild\$vsMajor.$vsMinor\Bin"
+ }
+ elseif ($vsMajor -eq 14) {
+ $msBuild = "c:\Program Files (x86)\MSBuild\$vsMajor\Bin"
+ }
+ else { return $null }
+ $msBuild = "$msBuild\MsBuild.exe"
+
+ $vs = new-object -typeName PsObject
+ $vs | add-member -memberType NoteProperty -name Path -value $vsPath
+ $vs | add-member -memberType NoteProperty -name Major -value $vsMajor
+ $vs | add-member -memberType NoteProperty -name Minor -value $vsMinor
+ $vs | add-member -memberType NoteProperty -name MsBuild -value $msBuild
+ return $vs
+}
diff --git a/build/Modules/Umbraco.Build/Set-UmbracoContinuousVersion.ps1 b/build/Modules/Umbraco.Build/Set-UmbracoContinuousVersion.ps1
new file mode 100644
index 000000000000..8996777292fd
--- /dev/null
+++ b/build/Modules/Umbraco.Build/Set-UmbracoContinuousVersion.ps1
@@ -0,0 +1,30 @@
+#
+# Set-UmbracoContinuousVersion
+# Sets the Umbraco version for continuous integration
+#
+# -Version
+# where is a Semver valid version
+# eg 1.2.3, 1.2.3-alpha, 1.2.3-alpha+456
+#
+# -BuildNumber
+# where is a string coming from the build server
+# eg 34, 126, 1
+#
+function Set-UmbracoContinuousVersion
+{
+ param (
+ [Parameter(Mandatory=$true)]
+ [string]
+ $version,
+ [Parameter(Mandatory=$true)]
+ [string]
+ $buildNumber
+ )
+
+ Write-Host "Version is currently set to $version"
+
+ $umbracoVersion = "$($version.Trim())-alpha$($buildNumber)"
+ Write-Host "Setting Umbraco Version to $umbracoVersion"
+
+ Set-UmbracoVersion $umbracoVersion
+}
\ No newline at end of file
diff --git a/build/Modules/Umbraco.Build/Set-UmbracoVersion.ps1 b/build/Modules/Umbraco.Build/Set-UmbracoVersion.ps1
new file mode 100644
index 000000000000..19681dcba026
--- /dev/null
+++ b/build/Modules/Umbraco.Build/Set-UmbracoVersion.ps1
@@ -0,0 +1,117 @@
+#
+# Set-UmbracoVersion
+# Sets the Umbraco version
+#
+# -Version
+# where is a Semver valid version
+# eg 1.2.3, 1.2.3-alpha, 1.2.3-alpha+456
+#
+function Set-UmbracoVersion
+{
+ param (
+ [Parameter(Mandatory=$true)]
+ [string]
+ $version
+ )
+
+ $uenv = Get-UmbracoBuildEnv
+
+ try
+ {
+ [Reflection.Assembly]::LoadFile($uenv.Semver) > $null
+ }
+ catch
+ {
+ Write-Error "Failed to load $uenv.Semver"
+ break
+ }
+
+ # validate input
+ $ok = [Regex]::Match($version, "^[0-9]+\.[0-9]+\.[0-9]+(\-[a-z0-9]+)?(\+[0-9]+)?$")
+ if (-not $ok.Success)
+ {
+ Write-Error "Invalid version $version"
+ break
+ }
+
+ # parse input
+ try
+ {
+ $semver = [SemVer.SemVersion]::Parse($version)
+ }
+ catch
+ {
+ Write-Error "Invalid version $version"
+ break
+ }
+
+ #
+ $release = "" + $semver.Major + "." + $semver.Minor + "." + $semver.Patch
+
+ # edit files and set the proper versions and dates
+ Write-Host "Update UmbracoVersion.cs"
+ Replace-FileText "$($uenv.SolutionRoot)\src\Umbraco.Core\Configuration\UmbracoVersion.cs" `
+ "(\d+)\.(\d+)\.(\d+)(.(\d+))?" `
+ "$release"
+ Replace-FileText "$($uenv.SolutionRoot)\src\Umbraco.Core\Configuration\UmbracoVersion.cs" `
+ "CurrentComment { get { return `"(.+)`"" `
+ "CurrentComment { get { return `"$($semver.PreRelease)`""
+ Write-Host "Update SolutionInfo.cs"
+ Replace-FileText "$($uenv.SolutionRoot)\src\SolutionInfo.cs" `
+ "AssemblyFileVersion\(`"(.+)?`"\)" `
+ "AssemblyFileVersion(`"$release`")"
+ Replace-FileText "$($uenv.SolutionRoot)\src\SolutionInfo.cs" `
+ "AssemblyInformationalVersion\(`"(.+)?`"\)" `
+ "AssemblyInformationalVersion(`"$semver`")"
+ $year = [System.DateTime]::Now.ToString("yyyy")
+ Replace-FileText "$($uenv.SolutionRoot)\src\SolutionInfo.cs" `
+ "AssemblyCopyright\(`"Copyright © Umbraco (\d{4})`"\)" `
+ "AssemblyCopyright(`"Copyright © Umbraco $year`")"
+
+ # edit csproj and set IIS Express port number
+ # this is a raw copy of ReplaceIISExpressPortNumber.exe
+ # it probably can be achieved in a much nicer way - l8tr
+ $source = @"
+ using System;
+ using System.IO;
+ using System.Xml;
+ using System.Globalization;
+
+ namespace Umbraco
+ {
+ public static class PortUpdater
+ {
+ public static void Update(string path, string release)
+ {
+ XmlDocument xmlDocument = new XmlDocument();
+ string fullPath = Path.GetFullPath(path);
+ xmlDocument.Load(fullPath);
+ int result = 1;
+ int.TryParse(release.Replace(`".`", `"`"), out result);
+ while (result < 1024)
+ result *= 10;
+ XmlNode xmlNode1 = xmlDocument.GetElementsByTagName(`"IISUrl`").Item(0);
+ if (xmlNode1 != null)
+ xmlNode1.InnerText = `"http://localhost:`" + (object) result;
+ XmlNode xmlNode2 = xmlDocument.GetElementsByTagName(`"DevelopmentServerPort`").Item(0);
+ if (xmlNode2 != null)
+ xmlNode2.InnerText = result.ToString((IFormatProvider) CultureInfo.InvariantCulture);
+ xmlDocument.Save(fullPath);
+ }
+ }
+ }
+"@
+
+ $assem = (
+ "System.Xml",
+ "System.IO",
+ "System.Globalization"
+ )
+
+ Write-Host "Update Umbraco.Web.UI.csproj"
+ add-type -referencedAssemblies $assem -typeDefinition $source -language CSharp
+ $csproj = "$($uenv.SolutionRoot)\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj"
+ [Umbraco.PortUpdater]::Update($csproj, $release)
+
+ return $semver
+}
diff --git a/build/Modules/Umbraco.Build/Umbraco.Build.psm1 b/build/Modules/Umbraco.Build/Umbraco.Build.psm1
new file mode 100644
index 000000000000..62ab7f186574
--- /dev/null
+++ b/build/Modules/Umbraco.Build/Umbraco.Build.psm1
@@ -0,0 +1,615 @@
+
+# Umbraco.Build.psm1
+#
+# $env:PSModulePath = "$pwd\build\Modules\;$env:PSModulePath"
+# Import-Module Umbraco.Build -Force -DisableNameChecking
+#
+# PowerShell Modules:
+# https://msdn.microsoft.com/en-us/library/dd878324%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
+#
+# PowerShell Module Manifest:
+# https://msdn.microsoft.com/en-us/library/dd878337%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
+#
+# See also
+# http://www.powershellmagazine.com/2014/08/15/pstip-taking-control-of-verbose-and-debug-output-part-5/
+
+
+. "$PSScriptRoot\Utilities.ps1"
+. "$PSScriptRoot\Get-VisualStudio.ps1"
+
+. "$PSScriptRoot\Get-UmbracoBuildEnv.ps1"
+. "$PSScriptRoot\Set-UmbracoVersion.ps1"
+. "$PSScriptRoot\Set-UmbracoContinuousVersion.ps1"
+. "$PSScriptRoot\Get-UmbracoVersion.ps1"
+. "$PSScriptRoot\Verify-NuGet.ps1"
+
+. "$PSScriptRoot\Build-UmbracoDocs.ps1"
+
+#
+# Prepares the build
+#
+function Prepare-Build
+{
+ param (
+ $uenv, # an Umbraco build environment (see Get-UmbracoBuildEnv)
+
+ [Alias("k")]
+ [switch]
+ $keep = $false
+ )
+
+ Write-Host ">> Prepare Build"
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+ $out = "$($uenv.SolutionRoot)\build.out"
+
+ # clear
+ Write-Host "Clear folders and files"
+
+ Remove-Directory "$src\Umbraco.Web.UI.Client\bower_components"
+
+ if (-not $keep)
+ {
+ Remove-Directory "$tmp"
+ Remove-Directory "$out"
+ }
+
+ if (-not (Test-Path "$tmp"))
+ {
+ mkdir "$tmp" > $null
+ }
+ if (-not (Test-Path "$out"))
+ {
+ mkdir "$out" > $null
+ }
+
+ # ensure proper web.config
+ $webUi = "$src\Umbraco.Web.UI"
+ Store-WebConfig $webUi
+ Write-Host "Create clean web.config"
+ Copy-File "$webUi\web.Template.config" "$webUi\web.config"
+}
+
+function Clear-EnvVar($var)
+{
+ $value = [Environment]::GetEnvironmentVariable($var)
+ if (test-path "env:$var") { rm "env:$var" }
+ return $value
+}
+
+function Set-EnvVar($var, $value)
+{
+ if ($value)
+ {
+ [Environment]::SetEnvironmentVariable($var, $value)
+ }
+ else
+ {
+ if (test-path "env:$var") { rm "env:$var" }
+ }
+}
+
+function Sandbox-Node
+{
+ param (
+ $uenv # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ )
+
+ $global:node_path = $env:path
+ $nodePath = $uenv.NodePath
+ $gitExe = (get-command git).Source
+ $gitPath = [System.IO.Path]::GetDirectoryName($gitExe)
+ $env:path = "$nodePath;$gitPath"
+
+ $global:node_nodepath = Clear-EnvVar "NODEPATH"
+ $global:node_npmcache = Clear-EnvVar "NPM_CONFIG_CACHE"
+ $global:node_npmprefix = Clear-EnvVar "NPM_CONFIG_PREFIX"
+}
+
+function Restore-Node
+{
+ $env:path = $node_path
+
+ Set-EnvVar "NODEPATH" $node_nodepath
+ Set-EnvVar "NPM_CONFIG_CACHE" $node_npmcache
+ Set-EnvVar "NPM_CONFIG_PREFIX" $node_npmprefix
+}
+
+#
+# Builds the Belle UI project
+#
+function Compile-Belle
+{
+ param (
+ $uenv, # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ $version # an Umbraco version object (see Get-UmbracoVersion)
+ )
+
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+ $src = "$($uenv.SolutionRoot)\src"
+
+ Write-Host ">> Compile Belle"
+ Write-Host "Logging to $tmp\belle.log"
+
+ # get a temp clean node env (will restore)
+ Sandbox-Node $uenv
+
+ push-location "$($uenv.SolutionRoot)\src\Umbraco.Web.UI.Client"
+ write "node version is:" > $tmp\belle.log
+ &node -v >> $tmp\belle.log 2>&1
+ write "npm version is:" >> $tmp\belle.log 2>&1
+ &npm -v >> $tmp\belle.log 2>&1
+ write "cleaning npm cache" >> $tmp\belle.log 2>&1
+ &npm cache clean >> $tmp\belle.log 2>&1
+ write "installing bower" >> $tmp\belle.log 2>&1
+ &npm install -g bower >> $tmp\belle.log 2>&1
+ write "installing gulp" >> $tmp\belle.log 2>&1
+ &npm install -g gulp >> $tmp\belle.log 2>&1
+ write "installing gulp-cli" >> $tmp\belle.log 2>&1
+ &npm install -g gulp-cli --quiet >> $tmp\belle.log 2>&1
+ write "executing npm install" >> $tmp\belle.log 2>&1
+ &npm install >> $tmp\belle.log 2>&1
+ write "executing gulp build for version $version" >> $tmp\belle.log 2>&1
+ &gulp build --buildversion=$version.Release >> $tmp\belle.log 2>&1
+ pop-location
+
+ # fixme - should we filter the log to find errors?
+ #get-content .\build.tmp\belle.log | %{ if ($_ -match "build") { write $_}}
+
+ # restore
+ Restore-Node
+
+ # setting node_modules folder to hidden
+ # used to prevent VS13 from crashing on it while loading the websites project
+ # also makes sure aspnet compiler does not try to handle rogue files and chokes
+ # in VSO with Microsoft.VisualC.CppCodeProvider -related errors
+ # use get-item -force 'cos it might be hidden already
+ write "Set hidden attribute on node_modules"
+ $dir = get-item -force "$src\Umbraco.Web.UI.Client\node_modules"
+ $dir.Attributes = $dir.Attributes -bor ([System.IO.FileAttributes]::Hidden)
+}
+
+#
+# Compiles Umbraco
+#
+function Compile-Umbraco
+{
+ param (
+ $uenv, # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ [string] $buildConfiguration = "Release"
+ )
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+ $out = "$($uenv.SolutionRoot)\build.out"
+
+ if ($uenv.VisualStudio -eq $null)
+ {
+ Write-Error "Build environment does not provide VisualStudio."
+ break
+ }
+
+ $toolsVersion = "4.0"
+ if ($uenv.VisualStudio.Major -eq 15)
+ {
+ $toolsVersion = "15.0"
+ }
+
+ Write-Host ">> Compile Umbraco"
+ Write-Host "Logging to $tmp\msbuild.umbraco.log"
+
+ # beware of the weird double \\ at the end of paths
+ # see http://edgylogic.com/blog/powershell-and-external-commands-done-right/
+ &$uenv.VisualStudio.MsBuild "$src\Umbraco.Web.UI\Umbraco.Web.UI.csproj" `
+ /p:WarningLevel=0 `
+ /p:Configuration=$buildConfiguration `
+ /p:Platform=AnyCPU `
+ /p:UseWPP_CopyWebApplication=True `
+ /p:PipelineDependsOnBuild=False `
+ /p:OutDir=$tmp\bin\\ `
+ /p:WebProjectOutputDir=$tmp\WebApp\\ `
+ /p:Verbosity=minimal `
+ /t:Clean`;Rebuild `
+ /tv:$toolsVersion `
+ /p:UmbracoBuild=True `
+ > $tmp\msbuild.umbraco.log
+
+ # /p:UmbracoBuild tells the csproj that we are building from PS
+}
+
+#
+# Prepare Tests
+#
+function Prepare-Tests
+{
+ param (
+ $uenv # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ )
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+
+ Write-Host ">> Prepare Tests"
+
+ # fixme - idea is to avoid rebuilding everything for tests
+ # but because of our weird assembly versioning (with .* stuff)
+ # everything gets rebuilt all the time...
+ #Copy-Files "$tmp\bin" "." "$tmp\tests"
+
+ # data
+ Write-Host "Copy data files"
+ if( -Not (Test-Path -Path "$tmp\tests\Packaging" ) )
+ {
+ Write-Host "Create packaging directory"
+ New-Item -ItemType directory -Path "$tmp\tests\Packaging"
+ }
+ Copy-Files "$src\Umbraco.Tests\Packaging\Packages" "*" "$tmp\tests\Packaging\Packages"
+
+ # required for package install tests
+ if( -Not (Test-Path -Path "$tmp\tests\bin" ) )
+ {
+ Write-Host "Create bin directory"
+ New-Item -ItemType directory -Path "$tmp\tests\bin"
+ }
+}
+
+#
+# Compiles Tests
+#
+function Compile-Tests
+{
+ param (
+ $uenv # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ )
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+ $out = "$tmp\tests"
+
+ $buildConfiguration = "Release"
+
+ if ($uenv.VisualStudio -eq $null)
+ {
+ Write-Error "Build environment does not provide VisualStudio."
+ break
+ }
+
+ $toolsVersion = "4.0"
+ if ($uenv.VisualStudio.Major -eq 15)
+ {
+ $toolsVersion = "15.0"
+ }
+
+ Write-Host ">> Compile Tests"
+ Write-Host "Logging to $tmp\msbuild.tests.log"
+
+ # beware of the weird double \\ at the end of paths
+ # see http://edgylogic.com/blog/powershell-and-external-commands-done-right/
+ &$uenv.VisualStudio.MsBuild "$src\Umbraco.Tests\Umbraco.Tests.csproj" `
+ /p:WarningLevel=0 `
+ /p:Configuration=$buildConfiguration `
+ /p:Platform=AnyCPU `
+ /p:UseWPP_CopyWebApplication=True `
+ /p:PipelineDependsOnBuild=False `
+ /p:OutDir=$out\\ `
+ /p:Verbosity=minimal `
+ /t:Build `
+ /tv:$toolsVersion `
+ /p:UmbracoBuild=True `
+ /p:NugetPackages=$src\packages `
+ > $tmp\msbuild.tests.log
+
+ # /p:UmbracoBuild tells the csproj that we are building from PS
+}
+
+#
+# Cleans things up and prepare files after compilation
+#
+function Prepare-Packages
+{
+ param (
+ $uenv # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ )
+
+ Write-Host ">> Prepare Packages"
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+ $out = "$($uenv.SolutionRoot)\build.out"
+
+ $buildConfiguration = "Release"
+
+ # restore web.config
+ Restore-WebConfig "$src\Umbraco.Web.UI"
+
+ # cleanup build
+ Write-Host "Clean build"
+ Remove-File "$tmp\bin\*.dll.config"
+ Remove-File "$tmp\WebApp\bin\*.dll.config"
+
+ # cleanup presentation
+ Write-Host "Cleanup presentation"
+ Remove-Directory "$tmp\WebApp\umbraco.presentation"
+
+ # create directories
+ Write-Host "Create directories"
+ mkdir "$tmp\Configs" > $null
+ mkdir "$tmp\Configs\Lang" > $null
+ mkdir "$tmp\WebApp\App_Data" > $null
+ #mkdir "$tmp\WebApp\Media" > $null
+ #mkdir "$tmp\WebApp\Views" > $null
+
+ # copy various files
+ Write-Host "Copy xml documentation"
+ cp -force "$tmp\bin\*.xml" "$tmp\WebApp\bin"
+
+ Write-Host "Copy transformed configs and langs"
+ # note: exclude imageprocessor/*.config as imageprocessor pkg installs them
+ Copy-Files "$tmp\WebApp\config" "*.config" "$tmp\Configs" `
+ { -not $_.RelativeName.StartsWith("imageprocessor") }
+ Copy-Files "$tmp\WebApp\config" "*.js" "$tmp\Configs"
+ Copy-Files "$tmp\WebApp\config\lang" "*.xml" "$tmp\Configs\Lang"
+ Copy-File "$tmp\WebApp\web.config" "$tmp\Configs\web.config.transform"
+
+ Write-Host "Copy transformed web.config"
+ Copy-File "$src\Umbraco.Web.UI\web.$buildConfiguration.Config.transformed" "$tmp\WebApp\web.config"
+
+ # offset the modified timestamps on all umbraco dlls, as WebResources
+ # break if date is in the future, which, due to timezone offsets can happen.
+ Write-Host "Offset dlls timestamps"
+ ls -r "$tmp\*.dll" | foreach {
+ $_.CreationTime = $_.CreationTime.AddHours(-11)
+ $_.LastWriteTime = $_.LastWriteTime.AddHours(-11)
+ }
+
+ # copy libs
+ Write-Host "Copy SqlCE libraries"
+ Copy-Files "$src\packages\SqlServerCE.4.0.0.1" "*.*" "$tmp\bin" `
+ { -not $_.Extension.StartsWith(".nu") -and -not $_.RelativeName.StartsWith("lib\") }
+ Copy-Files "$src\packages\SqlServerCE.4.0.0.1" "*.*" "$tmp\WebApp\bin" `
+ { -not $_.Extension.StartsWith(".nu") -and -not $_.RelativeName.StartsWith("lib\") }
+
+ # copy Belle
+ Write-Host "Copy Belle"
+ Copy-Files "$src\Umbraco.Web.UI\umbraco\assets" "*" "$tmp\WebApp\umbraco\assets"
+ Copy-Files "$src\Umbraco.Web.UI\umbraco\js" "*" "$tmp\WebApp\umbraco\js"
+ Copy-Files "$src\Umbraco.Web.UI\umbraco\lib" "*" "$tmp\WebApp\umbraco\lib"
+ Copy-Files "$src\Umbraco.Web.UI\umbraco\views" "*" "$tmp\WebApp\umbraco\views"
+ Copy-Files "$src\Umbraco.Web.UI\umbraco\preview" "*" "$tmp\WebApp\umbraco\preview"
+}
+
+#
+# Creates the Zip packages
+#
+function Package-Zip
+{
+ param (
+ $uenv # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ )
+
+ Write-Host ">> Create Zip packages"
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+ $out = "$($uenv.SolutionRoot)\build.out"
+
+ Write-Host "Zip all binaries"
+ &$uenv.Zip a -r "$out\UmbracoCms.AllBinaries.$($version.Semver).zip" `
+ "$tmp\bin\*" `
+ "-x!dotless.Core.*" `
+ > $null
+
+ Write-Host "Zip cms"
+ &$uenv.Zip a -r "$out\UmbracoCms.$($version.Semver).zip" `
+ "$tmp\WebApp\*" `
+ "-x!dotless.Core.*" "-x!Content_Types.xml" "-x!*.pdb"`
+ > $null
+}
+
+#
+# Prepares NuGet
+#
+function Prepare-NuGet
+{
+ param (
+ $uenv # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ )
+
+ Write-Host ">> Prepare NuGet"
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+ $out = "$($uenv.SolutionRoot)\build.out"
+
+ # add Web.config transform files to the NuGet package
+ Write-Host "Add web.config transforms to NuGet package"
+ mv "$tmp\WebApp\Views\Web.config" "$tmp\WebApp\Views\Web.config.transform"
+
+ # fixme - that one does not exist in .bat build either?
+ #mv "$tmp\WebApp\Xslt\Web.config" "$tmp\WebApp\Xslt\Web.config.transform"
+}
+
+#
+# Restores NuGet
+#
+function Restore-NuGet
+{
+ param (
+ $uenv # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ )
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+
+ Write-Host ">> Restore NuGet"
+ Write-Host "Logging to $tmp\nuget.restore.log"
+
+ &$uenv.NuGet restore "$src\Umbraco.sln" > "$tmp\nuget.restore.log"
+}
+
+#
+# Copies the Azure Gallery script to output
+#
+function Prepare-AzureGallery
+{
+ param (
+ $uenv # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ )
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+ $out = "$($uenv.SolutionRoot)\build.out"
+ $psScript = "$($uenv.SolutionRoot)\build\azuregalleryrelease.ps1"
+
+ Write-Host ">> Copy azuregalleryrelease.ps1 to output folder"
+ Copy-Item $psScript $out
+}
+
+#
+# Creates the NuGet packages
+#
+function Package-NuGet
+{
+ param (
+ $uenv, # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ $version # an Umbraco version object (see Get-UmbracoVersion)
+ )
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $tmp = "$($uenv.SolutionRoot)\build.tmp"
+ $out = "$($uenv.SolutionRoot)\build.out"
+ $nuspecs = "$($uenv.SolutionRoot)\build\NuSpecs"
+
+ Write-Host ">> Create NuGet packages"
+
+ # see https://docs.microsoft.com/en-us/nuget/schema/nuspec
+ # note - warnings about SqlCE native libs being outside of 'lib' folder,
+ # nothing much we can do about it as it's intentional yet there does not
+ # seem to be a way to disable the warning
+
+ &$uenv.NuGet Pack "$nuspecs\UmbracoCms.Core.nuspec" `
+ -Properties BuildTmp="$tmp" `
+ -Version $version.Semver.ToString() `
+ -Symbols -Verbosity quiet -outputDirectory $out
+
+ &$uenv.NuGet Pack "$nuspecs\UmbracoCms.nuspec" `
+ -Properties BuildTmp="$tmp" `
+ -Version $version.Semver.ToString() `
+ -Verbosity quiet -outputDirectory $out
+}
+
+#
+# Builds Umbraco
+#
+function Build-Umbraco
+{
+ [CmdletBinding()]
+ param (
+ [string]
+ $target = "all",
+ [string]
+ $buildConfiguration = "Release"
+ )
+
+ $target = $target.ToLowerInvariant()
+ Write-Host ">> Build-Umbraco <$target> <$buildConfiguration>"
+
+ Write-Host "Get Build Environment"
+ $uenv = Get-UmbracoBuildEnv
+
+ Write-Host "Get Version"
+ $version = Get-UmbracoVersion
+ Write-Host "Version $($version.Semver)"
+
+ if ($target -eq "pre-build")
+ {
+ Prepare-Build $uenv
+ #Compile-Belle $uenv $version
+
+ # set environment variables
+ $env:UMBRACO_VERSION=$version.Semver.ToString()
+ $env:UMBRACO_RELEASE=$version.Release
+ $env:UMBRACO_COMMENT=$version.Comment
+ $env:UMBRACO_BUILD=$version.Build
+
+ # set environment variable for VSO
+ # https://github.com/Microsoft/vsts-tasks/issues/375
+ # https://github.com/Microsoft/vsts-tasks/blob/master/docs/authoring/commands.md
+ Write-Host ("##vso[task.setvariable variable=UMBRACO_VERSION;]$($version.Semver.ToString())")
+ Write-Host ("##vso[task.setvariable variable=UMBRACO_RELEASE;]$($version.Release)")
+ Write-Host ("##vso[task.setvariable variable=UMBRACO_COMMENT;]$($version.Comment)")
+ Write-Host ("##vso[task.setvariable variable=UMBRACO_BUILD;]$($version.Build)")
+
+ Write-Host ("##vso[task.setvariable variable=UMBRACO_TMP;]$($uenv.SolutionRoot)\build.tmp")
+ }
+ elseif ($target -eq "pre-tests")
+ {
+ Prepare-Tests $uenv
+ }
+ elseif ($target -eq "compile-tests")
+ {
+ Compile-Tests $uenv
+ }
+ elseif ($target -eq "compile-umbraco")
+ {
+ Compile-Umbraco $uenv $buildConfiguration
+ }
+ elseif ($target -eq "pre-packages")
+ {
+ Prepare-Packages $uenv
+ }
+ elseif ($target -eq "pre-nuget")
+ {
+ Prepare-NuGet $uenv
+ }
+ elseif ($target -eq "restore-nuget")
+ {
+ Restore-NuGet $uenv
+ }
+ elseif ($target -eq "pkg-zip")
+ {
+ Package-Zip $uenv
+ }
+ elseif ($target -eq "compile-belle")
+ {
+ Compile-Belle $uenv $version
+ }
+ elseif ($target -eq "prepare-azuregallery")
+ {
+ Prepare-AzureGallery $uenv
+ }
+ elseif ($target -eq "all")
+ {
+ Prepare-Build $uenv
+ Restore-NuGet $uenv
+ Compile-Belle $uenv $version
+ Compile-Umbraco $uenv $buildConfiguration
+ Prepare-Tests $uenv
+ Compile-Tests $uenv
+ # not running tests...
+ Prepare-Packages $uenv
+ Package-Zip $uenv
+ Verify-NuGet $uenv
+ Prepare-NuGet $uenv
+ Package-NuGet $uenv $version
+ Prepare-AzureGallery $uenv
+ }
+ else
+ {
+ Write-Error "Unsupported target `"$target`"."
+ }
+}
+
+#
+# export functions
+#
+Export-ModuleMember -function Get-UmbracoBuildEnv
+Export-ModuleMember -function Set-UmbracoVersion
+Export-ModuleMember -function Set-UmbracoContinuousVersion
+Export-ModuleMember -function Get-UmbracoVersion
+Export-ModuleMember -function Build-Umbraco
+Export-ModuleMember -function Build-UmbracoDocs
+Export-ModuleMember -function Verify-NuGet
+
+#eof
\ No newline at end of file
diff --git a/build/Modules/Umbraco.Build/Utilities.ps1 b/build/Modules/Umbraco.Build/Utilities.ps1
new file mode 100644
index 000000000000..8ca24885cd98
--- /dev/null
+++ b/build/Modules/Umbraco.Build/Utilities.ps1
@@ -0,0 +1,95 @@
+# returns the full path if $file is relative to $pwd
+function Get-FullPath($file)
+{
+ $path = [System.IO.Path]::Combine($pwd, $file)
+ $path = [System.IO.Path]::GetFullPath($path)
+ return $path
+}
+
+# removes a directory, doesn't complain if it does not exist
+function Remove-Directory($dir)
+{
+ remove-item $dir -force -recurse -errorAction SilentlyContinue > $null
+}
+
+# removes a file, doesn't complain if it does not exist
+function Remove-File($file)
+{
+ remove-item $file -force -errorAction SilentlyContinue > $null
+}
+
+# copies a file, creates target dir if needed
+function Copy-File($source, $target)
+{
+ $ignore = new-item -itemType file -path $target -force
+ cp -force $source $target
+}
+
+# copies files to a directory
+function Copy-Files($source, $select, $target, $filter)
+{
+ $files = ls -r "$source\$select"
+ $files | foreach {
+ $relative = $_.FullName.SubString($source.Length+1)
+ $_ | add-member -memberType NoteProperty -name RelativeName -value $relative
+ }
+ if ($filter -ne $null) {
+ $files = $files | where $filter
+ }
+ $files |
+ foreach {
+ if ($_.PsIsContainer) {
+ $ignore = new-item -itemType directory -path "$target\$($_.RelativeName)" -force
+ }
+ else {
+ Copy-File $_.FullName "$target\$($_.RelativeName)"
+ }
+ }
+}
+
+# regex-replaces content in a file
+function Replace-FileText($filename, $source, $replacement)
+{
+ $filepath = Get-FullPath $filename
+ $text = [System.IO.File]::ReadAllText($filepath)
+ $text = [System.Text.RegularExpressions.Regex]::Replace($text, $source, $replacement)
+ $utf8bom = New-Object System.Text.UTF8Encoding $true
+ [System.IO.File]::WriteAllText($filepath, $text, $utf8bom)
+}
+
+# store web.config
+function Store-WebConfig($webUi)
+{
+ if (test-path "$webUi\web.config")
+ {
+ if (test-path "$webUi\web.config.temp-build")
+ {
+ Write-Host "Found existing web.config.temp-build"
+ $i = 0
+ while (test-path "$webUi\web.config.temp-build.$i")
+ {
+ $i = $i + 1
+ }
+ Write-Host "Save existing web.config as web.config.temp-build.$i"
+ Write-Host "(WARN: the original web.config.temp-build will be restored during post-build)"
+ mv "$webUi\web.config" "$webUi\web.config.temp-build.$i"
+ }
+ else
+ {
+ Write-Host "Save existing web.config as web.config.temp-build"
+ Write-Host "(will be restored during post-build)"
+ mv "$webUi\web.config" "$webUi\web.config.temp-build"
+ }
+ }
+}
+
+# restore web.config
+function Restore-WebConfig($webUi)
+{
+ if (test-path "$webUi\web.config.temp-build")
+ {
+ Write-Host "Restoring existing web.config"
+ Remove-File "$webUi\web.config"
+ mv "$webUi\web.config.temp-build" "$webUi\web.config"
+ }
+}
\ No newline at end of file
diff --git a/build/Modules/Umbraco.Build/Verify-NuGet.ps1 b/build/Modules/Umbraco.Build/Verify-NuGet.ps1
new file mode 100644
index 000000000000..1a2239393ccb
--- /dev/null
+++ b/build/Modules/Umbraco.Build/Verify-NuGet.ps1
@@ -0,0 +1,444 @@
+#
+# Verify-NuGet
+#
+
+function Format-Dependency
+{
+ param ( $d )
+
+ $m = $d.Id + " "
+ if ($d.MinInclude) { $m = $m + "[" }
+ else { $m = $m + "(" }
+ $m = $m + $d.MinVersion
+ if ($d.MaxVersion -ne $d.MinVersion) { $m = $m + "," + $d.MaxVersion }
+ if ($d.MaxInclude) { $m = $m + "]" }
+ else { $m = $m + ")" }
+
+ return $m
+}
+
+function Write-NuSpec
+{
+ param ( $name, $deps )
+
+ Write-Host ""
+ Write-Host "$name NuSpec dependencies:"
+
+ foreach ($d in $deps)
+ {
+ $m = Format-Dependency $d
+ Write-Host " $m"
+ }
+}
+
+function Write-Package
+{
+ param ( $name, $pkgs )
+
+ Write-Host ""
+ Write-Host "$name packages:"
+
+ foreach ($p in $pkgs)
+ {
+ Write-Host " $($p.Id) $($p.Version)"
+ }
+}
+
+function Verify-NuGet
+{
+ param (
+ $uenv # an Umbraco build environment (see Get-UmbracoBuildEnv)
+ )
+
+ if ($uenv -eq $null)
+ {
+ $uenv = Get-UmbracoBuildEnv
+ }
+
+ $source = @"
+
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.IO;
+ using System.Xml;
+ using System.Xml.Serialization;
+ using Semver;
+
+ namespace Umbraco.Build
+ {
+ public class NuGet
+ {
+ public static Dependency[] GetNuSpecDependencies(string filename)
+ {
+ NuSpec nuspec;
+ var serializer = new XmlSerializer(typeof(NuSpec));
+ using (var reader = new StreamReader(filename))
+ {
+ nuspec = (NuSpec) serializer.Deserialize(reader);
+ }
+ var nudeps = nuspec.Metadata.Dependencies;
+ var deps = new List();
+ foreach (var nudep in nudeps)
+ {
+ var dep = new Dependency();
+ dep.Id = nudep.Id;
+
+ var parts = nudep.Version.Split(',');
+ if (parts.Length == 1)
+ {
+ dep.MinInclude = parts[0].StartsWith("[");
+ dep.MaxInclude = parts[0].EndsWith("]");
+
+ SemVersion version;
+ if (!SemVersion.TryParse(parts[0].Substring(1, parts[0].Length-2).Trim(), out version)) continue;
+ dep.MinVersion = dep.MaxVersion = version; //parts[0].Substring(1, parts[0].Length-2).Trim();
+ }
+ else
+ {
+ SemVersion version;
+ if (!SemVersion.TryParse(parts[0].Substring(1).Trim(), out version)) continue;
+ dep.MinVersion = version; //parts[0].Substring(1).Trim();
+ if (!SemVersion.TryParse(parts[1].Substring(0, parts[1].Length-1).Trim(), out version)) continue;
+ dep.MaxVersion = version; //parts[1].Substring(0, parts[1].Length-1).Trim();
+ dep.MinInclude = parts[0].StartsWith("[");
+ dep.MaxInclude = parts[1].EndsWith("]");
+ }
+
+ deps.Add(dep);
+ }
+ return deps.ToArray();
+ }
+
+ public static IEnumerable DistinctBy(/*this*/ IEnumerable source, Func keySelector)
+ {
+ HashSet knownKeys = new HashSet();
+ foreach (TSource element in source)
+ {
+ if (knownKeys.Add(keySelector(element)))
+ {
+ yield return element;
+ }
+ }
+ }
+
+ public static Package[] GetProjectsPackages(string src, string[] projects)
+ {
+ var l = new List();
+ foreach (var project in projects)
+ {
+ var path = Path.Combine(src, project);
+ var packageConfig = Path.Combine(path, "packages.config");
+ if (File.Exists(packageConfig))
+ ReadPackagesConfig(packageConfig, l);
+ var csprojs = Directory.GetFiles(path, "*.csproj");
+ foreach (var csproj in csprojs)
+ {
+ ReadCsProj(csproj, l);
+ }
+ }
+ IEnumerable p = l.OrderBy(x => x.Id);
+ p = DistinctBy(p, x => x.Id + ":::" + x.Version);
+ return p.ToArray();
+ }
+
+ public static object[] GetPackageErrors(Package[] pkgs)
+ {
+ return pkgs
+ .GroupBy(x => x.Id)
+ .Where(x => x.Count() > 1)
+ .ToArray();
+ }
+
+ public static object[] GetNuSpecErrors(Package[] pkgs, Dependency[] deps)
+ {
+ var d = pkgs.ToDictionary(x => x.Id, x => x.Version);
+ return deps
+ .Select(x =>
+ {
+ SemVersion v;
+ if (!d.TryGetValue(x.Id, out v)) return null;
+
+ var ok = true;
+
+ /*
+ if (x.MinInclude)
+ {
+ if (v < x.MinVersion) ok = false;
+ }
+ else
+ {
+ if (v <= x.MinVersion) ok = false;
+ }
+
+ if (x.MaxInclude)
+ {
+ if (v > x.MaxVersion) ok = false;
+ }
+ else
+ {
+ if (v >= x.MaxVersion) ok = false;
+ }
+ */
+
+ if (!x.MinInclude || v != x.MinVersion) ok = false;
+
+ return ok ? null : new { Dependency = x, Version = v };
+ })
+ .Where(x => x != null)
+ .ToArray();
+ }
+
+ /*
+ public static Package[] GetProjectPackages(string path)
+ {
+ var l = new List();
+ var packageConfig = Path.Combine(path, "packages.config");
+ if (File.Exists(packageConfig))
+ ReadPackagesConfig(packageConfig, l);
+ var csprojs = Directory.GetFiles(path, "*.csproj");
+ foreach (var csproj in csprojs)
+ {
+ ReadCsProj(csproj, l);
+ }
+ return l.ToArray();
+ }
+ */
+
+ public static string GetDirectoryName(string filename)
+ {
+ return Path.GetFileName(Path.GetDirectoryName(filename));
+ }
+
+ public static void ReadPackagesConfig(string filename, List packages)
+ {
+ //Console.WriteLine("read " + filename);
+
+ PackagesConfigPackages pkgs;
+ var serializer = new XmlSerializer(typeof(PackagesConfigPackages));
+ using (var reader = new StreamReader(filename))
+ {
+ pkgs = (PackagesConfigPackages) serializer.Deserialize(reader);
+ }
+ foreach (var p in pkgs.Packages)
+ {
+ SemVersion version;
+ if (!SemVersion.TryParse(p.Version, out version)) continue;
+ packages.Add(new Package { Id = p.Id, Version = version, Project = GetDirectoryName(filename) });
+ }
+ }
+
+ public static void ReadCsProj(string filename, List packages)
+ {
+ //Console.WriteLine("read " + filename);
+
+ // if xmlns then it's not a VS2017 with PackageReference
+ var text = File.ReadAllLines(filename);
+ var line = text.FirstOrDefault(x => x.Contains(" x.Packages != null).SelectMany(x => x.Packages))
+ {
+ var sversion = p.VersionE ?? p.VersionA;
+ SemVersion version;
+ if (!SemVersion.TryParse(sversion, out version)) continue;
+ packages.Add(new Package { Id = p.Id, Version = version, Project = GetDirectoryName(filename) });
+ }
+ }
+
+ public class Dependency
+ {
+ public string Id { get; set; }
+ public SemVersion MinVersion { get; set; }
+ public SemVersion MaxVersion { get; set; }
+ public bool MinInclude { get; set; }
+ public bool MaxInclude { get; set; }
+ }
+
+ public class Package
+ {
+ public string Id { get; set; }
+ public SemVersion Version { get; set; }
+ public string Project { get; set; }
+ }
+
+ [XmlType(AnonymousType = true, Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd")]
+ [XmlRoot(Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd", IsNullable = false, ElementName = "package")]
+ public class NuSpec
+ {
+ [XmlElement("metadata")]
+ public NuSpecMetadata Metadata { get; set; }
+ }
+
+ [XmlType(AnonymousType = true, Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd", TypeName = "metadata")]
+ public class NuSpecMetadata
+ {
+ [XmlArray("dependencies")]
+ [XmlArrayItem("dependency", IsNullable = false)]
+ public NuSpecDependency[] Dependencies { get; set; }
+ }
+
+ [XmlType(AnonymousType = true, Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd", TypeName = "dependencies")]
+ public class NuSpecDependency
+ {
+ [XmlAttribute(AttributeName = "id")]
+ public string Id { get; set; }
+
+ [XmlAttribute(AttributeName = "version")]
+ public string Version { get; set; }
+ }
+
+ [XmlType(AnonymousType = true)]
+ [XmlRoot(Namespace = "", IsNullable = false, ElementName = "packages")]
+ public class PackagesConfigPackages
+ {
+ [XmlElement("package")]
+ public PackagesConfigPackage[] Packages { get; set; }
+ }
+
+ [XmlType(AnonymousType = true, TypeName = "package")]
+ public class PackagesConfigPackage
+ {
+ [XmlAttribute(AttributeName = "id")]
+ public string Id { get; set; }
+
+ [XmlAttribute(AttributeName = "version")]
+ public string Version { get; set; }
+ }
+
+ [XmlType(AnonymousType = true)]
+ [XmlRoot(Namespace = "", IsNullable = false, ElementName = "Project")]
+ public class CsProjProject
+ {
+ [XmlElement("ItemGroup")]
+ public CsProjItemGroup[] ItemGroups { get; set; }
+ }
+
+ [XmlType(AnonymousType = true, TypeName = "ItemGroup")]
+ public class CsProjItemGroup
+ {
+ [XmlElement("PackageReference")]
+ public CsProjPackageReference[] Packages { get; set; }
+ }
+
+ [XmlType(AnonymousType = true, TypeName = "PackageReference")]
+ public class CsProjPackageReference
+ {
+ [XmlAttribute(AttributeName = "Include")]
+ public string Id { get; set; }
+
+ [XmlAttribute(AttributeName = "Version")]
+ public string VersionA { get; set; }
+
+ [XmlElement("Version")]
+ public string VersionE { get; set;}
+ }
+ }
+ }
+
+"@
+
+ Write-Host ">> Verify NuGet consistency"
+
+ $assem = (
+ "System.Xml",
+ "System.Core", # "System.Collections.Generic"
+ "System.Linq",
+ "System.Xml.Serialization",
+ "System.IO",
+ "System.Globalization",
+ $uenv.Semver
+ )
+
+ try
+ {
+ # as long as the code hasn't changed it's fine to re-add, but if the code
+ # has changed this will throw - better warn the dev that we have an issue
+ add-type -referencedAssemblies $assem -typeDefinition $source -language CSharp
+ }
+ catch
+ {
+ if ($_.FullyQualifiedErrorId.StartsWith("TYPE_ALREADY_EXISTS,"))
+ { Write-Error "Failed to add type, did you change the code?" }
+ else
+ { Write-Error $_ }
+ }
+ if (-not $?) { break }
+
+ $nuspecs = (
+ "UmbracoCms",
+ "UmbracoCms.Core"
+ )
+
+ $projects = (
+ "Umbraco.Core",
+ "Umbraco.Web",
+ "Umbraco.Web.UI",
+ "UmbracoExamine"#,
+ #"Umbraco.Tests",
+ #"Umbraco.Tests.Benchmarks"
+ )
+
+ $src = "$($uenv.SolutionRoot)\src"
+ $pkgs = [Umbraco.Build.NuGet]::GetProjectsPackages($src, $projects)
+ if (-not $?) { break }
+ #Write-Package "All" $pkgs
+
+ $errs = [Umbraco.Build.NuGet]::GetPackageErrors($pkgs)
+ if (-not $?) { break }
+
+ if ($errs.Length -gt 0)
+ {
+ Write-Host ""
+ }
+ foreach ($err in $errs)
+ {
+ Write-Host $err.Key
+ foreach ($e in $err)
+ {
+ Write-Host " $($e.Version) required by $($e.Project)"
+ }
+ }
+ if ($errs.Length -gt 0)
+ {
+ Write-Error "Found non-consolidated package dependencies"
+ break
+ }
+
+ $nuerr = $false
+ $nupath = "$($uenv.SolutionRoot)\build\NuSpecs"
+ foreach ($nuspec in $nuspecs)
+ {
+ $deps = [Umbraco.Build.NuGet]::GetNuSpecDependencies("$nupath\$nuspec.nuspec")
+ if (-not $?) { break }
+ #Write-NuSpec $nuspec $deps
+
+ $errs = [Umbraco.Build.NuGet]::GetNuSpecErrors($pkgs, $deps)
+ if (-not $?) { break }
+
+ if ($errs.Length -gt 0)
+ {
+ Write-Host ""
+ Write-Host "$nuspec requires:"
+ $nuerr = $true
+ }
+ foreach ($err in $errs)
+ {
+ $m = Format-Dependency $err.Dependency
+ Write-Host " $m but projects require $($err.Version)"
+ }
+ }
+
+ if ($nuerr)
+ {
+ Write-Error "Found inconsistent NuGet dependencies"
+ break
+ }
+}
\ No newline at end of file
diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec
index e62185d4b2ef..ff299fdcfb50 100644
--- a/build/NuSpecs/UmbracoCms.Core.nuspec
+++ b/build/NuSpecs/UmbracoCms.Core.nuspec
@@ -21,88 +21,87 @@
-
-
-
+
+
+
-
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec
index 8d58ec43a3e5..22430a45160b 100644
--- a/build/NuSpecs/UmbracoCms.nuspec
+++ b/build/NuSpecs/UmbracoCms.nuspec
@@ -19,22 +19,22 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt
index 5c46ee20bfb8..e85b22a90261 100644
--- a/build/NuSpecs/tools/ReadmeUpgrade.txt
+++ b/build/NuSpecs/tools/ReadmeUpgrade.txt
@@ -8,13 +8,13 @@
----------------------------------------------------
-*** IMPORTANT NOTICE FOR 7.6 UPGRADES ***
+*** IMPORTANT NOTICE FOR 7.7 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
+https://our.umbraco.org/documentation/Getting-Started/Setup/Upgrading/version-specific#version-7-7-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.
+Depending on the version you are upgrading from, you may 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!
diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt
index e482eb5d6e36..5ee5ed32c6d3 100644
--- a/build/NuSpecs/tools/Web.config.install.xdt
+++ b/build/NuSpecs/tools/Web.config.install.xdt
@@ -15,6 +15,7 @@
+
@@ -23,6 +24,7 @@
+
@@ -30,6 +32,7 @@
+
@@ -368,19 +371,19 @@
-
+
-
+
-
+
-
+
diff --git a/build/NuSpecs/tools/trees.config.install.xdt b/build/NuSpecs/tools/trees.config.install.xdt
index 7d41835fb126..21dc791636a6 100644
--- a/build/NuSpecs/tools/trees.config.install.xdt
+++ b/build/NuSpecs/tools/trees.config.install.xdt
@@ -43,7 +43,7 @@
xdt:Locator="Match(application,alias)"
xdt:Transform="InsertIfMissing" />
-
-
-
+
+
-
+
+ xdt:Transform="Remove" />
&1 | % { $_.ToString() }
+
+# Remove everything so that unzipping the release later will update everything
+# Don't remove the readme file nor the git directory
+Write-Host "Cleaning up git directory before adding new version"
+Remove-Item -Recurse $workingDirectory\$env:GIT_REPOSITORYNAME\* -Exclude README.md,.git
+
+# Find release zip
+$zipsDir = "$workingDirectory\$env:BUILD_DEFINITIONNAME\zips"
+$pattern = "UmbracoCms.([0-9]{1,2}.[0-9]{1,3}.[0-9]{1,3}).zip"
+Write-Host "Searching for Umbraco release files in $workingDirectory\$zipsDir for a file with pattern $pattern"
+$file = (Get-ChildItem $zipsDir | Where-Object { $_.Name -match "$pattern" })
+
+if($file)
+{
+ # Get release name
+ $version = [regex]::Match($file.Name, $pattern).captures.groups[1].value
+ $releaseName = "Umbraco $version"
+ Write-Host "Found $releaseName"
+
+ # Unzip into repository to update release
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ Write-Host "Unzipping $($file.FullName) to $workingDirectory\$env:GIT_REPOSITORYNAME"
+ [System.IO.Compression.ZipFile]::ExtractToDirectory("$($file.FullName)", "$workingDirectory\$env:GIT_REPOSITORYNAME")
+
+ # Telling git who we are
+ git config --global user.email "coffee@umbraco.com" 2>&1 | % { $_.ToString() }
+ git config --global user.name "Umbraco HQ" 2>&1 | % { $_.ToString() }
+
+ # Commit
+ CD $env:GIT_REPOSITORYNAME
+ Write-Host "Committing Umbraco $version Release from Build Output"
+
+ git add . 2>&1 | % { $_.ToString() }
+ git commit -m " Release $releaseName from Build Output" 2>&1 | % { $_.ToString() }
+
+ # Tag the release
+ git tag -a "v$version" -m "v$version"
+
+ # Push release to master
+ $fullGitAuthUrl = "https://$($env:GIT_USERNAME):$GitHubPersonalAccessToken@$env:GIT_URL/$env:GIT_REPOSITORYNAME.git"
+ git push $fullGitAuthUrl 2>&1 | % { $_.ToString() }
+
+ #Push tag to master
+ git push $fullGitAuthUrl --tags 2>&1 | % { $_.ToString() }
+}
+else
+{
+ Write-Error "Umbraco release file not found, searched in $workingDirectory\$zipsDir for a file with pattern $pattern - cancelling"
+}
diff --git a/build/build.ps1 b/build/build.ps1
new file mode 100644
index 000000000000..72d8287d9c5a
--- /dev/null
+++ b/build/build.ps1
@@ -0,0 +1,67 @@
+param (
+ [Parameter(Mandatory=$false)]
+ [string]
+ $version,
+
+ [Parameter(Mandatory=$false)]
+ [Alias("mo")]
+ [switch]
+ $moduleOnly = $false
+)
+
+# the script can run either from the solution root,
+# or from the ./build directory - anything else fails
+if ([System.IO.Path]::GetFileName($pwd) -eq "build")
+{
+ $mpath = [System.IO.Path]::GetDirectoryName($pwd) + "\build\Modules\"
+}
+else
+{
+ $mpath = "$pwd\build\Modules\"
+}
+
+# look for the module and throw if not found
+if (-not [System.IO.Directory]::Exists($mpath + "Umbraco.Build"))
+{
+ Write-Error "Could not locate Umbraco build Powershell module."
+ break
+}
+
+# add the module path (if not already there)
+if (-not $env:PSModulePath.Contains($mpath))
+{
+ $env:PSModulePath = "$mpath;$env:PSModulePath"
+}
+
+# force-import (or re-import) the module
+Write-Host "Import Umbraco build Powershell module"
+Import-Module Umbraco.Build -Force -DisableNameChecking
+
+# module only?
+if ($moduleOnly)
+{
+ if (-not [string]::IsNullOrWhiteSpace($version))
+ {
+ Write-Host "(module only: ignoring version parameter)"
+ }
+ else
+ {
+ Write-Host "(module only)"
+ }
+ break
+}
+
+# get build environment
+Write-Host "Setup Umbraco build Environment"
+$uenv = Get-UmbracoBuildEnv
+
+# set the version if any
+if (-not [string]::IsNullOrWhiteSpace($version))
+{
+ Write-Host "Set Umbraco version to $version"
+ Set-UmbracoVersion $version
+}
+
+# full umbraco build
+Write-Host "Build Umbraco"
+Build-Umbraco
\ No newline at end of file
diff --git a/build/setversion.ps1 b/build/setversion.ps1
new file mode 100644
index 000000000000..99f1534bf55d
--- /dev/null
+++ b/build/setversion.ps1
@@ -0,0 +1,18 @@
+# Usage: powershell .\setversion.ps1 7.6.8
+# Or: powershell .\setversion 7.6.8-beta001
+
+param (
+ [Parameter(Mandatory=$true)]
+ [string]
+ $version
+)
+
+# report
+Write-Host "Setting Umbraco version to $version"
+
+# import Umbraco Build PowerShell module - $pwd is ./build
+$env:PSModulePath = "$pwd\Modules\;$env:PSModulePath"
+Import-Module Umbraco.Build -Force -DisableNameChecking
+
+# run commands
+$version = Set-UmbracoVersion -Version $version
\ No newline at end of file
diff --git a/src/.nuget/NuGet.Config b/src/.nuget/NuGet.Config
deleted file mode 100644
index 6a318ad9b75f..000000000000
--- a/src/.nuget/NuGet.Config
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/.nuget/NuGet.exe b/src/.nuget/NuGet.exe
deleted file mode 100644
index 6804fb7da737..000000000000
Binary files a/src/.nuget/NuGet.exe and /dev/null differ
diff --git a/src/.nuget/NuGet.targets b/src/.nuget/NuGet.targets
deleted file mode 100644
index 6ff51f6e83c7..000000000000
--- a/src/.nuget/NuGet.targets
+++ /dev/null
@@ -1,138 +0,0 @@
-
-
-
- $(MSBuildProjectDirectory)\..\
-
-
- false
-
-
- false
-
-
- true
-
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
- $([System.IO.Path]::Combine($(SolutionDir), ".nuget"))
- $([System.IO.Path]::Combine($(ProjectDir), "packages.config"))
-
-
-
-
- $(SolutionDir).nuget
- packages.config
-
-
-
-
- $(NuGetToolsPath)\NuGet.exe
- @(PackageSource)
-
- "$(NuGetExePath)"
- mono --runtime=v4.0.30319 $(NuGetExePath)
-
- $(TargetDir.Trim('\\'))
-
- -RequireConsent
- -NonInteractive
-
- "$(SolutionDir) "
- "$(SolutionDir)"
-
-
- $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir)
- $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols
-
-
-
- RestorePackages;
- $(BuildDependsOn);
-
-
-
-
- $(BuildDependsOn);
- BuildPackage;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj
index a5d86d5377cc..2f2bb5866dee 100644
--- a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj
+++ b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj
@@ -93,7 +93,6 @@
-
User Groups here which we currently are not doing
- //the password must be 'something' it could be empty if authenticating
- // with an external provider so we'll just generate one and prefix it, the
- // prefix will help us determine if the password hasn't actually been specified yet.
- if (member.RawPasswordValue.IsNullOrWhiteSpace())
- {
- //this will hash the guid with a salt so should be nicely random
- var aspHasher = new PasswordHasher();
- member.RawPasswordValue = "___UIDEMPTYPWORD__" +
- aspHasher.HashPassword(Guid.NewGuid().ToString("N"));
+ _userService.Save(userEntity);
- }
- _userService.Save(member);
-
- if (member.Id == 0) throw new DataException("Could not create the user, check logs for details");
+ if (userEntity.Id == 0) throw new DataException("Could not create the user, check logs for details");
//re-assign id
- user.Id = member.Id;
+ user.Id = userEntity.Id;
return Task.FromResult(0);
}
@@ -126,12 +124,15 @@ public async Task UpdateAsync(BackOfficeIdentityUser user)
var found = _userService.GetUserById(asInt.Result);
if (found != null)
{
+ // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
+ var isLoginsPropertyDirty = user.IsPropertyDirty("Logins");
+
if (UpdateMemberProperties(found, user))
{
_userService.Save(found);
}
- if (user.LoginsChanged)
+ if (isLoginsPropertyDirty)
{
var logins = await GetLoginsAsync(user);
_externalLoginService.SaveUserLogins(found.Id, logins);
@@ -199,7 +200,7 @@ public async Task FindByNameAsync(string userName)
return await Task.FromResult(result);
}
-
+
///
/// Set the user password hash
///
@@ -209,7 +210,7 @@ public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHas
{
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException("user");
- if (passwordHash.IsNullOrWhiteSpace()) throw new ArgumentNullException("passwordHash");
+ if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value cannot be null or empty.", "passwordHash");
user.PasswordHash = passwordHash;
@@ -225,7 +226,7 @@ public Task GetPasswordHashAsync(BackOfficeIdentityUser user)
{
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException("user");
-
+
return Task.FromResult(user.PasswordHash);
}
@@ -239,7 +240,7 @@ public Task HasPasswordAsync(BackOfficeIdentityUser user)
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException("user");
- return Task.FromResult(user.PasswordHash.IsNullOrWhiteSpace() == false);
+ return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false);
}
///
@@ -279,7 +280,9 @@ public Task GetEmailAsync(BackOfficeIdentityUser user)
public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user)
{
ThrowIfDisposed();
- throw new NotImplementedException();
+ if (user == null) throw new ArgumentNullException("user");
+
+ return Task.FromResult(user.EmailConfirmed);
}
///
@@ -290,7 +293,8 @@ public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user)
public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed)
{
ThrowIfDisposed();
- throw new NotImplementedException();
+ user.EmailConfirmed = confirmed;
+ return Task.FromResult(0);
}
///
@@ -374,12 +378,17 @@ public Task FindAsync(UserLoginInfo login)
var result = _externalLoginService.Find(login).ToArray();
if (result.Any())
{
- //return the first member that matches the result
- var output = (from l in result
- select _userService.GetUserById(l.UserId)
- into user
- where user != null
- select Mapper.Map(user)).FirstOrDefault();
+ //return the first user that matches the result
+ BackOfficeIdentityUser output = null;
+ foreach (var l in result)
+ {
+ var user = _userService.GetUserById(l.UserId);
+ if (user != null)
+ {
+ output = Mapper.Map(user);
+ break;
+ }
+ }
return Task.FromResult(AssignLoginsCallback(output));
}
@@ -389,63 +398,49 @@ into user
///
- /// Adds a user to a role (section)
+ /// Adds a user to a role (user group)
///
///
///
public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName)
- {
+ {
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException("user");
+ if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value cannot be null or whitespace.", "roleName");
- if (user.AllowedSections.InvariantContains(roleName)) return Task.FromResult(0);
-
- var asInt = user.Id.TryConvertTo();
- if (asInt == false)
- {
- throw new InvalidOperationException("The user id must be an integer to work with the Umbraco");
- }
-
- var found = _userService.GetUserById(asInt.Result);
+ var userRole = user.Roles.SingleOrDefault(r => r.RoleId == roleName);
- if (found != null)
+ if (userRole == null)
{
- found.AddAllowedSection(roleName);
+ user.AddRole(roleName);
}
return Task.FromResult(0);
}
///
- /// Removes the role (allowed section) for the user
+ /// Removes the role (user group) for the user
///
///
///
public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName)
- {
+ {
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException("user");
+ if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value cannot be null or whitespace.", "roleName");
- if (user.AllowedSections.InvariantContains(roleName) == false) return Task.FromResult(0);
+ var userRole = user.Roles.SingleOrDefault(r => r.RoleId == roleName);
- var asInt = user.Id.TryConvertTo();
- if (asInt == false)
- {
- throw new InvalidOperationException("The user id must be an integer to work with the Umbraco");
- }
-
- var found = _userService.GetUserById(asInt.Result);
-
- if (found != null)
+ if (userRole != null)
{
- found.RemoveAllowedSection(roleName);
+ user.Roles.Remove(userRole);
}
return Task.FromResult(0);
}
///
- /// Returns the roles for this user
+ /// Returns the roles (user groups) for this user
///
///
///
@@ -453,7 +448,7 @@ public Task> GetRolesAsync(BackOfficeIdentityUser user)
{
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException("user");
- return Task.FromResult((IList)user.AllowedSections.ToList());
+ return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList());
}
///
@@ -465,7 +460,7 @@ public Task IsInRoleAsync(BackOfficeIdentityUser user, string roleName)
{
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException("user");
- return Task.FromResult(user.AllowedSections.InvariantContains(roleName));
+ return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(roleName));
}
///
@@ -553,6 +548,9 @@ public Task GetLockoutEndDateAsync(BackOfficeIdentityUser user)
///
///
///
+ ///
+ /// Currently we do not suport a timed lock out, when they are locked out, an admin will have to reset the status
+ ///
public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset lockoutEnd)
{
if (user == null) throw new ArgumentNullException("user");
@@ -620,28 +618,40 @@ public Task SetLockoutEnabledAsync(BackOfficeIdentityUser user, bool enabled)
}
#endregion
- private bool UpdateMemberProperties(Models.Membership.IUser user, BackOfficeIdentityUser identityUser)
+ private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityUser)
{
var anythingChanged = false;
- //don't assign anything if nothing has changed as this will trigger
- //the track changes of the model
- if ((user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false)
+
+ //don't assign anything if nothing has changed as this will trigger the track changes of the model
+
+ if (identityUser.IsPropertyDirty("LastLoginDateUtc")
+ || (user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false)
|| identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value)
{
anythingChanged = true;
user.LastLoginDate = identityUser.LastLoginDateUtc.Value.ToLocalTime();
}
- if (user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false)
+ if (identityUser.IsPropertyDirty("EmailConfirmed")
+ || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false)
+ || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed))
+ {
+ anythingChanged = true;
+ user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null;
+ }
+ if (identityUser.IsPropertyDirty("Name")
+ && user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
user.Name = identityUser.Name;
}
- if (user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false)
+ if (identityUser.IsPropertyDirty("Email")
+ && user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
user.Email = identityUser.Email;
}
- if (user.FailedPasswordAttempts != identityUser.AccessFailedCount)
+ if (identityUser.IsPropertyDirty("AccessFailedCount")
+ && user.FailedPasswordAttempts != identityUser.AccessFailedCount)
{
anythingChanged = true;
user.FailedPasswordAttempts = identityUser.AccessFailedCount;
@@ -658,51 +668,81 @@ private bool UpdateMemberProperties(Models.Membership.IUser user, BackOfficeIden
}
}
- if (user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false)
+ if (identityUser.IsPropertyDirty("UserName")
+ && user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
user.Username = identityUser.UserName;
}
- if (user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false)
+ if (identityUser.IsPropertyDirty("PasswordHash")
+ && user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
user.RawPasswordValue = identityUser.PasswordHash;
}
- if (user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false)
+ if (identityUser.IsPropertyDirty("Culture")
+ && user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
user.Language = identityUser.Culture;
}
- if (user.StartMediaId != identityUser.StartMediaId)
+ if (identityUser.IsPropertyDirty("StartMediaIds")
+ && user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false)
{
anythingChanged = true;
- user.StartMediaId = identityUser.StartMediaId;
+ user.StartMediaIds = identityUser.StartMediaIds;
}
- if (user.StartContentId != identityUser.StartContentId)
+ if (identityUser.IsPropertyDirty("StartContentIds")
+ && user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false)
{
anythingChanged = true;
- user.StartContentId = identityUser.StartContentId;
+ user.StartContentIds = identityUser.StartContentIds;
}
if (user.SecurityStamp != identityUser.SecurityStamp)
{
anythingChanged = true;
user.SecurityStamp = identityUser.SecurityStamp;
}
-
- if (user.AllowedSections.ContainsAll(identityUser.AllowedSections) == false
- || identityUser.AllowedSections.ContainsAll(user.AllowedSections) == false)
+
+ //TODO: Fix this for Groups too
+ if (identityUser.IsPropertyDirty("Roles") || identityUser.IsPropertyDirty("Groups"))
{
- anythingChanged = true;
- foreach (var allowedSection in user.AllowedSections)
- {
- user.RemoveAllowedSection(allowedSection);
- }
- foreach (var allowedApplication in identityUser.AllowedSections)
+ var userGroupAliases = user.Groups.Select(x => x.Alias).ToArray();
+
+ var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray();
+ var identityUserGroups = identityUser.Groups.Select(x => x.Alias).ToArray();
+
+ var combinedAliases = identityUserRoles.Union(identityUserGroups).ToArray();
+
+ if (userGroupAliases.ContainsAll(combinedAliases) == false
+ || combinedAliases.ContainsAll(userGroupAliases) == false)
{
- user.AddAllowedSection(allowedApplication);
+ anythingChanged = true;
+
+ //clear out the current groups (need to ToArray since we are modifying the iterator)
+ user.ClearGroups();
+
+ //go lookup all these groups
+ var groups = _userService.GetUserGroupsByAlias(combinedAliases).Select(x => x.ToReadOnlyGroup()).ToArray();
+
+ //use all of the ones assigned and add them
+ foreach (var group in groups)
+ {
+ user.AddGroup(group);
+ }
+
+ //re-assign
+ identityUser.Groups = groups;
}
}
+
+ //we should re-set the calculated start nodes
+ identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService);
+ identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService);
+
+ //reset all changes
+ identityUser.ResetDirtyProperties(false);
return anythingChanged;
}
@@ -713,7 +753,6 @@ private void ThrowIfDisposed()
if (_disposed)
throw new ObjectDisposedException(GetType().Name);
}
-
-
+
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Security/BackOfficeUserValidator.cs b/src/Umbraco.Core/Security/BackOfficeUserValidator.cs
new file mode 100644
index 000000000000..58319e95a7b5
--- /dev/null
+++ b/src/Umbraco.Core/Security/BackOfficeUserValidator.cs
@@ -0,0 +1,29 @@
+using System.Threading.Tasks;
+using Microsoft.AspNet.Identity;
+using Umbraco.Core.Models.EntityBase;
+using Umbraco.Core.Models.Identity;
+
+namespace Umbraco.Core.Security
+{
+ ///
+ /// Custom validator to not validate a user's username or email if they haven't changed
+ ///
+ ///
+ internal class BackOfficeUserValidator : UserValidator
+ where T : BackOfficeIdentityUser
+ {
+ public BackOfficeUserValidator(UserManager manager) : base(manager)
+ {
+ }
+
+ public override async Task ValidateAsync(T item)
+ {
+ //Don't validate if the user's email or username hasn't changed otherwise it's just wasting SQL queries.
+ if (item.IsPropertyDirty("Email") || item.IsPropertyDirty("UserName"))
+ {
+ return await base.ValidateAsync(item);
+ }
+ return IdentityResult.Success;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs
index 807d528f3d6d..51d1b8220740 100644
--- a/src/Umbraco.Core/Security/EmailService.cs
+++ b/src/Umbraco.Core/Security/EmailService.cs
@@ -1,25 +1,61 @@
-using System.Net.Mail;
+using System;
+using System.ComponentModel;
+using System.Net.Mail;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
+using Umbraco.Core.Configuration;
namespace Umbraco.Core.Security
{
+ ///
+ /// The implementation for Umbraco
+ ///
public class EmailService : IIdentityMessageService
{
+ private readonly string _notificationEmailAddress;
+ private readonly IEmailSender _defaultEmailSender;
+
+ public EmailService(string notificationEmailAddress, IEmailSender defaultEmailSender)
+ {
+ _notificationEmailAddress = notificationEmailAddress;
+ _defaultEmailSender = defaultEmailSender;
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("Use the constructor specifying all dependencies")]
+ public EmailService()
+ : this(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, new EmailSender())
+ {
+ }
+
public async Task SendAsync(IdentityMessage message)
{
- using (var client = new SmtpClient())
- using (var mailMessage = new MailMessage())
+ var mailMessage = new MailMessage(
+ _notificationEmailAddress,
+ message.Destination,
+ message.Subject,
+ message.Body)
{
- mailMessage.Body = message.Body;
- mailMessage.To.Add(message.Destination);
- mailMessage.Subject = message.Subject;
-
- //TODO: This check could be nicer but that is the way it is currently
- mailMessage.IsBodyHtml = message.Body.IsNullOrWhiteSpace() == false
- && message.Body.Contains("<") && message.Body.Contains("");
+ IsBodyHtml = message.Body.IsNullOrWhiteSpace() == false
+ && message.Body.Contains("<") && message.Body.Contains("")
+ };
- await client.SendMailAsync(mailMessage);
+ try
+ {
+ //check if it's a custom message and if so use it's own defined mail sender
+ var umbMsg = message as UmbracoEmailMessage;
+ if (umbMsg != null)
+ {
+ await umbMsg.MailSender.SendAsync(mailMessage);
+ }
+ else
+ {
+ await _defaultEmailSender.SendAsync(mailMessage);
+ }
+ }
+ finally
+ {
+ mailMessage.Dispose();
}
}
}
diff --git a/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs
index 6ee65e0fefbe..1852be4c4c01 100644
--- a/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs
+++ b/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs
@@ -9,6 +9,17 @@ namespace Umbraco.Core.Security
///
public interface IBackOfficeUserPasswordChecker
{
+ ///
+ /// Checks a password for a user
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// This will allow a developer to auto-link a local account which is required if the user queried doesn't exist locally.
+ /// The user parameter will always contain the username, if the user doesn't exist locally, the other properties will not be filled in.
+ /// A developer can then create a local account by filling in the properties and using UserManager.CreateAsync
+ ///
Task CheckPasswordAsync(BackOfficeIdentityUser user, string password);
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Security/IMembershipProviderPasswordHasher.cs b/src/Umbraco.Core/Security/IMembershipProviderPasswordHasher.cs
new file mode 100644
index 000000000000..42715d280aa0
--- /dev/null
+++ b/src/Umbraco.Core/Security/IMembershipProviderPasswordHasher.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNet.Identity;
+
+namespace Umbraco.Core.Security
+{
+ ///
+ /// A password hasher that is based on the rules configured for a membership provider
+ ///
+ public interface IMembershipProviderPasswordHasher : IPasswordHasher
+ {
+ MembershipProviderBase MembershipProvider { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Security/IUserAwarePasswordHasher.cs b/src/Umbraco.Core/Security/IUserAwarePasswordHasher.cs
new file mode 100644
index 000000000000..cea4d5a144df
--- /dev/null
+++ b/src/Umbraco.Core/Security/IUserAwarePasswordHasher.cs
@@ -0,0 +1,18 @@
+using System;
+using Microsoft.AspNet.Identity;
+
+namespace Umbraco.Core.Security
+{
+ ///
+ /// A password hasher that is User aware so that it can process the hashing based on the user's settings
+ ///
+ ///
+ ///
+ public interface IUserAwarePasswordHasher
+ where TUser : class, IUser
+ where TKey : IEquatable
+ {
+ string HashPassword(TUser user, string password);
+ PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword);
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Security/MachineKeyGenerator.cs b/src/Umbraco.Core/Security/MachineKeyGenerator.cs
new file mode 100644
index 000000000000..9dd06f44bdc1
--- /dev/null
+++ b/src/Umbraco.Core/Security/MachineKeyGenerator.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Umbraco.Core.Security
+{
+ ///
+ /// Used to generate a machine key
+ ///
+ internal class MachineKeyGenerator
+ {
+ ///
+ /// Generates the string to be stored in the web.config
+ ///
+ ///
+ ///
+ /// Machine key details are here: https://msdn.microsoft.com/en-us/library/vstudio/w8h3skw9%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396
+ ///
+ public string GenerateConfigurationBlock()
+ {
+ var c = @"";
+
+ var Xxx = 3;
+
+ return string.Format(c, GenerateAESDecryptionKey(), GenerateHMACSHA256ValidationKey());
+ }
+
+ public string GenerateHMACSHA256ValidationKey()
+ {
+ //See: https://msdn.microsoft.com/en-us/library/vstudio/w8h3skw9%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396
+ //See: https://msdn.microsoft.com/en-us/library/ff649308.aspx?f=255&MSPPError=-2147217396
+ /*
+ key value Specifies a manually assigned key.
+ The validationKey value must be manually set to a string of hexadecimal
+ characters to ensure consistent configuration across all servers in a Web farm.
+ The length of the key depends on the hash algorithm that is used:
+
+ AES requires a 256-bit key (64 hexadecimal characters).
+ MD5 requires a 128-bit key (32 hexadecimal characters).
+ SHA1 requires a 160-bit key (40 hexadecimal characters).
+ 3DES requires a 192-bit key (48 hexadecimal characters).
+ HMACSHA256 requires a 256-bit key (64 hexadecimal characters) == DEFAULT
+ HMACSHA384 requires a 384-bit key (96 hexadecimal characters).
+ HMACSHA512 requires a 512-bit key (128 hexadecimal characters).
+ */
+
+ //64 in length = 256 bits
+ return GenerateKey(64);
+ }
+
+ public string GenerateAESDecryptionKey()
+ {
+ //See: //See: https://msdn.microsoft.com/en-us/library/vstudio/w8h3skw9%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396
+ /*
+ key value Specifies a manually assigned key.
+ The decryptionKey value must be manually set to a string of
+ hexadecimal characters to ensure consistent configuration across all servers in a Web farm.
+ The key should be 64 bits (16 hexadecimal characters) long for DES encryption, or 192 bits
+ (48 hexadecimal characters) long for 3DES. For AES, the key can be 128 bits (32 characters),
+ 192 bits (48 characters), or 256 bits (64 characters) long.
+ */
+
+ //64 in length = 256 bits
+ return GenerateKey(64);
+ }
+
+ private string GenerateKey(int len = 64)
+ {
+ var buff = new byte[len / 2];
+ var rng = new RNGCryptoServiceProvider();
+ rng.GetBytes(buff);
+ var sb = new StringBuilder(len);
+
+ for (int i = 0; i < buff.Length; i++)
+ sb.Append(string.Format("{0:X2}", buff[i]));
+
+ return sb.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Security/MembershipPasswordHasher.cs b/src/Umbraco.Core/Security/MembershipPasswordHasher.cs
deleted file mode 100644
index 56daa3efdd19..000000000000
--- a/src/Umbraco.Core/Security/MembershipPasswordHasher.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using Microsoft.AspNet.Identity;
-
-namespace Umbraco.Core.Security
-{
- ///
- /// A custom password hasher that conforms to the current password hashing done in Umbraco
- ///
- internal class MembershipPasswordHasher : IPasswordHasher
- {
- private readonly MembershipProviderBase _provider;
-
- public MembershipPasswordHasher(MembershipProviderBase provider)
- {
- _provider = provider;
- }
-
- public string HashPassword(string password)
- {
- return _provider.HashPasswordForStorage(password);
- }
-
- public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
- {
- return _provider.VerifyPassword(providedPassword, hashedPassword)
- ? PasswordVerificationResult.Success
- : PasswordVerificationResult.Failed;
- }
-
-
- }
-}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs
index 4d4dad3fe884..ea7296fed295 100644
--- a/src/Umbraco.Core/Security/MembershipProviderBase.cs
+++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Specialized;
+using System.ComponentModel.DataAnnotations;
using System.Configuration.Provider;
using System.Security.Cryptography;
using System.Text;
@@ -8,6 +9,7 @@
using System.Web.Configuration;
using System.Web.Hosting;
using System.Web.Security;
+using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
@@ -28,23 +30,24 @@ public string HashPasswordForStorage(string password)
public bool VerifyPassword(string password, string hashedPassword)
{
+ if (string.IsNullOrWhiteSpace(hashedPassword)) throw new ArgumentException("Value cannot be null or whitespace.", "hashedPassword");
return CheckPassword(password, hashedPassword);
}
///
- /// Providers can override this setting, default is 7
+ /// Providers can override this setting, default is 10
///
public virtual int DefaultMinPasswordLength
{
- get { return 7; }
+ get { return 10; }
}
///
- /// Providers can override this setting, default is 1
+ /// Providers can override this setting, default is 0
///
public virtual int DefaultMinNonAlphanumericChars
{
- get { return 1; }
+ get { return 0; }
}
///
@@ -64,6 +67,19 @@ public virtual bool AllowManuallyChangingPassword
{
get { return false; }
}
+
+ ///
+ /// Returns the raw password value for a given user
+ ///
+ ///
+ ///
+ ///
+ /// By default this will return an invalid attempt, inheritors will need to override this to support it
+ ///
+ protected virtual Attempt GetRawPassword(string username)
+ {
+ return Attempt.Fail();
+ }
private string _applicationName;
private bool _enablePasswordReset;
@@ -224,7 +240,7 @@ public override void Initialize(string name, NameValueCollection config)
base.Initialize(name, config);
_enablePasswordRetrieval = config.GetValue("enablePasswordRetrieval", false);
- _enablePasswordReset = config.GetValue("enablePasswordReset", false);
+ _enablePasswordReset = config.GetValue("enablePasswordReset", true);
_requiresQuestionAndAnswer = config.GetValue("requiresQuestionAndAnswer", false);
_requiresUniqueEmail = config.GetValue("requiresUniqueEmail", true);
_maxInvalidPasswordAttempts = GetIntValue(config, "maxInvalidPasswordAttempts", 5, false, 0);
@@ -299,7 +315,7 @@ protected internal enum PasswordValidityError
/// Processes a request to update the password for a membership user.
///
/// The user to update the password for.
- /// This property is ignore for this provider
+ /// Required to change a user password if the user is not new and AllowManuallyChangingPassword is false
/// The new password for the specified user.
///
/// true if the password was updated successfully; otherwise, false.
@@ -309,10 +325,17 @@ protected internal enum PasswordValidityError
///
public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
+ string rawPasswordValue = string.Empty;
if (oldPassword.IsNullOrWhiteSpace() && AllowManuallyChangingPassword == false)
- {
- //If the old password is empty and AllowManuallyChangingPassword is false, than this provider cannot just arbitrarily change the password
- throw new NotSupportedException("This provider does not support manually changing the password");
+ {
+ //we need to lookup the member since this could be a brand new member without a password set
+ var rawPassword = GetRawPassword(username);
+ rawPasswordValue = rawPassword.Success ? rawPassword.Result : string.Empty;
+ if (rawPassword.Success == false || rawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix) == false)
+ {
+ //If the old password is empty and AllowManuallyChangingPassword is false, than this provider cannot just arbitrarily change the password
+ throw new NotSupportedException("This provider does not support manually changing the password");
+ }
}
var args = new ValidatePasswordEventArgs(username, newPassword, false);
@@ -325,10 +348,14 @@ public override bool ChangePassword(string username, string oldPassword, string
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")
+ //Special cases to allow changing password without validating existing credentials
+ // * the member is new and doesn't have a password set
+ // * during installation to set the admin password
+ if (AllowManuallyChangingPassword == false
+ && (rawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix)
+ || (ApplicationContext.Current != null
+ && ApplicationContext.Current.IsConfigured == false
+ && oldPassword == "default")))
{
return PerformChangePassword(username, oldPassword, newPassword);
}
@@ -652,11 +679,7 @@ protected internal string FormatPasswordForStorage(string pass, string salt)
internal static bool IsEmailValid(string email)
{
- const string pattern = @"^(?!\.)(""([^""\r\\]|\\[""\r\\])*""|"
- + @"([-a-z0-9!#$%&'*+/=?^_`{|}~]|(?
protected internal bool CheckPassword(string password, string dbPassword)
{
+ if (string.IsNullOrWhiteSpace(dbPassword)) throw new ArgumentException("Value cannot be null or whitespace.", "dbPassword");
switch (PasswordFormat)
{
case MembershipPasswordFormat.Encrypted:
@@ -794,6 +823,7 @@ protected internal string DecryptPassword(string pass)
///
internal string StoredPassword(string storedString, out string salt)
{
+ if (string.IsNullOrWhiteSpace(storedString)) throw new ArgumentException("Value cannot be null or whitespace.", "storedString");
if (UseLegacyEncoding)
{
salt = string.Empty;
diff --git a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs
index 645de22ab84d..c423c1a94ac9 100644
--- a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs
+++ b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs
@@ -1,16 +1,11 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Security.Principal;
-using System.Text;
using System.Threading;
-using System.Threading.Tasks;
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
@@ -23,6 +18,9 @@ public static class MembershipProviderExtensions
///
///
///
+ ///
+ /// An Admin can always reset the password
+ ///
internal static bool CanResetPassword(this MembershipProvider provider, IUserService userService)
{
if (provider == null) throw new ArgumentNullException("provider");
@@ -31,13 +29,14 @@ internal static bool CanResetPassword(this MembershipProvider provider, IUserSer
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
+ //we need to check for the special case in which a user is an admin - in which case 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 user = userService.GetUserById(identity.Id.TryConvertTo().Result);
+ if (user == null) throw new InvalidOperationException("No user with username " + identity.Username + " found");
var userIsAdmin = user.IsAdmin();
if (userIsAdmin)
{
@@ -70,7 +69,7 @@ internal static MembershipUser CreateUser(this MembershipProvider provider, stri
}
///
- /// Method to get the Umbraco Members membership provider based on it's alias
+ /// Method to get the Umbraco Members membership provider based on its alias
///
///
public static MembershipProvider GetMembersMembershipProvider()
@@ -83,7 +82,7 @@ public static MembershipProvider GetMembersMembershipProvider()
}
///
- /// Method to get the Umbraco Users membership provider based on it's alias
+ /// Method to get the Umbraco Users membership provider based on its alias
///
///
public static MembershipProvider GetUsersMembershipProvider()
@@ -167,6 +166,5 @@ public static UmbracoMembershipProviderBase AsUmbracoMembershipProvider(this Mem
{
return (UmbracoMembershipProviderBase)membershipProvider;
}
-
}
}
diff --git a/src/Umbraco.Core/Security/MembershipProviderPasswordHasher.cs b/src/Umbraco.Core/Security/MembershipProviderPasswordHasher.cs
new file mode 100644
index 000000000000..f518f99c5590
--- /dev/null
+++ b/src/Umbraco.Core/Security/MembershipProviderPasswordHasher.cs
@@ -0,0 +1,34 @@
+using Microsoft.AspNet.Identity;
+
+namespace Umbraco.Core.Security
+{
+ ///
+ /// A password hasher that conforms to the password hashing done with membership providers
+ ///
+ public class MembershipProviderPasswordHasher : IMembershipProviderPasswordHasher
+ {
+ ///
+ /// Exposes the underlying MembershipProvider
+ ///
+ public MembershipProviderBase MembershipProvider { get; private set; }
+
+ public MembershipProviderPasswordHasher(MembershipProviderBase provider)
+ {
+ MembershipProvider = provider;
+ }
+
+ public string HashPassword(string password)
+ {
+ return MembershipProvider.HashPasswordForStorage(password);
+ }
+
+ public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
+ {
+ return MembershipProvider.VerifyPassword(providedPassword, hashedPassword)
+ ? PasswordVerificationResult.Success
+ : PasswordVerificationResult.Failed;
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Security/MembershipProviderPasswordValidator.cs b/src/Umbraco.Core/Security/MembershipProviderPasswordValidator.cs
new file mode 100644
index 000000000000..3331116b4eb3
--- /dev/null
+++ b/src/Umbraco.Core/Security/MembershipProviderPasswordValidator.cs
@@ -0,0 +1,38 @@
+using System.Threading.Tasks;
+using System.Web.Security;
+using Microsoft.AspNet.Identity;
+
+namespace Umbraco.Core.Security
+{
+ ///
+ /// Ensure that both the normal password validator rules are processed along with the underlying memberhsip provider rules
+ ///
+ public class MembershipProviderPasswordValidator : PasswordValidator
+ {
+ public MembershipProvider Provider { get; private set; }
+
+ public MembershipProviderPasswordValidator(MembershipProvider provider)
+ {
+ Provider = provider;
+
+ RequiredLength = Provider.MinRequiredPasswordLength;
+ RequireNonLetterOrDigit = Provider.MinRequiredNonAlphanumericCharacters > 0;
+ RequireDigit = false;
+ RequireLowercase = false;
+ RequireUppercase = false;
+ }
+
+ public override async Task ValidateAsync(string item)
+ {
+ var result = await base.ValidateAsync(item);
+ if (result.Succeeded == false)
+ return result;
+ var providerValidate = MembershipProviderBase.IsPasswordValid(item, Provider.MinRequiredNonAlphanumericCharacters, Provider.PasswordStrengthRegularExpression, Provider.MinRequiredPasswordLength);
+ if (providerValidate.Success == false)
+ {
+ return IdentityResult.Failed("Could not set password, password rules violated: " + providerValidate.Result);
+ }
+ return IdentityResult.Success;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs
index 1bc9902da5cf..39931341fadf 100644
--- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs
+++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs
@@ -48,12 +48,20 @@ public static UmbracoBackOfficeIdentity FromClaimsIdentity(ClaimsIdentity identi
|| realName == null || session == null)
throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since there are missing required claims");
- int startContentIdAsInt;
- int startMediaIdAsInt;
- if (int.TryParse(startContentId, out startContentIdAsInt) == false || int.TryParse(startMediaId, out startMediaIdAsInt) == false)
+ int[] startContentIdsAsInt;
+ int[] startMediaIdsAsInt;
+ if (startContentId.DetectIsJson() == false || startMediaId.DetectIsJson() == false)
+ throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly - either content or media start Ids are not JSON");
+
+ try
{
- throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly");
+ startContentIdsAsInt = JsonConvert.DeserializeObject(startContentId);
+ startMediaIdsAsInt = JsonConvert.DeserializeObject(startMediaId);
}
+ catch (Exception e)
+ {
+ throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly - either content or media start Ids could not be parsed as JSON", e);
+ }
var roles = identity.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToList();
var allowedApps = identity.FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToList();
@@ -67,8 +75,8 @@ public static UmbracoBackOfficeIdentity FromClaimsIdentity(ClaimsIdentity identi
Roles = roles.ToArray(),
Username = username,
RealName = realName,
- StartContentNode = startContentIdAsInt,
- StartMediaNode = startMediaIdAsInt
+ StartContentNodes = startContentIdsAsInt,
+ StartMediaNodes = startMediaIdsAsInt
};
return new UmbracoBackOfficeIdentity(identity, userData);
@@ -202,10 +210,10 @@ private void AddUserDataClaims()
AddClaim(new Claim(ClaimTypes.GivenName, UserData.RealName, ClaimValueTypes.String, Issuer, Issuer, this));
if (HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false)
- AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, StartContentNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this));
+ AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, JsonConvert.SerializeObject(StartContentNodes), ClaimValueTypes.Integer32, Issuer, Issuer, this));
if (HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false)
- AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, StartMediaNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this));
+ AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, JsonConvert.SerializeObject(StartMediaNodes), ClaimValueTypes.Integer32, Issuer, Issuer, this));
if (HasClaim(x => x.Type == ClaimTypes.Locality) == false)
AddClaim(new Claim(ClaimTypes.Locality, Culture, ClaimValueTypes.String, Issuer, Issuer, this));
@@ -259,14 +267,14 @@ public override string AuthenticationType
get { return _currentIssuer; }
}
- public int StartContentNode
+ public int[] StartContentNodes
{
- get { return UserData.StartContentNode; }
+ get { return UserData.StartContentNodes; }
}
- public int StartMediaNode
+ public int[] StartMediaNodes
{
- get { return UserData.StartMediaNode; }
+ get { return UserData.StartMediaNodes; }
}
public string[] AllowedApplications
diff --git a/src/Umbraco.Core/Security/UmbracoEmailMessage.cs b/src/Umbraco.Core/Security/UmbracoEmailMessage.cs
new file mode 100644
index 000000000000..9ef6205ebf14
--- /dev/null
+++ b/src/Umbraco.Core/Security/UmbracoEmailMessage.cs
@@ -0,0 +1,17 @@
+using Microsoft.AspNet.Identity;
+
+namespace Umbraco.Core.Security
+{
+ ///
+ /// A custom implementation for IdentityMessage that allows the customization of how an email is sent
+ ///
+ internal class UmbracoEmailMessage : IdentityMessage
+ {
+ public IEmailSender MailSender { get; private set; }
+
+ public UmbracoEmailMessage(IEmailSender mailSender)
+ {
+ MailSender = mailSender;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Security/UserAwareMembershipProviderPasswordHasher.cs b/src/Umbraco.Core/Security/UserAwareMembershipProviderPasswordHasher.cs
new file mode 100644
index 000000000000..7d19d72c3c8a
--- /dev/null
+++ b/src/Umbraco.Core/Security/UserAwareMembershipProviderPasswordHasher.cs
@@ -0,0 +1,30 @@
+using System;
+using Microsoft.AspNet.Identity;
+using Umbraco.Core.Models.Identity;
+
+namespace Umbraco.Core.Security
+{
+ ///
+ /// The default password hasher that is User aware so that it can process the hashing based on the user's settings
+ ///
+ public class UserAwareMembershipProviderPasswordHasher : MembershipProviderPasswordHasher, IUserAwarePasswordHasher
+ {
+ public UserAwareMembershipProviderPasswordHasher(MembershipProviderBase provider) : base(provider)
+ {
+ }
+
+ public string HashPassword(BackOfficeIdentityUser user, string password)
+ {
+ //TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
+ //NOTE: For now this just falls back to the hashing we are currently using
+ return base.HashPassword(password);
+ }
+
+ public PasswordVerificationResult VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword)
+ {
+ //TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
+ //NOTE: For now this just falls back to the hashing we are currently using
+ return base.VerifyHashedPassword(hashedPassword, providedPassword);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Security/UserData.cs b/src/Umbraco.Core/Security/UserData.cs
index 407d2782dd39..8761ada84750 100644
--- a/src/Umbraco.Core/Security/UserData.cs
+++ b/src/Umbraco.Core/Security/UserData.cs
@@ -46,12 +46,18 @@ public UserData(string sessionId)
[DataMember(Name = "name")]
public string RealName { get; set; }
-
+
+ ///
+ /// The start nodes on the UserData object for the auth ticket contains all of the user's start nodes including ones assigned to their user groups
+ ///
[DataMember(Name = "startContent")]
- public int StartContentNode { get; set; }
-
+ public int[] StartContentNodes { get; set; }
+
+ ///
+ /// The start nodes on the UserData object for the auth ticket contains all of the user's start nodes including ones assigned to their user groups
+ ///
[DataMember(Name = "startMedia")]
- public int StartMediaNode { get; set; }
+ public int[] StartMediaNodes { get; set; }
[DataMember(Name = "allowedApps")]
public string[] AllowedApplications { get; set; }
diff --git a/src/Umbraco.Core/Serialization/KnownTypeUdiJsonConverter.cs b/src/Umbraco.Core/Serialization/KnownTypeUdiJsonConverter.cs
new file mode 100644
index 000000000000..e6473e7f8e24
--- /dev/null
+++ b/src/Umbraco.Core/Serialization/KnownTypeUdiJsonConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Umbraco.Core.Serialization
+{
+ public class KnownTypeUdiJsonConverter : 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, true);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Serialization/UdiJsonConverter.cs b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs
index ff6253582574..f3dc678ce621 100644
--- a/src/Umbraco.Core/Serialization/UdiJsonConverter.cs
+++ b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs
@@ -4,12 +4,11 @@
namespace Umbraco.Core.Serialization
{
-
public class UdiJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
- return typeof(Udi).IsAssignableFrom(objectType);
+ return typeof (Udi).IsAssignableFrom(objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index 3cf8dfcff4bc..1c9d1b48a658 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -13,13 +13,11 @@
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Models.Rdbms;
using Umbraco.Core.Persistence;
-
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Publishing;
-using Umbraco.Core.Scoping;
namespace Umbraco.Core.Services
{
@@ -32,7 +30,6 @@ public class ContentService : ScopeRepositoryService, IContentService, IContentS
private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer();
private readonly IDataTypeService _dataTypeService;
private readonly IUserService _userService;
- private readonly IdkMap _idkMap;
//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.
@@ -44,8 +41,7 @@ public ContentService(
ILogger logger,
IEventMessagesFactory eventMessagesFactory,
IDataTypeService dataTypeService,
- IUserService userService,
- IdkMap idkMap)
+ IUserService userService)
: base(provider, repositoryFactory, logger, eventMessagesFactory)
{
if (dataTypeService == null) throw new ArgumentNullException("dataTypeService");
@@ -53,7 +49,6 @@ public ContentService(
_publishingStrategy = new PublishingStrategy(UowProvider.ScopeProvider, eventMessagesFactory, logger);
_dataTypeService = dataTypeService;
_userService = userService;
- _idkMap = idkMap;
}
#region Static Queries
@@ -114,27 +109,27 @@ public void ReplaceContentPermissions(EntityPermissionSet permissionSet)
}
///
- /// Assigns a single permission to the current content item for the specified user ids
+ /// Assigns a single permission to the current content item for the specified group ids
///
///
///
- ///
- public void AssignContentPermission(IContent entity, char permission, IEnumerable userIds)
+ ///
+ public void AssignContentPermission(IContent entity, char permission, IEnumerable groupIds)
{
using (var uow = UowProvider.GetUnitOfWork())
{
var repository = RepositoryFactory.CreateContentRepository(uow);
- repository.AssignEntityPermission(entity, permission, userIds);
+ repository.AssignEntityPermission(entity, permission, groupIds);
uow.Commit();
}
}
///
- /// Gets the list of permissions for the content item
+ /// Returns implicit/inherited permissions assigned to the content item for all user groups
///
///
///
- public IEnumerable GetPermissionsForEntity(IContent content)
+ public EntityPermissionCollection GetPermissionsForEntity(IContent content)
{
using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
{
@@ -143,6 +138,26 @@ public IEnumerable GetPermissionsForEntity(IContent content)
}
}
+ ///
+ /// Creates an object using the alias of the
+ /// that this Content should based on.
+ ///
+ ///
+ /// Note that using this method will simply return a new IContent without any identity
+ /// as it has not yet been persisted. It is intended as a shortcut to creating new content objects
+ /// that does not invoke a save operation against the database.
+ ///
+ /// Name of the Content object
+ /// Id of Parent for the new Content
+ /// Alias of the
+ /// Optional id of the user creating the content
+ ///
+ public IContent CreateContent(string name, Guid parentId, string contentTypeAlias, int userId = 0)
+ {
+ var parent = GetById(parentId);
+ return CreateContent(name, parent, contentTypeAlias, userId);
+ }
+
///
/// Creates an object using the alias of the
/// that this Content should based on.
@@ -166,7 +181,8 @@ public IContent CreateContent(string name, int parentId, string contentTypeAlias
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(content, contentTypeAlias, parentId)))
+ var newEventArgs = new NewEventArgs(content, contentTypeAlias, parentId);
+ if (uow.Events.DispatchCancelable(Creating, this, newEventArgs))
{
uow.Commit();
content.WasCancelled = true;
@@ -175,10 +191,9 @@ public IContent CreateContent(string name, int parentId, string contentTypeAlias
content.CreatorId = userId;
content.WriterId = userId;
+ newEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Created, this, newEventArgs);
- uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parentId));
-
- Audit(uow, AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id);
uow.Commit();
}
@@ -209,7 +224,8 @@ public IContent CreateContent(string name, IContent parent, string contentTypeAl
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(content, contentTypeAlias, parent)))
+ var newEventArgs = new NewEventArgs(content, contentTypeAlias, parent);
+ if (uow.Events.DispatchCancelable(Creating, this, newEventArgs))
{
uow.Commit();
content.WasCancelled = true;
@@ -218,10 +234,9 @@ public IContent CreateContent(string name, IContent parent, string contentTypeAl
content.CreatorId = userId;
content.WriterId = userId;
+ newEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Created, this, newEventArgs);
- 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();
}
@@ -250,14 +265,16 @@ public IContent CreateContentWithIdentity(string name, int parentId, string cont
{
//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)))
+ var newEventArgs = new NewEventArgs(content, contentTypeAlias, parentId);
+ if (uow.Events.DispatchCancelable(Creating, this, newEventArgs))
{
uow.Commit();
content.WasCancelled = true;
return content;
}
- if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content)))
+ var saveEventArgs = new SaveEventArgs(content);
+ if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
{
uow.Commit();
content.WasCancelled = true;
@@ -270,9 +287,10 @@ public IContent CreateContentWithIdentity(string name, int parentId, string cont
repository.AddOrUpdate(content);
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));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved");
+ newEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Created, this, newEventArgs);
Audit(uow, AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id);
uow.Commit();
@@ -305,14 +323,16 @@ public IContent CreateContentWithIdentity(string name, IContent parent, string c
{
//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)))
+ var newEventArgs = new NewEventArgs(content, contentTypeAlias, parent);
+ if (uow.Events.DispatchCancelable(Creating, this, newEventArgs))
{
uow.Commit();
content.WasCancelled = true;
return content;
}
- if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content)))
+ var saveEventArgs = new SaveEventArgs(content);
+ if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
{
uow.Commit();
content.WasCancelled = true;
@@ -325,9 +345,10 @@ public IContent CreateContentWithIdentity(string name, IContent parent, string c
repository.AddOrUpdate(content);
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));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved");
+ newEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Created, this, newEventArgs);
Audit(uow, AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id);
uow.Commit();
@@ -351,7 +372,7 @@ public IContent GetById(int id)
}
///
- /// Gets an object by Id
+ /// Gets objects by Ids
///
/// Ids of the Content to retrieve
///
@@ -378,6 +399,34 @@ public IEnumerable GetByIds(IEnumerable ids)
}
}
+ ///
+ /// Gets objects by Ids
+ ///
+ /// Ids of the Content to retrieve
+ ///
+ public IEnumerable GetByIds(IEnumerable ids)
+ {
+ var idsArray = ids.ToArray();
+ if (idsArray.Length == 0) return Enumerable.Empty();
+
+ using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
+ {
+ 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.Key, x => x);
+
+ var sortedResult = idsArray.Select(x =>
+ {
+ IContent c;
+ return content.TryGetValue(x, out c) ? c : null;
+ }).WhereNotNull();
+
+ return sortedResult;
+ }
+ }
+
///
/// Gets an object by its 'UniqueId'
///
@@ -385,13 +434,11 @@ public IEnumerable GetByIds(IEnumerable ids)
///
public IContent GetById(Guid key)
{
- // the repository implements a cache policy on int identifiers, not guids,
- // and we are not changing it now, but we still would like to rely on caching
- // instead of running a full query against the database, so relying on the
- // id-key map, which is fast.
-
- var a = _idkMap.GetIdForKey(key, UmbracoObjectTypes.Document);
- return a.Success ? GetById(a.Result) : null;
+ using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
+ {
+ var repository = RepositoryFactory.CreateContentRepository(uow);
+ return repository.Get(key);
+ }
}
///
@@ -637,7 +684,17 @@ public IEnumerable GetPagedDescendants(int id, long pageIndex, int pag
// get query - if the id is System Root, then just get all
var query = Query.Builder;
if (id != Constants.System.Root)
- query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar));
+ {
+ var entityRepository = RepositoryFactory.CreateEntityRepository(uow);
+ var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray();
+ if (contentPath.Length == 0)
+ {
+ totalChildren = 0;
+ return Enumerable.Empty();
+ }
+ query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0]), TextColumnType.NVarchar));
+ }
+
// get filter
IQuery filterQuery = null;
@@ -672,7 +729,16 @@ public IEnumerable GetPagedDescendants(int id, long pageIndex, int pag
// get query - if the id is System Root, then just get all
var query = Query.Builder;
if (id != Constants.System.Root)
- query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar));
+ {
+ var entityRepository = RepositoryFactory.CreateEntityRepository(uow);
+ var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray();
+ if (contentPath.Length == 0)
+ {
+ totalChildren = 0;
+ return Enumerable.Empty();
+ }
+ query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0]), TextColumnType.NVarchar));
+ }
return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter);
}
@@ -895,15 +961,43 @@ public bool HasPublishedVersion(int id)
/// True if the Content can be published, otherwise False
public bool IsPublishable(IContent content)
{
- //If the passed in content has yet to be saved we "fallback" to checking the Parent
- //because if the Parent is publishable then the current content can be Saved and Published
- if (content.HasIdentity == false)
+ int[] ids;
+ if (content.HasIdentity)
+ {
+ // get ids from path (we have identity)
+ // skip the first one that has to be -1 - and we don't care
+ // skip the last one that has to be "this" - and it's ok to stop at the parent
+ ids = content.Path.Split(',').Skip(1).SkipLast().Select(int.Parse).ToArray();
+ }
+ else
{
+ // no path yet (no identity), have to move up to parent
+ // skip the first one that has to be -1 - and we don't care
+ // don't skip the last one that is "parent"
var parent = GetById(content.ParentId);
- return IsPublishable(parent, true);
+ if (parent == null) return false;
+ ids = parent.Path.Split(',').Skip(1).Select(int.Parse).ToArray();
}
+ if (ids.Length == 0)
+ return false;
+
+ // if the first one is recycle bin, fail fast
+ if (ids[0] == Constants.System.RecycleBinContent)
+ return false;
- return IsPublishable(content, false);
+ // fixme - move to repository?
+ using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
+ {
+ var sql = new Sql(@"
+ SELECT id
+ FROM umbracoNode
+ JOIN cmsDocument ON umbracoNode.id=cmsDocument.nodeId AND cmsDocument.published=@0
+ WHERE umbracoNode.trashed=@1 AND umbracoNode.id IN (@2)",
+ true, false, ids);
+ Console.WriteLine(sql.SQL);
+ var x = uow.Database.Fetch(sql);
+ return ids.Length == x.Count;
+ }
}
///
@@ -1018,14 +1112,16 @@ private Attempt MoveToRecycleBinDo(IContent content, int userId
//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"))
+ var moveEventInfo = new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent);
+ var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo);
+ if (uow.Events.DispatchCancelable(Trashing, this, moveEventArgs, "Trashing"))
{
uow.Commit();
return OperationStatus.Cancelled(evtMsgs);
}
var moveInfo = new List>
{
- new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)
+ moveEventInfo
};
//get descendents to process of the content item that is being moved to trash - must be done before changing the state below
@@ -1056,7 +1152,9 @@ private Attempt MoveToRecycleBinDo(IContent content, int userId
moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId));
}
- uow.Events.Dispatch(Trashed, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), "Trashed");
+ moveEventArgs.CanCancel = false;
+ moveEventArgs.MoveInfoCollection = moveInfo;
+ uow.Events.Dispatch(Trashed, this, moveEventArgs, "Trashed");
Audit(uow, AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id);
uow.Commit();
@@ -1158,6 +1256,95 @@ public Attempt SaveAndPublishWithStatus(IContent content, int use
return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents);
}
+ public IContent GetBlueprintById(int id)
+ {
+ using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
+ {
+ var repository = RepositoryFactory.CreateContentBlueprintRepository(uow);
+ var blueprint = repository.Get(id);
+ if (blueprint != null)
+ ((Content) blueprint).IsBlueprint = true;
+ return blueprint;
+ }
+ }
+
+ public IContent GetBlueprintById(Guid id)
+ {
+ using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
+ {
+ var repository = RepositoryFactory.CreateContentBlueprintRepository(uow);
+ var blueprint = repository.Get(id);
+ if (blueprint != null)
+ ((Content)blueprint).IsBlueprint = true;
+ return blueprint;
+ }
+ }
+
+ public void SaveBlueprint(IContent content, int userId = 0)
+ {
+ //always ensure the blueprint is at the root
+ if (content.ParentId != -1)
+ content.ParentId = -1;
+
+ ((Content) content).IsBlueprint = true;
+
+ using (new WriteLock(Locker))
+ {
+ using (var uow = UowProvider.GetUnitOfWork())
+ {
+ if (string.IsNullOrWhiteSpace(content.Name))
+ {
+ throw new ArgumentException("Cannot save content blueprint with empty name.");
+ }
+
+ var repository = RepositoryFactory.CreateContentBlueprintRepository(uow);
+
+ if (content.HasIdentity == false)
+ {
+ content.CreatorId = userId;
+ }
+ content.WriterId = userId;
+
+ repository.AddOrUpdate(content);
+
+ uow.Events.Dispatch(SavedBlueprint, this, new SaveEventArgs(content), "SavedBlueprint");
+
+ uow.Commit();
+ }
+ }
+ }
+
+ public void DeleteBlueprint(IContent content, int userId = 0)
+ {
+ using (new WriteLock(Locker))
+ {
+ using (var uow = UowProvider.GetUnitOfWork())
+ {
+ var repository = RepositoryFactory.CreateContentBlueprintRepository(uow);
+ repository.Delete(content);
+ uow.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(content), "DeletedBlueprint");
+ uow.Commit();
+ }
+ }
+ }
+
+ public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0)
+ {
+ if (blueprint == null) throw new ArgumentNullException("blueprint");
+
+ var contentType = blueprint.ContentType;
+ var content = new Content(name, -1, contentType);
+ content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id);
+
+ content.CreatorId = userId;
+ content.WriterId = userId;
+
+ foreach (var property in blueprint.Properties)
+ content.SetValue(property.Alias, property.Value);
+
+ return content;
+ }
+
///
/// Saves a single object
///
@@ -1183,7 +1370,8 @@ Attempt IContentServiceOperations.Save(IEnumerable co
using (var uow = UowProvider.GetUnitOfWork())
{
- if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(asArray, evtMsgs)))
+ var saveEventArgs = new SaveEventArgs(asArray, evtMsgs);
+ if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
{
uow.Commit();
return OperationStatus.Cancelled(evtMsgs);
@@ -1224,7 +1412,10 @@ Attempt IContentServiceOperations.Save(IEnumerable co
}
if (raiseEvents)
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(asArray, false, evtMsgs));
+ {
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved");
+ }
Audit(uow, AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root);
uow.Commit();
@@ -1250,7 +1441,8 @@ Attempt IContentServiceOperations.Delete(IContent content, int
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(content, evtMsgs), "Deleting"))
+ var deleteEventArgs = new DeleteEventArgs(content, evtMsgs);
+ if (uow.Events.DispatchCancelable(Deleting, this, deleteEventArgs, "Deleting"))
{
uow.Commit();
return OperationStatus.Cancelled(evtMsgs);
@@ -1273,8 +1465,8 @@ Attempt IContentServiceOperations.Delete(IContent content, int
repository.Delete(content);
- var args = new DeleteEventArgs(content, false, evtMsgs);
- uow.Events.Dispatch(Deleted, this, args, "Deleted");
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Deleted, this, deleteEventArgs, "Deleted");
Audit(uow, AuditType.Delete, "Delete Content performed by user", userId, content.Id);
uow.Commit();
@@ -1402,7 +1594,8 @@ public void DeleteVersions(int id, DateTime versionDate, int userId = 0)
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), "DeletingVersions"))
+ var deleteRevisionsEventArgs = new DeleteRevisionsEventArgs(id, dateToRetain: versionDate);
+ if (uow.Events.DispatchCancelable(DeletingVersions, this, deleteRevisionsEventArgs, "DeletingVersions"))
{
uow.Commit();
return;
@@ -1410,8 +1603,8 @@ public void DeleteVersions(int id, DateTime versionDate, int userId = 0)
var repository = RepositoryFactory.CreateContentRepository(uow);
repository.DeleteVersions(id, versionDate);
-
- uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), "DeletedVersions");
+ deleteRevisionsEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs, "DeletedVersions");
Audit(uow, AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root);
uow.Commit();
@@ -1490,7 +1683,9 @@ public void Move(IContent content, int parentId, int userId = 0)
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Moving, this, new MoveEventArgs(new MoveEventInfo(content, content.Path, parentId)), "Moving"))
+ var moveEventInfo = new MoveEventInfo(content, content.Path, parentId);
+ var moveEventArgs = new MoveEventArgs(moveEventInfo);
+ if (uow.Events.DispatchCancelable(Moving, this, moveEventArgs, "Moving"))
{
uow.Commit();
return;
@@ -1502,7 +1697,9 @@ public void Move(IContent content, int parentId, int userId = 0)
//call private method that does the recursive moving
PerformMove(content, parentId, userId, moveInfo);
- uow.Events.Dispatch(Moved, this, new MoveEventArgs(false, moveInfo.ToArray()), "Moved");
+ moveEventArgs.MoveInfoCollection = moveInfo;
+ moveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Moved, this, moveEventArgs, "Moved");
Audit(uow, AuditType.Move, "Move Content performed by user", userId, content.Id);
uow.Commit();
@@ -1517,7 +1714,7 @@ public void EmptyRecycleBin()
{
using (new WriteLock(Locker))
{
- var nodeObjectType = new Guid(Constants.ObjectTypes.Document);
+ var nodeObjectType = Constants.ObjectTypes.DocumentGuid;
using (var uow = UowProvider.GetUnitOfWork())
{
@@ -1531,15 +1728,17 @@ public void EmptyRecycleBin()
var files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField();
- if (uow.Events.DispatchCancelable(EmptyingRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, entities, files)))
+ var recycleBinEventArgs = new RecycleBinEventArgs(nodeObjectType, entities, files);
+ if (uow.Events.DispatchCancelable(EmptyingRecycleBin, this, recycleBinEventArgs))
{
uow.Commit();
return;
}
var success = repository.EmptyRecycleBin();
-
- uow.Events.Dispatch(EmptiedRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, entities, files, success));
+ recycleBinEventArgs.CanCancel = false;
+ recycleBinEventArgs.RecycleBinEmptiedSuccessfully = success;
+ uow.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs);
Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent);
uow.Commit();
@@ -1586,7 +1785,8 @@ public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Copying, this, new CopyEventArgs(content, copy, parentId)))
+ var copyEventArgs = new CopyEventArgs(content, copy, true, parentId, relateToOriginal);
+ if (uow.Events.DispatchCancelable(Copying, this, copyEventArgs))
{
uow.Commit();
return null;
@@ -1598,8 +1798,20 @@ public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool
copy.CreatorId = userId;
copy.WriterId = userId;
+ //get the current permissions, if there are any explicit ones they need to be copied
+ var currentPermissions = GetPermissionsForEntity(content);
+ currentPermissions.RemoveWhere(p => p.IsDefaultPermissions);
+
repository.AddOrUpdate(copy);
repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c));
+
+ //add permissions
+ if (currentPermissions.Count > 0)
+ {
+ var permissionSet = new ContentPermissionSet(copy, currentPermissions);
+ repository.AddOrUpdatePermissions(permissionSet);
+ }
+
uow.Commit(); // todo - this should flush, not commit
//Special case for the associated tags
@@ -1627,7 +1839,8 @@ public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool
Copy(child, copy.Id, relateToOriginal, true, userId);
}
}
- uow.Events.Dispatch(Copied, this, new CopyEventArgs(content, copy, false, parentId, relateToOriginal));
+ copyEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Copied, this, copyEventArgs);
Audit(uow, AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id);
uow.Commit();
}
@@ -1647,7 +1860,8 @@ public bool SendToPublication(IContent content, int userId = 0)
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SendingToPublish, this, new SendToPublishEventArgs(content)))
+ var sendToPublishEventArgs = new SendToPublishEventArgs(content);
+ if (uow.Events.DispatchCancelable(SendingToPublish, this, sendToPublishEventArgs))
{
uow.Commit();
return false;
@@ -1655,8 +1869,8 @@ public bool SendToPublication(IContent content, int userId = 0)
//Save before raising event
Save(content, userId);
-
- uow.Events.Dispatch(SentToPublish, this, new SendToPublishEventArgs(content, false));
+ sendToPublishEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs);
Audit(uow, AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id);
uow.Commit();
@@ -1683,7 +1897,8 @@ public IContent Rollback(int id, Guid versionId, int userId = 0)
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(RollingBack, this, new RollbackEventArgs(content)))
+ var rollbackEventArgs = new RollbackEventArgs(content);
+ if (uow.Events.DispatchCancelable(RollingBack, this, rollbackEventArgs))
{
uow.Commit();
return content;
@@ -1697,8 +1912,8 @@ public IContent Rollback(int id, Guid versionId, int userId = 0)
repository.AddOrUpdate(content);
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c));
-
- uow.Events.Dispatch(RolledBack, this, new RollbackEventArgs(content, false));
+ rollbackEventArgs.CanCancel = false;
+ uow.Events.Dispatch(RolledBack, this, rollbackEventArgs);
Audit(uow, AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id);
uow.Commit();
@@ -1729,7 +1944,8 @@ public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents =
using (var uow = UowProvider.GetUnitOfWork())
{
var asArray = items.ToArray();
- if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(asArray)))
+ var saveEventArgs = new SaveEventArgs(asArray);
+ if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
{
uow.Commit();
return false;
@@ -1774,7 +1990,10 @@ public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents =
}
if (raiseEvents)
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(asArray, false));
+ {
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved");
+ }
if (shouldBePublished.Any())
{
@@ -1790,6 +2009,25 @@ public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents =
return true;
}
+ public IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeIds)
+ {
+ using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
+ {
+ var repository = RepositoryFactory.CreateContentBlueprintRepository(uow);
+
+ var query = new Query();
+ if (documentTypeIds.Length > 0)
+ {
+ query.Where(x => documentTypeIds.Contains(x.ContentTypeId));
+ }
+ return repository.GetByQuery(query).Select(x =>
+ {
+ ((Content) x).IsBlueprint = true;
+ return x;
+ });
+ }
+ }
+
///
/// Gets paged content descendants as XML by path
///
@@ -2138,7 +2376,8 @@ private Attempt SaveAndPublishDo(IContent content, int userId = 0
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content, evtMsgs)))
+ var saveEventArgs = new SaveEventArgs(content, evtMsgs);
+ if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
{
uow.Commit();
return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs));
@@ -2193,7 +2432,10 @@ private Attempt SaveAndPublishDo(IContent content, int userId = 0
}
if (raiseEvents)
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false, evtMsgs));
+ {
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved");
+ }
//Save xml to db and call following method to fire event through PublishingStrategy to update cache
if (published)
@@ -2232,7 +2474,8 @@ private Attempt Save(IContent content, bool changeState, int us
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content, evtMsgs)))
+ var saveEventArgs = new SaveEventArgs(content, evtMsgs);
+ if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
{
uow.Commit();
return OperationStatus.Cancelled(evtMsgs);
@@ -2261,7 +2504,10 @@ private Attempt Save(IContent content, bool changeState, int us
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c));
if (raiseEvents)
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false, evtMsgs));
+ {
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved");
+ }
Audit(uow, AuditType.Save, "Save Content performed by user", userId, content.Id);
uow.Commit();
@@ -2271,40 +2517,6 @@ private Attempt Save(IContent content, bool changeState, int us
}
}
- ///
- /// Checks if the passed in can be published based on the anscestors publish state.
- ///
- ///
- /// Check current is only used when falling back to checking the Parent of non-saved content, as
- /// non-saved content doesn't have a valid path yet.
- ///
- /// to check if anscestors are published
- /// Boolean indicating whether the passed in content should also be checked for published versions
- /// True if the Content can be published, otherwise False
- private bool IsPublishable(IContent content, bool checkCurrent)
- {
- var ids = content.Path.Split(',').Select(int.Parse).ToList();
- foreach (var id in ids)
- {
- //If Id equals that of the recycle bin we return false because nothing in the bin can be published
- if (id == Constants.System.RecycleBinContent)
- return false;
-
- //We don't check the System Root, so just continue
- if (id == Constants.System.Root) continue;
-
- //If the current id equals that of the passed in content and if current shouldn't be checked we skip it.
- if (checkCurrent == false && id == content.Id) continue;
-
- //Check if the content for the current id is published - escape the loop if we encounter content that isn't published
- var hasPublishedVersion = HasPublishedVersion(id);
- if (hasPublishedVersion == false)
- return false;
- }
-
- return true;
- }
-
private PublishStatusType CheckAndLogIsPublishable(IContent content)
{
//Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published
@@ -2524,6 +2736,17 @@ public static event TypedEventHandler
public static event TypedEventHandler EmptiedRecycleBin;
+
+ ///
+ /// Occurs after a blueprint has been saved.
+ ///
+ public static event TypedEventHandler> SavedBlueprint;
+
+ ///
+ /// Occurs after a blueprint has been deleted.
+ ///
+ public static event TypedEventHandler> DeletedBlueprint;
+
#endregion
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs
index d2ce1de2f850..6e3f75c42dcd 100644
--- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Models;
@@ -8,8 +9,43 @@
namespace Umbraco.Core.Services
{
+ ///
+ /// Content service extension methods
+ ///
public static class ContentServiceExtensions
- {
+ {
+ public static IEnumerable GetByIds(this IContentService contentService, IEnumerable ids)
+ {
+ var guids = new List();
+ foreach (var udi in ids)
+ {
+ var guidUdi = udi as GuidUdi;
+ if (guidUdi == null)
+ throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content");
+ guids.Add(guidUdi);
+ }
+
+ return contentService.GetByIds(guids.Select(x => x.Guid));
+ }
+
+ ///
+ /// Method to create an IContent object based on the Udi of a parent
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IContent CreateContent(this IContentService contentService, string name, Udi parentId, string mediaTypeAlias, int userId = 0)
+ {
+ var guidUdi = parentId as GuidUdi;
+ if (guidUdi == null)
+ throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content");
+ var parent = contentService.GetById(guidUdi.Guid);
+ return contentService.CreateContent(name, parent, mediaTypeAlias, userId);
+ }
+
///
/// Remove all permissions for this user for all nodes
///
@@ -17,7 +53,7 @@ public static class ContentServiceExtensions
///
public static void RemoveContentPermissions(this IContentService contentService, int contentId)
{
- contentService.ReplaceContentPermissions(new EntityPermissionSet(contentId, Enumerable.Empty()));
+ contentService.ReplaceContentPermissions(new EntityPermissionSet(contentId, new EntityPermissionCollection()));
}
///
diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs
index 1b6735153df8..7de374b1407f 100644
--- a/src/Umbraco.Core/Services/ContentTypeService.cs
+++ b/src/Umbraco.Core/Services/ContentTypeService.cs
@@ -54,7 +54,8 @@ public Attempt> CreateCont
CreatorId = userId
};
- if (uow.Events.DispatchCancelable(SavingContentTypeContainer, this, new SaveEventArgs(container, evtMsgs)))
+ var saveEventArgs = new SaveEventArgs(container, evtMsgs);
+ if (uow.Events.DispatchCancelable(SavingContentTypeContainer, this, saveEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -62,8 +63,8 @@ public Attempt> CreateCont
repo.AddOrUpdate(container);
uow.Commit();
-
- uow.Events.Dispatch(SavedContentTypeContainer, this, new SaveEventArgs(container, evtMsgs), "SavedContentTypeContainer");
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedContentTypeContainer, this, saveEventArgs, "SavedContentTypeContainer");
//TODO: Audit trail ?
return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs));
@@ -76,6 +77,41 @@ public Attempt> CreateCont
}
}
+ public Attempt> RenameContentTypeContainer(int id, string name, int userId = 0)
+ {
+ return RenameTypeContainer(id, name, Constants.ObjectTypes.DocumentTypeContainerGuid);
+ }
+
+ private Attempt> RenameTypeContainer(int id, string name, Guid typeCode)
+ {
+ var evtMsgs = EventMessagesFactory.Get();
+ var uow = UowProvider.GetUnitOfWork();
+ using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, typeCode))
+ {
+ try
+ {
+ var container = repo.Get(id);
+
+ //throw if null, this will be caught by the catch and a failed returned
+ if (container == null)
+ throw new InvalidOperationException("No container found with id " + id);
+
+ container.Name = name;
+
+ repo.AddOrUpdate(container);
+ uow.Commit();
+
+ uow.Events.Dispatch(SavedContentTypeContainer, this, new SaveEventArgs(container, evtMsgs), "RenamedContainer");
+
+ return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs));
+ }
+ catch (Exception ex)
+ {
+ return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex);
+ }
+ }
+ }
+
public Attempt> CreateMediaTypeContainer(int parentId, string name, int userId = 0)
{
var evtMsgs = EventMessagesFactory.Get();
@@ -92,7 +128,8 @@ public Attempt> CreateMedi
CreatorId = userId
};
- if (uow.Events.DispatchCancelable(SavingMediaTypeContainer, this, new SaveEventArgs(container, evtMsgs)))
+ var saveEventArgs = new SaveEventArgs(container, evtMsgs);
+ if (uow.Events.DispatchCancelable(SavingMediaTypeContainer, this, saveEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -100,8 +137,8 @@ public Attempt> CreateMedi
repo.AddOrUpdate(container);
uow.Commit();
-
- uow.Events.Dispatch(SavedMediaTypeContainer, this, new SaveEventArgs(container, evtMsgs), "SavedMediaTypeContainer");
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedMediaTypeContainer, this, saveEventArgs, "SavedMediaTypeContainer");
//TODO: Audit trail ?
return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs));
@@ -114,6 +151,16 @@ public Attempt> CreateMedi
}
}
+ public Attempt> RenameMediaTypeContainer(int id, string name, int userId = 0)
+ {
+ return RenameTypeContainer(id, name, Constants.ObjectTypes.MediaTypeContainerGuid);
+ }
+
+ public Attempt> RenameDataTypeContainer(int id, string name, int userId = 0)
+ {
+ return RenameTypeContainer(id, name, Constants.ObjectTypes.DataTypeContainerGuid);
+ }
+
public Attempt SaveContentTypeContainer(EntityContainer container, int userId = 0)
{
return SaveContainer(
@@ -211,7 +258,7 @@ public IEnumerable GetMediaTypeContainers(string name, int leve
public IEnumerable GetMediaTypeContainers(IMediaType mediaType)
{
- var ancestorIds = mediaType.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries)
+ var ancestorIds = mediaType.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x =>
{
var asInt = x.TryConvertTo();
@@ -240,7 +287,7 @@ public IEnumerable GetContentTypeContainers(int[] containerIds)
public IEnumerable GetContentTypeContainers(IContentType contentType)
{
- var ancestorIds = contentType.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries)
+ var ancestorIds = contentType.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x =>
{
var asInt = x.TryConvertTo();
@@ -289,7 +336,8 @@ public Attempt DeleteContentTypeContainer(int containerId, int
return OperationStatus.NoOperation(evtMsgs);
}
- if (uow.Events.DispatchCancelable(DeletingContentTypeContainer, this, new DeleteEventArgs(container, evtMsgs)))
+ var deleteEventArgs = new DeleteEventArgs(container, evtMsgs);
+ if (uow.Events.DispatchCancelable(DeletingContentTypeContainer, this, deleteEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -297,8 +345,8 @@ public Attempt DeleteContentTypeContainer(int containerId, int
repo.Delete(container);
uow.Commit();
-
- uow.Events.Dispatch(DeletedContentTypeContainer, this, new DeleteEventArgs(container, evtMsgs), "DeletedContentTypeContainer");
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedContentTypeContainer, this, deleteEventArgs, "DeletedContentTypeContainer");
return OperationStatus.Success(evtMsgs);
//TODO: Audit trail ?
@@ -319,7 +367,8 @@ public Attempt DeleteMediaTypeContainer(int containerId, int us
return OperationStatus.NoOperation(evtMsgs);
}
- if (uow.Events.DispatchCancelable(DeletingMediaTypeContainer, this, new DeleteEventArgs(container, evtMsgs)))
+ var deleteEventArgs = new DeleteEventArgs(container, evtMsgs);
+ if (uow.Events.DispatchCancelable(DeletingMediaTypeContainer, this, deleteEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -327,8 +376,8 @@ public Attempt DeleteMediaTypeContainer(int containerId, int us
repo.Delete(container);
uow.Commit();
-
- uow.Events.Dispatch(DeletedMediaTypeContainer, this, new DeleteEventArgs(container, evtMsgs));
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedMediaTypeContainer, this, deleteEventArgs);
return OperationStatus.Success(evtMsgs);
//TODO: Audit trail ?
@@ -367,6 +416,15 @@ public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes)
}
}
+ public IEnumerable GetAllContentTypeIds(string[] aliases)
+ {
+ using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
+ {
+ var repository = RepositoryFactory.CreateContentTypeRepository(uow);
+ return repository.GetAllContentTypeIds(aliases);
+ }
+ }
+
///
/// Copies a content type as a child under the specified parent if specified (otherwise to the root)
///
@@ -426,7 +484,7 @@ public IContentType Copy(IContentType original, string alias, string name, ICont
clone.Name = name;
- var compositionAliases = clone.CompositionAliases().Except(new[] {alias}).ToList();
+ var compositionAliases = clone.CompositionAliases().Except(new[] { alias }).ToList();
//remove all composition that is not it's current alias
foreach (var a in compositionAliases)
{
@@ -756,7 +814,8 @@ public void Save(IContentType contentType, int userId = 0)
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SavingContentType, this, new SaveEventArgs(contentType)))
+ var saveEventArgs = new SaveEventArgs(contentType);
+ if (uow.Events.DispatchCancelable(SavingContentType, this, saveEventArgs))
{
uow.Commit();
return;
@@ -779,7 +838,8 @@ public void Save(IContentType contentType, int userId = 0)
UpdateContentXmlStructure(contentType);
- uow.Events.Dispatch(SavedContentType, this, new SaveEventArgs(contentType, false));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedContentType, this, saveEventArgs);
Audit(uow, AuditType.Save, "Save ContentType performed by user", userId, contentType.Id);
uow.Commit();
@@ -800,7 +860,8 @@ public void Save(IEnumerable contentTypes, int userId = 0)
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SavingContentType, this, new SaveEventArgs(asArray)))
+ var saveEventArgs = new SaveEventArgs(asArray);
+ if (uow.Events.DispatchCancelable(SavingContentType, this, saveEventArgs))
{
uow.Commit();
return;
@@ -825,7 +886,8 @@ public void Save(IEnumerable contentTypes, int userId = 0)
UpdateContentXmlStructure(asArray.Cast().ToArray());
- uow.Events.Dispatch(SavedContentType, this, new SaveEventArgs(asArray, false));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedContentType, this, saveEventArgs);
Audit(uow, AuditType.Save, "Save ContentTypes performed by user", userId, -1);
uow.Commit();
@@ -845,7 +907,8 @@ public void Delete(IContentType contentType, int userId = 0)
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(DeletingContentType, this, new DeleteEventArgs(contentType)))
+ var deleteEventArgs = new DeleteEventArgs(contentType);
+ if (uow.Events.DispatchCancelable(DeletingContentType, this, deleteEventArgs))
{
uow.Commit();
return;
@@ -854,14 +917,15 @@ public void Delete(IContentType contentType, int userId = 0)
var repository = RepositoryFactory.CreateContentTypeRepository(uow);
//If we are deleting this content type, we are also deleting it's descendents!
- var deletedContentTypes = new List {contentType};
+ 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));
+ deleteEventArgs.DeletedEntities = deletedContentTypes.DistinctBy(x => x.Id);
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedContentType, this, deleteEventArgs);
Audit(uow, AuditType.Delete, string.Format("Delete ContentType performed by user"), userId, contentType.Id);
uow.Commit();
@@ -885,7 +949,8 @@ public void Delete(IEnumerable contentTypes, int userId = 0)
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(DeletingContentType, this, new DeleteEventArgs(asArray)))
+ var deleteEventArgs = new DeleteEventArgs(asArray);
+ if (uow.Events.DispatchCancelable(DeletingContentType, this, deleteEventArgs))
{
uow.Commit();
return;
@@ -906,8 +971,9 @@ public void Delete(IEnumerable contentTypes, int userId = 0)
{
repository.Delete(contentType);
}
-
- uow.Events.Dispatch(DeletedContentType, this, new DeleteEventArgs(deletedContentTypes.DistinctBy(x => x.Id), false));
+ deleteEventArgs.DeletedEntities = deletedContentTypes.DistinctBy(x => x.Id);
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedContentType, this, deleteEventArgs);
Audit(uow, AuditType.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1);
uow.Commit();
@@ -1056,7 +1122,9 @@ public Attempt> MoveMediaType(IMediaTyp
var moveInfo = new List>();
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(MovingMediaType, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId))))
+ var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, containerId);
+ var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo);
+ if (uow.Events.DispatchCancelable(MovingMediaType, this, moveEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -1081,7 +1149,9 @@ public Attempt> MoveMediaType(IMediaTyp
return Attempt.Fail(new OperationStatus(ex.Operation, evtMsgs));
}
uow.Commit();
- uow.Events.Dispatch(MovedMediaType, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()));
+ moveEventArgs.MoveInfoCollection = moveInfo;
+ moveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(MovedMediaType, this, moveEventArgs);
}
return Attempt.Succeed(
@@ -1095,7 +1165,9 @@ public Attempt> MoveContentType(IConten
var moveInfo = new List>();
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(MovingContentType, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId))))
+ var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, containerId);
+ var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo);
+ if (uow.Events.DispatchCancelable(MovingContentType, this, moveEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -1119,8 +1191,10 @@ public Attempt> MoveContentType(IConten
uow.Commit();
return Attempt.Fail(new OperationStatus(ex.Operation, evtMsgs));
}
+ moveEventArgs.MoveInfoCollection = moveInfo;
+ moveEventArgs.CanCancel = false;
uow.Commit();
- uow.Events.Dispatch(MovedContentType, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()));
+ uow.Events.Dispatch(MovedContentType, this, moveEventArgs);
}
return Attempt.Succeed(
@@ -1228,7 +1302,8 @@ public void Save(IMediaType mediaType, int userId = 0)
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SavingMediaType, this, new SaveEventArgs(mediaType)))
+ var saveEventArgs = new SaveEventArgs(mediaType);
+ if (uow.Events.DispatchCancelable(SavingMediaType, this, saveEventArgs))
{
uow.Commit();
return;
@@ -1244,8 +1319,8 @@ public void Save(IMediaType mediaType, int userId = 0)
uow.Commit();
UpdateContentXmlStructure(mediaType);
-
- uow.Events.Dispatch(SavedMediaType, this, new SaveEventArgs(mediaType, false));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedMediaType, this, saveEventArgs);
Audit(uow, AuditType.Save, "Save MediaType performed by user", userId, mediaType.Id);
uow.Commit();
@@ -1266,7 +1341,8 @@ public void Save(IEnumerable mediaTypes, int userId = 0)
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SavingMediaType, this, new SaveEventArgs(asArray)))
+ var saveEventArgs = new SaveEventArgs(asArray);
+ if (uow.Events.DispatchCancelable(SavingMediaType, this, saveEventArgs))
{
uow.Commit();
return;
@@ -1290,8 +1366,8 @@ public void Save(IEnumerable mediaTypes, int userId = 0)
uow.Commit();
UpdateContentXmlStructure(asArray.Cast().ToArray());
-
- uow.Events.Dispatch(SavedMediaType, this, new SaveEventArgs(asArray, false));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedMediaType, this, saveEventArgs);
Audit(uow, AuditType.Save, "Save MediaTypes performed by user", userId, -1);
uow.Commit();
@@ -1313,7 +1389,8 @@ public void Delete(IMediaType mediaType, int userId = 0)
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(DeletingMediaType, this, new DeleteEventArgs(mediaType)))
+ var deleteEventArgs = new DeleteEventArgs(mediaType);
+ if (uow.Events.DispatchCancelable(DeletingMediaType, this, deleteEventArgs))
{
uow.Commit();
return;
@@ -1329,7 +1406,9 @@ public void Delete(IMediaType mediaType, int userId = 0)
repository.Delete(mediaType);
- uow.Events.Dispatch(DeletedMediaType, this, new DeleteEventArgs(deletedMediaTypes.DistinctBy(x => x.Id), false));
+ deleteEventArgs.CanCancel = false;
+ deleteEventArgs.DeletedEntities = deletedMediaTypes.DistinctBy(x => x.Id);
+ uow.Events.Dispatch(DeletedMediaType, this, deleteEventArgs);
Audit(uow, AuditType.Delete, "Delete MediaType performed by user", userId, mediaType.Id);
uow.Commit();
@@ -1353,7 +1432,8 @@ public void Delete(IEnumerable mediaTypes, int userId = 0)
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(DeletingMediaType, this, new DeleteEventArgs(asArray)))
+ var deleteEventArgs = new DeleteEventArgs(asArray);
+ if (uow.Events.DispatchCancelable(DeletingMediaType, this, deleteEventArgs))
{
uow.Commit();
return;
@@ -1375,7 +1455,9 @@ public void Delete(IEnumerable mediaTypes, int userId = 0)
repository.Delete(mediaType);
}
- uow.Events.Dispatch(DeletedMediaType, this, new DeleteEventArgs(deletedMediaTypes.DistinctBy(x => x.Id), false));
+ deleteEventArgs.DeletedEntities = deletedMediaTypes.DistinctBy(x => x.Id);
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedMediaType, this, deleteEventArgs);
Audit(uow, AuditType.Delete, "Delete MediaTypes performed by user", userId, -1);
uow.Commit();
diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs
index c01b13136e58..0ee3e9b82337 100644
--- a/src/Umbraco.Core/Services/DataTypeService.cs
+++ b/src/Umbraco.Core/Services/DataTypeService.cs
@@ -312,7 +312,9 @@ public Attempt> Move(IDataTypeDefinitio
var moveInfo = new List